Storehouse.S3 (Storehouse v0.2.0)

Copy Markdown View Source

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_token for 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 is false for AWS, true with an :endpoint.
  • :public_url - the base URL of the files for url/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 - the Cache-Control the 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_control takes its place (Storehouse.store/2).
  • :if_none_match - whether an upload straight to the bucket is signed with If-None-Match: *, so the bucket takes it only where no file is: its URL cannot replace a file once it was checked. The default is true; AWS S3, R2 and MinIO take it. Set it to false for 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

__using__(opts)

(macro)

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, ….