Production-ready file upload dependency and storage toolkit for FastAPI
Project description
filestore
filestore is a file upload dependency and storage toolkit for FastAPI with a dependency-based API and production-grade defaults.
- Pluggable storage engines: local disk, in-memory, S3, Google Cloud Storage, Azure Blob Storage
- Presigned upload/download URLs for every cloud backend — let clients talk to storage directly
- Safe local file writes (atomic rename, collision handling, path sanitization)
- Multi-field uploads with per-field configuration and per-field engines
- Sync or async callbacks for filenames, destinations, filters, and metadata
- Validation for size, extension, and content type
- Pydantic result models — return the
Storestraight from your route - Optional cloud SDKs that never break the base install
Read the documentation, follow the 2.0 migration guide, or review the changelog before upgrading.
Installation
2.0 prerelease: This documentation targets the 2.0 API. Until the final 2.0 release is published, opt in with
pip install --pre filestoreor pin the beta explicitly. Stable pip installs ignore prereleases by default.
pip install filestore # base install (local + memory engines)
pip install "filestore[s3]" # Amazon S3 / S3-compatible
pip install "filestore[gcp]" # Google Cloud Storage
pip install "filestore[azure]" # Azure Blob Storage
pip install "filestore[all]" # everything
Quick Start
from fastapi import Depends, FastAPI
from filestore import FileStore, LocalEngine, Store
app = FastAPI()
storage = FileStore(
"file",
required=True,
engine=LocalEngine(base_dir="uploads", base_url="/media"),
)
@app.post("/upload")
async def upload(store: Store = Depends(storage)) -> Store:
return store # Store is a Pydantic model — FastAPI serializes it directly
Core API
Engines are instances
Configure an engine once and share it between stores; credentials and clients are cached on the engine.
from filestore import FileStore, LocalEngine, MemoryEngine, S3Engine
local = LocalEngine(base_dir="uploads", base_url="/media")
s3 = S3Engine(bucket="my-bucket", region="us-east-1")
storage = FileStore("avatar", engine=s3)
Available engines:
| Engine | Extra | Presigned URLs | Delete |
|---|---|---|---|
LocalEngine(base_dir=, base_url=) |
— | ✗ | ✓ |
MemoryEngine() |
— | ✗ | ✗ |
S3Engine(bucket=, region=, endpoint_url=, client=) |
s3 |
✓ | ✓ |
GCSEngine(bucket=, project=, credentials=, endpoint_url=, client=) |
gcp |
✓ | ✓ |
AzureBlobEngine(container=, connection_string=, account_url=, credential=, client=) |
azure |
✓ | ✓ |
Every cloud option also falls back to the usual environment variables
(AWS_BUCKET_NAME, AWS_DEFAULT_REGION, GCP_BUCKET_NAME, GCP_PROJECT,
AZURE_STORAGE_CONTAINER, AZURE_STORAGE_CONNECTION_STRING, AZURE_STORAGE_ACCOUNT_URL).
Single field
storage = FileStore("avatar", count=1, required=True, engine=MemoryEngine())
Multiple fields
Field-level config overrides store-level config; a field can also use its own engine.
from filestore import FileField, FileStore, StoreConfig
storage = FileStore(
fields=[
FileField(name="avatar", required=True, config={"destination": "avatars"}),
FileField(name="resume", max_count=3, engine=S3Engine(bucket="cvs")),
],
engine=local,
config=StoreConfig(max_file_size=10 * 1024 * 1024),
)
Reading results
The dependency returns a Store (a Pydantic model):
store.status # UploadStatus: "completed" | "partial" | "failed" | "empty"
bool(store) # True only when status is "completed"
store.files # dict[str, list[FileData]]
store.flat_files # all files in one list
store.successful_files # only successful files
store.failed_files # only failed files
store.total_files # total count (successful + failed)
store.total_size # sum of sizes for successful files
store.first("avatar") # first FileData for a field, or None
store.message # summary message
store.error # first error, or None
store.errors # all errors
Each FileData carries field_name, filename, original_filename, content_type,
size, path (local), url, key (object key / relative path), status, error,
storage, metadata, and file (raw bytes, memory engine only — excluded from JSON).
Presigned URLs
Cloud engines can mint presigned URLs so clients upload or download directly, without the payload passing through your server:
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, content_type=content_type)
@app.get("/downloads/presign")
async def presign_download(key: str) -> PresignedURL:
return await s3.presign_download(key, expires_in=600)
The client then sends 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});
Notes per backend:
- S3 — works with any credentials; SigV4 is used automatically. Works with MinIO/LocalStack via
endpoint_url. - GCS — V4 signed URLs require credentials with a private key (e.g. a service account).
- Azure — SAS URLs require an account key (connection-string or shared-key auth); upload URLs include the
x-ms-blob-typeheader clients must send.
Engines also support await engine.delete(key) for removing stored objects.
Validation and Callbacks
All callbacks receive a single UploadContext with .request, .form, .field_name, and .file.
They may be sync or async.
Validation
storage = FileStore(
"image",
config={
"allowed_extensions": [".jpg", ".png"],
"allowed_content_types": ["image/jpeg", "image/png"],
"max_file_size": 5 * 1024 * 1024,
},
)
Dynamic destination
async def destination(ctx):
user_id = ctx.request.headers.get("X-User-ID", "anonymous")
return f"uploads/{user_id}"
storage = FileStore("file", config={"destination": destination})
Dynamic filename
The filename callback can return a string/path or an UploadFile whose filename has been updated.
import uuid
from pathlib import Path
def unique_name(ctx):
return f"reports/{uuid.uuid4()}{Path(ctx.file.filename or '').suffix}"
storage = FileStore("report", config={"filename": unique_name})
Filters
Return True to accept, False to reject, or a string to reject with a custom message.
Store-level and field-level filters are concatenated (store first).
async def allow_text(ctx):
return ctx.file.content_type == "text/plain" or "Only plain text files are allowed"
storage = FileStore("notes", engine=MemoryEngine(), config={"filters": [allow_text]})
Metadata
def extra_metadata(ctx):
return {"request_id": ctx.request.headers.get("X-Request-ID")}
storage = FileStore("file", config={"metadata": extra_metadata})
Configuration Reference
StoreConfig (a dict works everywhere a StoreConfig is accepted):
destination: upload directory or cloud key prefix; value or callback.filename: override the stored filename; value or callback.filters: one filter callback or a list.metadata: extra per-file metadata; dict or callback.allowed_extensions: allowlist of extensions (leading dot optional, case-insensitive).allowed_content_types: allowlist of MIME types (case-insensitive).max_file_size/min_file_size: size bounds in bytes.max_files/max_fields/max_part_size: multipart parsing limits (store level).chunk_size: local write chunk size.overwrite: allow overwriting existing objects (local + cloud).sanitize_filename: normalize names and strip unsafe path segments (defaultTrue).base_url: public URL prefix for local files.extra_args: extra keyword arguments passed through to the backend upload call.
Engine-specific settings (buckets, containers, credentials, endpoints) live on the
engine constructor, not in StoreConfig.
Custom Engines
Subclass StorageEngine and implement save(); optionally override delete(),
presign_upload(), and presign_download():
from filestore import FileData, StorageEngine, StoreConfig
from starlette.datastructures import UploadFile
class MyEngine(StorageEngine):
name = "custom"
async def save(self, *, file: UploadFile, filename: str, destination: str | None,
config: StoreConfig, field_name: str = "") -> FileData:
...
return FileData(filename=filename, size=..., key=..., storage=self.name)
Migrating from 1.x
Version 2.0 is a redesign. The main breaking changes are summarized below; the complete migration guide includes side-by-side examples and a release checklist.
LocalStorage/MemoryStorage/S3Storage/GCSStorage/AzureStorageare replaced byFileStore(engine=...)with engine instances (LocalEngine,MemoryEngine,S3Engine,GCSEngine,AzureBlobEngine).- Backend settings (
AWS_BUCKET_NAME,GCP_*,AZURE_*,endpoint_url) moved fromConfigto engine constructors. - Callbacks take a single
UploadContextinstead of(request, form, field_name, file). FileData,Store,FileField, andStoreConfigare Pydantic models; usemodel_dump()instead ofto_dict().FileData.messagewas removed (useerror/Store.message);FileData.keywas added.Store.statusis anUploadStatusstring enum ("completed","partial","failed","empty") instead of a bool; useif store:/bool(store)for the old boolean meaning.FileStorewith no fields now raisesConfigurationErrorat startup instead of failing per request.
Development
uv sync --locked --all-extras --dev
uv run ruff format --check .
uv run ruff check .
uv run coverage run -m pytest
uv run coverage report
Build and validate the package metadata before publishing:
uv build
uv run twine check dist/*
Releases are published from GitHub Releases through the Trusted Publishing workflow in .github/workflows/publish.yml.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file filestore-2.0.0b1.tar.gz.
File metadata
- Download URL: filestore-2.0.0b1.tar.gz
- Upload date:
- Size: 2.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58389895df27c804e92b2884279bd582ccefa30e87c4ffde47f3fc6feb202edf
|
|
| MD5 |
1a2b31e277dcdd9f726e4fe9c009f6e6
|
|
| BLAKE2b-256 |
5485b76457b719ba0ffbc2d63b56be0c0cdc3bf065e780b12008811e17545b29
|
File details
Details for the file filestore-2.0.0b1-py3-none-any.whl.
File metadata
- Download URL: filestore-2.0.0b1-py3-none-any.whl
- Upload date:
- Size: 28.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0295053210e07857edcfd6835dfd05a1b7c0990e7b591a41d83bcfb92f0130c5
|
|
| MD5 |
2b5fbeca577fd0647dddb69eb28282f1
|
|
| BLAKE2b-256 |
bd235bcb2d82d4f8f3d1f76935796a61509ab2a879183300d575729c63e69c25
|