Packages
Files for Phoenix: uploads checked by their bytes, stored under safe keys on the local disk or in a store of your own, and served safely.
Current section
Files
Jump to
Current section
Files
storehouse
README.md
README.md
# Storehouse

Storehouse keeps the files of a Phoenix app: the photos people upload,
the attachments of a document, the images on a canvas, the files an app
makes itself. Each file is checked by its bytes, stored under a new, safe
key, in a store of your choosing, and served so that it can never run a
script on your app's origin.
It is the storage of the Hattori libraries, taken out of them: Kotoba's
attachments and Hakushi's images keep their files with it, and so can
your app.
> **Status: early release.** Checking, storing, the local disk and S3
> (and the stores that speak its API), uploads straight to the store,
> uploads streamed through the app with no temporary file, files in
> Ecto records (temporary until saved, permanent then), signed URLs for
> private files, checksums, telemetry, files from an address, and
> LiveView uploads are here. Before 1.0,
> a minor version may change the API; see the roadmap for what comes
> next.
## Installation
```elixir
def deps do
[
{:storehouse, "~> 0.1"}
]
end
```
Storehouse needs Elixir 1.18 or later and, for `Storehouse.Live`,
Phoenix LiveView 1.1 or later. The rest works in any Plug app.
It gives your app:
* **files checked by their bytes**, not their names: PNG, JPEG, GIF and
WebP images (with their width and height, a phone's photo turned as
its EXIF says), PDF, MP4 and WebM video, ZIP and Office documents, and
plain text. Anything else (HTML, SVG, JavaScript, a page renamed
`.png`) is `application/octet-stream`, a download, or refused;
* **safe keys**: `yyyy/mm/<uuid>-<safe name><extension>`, whose
extension comes from the checked type, so a key never ends in `.html`;
an adapter refuses any key that could leave its root;
* **stores**: the local disk, and S3 with the stores that speak its API
(Cloudflare R2, MinIO, Tigris, LocalStack), with no other library; or
one of your own, in three callbacks;
* **uploads straight to the store**: the browser sends a file to the
bucket at a URL your app signed (its type, its size, ten minutes), so a
large file never goes through your app, and Storehouse checks it there
by its first bytes; in development, to your app's own plug, the same
way;
* **uploads streamed through your app** with no file on its disk: to S3
in the parts of a multipart upload as the chunks come, checked by their
first bytes on the way, two parts in flight from a process of their
own, the upload held back when the bucket is slower than the browser;
* **files in Ecto records**: a column of the file (`Storehouse.Ecto.File`),
no table of files; an upload is temporary until its record is saved,
made permanent then by a copy in the store, and a bucket's lifecycle
rule deletes those of forms never saved; files replaced or removed are
deleted once the database has the change;
* **private files**: signed URLs that expire, from S3 or the local plug,
and `Storehouse.Redirect`, a lasting URL of your app that checks the
person and redirects to a fresh one;
* **serving that is safe**: `Storehouse.Local.Plug` sends the checked
type, `nosniff`, a sandboxing CSP, downloads for every type a browser
should not show, and byte ranges, for video;
* **LiveView uploads, stored**: `Storehouse.Live.consume/3` stores each
finished entry once, cancels those the upload refused, and gives you a
result for each;
* **files from an address**, from the hosts you allow only, with
redirects checked and the download stopped at the size limit: an image
pasted by its address, a file an agent names;
* **limits**: the largest file, the most pixels of an image, the types
you take;
* **checksums**: each file's SHA-256, kept with it, and checked by S3 as
it takes the file;
* **telemetry**: `:telemetry` spans for storing, fetching, signing,
checking and deleting, with what was refused and why;
* **previews**: a function of yours gives a smaller image of a file (a
thumbnail, a PDF's first page), kept with it.
## A sketch
```elixir
# config/runtime.exs
config :storehouse, Storehouse.Local, root: "/var/lib/my_app/uploads"
# lib/my_app_web/endpoint.ex, before the router
plug Storehouse.Local.Plug
```
```elixir
{:ok, file} = Storehouse.store("priv/seeds/palette.png")
file
#=> %Storehouse.File{
# key: "2026/10/0b6…-palette.png",
# url: "/uploads/2026/10/0b6…-palette.png",
# name: "palette.png",
# content_type: "image/png",
# bytes: 48_211,
# width: 1600,
# height: 1000,
# preview: nil
# }
```
In a LiveView:
```elixir
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:photos, [])
|> allow_upload(:photos,
accept: ~w(.png .jpg .jpeg .gif .webp),
max_file_size: 10_000_000,
auto_upload: true,
progress: &handle_progress/3
)}
end
defp handle_progress(:photos, _entry, socket) do
{socket, results} = Storehouse.Live.consume(socket, :photos, accept: Storehouse.Check.images())
{:noreply,
Enum.reduce(results, socket, fn
{_entry, {:ok, file}}, socket -> update(socket, :photos, &[file | &1])
{entry, {:error, reason}}, socket -> put_flash(socket, :error, "#{entry.client_name}: #{inspect(reason)}")
end)}
end
```
From an address:
```elixir
Storehouse.fetch("https://images.example.com/board.png",
hosts: ["images.example.com"],
accept: Storehouse.Check.images(),
max_bytes: 10_000_000
)
```
Straight to the store, with S3 in production and the local disk (or
LocalStack) in development:
```elixir
socket
|> Storehouse.Live.allow(:photos, accept: ~w(.png .jpg .jpeg), max_file_size: 50_000_000, auto_upload: true, progress: &handle_progress/3)
```
```javascript
import {Uploaders} from "storehouse"
let liveSocket = new LiveSocket("/live", Socket, {uploaders: Uploaders, params: {_csrf_token: csrfToken}})
```
A private file:
```elixir
{:ok, url} = Storehouse.signed_url(file.key, expires_in: 300)
```
In a record:
```elixir
schema "users" do
field :avatar, Storehouse.Ecto.File
end
user
|> User.changeset(attrs)
|> Storehouse.Ecto.put_file(:avatar, avatar, accept: Storehouse.Check.images(), max_bytes: 5_000_000)
|> then(&Storehouse.Ecto.update(Repo, &1))
```
The guides: [Stores](guides/stores.md) (the local disk, S3, LocalStack,
development), [Direct uploads](guides/direct_uploads.md) (and streamed
ones), [Ecto](guides/ecto.md), [Private files](guides/private_files.md),
[Serving](guides/serving.md) and [LiveView](guides/liveview.md).
## Roadmap
* **Work after the upload, with no job queue**: versions of an image made
when it lands, previews, analysis, in supervised processes of the app
(Oban, when the work must outlive a deploy).
* **Variants**: images resized and converted on demand, and cached, each
made once however many ask for it at the same time, across a cluster.
* **Previews and analysis**: a PDF's first page, a video's poster, its
length and size, audio; metadata of the app's own, read again later.
* **Live progress beyond the upload**: the versions being made, a video
being read, for everyone who sees the file (PubSub).
* **Cleanup of unused files**: deleted files and abandoned uploads, by a
supervised process, with no cron.
* **Resumable uploads** that survive a reconnect (and the tus protocol),
and multipart uploads straight from the browser, for very large files.
* **Endpoints for other clients**: presign and upload endpoints, for Uppy
and for apps with no LiveView.
* **Mirror**: writing to two stores at once, for migrating between them.
* **Proxy mode**: streaming private bucket files through the app, for a
CDN in front.
* **Azure and native Google Cloud Storage**: Google Cloud Storage works
through its S3-compatible API today.
## Development
```sh
mix deps.get
mix test
mix credo --strict
mix dialyzer
```
## License
MIT. See [LICENSE](https://github.com/hattoriai/storehouse/blob/main/LICENSE).