Skip to main content

Production-ready file upload dependency and storage toolkit for FastAPI

Project description

filestore

PyPI version Python versions Package status CI Docs License Ruff Typed

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 Store straight 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 filestore or 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-type header 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 (default True).
  • 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 / AzureStorage are replaced by FileStore(engine=...) with engine instances (LocalEngine, MemoryEngine, S3Engine, GCSEngine, AzureBlobEngine).
  • Backend settings (AWS_BUCKET_NAME, GCP_*, AZURE_*, endpoint_url) moved from Config to engine constructors.
  • Callbacks take a single UploadContext instead of (request, form, field_name, file).
  • FileData, Store, FileField, and StoreConfig are Pydantic models; use model_dump() instead of to_dict().
  • FileData.message was removed (use error / Store.message); FileData.key was added.
  • Store.status is an UploadStatus string enum ("completed", "partial", "failed", "empty") instead of a bool; use if store: / bool(store) for the old boolean meaning.
  • FileStore with no fields now raises ConfigurationError at 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

filestore-2.0.0b1.tar.gz (2.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

filestore-2.0.0b1-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

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

Hashes for filestore-2.0.0b1.tar.gz
Algorithm Hash digest
SHA256 58389895df27c804e92b2884279bd582ccefa30e87c4ffde47f3fc6feb202edf
MD5 1a2b31e277dcdd9f726e4fe9c009f6e6
BLAKE2b-256 5485b76457b719ba0ffbc2d63b56be0c0cdc3bf065e780b12008811e17545b29

See more details on using hashes here.

File details

Details for the file filestore-2.0.0b1-py3-none-any.whl.

File metadata

File hashes

Hashes for filestore-2.0.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 0295053210e07857edcfd6835dfd05a1b7c0990e7b591a41d83bcfb92f0130c5
MD5 2b5fbeca577fd0647dddb69eb28282f1
BLAKE2b-256 bd235bcb2d82d4f8f3d1f76935796a61509ab2a879183300d575729c63e69c25

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page