tfwright

tfwright

Capsule - a modular file upload and storage utility

Today, I’m releasing what I’m calling a “preview” version of a new file upload utility for Elixir (with optional Ecto integration). If you want to get straight to the code, the source README should outline the basics. If you’d like to hear more about why, rationale is below.

Last week I needed to add a pretty simple upload feature to an app I’m working on. Since this isn’t something I’ve done before in Elixir/Phoenix, I posted here asking what others were doing. I didn’t get much of a response, which leads me to believe people were either using Arc, Waffle (the fork of Arc) or just creating their own solution. I wasn’t super excited about either lib, and my use case was pretty simple, so I decided to implement my own custom solution. But after a few hours of playing with it I decided it might be worth separating out and offering my own library.

On my last big Rails project, which involved a good amount of file upload and processing, we used Shrine. Despite the usual gripes with dealing with opaque behavior and callback mazes one has with a lot of Ruby projects that tightly couple with ActiveRecord (and Shrine isn’t even that coupled), I was really impressed with its power and simple API, and the plugin system that gave you access to a kind of “middleware” layer between the upload and the storage phases (although in practice they ended up somewhat entangled in sometimes unexpected and confusing ways). Almost immediately I could see that Elixir offered some really exciting possibilities to build a similar design, but with even more clarity and simplicity.

From the beginning I knew I wanted the following:

  1. Modular system so I could use just what I needed and nothing more
  2. Flexible interface so I could build custom use cases on top of that with minimal hassle
  3. Clean sensible DSL so I could avoid the cognitive load of FS details when I just want to move file from A to B

What excited me was not only how easy it was to accomplish (I think) these goals in a relatively short period of time, but the increasing conviction that the stuff I wasn’t planning to bother to do, ostensibly because other libraries already did it fairly well, I didn’t need to do and in fact shouldn’t do. This is probably the most opinionated aspect of this library and the reason I’m sharing it here. Versions, “convert” commands, backgrounding, these are things are certainly possible to do with Elixir. But once you are adding those kind of features to file upload in an app, two things are true: 1) there are some details about what you need that no library will be able to predict, and so can’t support well, and if you have the three things above, the added work to add these on top is vanishingly small.

As I worked on this, I’ve been thinking back to one of my first impressions of (what I take to be) the “Elixir Way” (every language has its Way, right?) when I started learning Ecto syntax a few years ago, one I’ve since seen repeated many times by other new learners. Where is my Model.last? Do I really have to type MyApp.Context.Model |> Ecto.Query.last |> MyApp.Repo.one? The point I started really getting into Elixir is when I realized this wasn’t a defect in the design, or even an oversight, but an intentional decision built deep into Ecto, Elixir itself, and I assume at least in part, the underlying VM. And that was another impression–how much the people I could tell really understood Elixir talked about Erlang! I remember thinking, “isn’t this an Elixir forum??” How often do you catch a Rubyist waxing eloquent about C? Not often…

So, as usual, I’d welcome any feedback the community has about my approach here. Specifically I’d love to have a conversation around the following questions:

  1. Does the model make sense? Are there common (or even uncommon) file upload patterns that would be difficult to implement with the current design?
  2. Is the DSL sensible? Should I choose different terminology for any of the API?
  3. (And because I’m arguably somewhat out of my depth as a web dev here) Are there especially serious security/performance issues I should consider before moving forward?

Thanks!

Most Liked

tfwright

tfwright

I was asked a question via DM about handling file cleanup that prompted me to add a section to the capsule_ecto README (it applies equally well without Ecto, although the need might not be quite as obvious):

tfwright

tfwright

And we’re back with a new feature: uploaders!

Most file upload libraries come with something like this built in but I put off adding it since they come with a disproportionate amount of complexity and I wanted to see if they were really necessary. Well after dealing with several bugs related to slight variations in metadata/file locations, I found that I was adding something very like uploaders to the main live project I am using Capsule in, and felt the time had come to abstract some of that back out into the library.

Check the README for details, but the concept should be familiar to anyone who has used general purpose uploading libraries before (especially Shrine, which Capsule is loosely based on). Essentially it provides an api for linking specific file storages to sets of options (including file locations) and metadata. For example, if you want to store the mime type every time a file is stored somewhere new (and possibly converted in between), you can implement the build_metadata function on an uploader and use uploader.store(some_upload, :your_storage) and Capsule will take care of it for you.

Some notable design choices:

  • Uploaders are not linked with any “type” of file being uploaded by default, only storages. Although generally speaking different files will need different metadata/locations and thus separate uploaders, Capsule doesn’t impose that (and so allows you to even use a single uploader to generate different sets of metadata using options if that’s what you prefer).
  • Uploaders do not provide any built in utilities for implementing file processing (type conversion, mime extraction etc) but they are certainly a convenient place to organize that logic if you choose using Plain Old Elixir Functions.
  • Although uploaders allow you to set aliases for storages (“cache” meaning Disk, for example), they don’t make any assumptions about the order or priority of storages. Feel free to have as many “permanent” storage locations as you want, and by matching on the alias you can modify metadata and other options when copying the file from one storage to the next.
  • As usual nothing is deleted automatically.

The other change is additional validation for the Locator initializer, as well as a bang version. This helps prevent
accidentally persisting invalid file data, especially when used in conjunction with the new Ecto helper release:

This is a small change but it has major repercussions: storing a plain map will no longer work. This encourages the use of the initializer to ensure that a valid Locator is being passed to Ecto. Allowing maps to be dumped was a major design error that I decided was important to correct despite the fact that it may break many existing uses! If you try to update and find that is the case, the easiest way to fix is to simply convert the maps to Capsule.Locator structs. This won’t give you the validation however, so I would encourage moving to using the Locator.new function, which as least checks that all required keys are present. This is especially important if you are handling the initial upload on the client side!

tfwright

tfwright

Kicking off Hacktoberfest with a new release.

Aside from various bugfixes, it contains 2 significant, non-backwards-compatible changes:

  • The Storage.move callback has been removed in favor of copy. If you were using this API you will need to replace it with a combination of copy and delete
  • All built in Storage and Upload implementations have been relocated to a new, separate repo: https://github.com/elixir-capsule/supplement

The latter comes with a new Storage and Upload implementation as well: S3 and Plug.Upload respectively.

I’ll be looking to add more, so if you want to get your t-shirt (or tree) feel free to pitch in :wink:

Where Next?

Popular in Announcing Top

josevalim
Yes, yet another parser combinator library! Most of the parser combinators in the ecosystem are either compile-time, often using AST tra...
159 19103 141
New
seancribbs
Today I released a new dialyzer Mix task as the dialyzex package! At the time we started writing this task, the existing dialyzer integra...
New
mplatts
With HEEX released we decided to start a components library using Tailwind CSS - check it out here: Petal Components. We also have a boi...
New
Crowdhailer
I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention. Assuming you have...
New
mbuhot
Leverage Open Api 3.0 (Swagger) to document, test, validate and explore your Plug and Phoenix APIs. Generate and serve a JSON Open API ...
New
bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
384 13673 119
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
Hal9000
Here is my first stab at this. README pasted below. https://github.com/Hal9000/elixir_random Comments and critiques are welcome. Th...
New
Qqwy
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. Core ideas Type- and function specifications are const...
336 14302 100
New
mattludwigs
Grizzly is a library for working with Z-Wave devices. Z-Wave is a low-frequency radio protocol for controlling smart home devices on a me...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41454 115
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30840 112
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New

We're in Beta

About us Mission Statement