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).
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"],
"MaxAgeSeconds": 3600}]