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.
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]" # everythingfrom 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 directlyConfigure 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).
storage = FileStore("avatar", count=1, required=True, engine=MemoryEngine())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),
)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 errorsEach 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).
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.
All callbacks receive a single UploadContext with .request, .form, .field_name, and .file.
They may be sync or async.
storage = FileStore(
"image",
config={
"allowed_extensions": [".jpg", ".png"],
"allowed_content_types": ["image/jpeg", "image/png"],
"max_file_size": 5 * 1024 * 1024,
},
)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})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})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]})def extra_metadata(ctx):
return {"request_id": ctx.request.headers.get("X-Request-ID")}
storage = FileStore("file", config={"metadata": extra_metadata})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.
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)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.
uv sync --locked --all-extras --dev
uv run ruff format --check .
uv run ruff check .
uv run coverage run -m pytest
uv run coverage reportBuild 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.
MIT