# capsule

Upload and store files in Elixir apps with minimal (currently zero) dependencies.


:warning: Capsule is still in active development, and is not production ready. Accepting file uploads introduces security vulnerabilities. Use at your own risk.

## Not-so-jagged little pill

Capsule intentionally strips file storage logic down to its most composable parts and lets you decide how you want to use them. Here's a complete working example with an Ecto schema, that saves the file onto a local file system and extracts some metadata:

  def create_attachment(url) do
    |>, fn _, _ ->
      Disk.put(URI.parse(url), prefix: :crypto.hash(:md5, [, url]) |> Base.encode16())
    |> Multi.insert(:attachment, fn %{upload: file_data} ->
      Source.changeset(%Attachment{}, %{
        file_data: file_data |> Capsule.add_metadata(%{name:}) |> Map.from_struct(),
    |> Repo.transaction()

Then to access your file:

%Attachment{file_data: file} = attachment

{:ok, iodata} =

## concepts

There are three main concepts in capsule: storage, upload, and the special one, "encapsulation."

### storage

A "storage" is a [behaviour]( that implements the following "file-like" callbacks:

* open
* put
* move
* delete

Currently, capsule only supports the Disk storage. But implementing your own storage is as easy as creating a module that quacks this way.

### upload

Upload is a [protocol]( consisting of the following two functions:

* contents
* name

A storage uses this interface to figure how to extract the file data from a given struct and how to identify it. Currently capsule only implements the upload protocol for the URI module, because URI is a standard lib. The following is the example of how you might implement the protocol for `Plug.Upload`:

defimpl Capsule.Upload, for: Plug.Upload do
  def contents(%{path: path}) do
    case do
      {:error, reason} -> {:error, "Could not read path: #{reason}"}
      success_tuple -> success_tuple

  def name(%{filename: name}), do: name


### encapsulation

Encapsulations are the mediators between storages and uploads. They represent the result of `put`ting an upload into a storage. However, they also implement the upload protocol themselves, which means moving a file from one storage to another is as easy as this:

old_busted_encapsulation = Disk.put(upload)

new_shiny_encapsulation = YourCoolStorage.put(encapsulation)

Note: you'll still need to take care of cleaning up the old file (or pass the work on to some poor async Task):


That's it! Happy uploading.