Files for Phoenix: a file checked by its bytes (Storehouse.Check),
stored under a new, safe key through a store of the app's
(Storehouse.Adapter: the local disk, S3, …), and served safely
(Storehouse.Local.Plug).
{:ok, file} = Storehouse.store("priv/seeds/palette.png")
file.url
#=> "/uploads/2026/10/0b6…-palette.png"store/2 gives a Storehouse.File, which the app keeps (its key, URL,
name, type, size, and an image's width and height). A LiveView upload
goes the same way, with Storehouse.Live.consume/3; a file at an
address of a host the app allows, with fetch/2.
The store
config :storehouse, adapter: Storehouse.Local
config :storehouse, Storehouse.Local, root: "/var/lib/my_app/uploads"Storehouse.Local (the default) keeps the files on the local disk; an
app writes its own adapter for S3 or another store (see
Storehouse.Adapter). Each call takes adapter: too.
Keys
A key is a relative path with segments of ASCII letters, digits, .,
- and _, separated by /. A segment is never . or ... key/2
makes a new key in the form yyyy/mm/<uuid>-<safe name><extension>.
The extension comes from the checked content type
(Storehouse.Check.extension/1), never from the client's file name, so
a key never ends in .html or .svg. An adapter refuses a key that
valid_key?/1 refuses.
Telemetry
Storehouse emits :telemetry spans, each a :start, then a :stop
(or an :exception) event, with the :duration of the work:
[:storehouse, :store, ...]-store/2, with:nameand:adapter;[:storehouse, :fetch, ...]-fetch/2, with:url(without its query) and:adapter(itsstore/2has a span of its own);[:storehouse, :presign_upload, ...]-presign_upload/4, with:name,:content_type,:bytesand:adapter;[:storehouse, :verify_upload, ...]-verify_upload/2, with:keyand:adapter;[:storehouse, :delete, ...]-delete/2, with:keyand:adapter;[:storehouse, :promote, ...]-promote/2, with:key(the temporary one) and:adapter.
The :stop event's metadata has the outcome: result: :ok with the
:file (or the :key), or result: :error with the :reason (a file
refused: :not_accepted, :too_large, :not_allowed; a store's
error). For example, to count the files refused:
:telemetry.attach("storehouse-refused", [:storehouse, :store, :stop], &MyApp.Metrics.file/4, nil)
Summary
Functions
Returns the configured adapter (config :storehouse, adapter:), by
default Storehouse.Local.
The prefix of the keys of temporary files (cache: true): "cache/",
or config :storehouse, cache_prefix: "tmp/". A bucket's lifecycle rule
deletes what stays there (see the Ecto guide).
Whether a file (or a key) is a temporary one, under cache_prefix/0.
The SHA-256 of the file at path, in Base64: a Storehouse.File's
checksum, to compare with a copy's.
Returns a file name for display: control and format characters (bidi
controls included) removed, at most 200 characters, and
"file" when nothing is left.
Deletes the file with key, through the adapter (:adapter, or adapter/0).
Downloads the file at url, from a host of :hosts, then checks and
stores it as store/2 does: an image pasted by its address, a file an
agent names.
Whether fetch/2 takes url with hosts (https only).
Returns a new key for a file: yyyy/mm/<uuid>-<safe name><extension>,
with the year and month of now (UTC), a random UUID, and the extension
of the checked content type (see safe_name/2).
Signs an upload that a browser makes straight to the store, without the
file going through the app: of the file name, of the claimed type
claim and of bytes bytes exactly. Gives the file's new key and the
request to make (method, url, headers). Storehouse.Live.allow/3
does this for a LiveView upload.
Makes a temporary file (cache: true) permanent: copies it, in the
store, to its key without the temporary prefix, deletes the temporary
one, and gives the file at its new key. A file that is not temporary is
given as it is.
Returns a safe file name for a key: the client's name without its
extension, with only ASCII letters, digits, ., - and _, then the
extension of the checked content type. At most 100 characters.
Whether url is safe to show and link to: http or https, or a path
of the app (/uploads/…, not //host).
A URL of a file that is valid for a while, for a file that is not
public: a bucket that is private, a local store whose plug serves only
signed URLs (private: true). Keep the key, not this URL: it expires.
Storehouse.Redirect gives a lasting URL of your app that redirects to
a fresh one.
Checks the file at path and stores it under a new key, through the
adapter. The file at path is left where it is.
Returns the URL of the file with key, through the adapter (:adapter, or adapter/0).
Returns true when key is a valid storage key (see the module doc).
Checks a file a browser uploaded straight to the store
(presign_upload/4), by its first bytes, and gives it as store/2
does. A file whose bytes are not of the type it was signed for (a page
sent as a PNG), of a type :accept refuses, or over a limit, is
deleted, and gives {:error, :not_accepted} or {:error, :too_large}.
Functions
@spec adapter() :: module()
Returns the configured adapter (config :storehouse, adapter:), by
default Storehouse.Local.
@spec cache_prefix() :: String.t()
The prefix of the keys of temporary files (cache: true): "cache/",
or config :storehouse, cache_prefix: "tmp/". A bucket's lifecycle rule
deletes what stays there (see the Ecto guide).
@spec cached?(Storehouse.File.t() | Storehouse.Adapter.key()) :: boolean()
Whether a file (or a key) is a temporary one, under cache_prefix/0.
The SHA-256 of the file at path, in Base64: a Storehouse.File's
checksum, to compare with a copy's.
Returns a file name for display: control and format characters (bidi
controls included) removed, at most 200 characters, and
"file" when nothing is left.
Examples
iex> Storehouse.clean_name("cat\u0000.png")
"cat.png"
iex> Storehouse.clean_name("a\u202Egnp.exe")
"agnp.exe"
@spec delete(Storehouse.Adapter.key(), keyword()) :: :ok | {:error, term()}
Deletes the file with key, through the adapter (:adapter, or adapter/0).
@spec fetch(String.t(), keyword()) :: {:ok, Storehouse.File.t()} | {:error, term()}
Downloads the file at url, from a host of :hosts, then checks and
stores it as store/2 does: an image pasted by its address, a file an
agent names.
Only https addresses (and http, with http: true) of the hosts the
app allows are fetched, and a redirect only to one of them: the server
never fetches an address a person or an agent chose freely, which could
be one of the app's own network. A host is a name ("images.example.com")
or a name with a leading *. for its subdomains ("*.example.com").
Options
:hosts- the hosts it fetches from (required).:http- whetherhttpaddresses are fetched. The default isfalse.:timeout- in milliseconds, to connect and for the answer. The default is 10 seconds.- the options of
store/2;:max_bytesstops a download as soon as it is larger.
Whether fetch/2 takes url with hosts (https only).
Examples
iex> Storehouse.fetchable?("https://images.example.com/a.png", ["*.example.com"])
true
iex> Storehouse.fetchable?("https://example.com.evil.org/a.png", ["*.example.com"])
false
@spec key(String.t(), String.t()) :: Storehouse.Adapter.key()
Returns a new key for a file: yyyy/mm/<uuid>-<safe name><extension>,
with the year and month of now (UTC), a random UUID, and the extension
of the checked content type (see safe_name/2).
Examples
iex> key = Storehouse.key("My cat (1).png", "image/png")
iex> [_year, _month, file] = String.split(key, "/")
iex> String.slice(file, 37..-1//1)
"My_cat_1_.png"
iex> "evil.svg" |> Storehouse.key("application/octet-stream") |> Path.extname()
".bin"
@spec presign_upload(String.t(), String.t() | nil, non_neg_integer(), keyword()) :: {:ok, %{ key: Storehouse.Adapter.key(), method: String.t(), url: String.t(), headers: map() }} | {:error, term()}
Signs an upload that a browser makes straight to the store, without the
file going through the app: of the file name, of the claimed type
claim and of bytes bytes exactly. Gives the file's new key and the
request to make (method, url, headers). Storehouse.Live.allow/3
does this for a LiveView upload.
The key's extension is the claimed type's, as the checked type it would
be (Storehouse.Check.claimed/1). When the file is there, check it
with verify_upload/2: until then, it is what the browser said it is.
Options
:accept- the types to take: a claim of another gives{:error, :not_accepted}at once.:max_bytes- a larger file gives{:error, :too_large}at once.:expires_in- how long the browser has to start the upload, in seconds. The default is 600.:adapterand:key, as forstore/2. The adapter must sign uploads (Storehouse.Adapter.presign_upload/3):Storehouse.LocalandStorehouse.S3do; another gives{:error, :not_supported}.
@spec promote(Storehouse.File.t(), keyword()) :: {:ok, Storehouse.File.t()} | {:error, term()}
Makes a temporary file (cache: true) permanent: copies it, in the
store, to its key without the temporary prefix, deletes the temporary
one, and gives the file at its new key. A file that is not temporary is
given as it is.
It can run again for the same file: when the temporary file is gone and the permanent one is there (a save that rolled back after it, and was tried again), it gives the permanent one.
Options
:adapter- as forstore/2. The adapter copies files in the store (Storehouse.Adapter.copy/2):Storehouse.LocalandStorehouse.S3do; another gives{:error, :not_supported}.:keep_cached-truekeeps the temporary file. The default isfalse.
Returns a safe file name for a key: the client's name without its
extension, with only ASCII letters, digits, ., - and _, then the
extension of the checked content type. At most 100 characters.
Every run of other characters becomes one _, and a run of dots becomes
one dot. A name with nothing left is "file".
Examples
iex> Storehouse.safe_name("résumé 2026.pdf", "application/pdf")
"r_sum_2026.pdf"
iex> Storehouse.safe_name("photo.html", "image/png")
"photo.png"
iex> Storehouse.safe_name("../../etc/passwd", "application/octet-stream")
"._._etc_passwd.bin"
iex> Storehouse.safe_name("", "text/plain")
"file.txt"
Whether url is safe to show and link to: http or https, or a path
of the app (/uploads/…, not //host).
Examples
iex> Storehouse.safe_url?("/uploads/a.png")
true
iex> Storehouse.safe_url?("javascript:alert(1)")
false
iex> Storehouse.safe_url?("//evil.example/a.png")
false
@spec signed_url(Storehouse.Adapter.key() | Storehouse.File.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
A URL of a file that is valid for a while, for a file that is not
public: a bucket that is private, a local store whose plug serves only
signed URLs (private: true). Keep the key, not this URL: it expires.
Storehouse.Redirect gives a lasting URL of your app that redirects to
a fresh one.
Options
:expires_in- in seconds. The default is 300.:disposition-"inline"or"attachment"(a download).:filename- the name of the download.:adapter- as forstore/2. The adapter must sign URLs (Storehouse.Adapter.signed_url/2):Storehouse.LocalandStorehouse.S3do; another gives{:error, :not_supported}.
@spec store(Path.t(), keyword()) :: {:ok, Storehouse.File.t()} | {:error, term()}
Checks the file at path and stores it under a new key, through the
adapter. The file at path is left where it is.
The file's type is the one its bytes prove (Storehouse.Check): a file
that is not of a passive type is stored as application/octet-stream,
with a key that ends in .bin, unless :accept refuses it. When the
adapter stores the file but gives a URL that is not safe to show
(safe_url?/1), the stored file is deleted.
Options
:name- the file's name, for its key and for display. The default is the base name ofpath.:content_type- the claimed type, as a browser sends it. The default is the type of the name's extension. Only the bytes make an image an image.:accept- the checked types to take, a list; another gives{:error, :not_accepted}. The default takes every one.:max_bytes,:max_pixels- asStorehouse.Check.describe/3's.:adapter- the store. The default isadapter/0.:key- a function of the name and the checked content type that gives the key. The default is&key/2.:cache-truestores the file under the temporary prefix (cache_prefix/0), untilpromote/2makes it permanent: an upload of a form that may never be saved.:preview- a function of the stored file andpaththat gives the URL of a smaller image of it (a thumbnail, a PDF's first page, a video's poster), ornil. It runs whilepathis still there; the app renders and stores the image. A result that is not a safe URL, or an exception, gives none and logs a warning.
@spec url(Storehouse.Adapter.key(), keyword()) :: String.t()
Returns the URL of the file with key, through the adapter (:adapter, or adapter/0).
Returns true when key is a valid storage key (see the module doc).
Examples
iex> Storehouse.valid_key?("2026/10/a-cat.png")
true
iex> Storehouse.valid_key?("2026/../../etc/passwd")
false
iex> Storehouse.valid_key?("/etc/passwd")
false
@spec verify_upload(Storehouse.Adapter.key(), keyword()) :: {:ok, Storehouse.File.t()} | {:error, term()}
Checks a file a browser uploaded straight to the store
(presign_upload/4), by its first bytes, and gives it as store/2
does. A file whose bytes are not of the type it was signed for (a page
sent as a PNG), of a type :accept refuses, or over a limit, is
deleted, and gives {:error, :not_accepted} or {:error, :too_large}.