Direct-to-Cloud Uploads¶
For large files, don't proxy the bytes through your API — presign an upload URL and let the client talk to S3 (or GCS/Azure) directly. This example presigns uploads and downloads, tracks keys, and cleans up.
Full Example¶
app.py
import uuid
from pathlib import PurePosixPath
from fastapi import FastAPI, HTTPException
from filestore import NotSupportedError, PresignedURL, S3Engine
from pydantic import BaseModel
app = FastAPI()
engine = S3Engine(bucket="my-uploads", region="us-east-1")
ALLOWED_TYPES = {"image/png", "image/jpeg", "application/pdf"}
class PresignRequest(BaseModel):
filename: str
content_type: str
@app.post("/uploads/presign")
async def presign_upload(body: PresignRequest) -> PresignedURL:
if body.content_type not in ALLOWED_TYPES:
raise HTTPException(422, f"Content type {body.content_type!r} not allowed")
# Never trust the client filename for the key
suffix = PurePosixPath(body.filename).suffix.lower()
key = f"uploads/{uuid.uuid4()}{suffix}"
return await engine.presign_upload(
key,
expires_in=600,
content_type=body.content_type, # locked into the signature
)
@app.get("/uploads/{key:path}/download")
async def presign_download(key: str) -> PresignedURL:
try:
return await engine.presign_download(key, expires_in=300)
except NotSupportedError:
raise HTTPException(400, "This backend does not support presigned downloads")
@app.delete("/uploads/{key:path}")
async def delete_upload(key: str):
await engine.delete(key)
return {"deleted": key}
Client Side¶
// 1. Ask the API for a presigned URL
const presigned = await (
await fetch("/uploads/presign", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({filename: file.name, content_type: file.type}),
})
).json();
// 2. Upload straight to cloud storage — bytes never touch your API
await fetch(presigned.url, {
method: presigned.method, // "PUT"
headers: presigned.headers, // must be sent verbatim
body: file,
});
// 3. Tell your API the upload finished (verify + persist presigned.key)
Key Points¶
- Server generates the key — a UUID plus the original extension; the client filename is never trusted
- Content type is signed — the client can't upload a different type than it declared
- Short expiries — 10 minutes for uploads, 5 for downloads
- Works identically with
GCSEngine(service-account credentials) andAzureBlobEngine(connection-string auth) - Swap in
endpoint_url="http://localhost:9000"onS3Engineto develop against MinIO