Presigned URLs¶
Cloud engines can mint presigned URLs so clients upload or download files directly against S3, GCS, or Azure — the payload never passes through your FastAPI server. This is the recommended pattern for large files.
sequenceDiagram
participant C as Client
participant A as FastAPI App
participant S as Cloud Storage
C->>A: POST /uploads/presign (filename, content type)
A->>A: engine.presign_upload(key)
A-->>C: PresignedURL {url, method, headers}
C->>S: PUT file directly to signed URL
S-->>C: 200 OK
Presigning an Upload¶
from fastapi import FastAPI
from filestore import PresignedURL, S3Engine
app = FastAPI()
s3 = S3Engine(bucket="my-bucket", region="us-east-1")
@app.post("/uploads/presign")
async def presign_upload(filename: str, content_type: str) -> PresignedURL:
return await s3.presign_upload(
f"uploads/{filename}",
expires_in=600, # seconds
content_type=content_type, # locked into the signature
)
The returned PresignedURL is a Pydantic model:
{
"url": "https://my-bucket.s3.us-east-1.amazonaws.com/uploads/a.png?X-Amz-...",
"method": "PUT",
"headers": {"Content-Type": "image/png"},
"fields": {},
"key": "uploads/a.png",
"expires_in": 600,
"storage": "s3"
}
Uploading from the Client¶
Send the file with the returned method, url, and headers:
const presigned = await (
await fetch("/uploads/presign?filename=a.png&content_type=image/png", {method: "POST"})
).json();
await fetch(presigned.url, {
method: presigned.method,
headers: presigned.headers,
body: file,
});
Always send the returned headers
The signature covers the headers. Azure upload URLs include
x-ms-blob-type: BlockBlob, and any Content-Type passed at presign time must be
sent verbatim — otherwise storage rejects the request with a signature mismatch.
Presigning a Download¶
@app.get("/downloads/presign")
async def presign_download(key: str) -> PresignedURL:
return await s3.presign_download(key, expires_in=600)
The client performs a plain GET on the returned URL.
Backend Notes¶
- Works with any boto3 credentials; SigV4 is used automatically.
- Works against MinIO/LocalStack via
endpoint_url. - Signing is local — no network call is made to presign.
- Generates V4 signed URLs.
- Requires credentials with a private key (e.g. a service account JSON). Plain Application Default Credentials from a metadata server cannot sign.
- Generates SAS URLs.
- Requires an account key — construct the engine with a connection string or
shared-key credential. Token credentials (
DefaultAzureCredential) raiseConfigurationErrorwhen presigning. - Upload URLs include the required
x-ms-blob-type: BlockBlobheader.
- Presigning is not applicable; both methods raise
NotSupportedError.
Deleting Objects¶
Engines that persist data also support deletion by key:
await s3.delete("uploads/a.png") # S3
await gcs.delete("uploads/a.png") # GCS
await azure.delete("uploads/a.png") # Azure
await local.delete("a/b.txt") # Local (resolved against base_dir)
FileData.key holds exactly the value to pass here — store it when the upload
completes:
@app.post("/upload")
async def upload(store: Store = Depends(storage)):
saved = store.first("file")
db.save(key=saved.key) # later: await engine.delete(key)
return store
Note
LocalEngine.delete() refuses keys that escape base_dir (path traversal),
and MemoryEngine raises NotSupportedError since nothing is persisted.
Tying It Together¶
A common production flow:
- Client asks your API to presign an upload (
presign_upload) — you decide the key, enforce auth, and record a pending upload. - Client PUTs the file directly to storage.
- Client confirms completion; you verify the object exists and mark it live.
- To serve private files, presign downloads (
presign_download) with short expiries. - To remove files, call
engine.delete(key).