A Storehouse.Adapter for Amazon S3, and the stores that speak its API:
Cloudflare R2, MinIO, Tigris, Backblaze B2, DigitalOcean Spaces, Google
Cloud Storage's interoperability. With no dependency: it signs its
requests itself (AWS Signature Version 4).
# config/runtime.exs
config :storehouse, adapter: Storehouse.S3
config :storehouse, Storehouse.S3,
bucket: "my-app-uploads",
region: "eu-west-1",
access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")For another store, its endpoint (whose URLs name the bucket in the
path, path_style, unless told otherwise):
config :storehouse, Storehouse.S3,
bucket: "uploads",
region: "auto",
endpoint: "https://<account>.r2.cloudflarestorage.com",
access_key_id: System.fetch_env!("R2_ACCESS_KEY_ID"),
secret_access_key: System.fetch_env!("R2_SECRET_ACCESS_KEY"),
public_url: "https://files.example.com"Options
:bucket,:region,:access_key_id,:secret_access_key(required), and:session_tokenfor temporary credentials.:endpoint- the store's URL. The default is AWS's,https://s3.<region>.amazonaws.com.:path_style- whether URLs name the bucket in their path (https://endpoint/bucket/key) rather than their host (https://bucket.endpoint/key). The default isfalsefor AWS,truewith an:endpoint.:public_url- the base URL of the files forurl/1: a CDN, a custom domain. The default is the bucket's URL. A bucket that is not public serves its files at signed URLs (Storehouse.signed_url/2).:cache_control- theCache-Controlthe bucket serves each file with, kept with it when it is stored, uploaded from the browser, streamed in or copied in: for a CDN in front. Keys are never reused, so a file never changes:"public, max-age=31536000, immutable". The default is none, the bucket's own. A call's:cache_controltakes its place (Storehouse.store/2).:if_none_match- whether an upload straight to the bucket is signed withIf-None-Match: *, so the bucket takes it only where no file is: its URL cannot replace a file once it was checked. The default istrue; AWS S3, R2 and MinIO take it. Set it tofalsefor a store that refuses the header.
Each file is stored with its checked content type, and with
Content-Disposition: attachment for every type a browser should not
show (Storehouse.Check.inline?/1): the store never serves an
uploaded page as a page.
Uploads straight to the bucket
presign_upload/3 signs a PUT of one file, of its type and its size
exactly, which the browser sends to the bucket without going through
the app (Storehouse.Live.allow/3). The bucket must allow it with CORS:
[{"AllowedOrigins": ["https://my-app.example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["content-type", "content-disposition", "if-none-match"],
"MaxAgeSeconds": 3600}]The URL writes only where no file is (If-None-Match: *): once the
file was checked (Storehouse.verify_upload/2), the same URL cannot
put other bytes in its place.
An upload presigned with its checksum (:checksum) is signed with
x-amz-checksum-sha256 too, and the bucket refuses bytes that do not
have it: add it and x-amz-sdk-checksum-algorithm to AllowedHeaders.
A store with a :cache_control signs it too: add cache-control.
Summary
Functions
Defines a store of its own, with a config of its own: a second bucket beside the first (public and private files, media on R2, files kept in another region).
Functions
Defines a store of its own, with a config of its own: a second bucket beside the first (public and private files, media on R2, files kept in another region).
defmodule MyApp.PrivateFiles, do: use Storehouse.S3
config :storehouse, MyApp.PrivateFiles, bucket: "acme-private", region: "eu-west-1", …The module is a Storehouse.Adapter: give it as adapter: (or a field's
store:, Storehouse.Ecto.File). With otp_app: :my_app, its config is
config :my_app, MyApp.PrivateFiles, ….