Simplest (and cheapest) way to upload/store/retrieve images

I was a bit surprised to learn that Elm didn’t have a way to write images to some dedicated folder, but file upload seems reasonably easy (although I’m not sure how you’d handle errors).

What I’m looking for is anyone who’s successfully used native code to:

  1. Upload an image
  2. Rename the file (which a lot of image upload sites do)[1]
  3. Post that to a server with Http.fileBody (and I guess Http.post) …
  4. Retrieve the URL of a successfully stored image
  5. Store it in the model ready to add to a form
  6. Any security concerns? (api secrets etc)

So it’d be a two-step form I guess, where you have an image upload URL that later gets attached to a Http.post stored as json. I know how to do (1) and (5) but the others are fuzzy.

Amazon S3 would be one obvious choice, but last time I checked their admin isn’t particularly user-friendly, and I’m not sure how much they’d charge for image retrieval. Here’s another affordable image server at $100 per year. The alternative is to post them and manually migrate to my own hosting.

Anyway, it seems like quite an essential feature of any app to upload files, so I’m wondering how people are handling it currently. I’m not much a fan of ports, but if it’s simple to understand that’s another option.

I’ve seen a few posts on the topic, but some seem outdated or a bit complex.


  1. Helps avoid duplicate names if the filename can be randomised ↩︎

1 Like

When Elm Designer was online it used Imgbb to store bitmap images (not SVG) uploaded by users of the app.

API access is free and after signup you can get a key to make calls. It is just a HTTP POST call. I consider the service quality at “demo level” but if you need to cook up something quickly I would use it.

You can grab the Imgbb module here elm-designer/src/Imgbb.elm at master · passiomatic/elm-designer · GitHub

You have to do some wiring, especially for the track upload progress which is important for the UX. Take a look here elm-designer/src/Main.elm at master · passiomatic/elm-designer · GitHub and subsequent Msg’s.

The Elm Designer wiring code is quite complex, because the app allows you to select multiple image(s) from disk or drag&drop them, and show process for each upload. You might not need all this complexity.

1 Like

Hey @passiomatic yes for now I think I prefer jQuery-style simplicity, without worrying about too many bells and whistles :wink:

So far I’ve made a few basic tests of another server (I’ve been a bit put off Imgbb due to negative reviews, but I guess you get what you pay for). It seems like you can upload local files with FILES["source"] but I can’t figure out how to do that with CURL.

As for your Imgbb module, it would be great to see some comments so I can follow it a bit better (there’s a lot of new stuff there, and I’m not sure what Http.track is doing — is that the track upload progress you’re talking about?)

Yeah your Msg has a LOT going on (with a lot of stuff I don’t understand)! From a UX perspective that’s probably nicer, but I’ve decided that to keep my learning light for now (I’m pretty much a one-man-band at the moment!), so a simple button suits me. Perhaps with a progress bar.

Fancy UI looks cool, but it seems to add a lot of overhead. I think I’m also going to have to look into breaking up a file and Msg into their own modules. There’s a lot to keep in your head there.

So TL;DR:

  1. Is attaching a local file possible? Or must you convert it to base64?
  2. Is toString the way to convert to base64?
  3. https://freeimage.host/ automatically renames the file, so (2) is covered …
    • Just for posterity, how might you rename the file?
  4. It seems a bit simpler now I know it’s just a Http.post
    • What’s the benefit of multipartBody over fileBody?
  5. So [image/jpg] limits ONLY to files with jpg extension? (Seems to be)
  6. Finally, in your Imgbb example I don’t see your API key
    • Are you providing this in config somehow for security?

Seems like I’m getting closer!

We use S3 and native browser/Elm file upload for handling PDFs at work. On the Elm side there hasn’t been too much too do, most of the work has had to been server side with scoping files between users and the actual handling of storing/retrieving files.


@jxxcarlson might have more thoughts on images specifically as I believe he’s done some work here with his Lamdera apps.

Oh ok. Yes I definitely don’t want to deal with anything server side. I’d hire someone to do that I reckon. Any clues from @jxxcarlson would be much appreciated.

This is my current (mostly working) attemptThis is my current (mostly working) attempt

  1. I keep getting a damn server Http.Error.
    • See this file for a working URL.
    • Funnily enough even with curl a base64 url string is throwing a server error[1]
  2. @wolfadex Yup I figured that out after double checking the csv file example.
    • File.toUrl converts an image to a base64 url string …
    • However, it seems .png files are NOT converting?

I’ve sent a support message to that image hosting site, perhaps I’ll try Imgbb as well, make sure it’s not an Elm problem. Perhaps it’s due to localhost not being https?


  1. {"status_code":400,"error":{"message":"Invalid base64 string.","code":120},"status_txt":"Bad Request"} ↩︎

I think Elm doesn’t like modules and package name clashes (like File)

Somewhat correct. If you have 2 modules named Rob and try to

import Wolf

Elm doesn’t have a way to differentiate between them. You can, if you want, do

import Wolf
import Not.Wolf as Wolf

and then you can do Wolf.function. This can cause issues though if both modules expose functions/types with the same name.

What if our Task.perform fails? And what does Never mean. It can never fail?

Never is a value that can never happen. So yes Task.perform having an error value of Never means it can never fail.

but we still need to unpack the bloody Maybe

Why don’t you send the model.image along with in your message since at the point where the message is sent you know exactly that your image isn’t a Nothing?

Should File.name function be the second param to Task.perform?

Why do you send the file name in the message instead of setting it directly on the model?

Should File.toString be File.toUrl?

File.toString gives you the string contents, like for a text file, while File.toUrl gives you a base64 encoded string, excellent for use with <img />.

Thanks @wolfadex I guess I’ll wrap my head around modules as I improve. It’s probably a better idea to make sure there’s no naming conflicts with elm/... packages. For now I’m using File_.SomeModule.

  1. Thanks for clarifying that Never. It feels a bit weird to me!
  2. I didn’t think to do that. Do you mean Just urlonClick (SendToServer url)?
  3. Why do you send the file name in the message instead of setting it directly on the model?
    • In the csv example he’s using a Task to convert to a string …
    • So I thought to do the same thing with the File.toUrl
    • At which point (in the update) you wouldn’t be able to grab the name anymore.
    • Is there a better way to do this?

I think the server problem is either me doing something wrong in the url, or the server has the problem, as I’ve encoded and decoded a few base64 images and they’re not posting with curl either.

Thanks!

You should be fine with File.SomeModule. If anything I’d give a more accurate name instead of File. E.g. your modules seem to deal with images specifically so a more helpful name might be File.Image or ImageFile or even Image.

I also wouldn’t worry specifically about the elm/* packages. It’s more of a general issue, like if you used elm-ui 1.1.8 then you wouldn’t be able to name any of your local modules Element. I’d focus more on giving your modules names that make sense when you’re looking at the name along.

Exactly!

Which makes sense because a csv file is a specialized text file. If you open it in your text editor it’s still legible.

ImageSelected file ->
      ( model
      , Task.perform (ImageLoaded (File.name file)) (File.toString file)
      )

ImageLoaded filename content ->
      ( { model
            | image = Just content
            , imageName = filename
        }
      , Cmd.none
      )

can be converted to

ImageSelected file ->
      ( { model | imageName = File.name file }
      , Task.perform ImageLoaded (File.toString file)
      )

ImageLoaded content ->
      ( { model
            | image = Just content
        }
      , Cmd.none
      )

there’s no need to pass the name through the message+update loop again (as best I can tell).


A small stylistic opinion. I’d merge all of your File_.* modules into a single module. A module should generally be a container of sorts around a specific type. I’d also recommend Components | Elm Land for a very solid guide about how to model your component like modules.