<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wannabe</title>
    <description>The latest articles on DEV Community by Wannabe (@wannabe_a68a31ba67e23aa4e).</description>
    <link>https://dev.to/wannabe_a68a31ba67e23aa4e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2765043%2Fe9311355-ee7e-495b-88ed-a5da6fe8644c.jpg</url>
      <title>DEV Community: Wannabe</title>
      <link>https://dev.to/wannabe_a68a31ba67e23aa4e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wannabe_a68a31ba67e23aa4e"/>
    <language>en</language>
    <item>
      <title>How to upload and SAVE images in Elixir</title>
      <dc:creator>Wannabe</dc:creator>
      <pubDate>Sun, 02 Feb 2025 06:54:09 +0000</pubDate>
      <link>https://dev.to/wannabe_a68a31ba67e23aa4e/how-to-upload-and-save-images-in-elixir-2k1b</link>
      <guid>https://dev.to/wannabe_a68a31ba67e23aa4e/how-to-upload-and-save-images-in-elixir-2k1b</guid>
      <description>&lt;p&gt;While working on &lt;a href="https://faelib.com" rel="noopener noreferrer"&gt;faelib.com&lt;/a&gt;, I had to implement uploading images and saving them in the file system. Below is how it went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I am writing this
&lt;/h2&gt;

&lt;p&gt;I could not find plenty documentation or articles about this online. And the ones I did find were either too complex or didn't quite fit my use case. You can find one of the good ones &lt;a href="https://www.poeticoding.com/step-by-step-tutorial-to-build-a-phoenix-app-that-supports-user-uploads/" rel="noopener noreferrer"&gt;here&lt;/a&gt;, but it implements quite different flow.&lt;/p&gt;

&lt;p&gt;So, here's my journey that hopefully will save someone else a few hours of head-scratching!&lt;br&gt;
(&lt;em&gt;And if you're on a more experienced side, once you see my final solution, please do let me know why it's wrong.&lt;/em&gt;)&lt;/p&gt;
&lt;h2&gt;
  
  
  Use case
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;have a form to create tool and save it to database&lt;/li&gt;
&lt;li&gt;as part of creating, upload an image (but don't save it to database)&lt;/li&gt;
&lt;li&gt;save an image somewhere in the file system as &lt;code&gt;&amp;lt;tool_id&amp;gt;.png&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;on the object info page, display that image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds simple, right?&lt;/p&gt;
&lt;h2&gt;
  
  
  First attempt
&lt;/h2&gt;

&lt;p&gt;I started with the basics. Phoenix provides a nice &lt;a href="https://hexdocs.pm/phoenix/file_uploads.html" rel="noopener noreferrer"&gt;upload component&lt;/a&gt; out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# in the live view
@allowed_mime_types ~w(image/png)

def mount(_params, _session, socket) do
    {:ok,
     socket
     ...
     |&amp;gt; allow_upload(:image,
       accept: @allowed_mime_types,
       max_file_size: 5_000_000
     )}
  end

# in the heex template
&amp;lt;.live_file_input upload={@uploads.image} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, when the Save button is clicked, I saved the image to &lt;code&gt;priv/static/images&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def handle_event("save", %{"tool" =&amp;gt; tool_params}, socket) do
    save_tool(socket, socket.assigns.tool, tool_params)
end

defp save_tool(socket, nil, params) do
    ...

    case Tools.create_tool(params) do
      {:ok, tool} -&amp;gt;
        save_image(socket, tool)

        # put flash, redirect or do whatever here
        {:noreply, socket}

      {:error, %Ecto.Changeset{} = changeset} -&amp;gt;
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

defp save_image(socket, tool) do
    consume_uploaded_entries(socket, :image, fn %{path: path}, _entry -&amp;gt;
      dest = "/priv/static/images/tools/"
      File.cp!(path, dest)
      {:ok, "/images/tools/#{tool.id}.png"}
    end)
 end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, when it comes to displaying images, the code is as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img src="/images/tools/&amp;lt;tool_id&amp;gt;.png} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked like a charm, because Phoenix already has &lt;code&gt;priv/static/images&lt;/code&gt; served as static paths, in &lt;code&gt;_web.ex&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def static_paths, do: ~w(assets fonts images favicon.ico robots.txt sitemap.xml)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything worked beautifully on localhost - upload an image, see it appear, &lt;del&gt;celebration&lt;/del&gt; deployment time! 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Plot Twist: Enter Fly.io
&lt;/h2&gt;

&lt;p&gt;I have Faelib deployed on Fly.io. Feeling confident, I deployed my changes... And that's when things got interesting. My perfectly working image upload system suddenly... wasn't working. At all. &lt;code&gt;ENOENT&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;Coming from the native iOS development, debugging code in webdev feels like a special form of punishment, many times so in production.&lt;/p&gt;

&lt;p&gt;After some investigation, I discovered that &lt;code&gt;priv/static&lt;/code&gt; doesn't exist in the release version of the app. Well, that explains things! 🤔&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plot Thickens
&lt;/h2&gt;

&lt;p&gt;After some googling on how to upload images at all &lt;/p&gt;

&lt;p&gt;(&lt;em&gt;that's where AI does us all a disservice, I should've started by finding out how to do it properly in the first place!&lt;/em&gt;),&lt;/p&gt;

&lt;p&gt;I figured out that I have to have a dedicated folder in the file system to save my images. And then serve them from there (sounds reasonable, but I had no idea how.)&lt;/p&gt;

&lt;p&gt;I started with creating the folders. I did it manually, via connecting to fly.io machine: &lt;code&gt;fly ssh console -s&lt;/code&gt; then &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;mkdir&lt;/code&gt; and all that stuff. But that did not feel right, it should happen automatically.&lt;/p&gt;

&lt;p&gt;As Fly.io provides a Docker file for the deployment, all I had to do is modify it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN mkdir -p /images/tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediate problem, my app can't write to this folder. That's a quick fix, my Docker command turned into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN mkdir -p /images/tools &amp;amp;&amp;amp; \
  chown -R nobody: /images &amp;amp;&amp;amp; \
  chmod -R 755 /images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, I save images to &lt;code&gt;/images/tools&lt;/code&gt; and serve them from &lt;code&gt;/images/tools&lt;/code&gt;. Should work, right?&lt;/p&gt;

&lt;p&gt;It did work... for about two hours. Then all my images just vanished. &lt;/p&gt;

&lt;p&gt;Turns out (after another share of googling) Fly.io has virtual file system, which means it wipes all data when you deploy new code or when the machine automatically restarts. I should have known that! 🫣 (It's not even the first app that I host with them. Mea culpa!)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Persistent Volumes
&lt;/h2&gt;

&lt;p&gt;Finally, I discovered Fly.io volumes - persistent storage that survives deployments and restarts. They provide pretty &lt;a href="https://fly.io/docs/volumes/overview/" rel="noopener noreferrer"&gt;decent documentation&lt;/a&gt; on how to work with volumes. I did just what I was instructed to do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# fly.toml
[mounts]
  source = "tool_images"
  destination = "/app/bin/images"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then &lt;code&gt;fly volumes create &amp;lt;my_volume_name&amp;gt;&lt;/code&gt; and &lt;code&gt;fly deploy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The only thing left to do is serving the images from that folder. So...&lt;/p&gt;

&lt;p&gt;Here's the overview of the final working solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I have created a volume on my machine at &lt;code&gt;/app/bin/images&lt;/code&gt;; that's where I save the uploaded images&lt;br&gt;
(&lt;em&gt;after another debugging session I found out that I need the path to start with "/app"&lt;/em&gt;)&lt;br&gt;
I gave that folder proper permissions with &lt;code&gt;chmod -R 755 bin/images&lt;/code&gt; command.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I specified paths for uploading and serving, depending on the environment.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dev.exs
config :faelib,
  uploads_directory: Path.join("priv/static/images", "tools"),
  static_paths: "priv/static"

# prod.exs
config :faelib,
  uploads_directory: Path.join("images", "tools"),
  static_paths: "images"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I still want my uploading to work in the localhost as it used to work in the beginning, saving images to &lt;code&gt;priv/static/&lt;/code&gt; folder.&lt;br&gt;
As a small touch, I also excluded that folder from git, there's no need to commit the image files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I instruct my app how to serve the files from the upload folder:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# endpoint.ex

plug Plug.Static,
    at: "images",
    from: {__MODULE__, :static_directory, []},
    gzip: false

def static_directory do
    Faelib.ImageHandler.statics_path()
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;The code in live view and heex template almost did not change. I just had to provide the image paths depending on the environment that I am in. I made a dedicated module for that (to be completely honest, I already had it from the very start, but did not mention it above to not distract you from how silly I was):
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Faelib.ImageHandler do
  # see endpoint.ex for Plug that serves images from "/images"
  @image_path "/images/tools"

  def get_image_path(tool) do
    Path.join(@image_path, "#{tool.id}.png")
  end

  def destination_image_path(tool) do
    Path.join(uploads_dir(), "#{tool.id}.png")
  end

  # used in endpoint.ex
  def statics_path do
    Application.get_env(:faelib, :static_paths)
  end

  defp uploads_dir do
    Application.get_env(:faelib, :uploads_directory)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Uploading image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# in the live view
@allowed_mime_types ~w(image/png)

def mount(_params, _session, socket) do
    {:ok,
     socket
     ...
     |&amp;gt; allow_upload(:image,
       accept: @allowed_mime_types,
       max_file_size: 5_000_000
     )}
  end

# in the heex template
&amp;lt;.live_file_input upload={@uploads.image} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handling uploaded image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def handle_event("save", %{"tool" =&amp;gt; tool_params}, socket) do
    save_tool(socket, socket.assigns.tool, tool_params)
end

defp save_tool(socket, nil, params) do
    ...

    case Tools.create_tool(params) do
      {:ok, tool} -&amp;gt;
        save_image(socket, tool)

        # put flash, redirect or do whatever here
        {:noreply, socket}

      {:error, %Ecto.Changeset{} = changeset} -&amp;gt;
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

defp save_image(socket, tool) do
    consume_uploaded_entries(socket, :image, fn %{path: path}, _entry -&amp;gt;
      dest = Faelib.ImageHandler.destination_image_path(tool)
      File.cp!(path, dest)
      {:ok, "/images/tools/#{tool.id}.png"}
    end)
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serving image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img
  class={"#{@size} rounded-lg"}
  src={Faelib.ImageHandler.get_image_path(@tool)}
  alt={@tool.name}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Looking Ahead
&lt;/h3&gt;

&lt;p&gt;While this solution works well for now, I know it's not the end of the story. As the application grows, I'll need to look into more scalable solutions like AWS S3 or similar cloud storage services. But for an MVP or small application? This setup does the job perfectly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Now, that I wrote it...
&lt;/h3&gt;

&lt;p&gt;I will remind you why I did it, because after all this long read you have probably forgotten. I wrote it with two thoughts in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If it worked for me, it can work for someone else. Hopefully, saving them some time.&lt;/li&gt;
&lt;li&gt;Despite the working solution, it still very well can suck. So I am asking you, more experienced devs: What did I do wrong?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>elixir</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building Faelib</title>
      <dc:creator>Wannabe</dc:creator>
      <pubDate>Fri, 31 Jan 2025 11:38:55 +0000</pubDate>
      <link>https://dev.to/wannabe_a68a31ba67e23aa4e/building-faelib-2oho</link>
      <guid>https://dev.to/wannabe_a68a31ba67e23aa4e/building-faelib-2oho</guid>
      <description>&lt;p&gt;Hello! 👋 &lt;/p&gt;

&lt;p&gt;In my everyday work life I am an iOS developer. However, in my free time I'm building &lt;a href="https://faelib.com" rel="noopener noreferrer"&gt;Faelib&lt;/a&gt; - a web platform to help developers find and compare software libraries and components. I want to share my journey with you, get your feedback, and hopefully create something truly useful for our community.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea
&lt;/h2&gt;

&lt;p&gt;As a professional software engineer, I had to do it countless times: implement complex new feature that the business needs yesterday. The choice often comes to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implement ourselves&lt;/li&gt;
&lt;li&gt;use existing component, whether commercial or open source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But be it the right calendar component or a PDF processing library, the process always felt the same - hours of research and investigation across different websites, trying to find pricing plans and capabilities, compiling it into one big comparison table, trying to figure out which parameters are even important.&lt;/p&gt;

&lt;p&gt;That's why I started &lt;a href="https://faelib.com" rel="noopener noreferrer"&gt;Faelib&lt;/a&gt; - to create one place where developers can easily find and compare ready-made solutions for their projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyuczpj4k7sgch11erv6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyuczpj4k7sgch11erv6.png" alt="The simplest home page I could think of" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Current State
&lt;/h2&gt;

&lt;p&gt;After couple weeks of development, the MVP is live at &lt;a href="http://www.faelib.com/" rel="noopener noreferrer"&gt;faelib.com&lt;/a&gt; with these core features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search across 30+ popular libraries and frameworks (I know this number is nothing, but I have to start somewhere)&lt;/li&gt;
&lt;li&gt;Filter by platform (web/mobile), language, licensing type (free/paid)&lt;/li&gt;
&lt;li&gt;Side-by-side comparison of up to 4 tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fboxjbjak4fynhw442s3a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fboxjbjak4fynhw442s3a.png" alt="Image description" width="800" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The North Star
&lt;/h2&gt;

&lt;p&gt;I hope for Faelib to become the go-to platform where developers start their search for components and libraries.&lt;br&gt;
Two very important things for me are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;free for the visitor, no ads, no promoted libraries&lt;/li&gt;
&lt;li&gt;community driven platform: I do add tools and libraries myself, but I strongly encourage everyone, users and owners, to suggest them as well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a library/framework to the platform is free and will always be. Whether you have a library of your own, or a fan of someone's work, please suggest it via the &lt;a href="https://faelib.com/suggest-tool" rel="noopener noreferrer"&gt;dedicated form&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;For those interested, the Faelib's tech stack is the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: Phoenix/LiveView/Elixir&lt;/li&gt;
&lt;li&gt;Backend: Phoenix/Elixir&lt;/li&gt;
&lt;li&gt;Database: PostgreSQL&lt;/li&gt;
&lt;li&gt;Hosting: fly.io&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I have talked to several people publicly and privately already and collected some great feedback. I am at the very beginning and have uncountable things to improve.&lt;/p&gt;

&lt;p&gt;The immediate roadmap includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Expanding the database to include more libraries and frameworks&lt;/li&gt;
&lt;li&gt;Building up the comparison feature.&lt;/li&gt;
&lt;li&gt;Improving search and filtering capabilities&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  I Need Your Help!
&lt;/h2&gt;

&lt;p&gt;This is where you come in - I'd love to hear your thoughts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What features would make this truly useful for you?&lt;/li&gt;
&lt;li&gt;Which libraries or frameworks should I add next?&lt;/li&gt;
&lt;li&gt;How do you currently choose between different tools?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop your comments below or reach out directly. Your feedback will help shape Faelib into something we all want to use.&lt;/p&gt;

&lt;p&gt;Let's build something awesome together! 🚀&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>devjournal</category>
      <category>elixir</category>
    </item>
  </channel>
</rss>
