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;[:storehouse, :head, ...],[:storehouse, :read, ...]and[:storehouse, :download, ...]-head/2,read/2anddownload/3, with:keyand:adapter;[:storehouse, :stream, ...]-stream/2, with:keyand:adapter: the stream's opening, not its reading.
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
Gives up an upload (presign_multipart/4): its parts are deleted from the store.
Returns the configured adapter (config :storehouse, adapter:), by
default Storehouse.Local.
Runs the analysis steps of a file at once (Storehouse.Analyzer), each
one's analysis kept in the file's :analysis: {:ok, file}.
A step that fails keeps why, and leaves the others' results;
{:error, reason} is for a file that could not be read for them.
Runs the analysis steps of a file out of the request, then gives the
file with their results to the app's :then, to save them:
Storehouse.Ecto.analyze_later/5 saves them in a record's column.
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.
A SHA-256 as checksum/1 gives it (Base64), from hex or Base64, as a
browser may send it: {:ok, checksum}, {:ok, nil} for none, or
{:error, :invalid_checksum}.
Returns a file name for display: control and format characters (bidi
controls included) removed, at most 200 characters, and
"file" when nothing is left.
Puts an upload's parts together (presign_multipart/4), then checks the
file as verify_upload/2 does (its type by its first bytes, its size,
the owner's room, the same file again) and gives it: {:ok, file},
{:existing, file}, or an error, the file deleted.
Copies the file with key from one store to another, under the same
key: to move to another provider, keep a second copy, or bring a
production account's files into development.
Deletes the file with key, through the adapter (:adapter, or
adapter/0), and its stored variants (Storehouse.Variants), where the
store lists files.
Deletes the file with key later, and again until the store answers:
a file replaced or removed, whose record is already saved. The deletion
is a job of the app's runner (Storehouse.Runner), tried again when the
store fails.
Deletes every file whose key starts with prefix (see list/2): an
account's files when it goes, every version of a file. A thousand keys
at a time where the store deletes many at once
(Storehouse.Adapter.delete_many/1), one at a time where it does not.
Gives {:ok, count}, the number of files deleted.
As delete_later/2, for every file whose key starts with prefix
(delete_prefix/2): an account's files when it goes. Gives :ok, or
{:error, :invalid_prefix}.
Writes the file with key to path, a chunk at a time (stream/2):
for a tool that wants a file on the disk (libvips, ffmpeg, a zip).
path is replaced; on an error, nothing is left there.
When a file (or a key) expires (expiring_key/2), or nil for one that
does not.
The key of a file that expires in expires_in seconds: under
expiring_prefix/0, then the days it is kept (rounded up, for a
bucket's lifecycle rule of that many days), then when it expires, in
Unix seconds, then key.
The prefix of the keys of expiring files (:expires_in): "expiring/",
or config :storehouse, expiring_prefix: "expires/".
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).
What the store knows of the file with key, without reading it: its
:bytes, its :content_type as stored (or nil), and its :checksum
(or nil). A missing file gives {:error, :enoent}.
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).
The files whose keys start with prefix, as a stream of maps with their
:key, :bytes and :modified_at (a DateTime), in the order of their
keys: an account's files, the temporary ones, those of a file's
versions. The store is asked a page at a time, as the stream is taken,
and a store's error then raises Storehouse.ReadError. A key the store
has that valid_key?/1 refuses is left out.
The parts the store has of an upload (presign_multipart/4), by number,
with their :bytes and :checksum: what a browser that was reloaded
need not send again.
The key key would have under owner's prefix: "<owner>/<key>", as
store/2 and presign_upload/4 make it with :owner.
Whether key is one of owner's files: under its prefix, as :owner
puts it, temporary (cache: true) or permanent. Check a key a browser
sends back before you take it, so that no one claims another's file.
verify_upload/2 and Storehouse.Ecto.put_file/4 check it with
:owner.
Starts an upload of a very large file that the browser sends straight to
the store in parts, a few at a time, each tried again on its own: a
video, an archive to import, past what one PUT sends well (and S3's
5 GB). Checked as presign_upload/4 checks one (its type, size, the
owner's room, the same file again), and given the parts' size
Signs the browser's PUTs of parts of an upload (presign_multipart/4):
each a part number, or {number, checksum} with the part's SHA-256 in
Base64, which the store then checks. Gives {:ok, [%{number: n, method: "PUT", url: …, headers: %{…}}]}, at most 1,000 at a time; a number
outside 1 to 10,000 gives {:error, :invalid_part}.
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.
The bytes of the file with key, all at once: to work on a file the
app keeps (read its metadata, make a smaller image of it, send it on).
For a large file, stream/2 or download/3.
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.
The file with key as a stream of its bytes, :chunk_bytes at a
time, each read from the store as the stream is taken: a large file
sent on, into a zip, through the app, with no more of it in memory than
a chunk.
Returns the URL of the file with key, through the adapter (:adapter, or adapter/0).
What owner's files take in the store, counted from the store itself
(list/2): {:ok, %{bytes: bytes, files: count}}. A store of many files
lists them all: an app that keeps its files in records sums them there,
and this checks it.
Returns true when key is a valid storage key (see the module doc).
Whether prefix is a key or the start of one, for list/2 and
delete_prefix/2: never empty, never "/".
file's variant of a preset, found or made once, however many ask for
it: {:ok, %Storehouse.File{}}. See Storehouse.Variants.get/3.
The URL of file's variant of a preset: a thumbnail, one size of a
srcset. See Storehouse.Variants.url/3.
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
Gives up an upload (presign_multipart/4): its parts are deleted from the store.
@spec adapter() :: module()
Returns the configured adapter (config :storehouse, adapter:), by
default Storehouse.Local.
@spec analyze(Storehouse.File.t(), [Storehouse.Analyzer.step()], keyword()) :: {:ok, Storehouse.File.t()} | {:error, term()}
Runs the analysis steps of a file at once (Storehouse.Analyzer), each
one's analysis kept in the file's :analysis: {:ok, file}.
A step that fails keeps why, and leaves the others' results;
{:error, reason} is for a file that could not be read for them.
Options
:force- runs the steps whose result the file has already. The default isfalse.:max_bytes- the largest file to read for them (download/3).:adapter- as forstore/2.
@spec analyze_later(Storehouse.File.t(), [Storehouse.Analyzer.step()], keyword()) :: :ok | {:error, term()}
Runs the analysis steps of a file out of the request, then gives the
file with their results to the app's :then, to save them:
Storehouse.Ecto.analyze_later/5 saves them in a record's column.
Storehouse.analyze_later(file, [MyApp.Analyzers.Pages],
then: {MyApp.Documents, :analyzed, [document.id]}
):then is {module, function, args}, called with the file first, then
args. The work is a job of the app's runner (Storehouse.Runner):
Oban's when the app runs Oban, else a process of the app's. A file that
cannot be read for the steps gives the function nothing, and the job is
tried again.
Options
:then- the function (required).:runner- the runner, in place of the app's (Storehouse.Runner).:force,:max_bytesand:adapter- as foranalyze/3.
@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.
A SHA-256 as checksum/1 gives it (Base64), from hex or Base64, as a
browser may send it: {:ok, checksum}, {:ok, nil} for none, or
{:error, :invalid_checksum}.
Examples
iex> Storehouse.claimed_checksum("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
{:ok, "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="}
iex> Storehouse.claimed_checksum("not one")
{:error, :invalid_checksum}
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 complete_multipart(String.t(), String.t(), non_neg_integer(), keyword()) :: {:ok, Storehouse.File.t()} | {:existing, Storehouse.File.t()} | {:error, term()}
Puts an upload's parts together (presign_multipart/4), then checks the
file as verify_upload/2 does (its type by its first bytes, its size,
the owner's room, the same file again) and gives it: {:ok, file},
{:existing, file}, or an error, the file deleted.
The parts are the store's, never the browser's word: every part from 1
up, and together exactly bytes, or it gives {:error, {:missing_parts, numbers}} or {:error, {:wrong_size, bytes_the_store_has}}, and the
upload stays, for the browser to send what is missing. bytes past
:max_bytes, or more than S3 takes, gives {:error, :too_large} before
the store is asked.
Takes the options of verify_upload/2.
@spec copy(Storehouse.Adapter.key(), keyword()) :: {:ok, :copied | :skipped} | {:error, term()}
Copies the file with key from one store to another, under the same
key: to move to another provider, keep a second copy, or bring a
production account's files into development.
Storehouse.copy("2026/10/0b6…-board.png", from: MyApp.OldMedia, to: MyApp.Media)
#=> {:ok, :copied}A file the destination has already, the same size and, where both
stores keep one, the same checksum, is not copied again: {:ok, :skipped}. Its type stays as it was.
Two buckets the same credentials reach (Storehouse.S3), or two folders
of this disk (Storehouse.Local), copy it themselves, with no byte
through the app. Otherwise it is read a chunk at a time (stream/2)
into the destination (Storehouse.Adapter.stream_open/2, else a
temporary file and Storehouse.Adapter.put/3), and its checksum,
computed on the way, must be the source's, else nothing is kept and
the copy gives {:error, :checksum_mismatch}.
Options
:fromand:to- the stores, two of them. Both must read files back (head/2).:verify- when either store keeps no checksum of the whole file (the local disk; S3, of a file sent in parts), read both to compare a file of the same size before it is skipped. The default isfalse: the same size is the same file.
@spec copy_prefix(String.t(), keyword()) :: {:ok, %{copied: non_neg_integer(), skipped: non_neg_integer(), failed: list()}} | {:error, term()}
Copies every file whose key starts with prefix (see list/2) from one
store to another (copy/2), several at once. Files the destination has
already are skipped, so it can be run again: after an error, or to
catch up with what was written since.
Storehouse.copy_prefix("accounts/42/", from: MyApp.Media, to: MyApp.DevMedia)
#=> {:ok, %{copied: 1_204, skipped: 0, failed: []}}"" copies every file. mix storehouse.copy runs it from the command
line, with its progress.
Options
:from,:toand:verify- as forcopy/2.:max_concurrency- how many files are copied at once. The default is 4.:on_progress- a function of each file's key and result ({:ok, :copied}, …), as each is done.
@spec delete(Storehouse.Adapter.key(), keyword()) :: :ok | {:error, term()}
Deletes the file with key, through the adapter (:adapter, or
adapter/0), and its stored variants (Storehouse.Variants), where the
store lists files.
@spec delete_later(Storehouse.Adapter.key(), keyword()) :: :ok | {:error, term()}
Deletes the file with key later, and again until the store answers:
a file replaced or removed, whose record is already saved. The deletion
is a job of the app's runner (Storehouse.Runner), tried again when the
store fails.
Gives :ok once the job is kept, or {:error, :invalid_key} for a key
valid_key?/1 refuses. Takes :adapter, as delete/2 does, and
:runner.
@spec delete_prefix(String.t(), keyword()) :: {:ok, non_neg_integer()} | {:error, term()}
Deletes every file whose key starts with prefix (see list/2): an
account's files when it goes, every version of a file. A thousand keys
at a time where the store deletes many at once
(Storehouse.Adapter.delete_many/1), one at a time where it does not.
Gives {:ok, count}, the number of files deleted.
The prefix is never empty: "" and "/" give {:error, :invalid_prefix},
as every prefix list/2 refuses does.
Options
:adapter- as forlist/2.
As delete_later/2, for every file whose key starts with prefix
(delete_prefix/2): an account's files when it goes. Gives :ok, or
{:error, :invalid_prefix}.
@spec download(Storehouse.Adapter.key() | Storehouse.File.t(), Path.t(), keyword()) :: :ok | {:error, term()}
Writes the file with key to path, a chunk at a time (stream/2):
for a tool that wants a file on the disk (libvips, ffmpeg, a zip).
path is replaced; on an error, nothing is left there.
Options
:chunk_bytes,:max_bytesand:adapter- as forstream/2.
@spec expires_at(Storehouse.File.t() | Storehouse.Adapter.key()) :: DateTime.t() | nil
When a file (or a key) expires (expiring_key/2), or nil for one that
does not.
iex> Storehouse.expires_at("expiring/1d/1792843200/exports/42.zip")
~U[2026-10-24 12:00:00Z]
iex> Storehouse.expires_at("exports/42.zip")
nil
@spec expiring_key(Storehouse.Adapter.key(), pos_integer(), DateTime.t()) :: Storehouse.Adapter.key()
The key of a file that expires in expires_in seconds: under
expiring_prefix/0, then the days it is kept (rounded up, for a
bucket's lifecycle rule of that many days), then when it expires, in
Unix seconds, then key.
iex> Storehouse.expiring_key("exports/42.zip", 86_400, ~U[2026-10-23 12:00:00Z])
"expiring/1d/1792843200/exports/42.zip"A bucket needs one lifecycle rule a day count it is given, on its
prefix: "expiring/1d/" expires after 1 day, "expiring/7d/" after 7.
See "Files that expire" in the Stores guide.
@spec expiring_prefix() :: String.t()
The prefix of the keys of expiring files (:expires_in): "expiring/",
or config :storehouse, expiring_prefix: "expires/".
@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.
:max_bytes holds for every answer, a redirect's or an error's as much
as the file's, and :timeout for the whole of it: no answer is read
further than either.
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 head(Storehouse.Adapter.key() | Storehouse.File.t(), keyword()) :: {:ok, Storehouse.Adapter.head()} | {:error, term()}
What the store knows of the file with key, without reading it: its
:bytes, its :content_type as stored (or nil), and its :checksum
(or nil). A missing file gives {:error, :enoent}.
The size a browser declares for an upload is a claim: ask the store before reading a file it sent.
Options
:adapter- as forstore/2. The adapter must read files (Storehouse.Adapter.head/1andStorehouse.Adapter.read_range/3):Storehouse.LocalandStorehouse.S3do; another gives{:error, :not_supported}.
@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 list(String.t(), keyword()) :: {:ok, Enumerable.t()} | {:error, term()}
The files whose keys start with prefix, as a stream of maps with their
:key, :bytes and :modified_at (a DateTime), in the order of their
keys: an account's files, the temporary ones, those of a file's
versions. The store is asked a page at a time, as the stream is taken,
and a store's error then raises Storehouse.ReadError. A key the store
has that valid_key?/1 refuses is left out.
A prefix is a key, or the start of one: "cache/", "accounts/42/",
"2026/10/0b6". Another gives {:error, :invalid_prefix}.
Options
:adapter- as forstore/2. The adapter must list files (Storehouse.Adapter.list/1):Storehouse.LocalandStorehouse.S3do; another gives{:error, :not_supported}.
@spec multipart_parts(String.t(), String.t(), keyword()) :: {:ok, [ %{ number: pos_integer(), bytes: non_neg_integer(), checksum: String.t() | nil } ]} | {:error, term()}
The parts the store has of an upload (presign_multipart/4), by number,
with their :bytes and :checksum: what a browser that was reloaded
need not send again.
@spec owner_key(String.t() | non_neg_integer(), Storehouse.Adapter.key()) :: {:ok, Storehouse.Adapter.key()} | {:error, :invalid_owner}
The key key would have under owner's prefix: "<owner>/<key>", as
store/2 and presign_upload/4 make it with :owner.
Examples
iex> Storehouse.owner_key("accounts/42", "2026/10/a.png")
{:ok, "accounts/42/2026/10/a.png"}
iex> Storehouse.owner_key("../42", "a.png")
{:error, :invalid_owner}
@spec owns?(String.t() | non_neg_integer(), Storehouse.Adapter.key()) :: boolean()
Whether key is one of owner's files: under its prefix, as :owner
puts it, temporary (cache: true) or permanent. Check a key a browser
sends back before you take it, so that no one claims another's file.
verify_upload/2 and Storehouse.Ecto.put_file/4 check it with
:owner.
Examples
iex> Storehouse.owns?("accounts/42", "accounts/42/2026/10/a.png")
true
iex> Storehouse.owns?("accounts/42", "accounts/421/2026/10/a.png")
false
iex> Storehouse.owns?(42, "cache/42/2026/10/a.png")
true
iex> Storehouse.owns?("accounts/42", "accounts/42/../7/a.png")
false
@spec presign_multipart(String.t(), String.t() | nil, non_neg_integer(), keyword()) :: {:ok, %{ key: String.t(), upload_id: String.t(), part_bytes: pos_integer(), parts: pos_integer() }} | {:existing, Storehouse.File.t()} | {:error, term()}
Starts an upload of a very large file that the browser sends straight to
the store in parts, a few at a time, each tried again on its own: a
video, an archive to import, past what one PUT sends well (and S3's
5 GB). Checked as presign_upload/4 checks one (its type, size, the
owner's room, the same file again), and given the parts' size:
{:ok, %{key: key, upload_id: id, part_bytes: 16_777_216, parts: 128}} =
Storehouse.presign_multipart("film.mov", "video/quicktime", 2_147_483_648, owner: owner)Then presign_parts/4 signs the parts, multipart_parts/3 says which
the store has (to go on after a reload), and complete_multipart/4
puts them together and checks the file as verify_upload/2 does.
abort_multipart/3 gives it up. Storehouse.Plug.Multipart is the
same as JSON endpoints, and the JavaScript client sends large files
through it.
Options
Those of presign_upload/4, and:
:part_bytes- the size of each part but the last, at least 5 MiB. The default is 16 MiB, or more for a file of more than 10,000 such parts (S3's most).
The adapter must take multipart uploads (Storehouse.Adapter.multipart_start/2):
Storehouse.S3 does; another gives {:error, :not_supported}.
@spec presign_parts( String.t(), String.t(), [pos_integer() | {pos_integer(), String.t()}], keyword() ) :: {:ok, [map()]} | {:error, term()}
Signs the browser's PUTs of parts of an upload (presign_multipart/4):
each a part number, or {number, checksum} with the part's SHA-256 in
Base64, which the store then checks. Gives {:ok, [%{number: n, method: "PUT", url: …, headers: %{…}}]}, at most 1,000 at a time; a number
outside 1 to 10,000 gives {:error, :invalid_part}.
Options
:bytesand:part_bytes- the upload's size and its parts' (aspresign_multipart/4gave them): each URL then takes a part of its size only, and a number past the upload's last part gives{:error, :invalid_part}.Storehouse.Plug.Multipartgives them from the upload's token.:expires_in- how long each URL is valid, in seconds. The default is an hour.:adapter- as forpresign_multipart/4.
@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() }} | {:existing, Storehouse.File.t()} | {: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.:ownerand:room, as forstore/2: the key under the owner's prefix, and a file larger than the room left refused at once, with{:error, :storage_full}.:checksum- the file's SHA-256, as the browser read it (hex, or Base64): the upload is signed with it, so the store takes only those bytes. An invalid one gives{:error, :invalid_checksum}.:existing- as forstore/2, with:checksum: a file the app has already gives{:existing, file}, and nothing is uploaded.:cache_control- as forstore/2: signed, so the browser sends it, and the store keeps it.: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.
@spec read(Storehouse.Adapter.key() | Storehouse.File.t(), keyword()) :: {:ok, binary()} | {:error, term()}
The bytes of the file with key, all at once: to work on a file the
app keeps (read its metadata, make a smaller image of it, send it on).
For a large file, stream/2 or download/3.
Its size is asked first (head/2): a file larger than :max_bytes
gives {:error, {:too_large, bytes}} before a byte of it is read, and
no more than that size is ever read.
Options
:max_bytes- the largest file to read. The default is no limit.:adapter- as forhead/2.
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. An expiring file's URL (expiring_key/2) expires with it, at the latest; one expired already gives{:error, :expired}.: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()} | {:existing, 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.:owner- whose file it is, for an app whose files belong to someone (an account, a workspace): its key is put under the owner's prefix,"<owner>/…"(owner_key/2). A string of key segments ("accounts/42") or an integer.:room- how many bytes the owner may still store: a larger file gives{:error, :storage_full}, and is not stored. The default is no limit.:existing- a function of a checksum (checksum/1's, Base64) that gives the file the app has already with it, ornil: the same file stored again is then that file,{:existing, file}, and nothing is stored. With:owner, a file of another owner is never given back. See "The same file again" in the Stores guide.:analyze- analysis steps (Storehouse.Analyzer) to run once the file is stored, frompath, with no download: the file comes with their results. A step that fails keeps why, and the file is stored either way. For work out of the request,analyze_later/3.:metadata- the file's own (Storehouse.Metadata): a caption, a credit. Metadata that JSON cannot hold gives{:error, {:invalid_metadata, path}}, and nothing is stored.: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.:expires_in- seconds after which the file is deleted: an export, a report, a PDF to download for a week. Its key is an expiring one (expiring_key/2): a bucket's lifecycle rule deletes it, orStorehouse.Sweeperdoes, andsigned_url/2never signs it past then. Not with:cache.:cache_control- theCache-Controlthe store serves the file with, in place of its own (Storehouse.S3's:cache_control).: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 stream(Storehouse.Adapter.key() | Storehouse.File.t(), keyword()) :: {:ok, Enumerable.t()} | {:error, term()}
The file with key as a stream of its bytes, :chunk_bytes at a
time, each read from the store as the stream is taken: a large file
sent on, into a zip, through the app, with no more of it in memory than
a chunk.
Its size is asked at once (head/2), so a missing file gives
{:error, :enoent} here and a file larger than :max_bytes
{:error, {:too_large, bytes}}. A store's error while the stream is
taken raises Storehouse.ReadError.
Options
:chunk_bytes- the size of each read. The default is 4 MiB.:max_bytes- the largest file to stream. The default is no limit.:adapter- as forhead/2.
@spec url(Storehouse.Adapter.key(), keyword()) :: String.t()
Returns the URL of the file with key, through the adapter (:adapter, or adapter/0).
@spec usage(String.t() | non_neg_integer(), keyword()) :: {:ok, %{bytes: non_neg_integer(), files: non_neg_integer()}} | {:error, term()}
What owner's files take in the store, counted from the store itself
(list/2): {:ok, %{bytes: bytes, files: count}}. A store of many files
lists them all: an app that keeps its files in records sums them there,
and this checks it.
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
Whether prefix is a key or the start of one, for list/2 and
delete_prefix/2: never empty, never "/".
Examples
iex> Storehouse.valid_prefix?("accounts/42/")
true
iex> Storehouse.valid_prefix?("2026/10/0b6")
true
iex> Storehouse.valid_prefix?("/")
false
iex> Storehouse.valid_prefix?("../")
false
@spec variant(Storehouse.File.t() | String.t(), atom(), keyword()) :: {:ok, Storehouse.File.t()} | {:error, term()}
file's variant of a preset, found or made once, however many ask for
it: {:ok, %Storehouse.File{}}. See Storehouse.Variants.get/3.
@spec variant_url(Storehouse.File.t() | String.t(), atom(), keyword()) :: String.t()
The URL of file's variant of a preset: a thumbnail, one size of a
srcset. See Storehouse.Variants.url/3.
@spec verify_upload(Storehouse.Adapter.key(), keyword()) :: {:ok, Storehouse.File.t()} | {:existing, 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}.
Options
:name- the file's name, for display. The default is the key's.:metadata- as forstore/2.:owner- the owner the file must belong to (owns?/2): a key under another's prefix gives{:error, :not_owner}, and the file there is left as it is.:room- the bytes the owner may still store, against the size the store gives (not the one the browser declared): a larger file gives{:error, :storage_full}, and is deleted.:existing- as forstore/2, with the checksum the store keeps: a file the app has already (the same file uploaded twice at once) gives{:existing, file}, and the copy just uploaded is deleted.:accept,:max_bytes,:max_pixels,:adapter, as forstore/2.:preview- as forstore/2, withnilfor the path: the file is not here.