Object storage¶
Experimental
New in 0.7: its API may still change in any release, as its use shows what's missing. Scope and stability lists the tools that are settled.
Files in S3, or anything that speaks its API (MinIO, Cloudflare R2, Backblaze B2, Google Cloud
Storage's interoperability...), without passing through the app: S3Presigner signs URLs and
upload forms with AWS's Signature Version 4, and the browser uses them directly. Nothing to
install, and no SDK.
Every class and function: the API reference.
from fastapi_alcazaba.uploads import S3Presigner
s3 = S3Presigner(
bucket="uploads",
region="eu-west-1",
access_key=settings.s3_access_key,
secret_key=settings.s3_secret_key, # a SecretStr
)
The bucket stays private: each URL is for one object, one method, and a few minutes (expires,
300 seconds by default, 7 days at most). Give the access key a user that may only do what the
URLs need on this bucket.
Uploads¶
@app.post("/avatar/upload")
async def avatar_upload(user: CurrentUser) -> UploadForm:
return s3.upload_form(
f"avatars/{user.id}/{uuid4()}",
max_size=2_000_000,
content_type_prefix="image/",
)
The browser posts the file to url, with fields first and the file last, as file:
const body = new FormData();
for (const [name, value] of Object.entries(form.fields)) body.append(name, value);
body.append("Content-Type", file.type);
body.append("file", file);
await fetch(form.url, { method: "POST", body });
- Bounded by the service: a file smaller than
min_size(1 byte) or larger thanmax_sizeis refused by S3, as is one that declares another type thancontent_type, or one not starting withcontent_type_prefix.upload_urlsigns a PUT instead, of any size: prefer the form. - A key of its own for each upload: a form overwrites what's under its key.
- Checked before use: the form bounds the size and the declared type, not what the file is. Read it back from the bucket and check it before using it, or serving it.
Downloads¶
@app.get("/invoices/{invoice_id}")
async def invoice(invoice_id: int, user: CurrentUser) -> RedirectResponse:
invoice = await db.invoices.get(invoice_id, owner=user.id) # yours to authorize
url = s3.download_url(f"invoices/{invoice.id}.pdf", filename="invoice.pdf", expires=60)
return RedirectResponse(url, status_code=303)
With filename, browsers download the object as that file (Content-Disposition: attachment)
rather than show it, so an uploaded HTML or SVG never runs on the bucket's origin.
content_type overrides the type it's served with.
Keys and services¶
- Object keys are 1 to 1024 bytes, without a leading
/, an empty,.or..segment, or control characters: a key made from user input can't escape its prefix. - Other services:
endpointis their URL (https://<account>.r2.cloudflarestorage.com,http://localhost:9000for MinIO), and the bucket goes in the path: what most of them want. With AWS, the bucket is in the host (path_style=False). Plainhttpis only forlocalhost. - Temporary credentials (STS, an instance's role):
session_tokenis signed into each URL. The URLs stop working when the credentials expire, whatever theirexpires.