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

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.0.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.0-py3-none-any.whl (29.0 kB view details)

Uploaded Python 3

File details

Details for the file filestore-2.0.0.tar.gz.

File metadata

  • Download URL: filestore-2.0.0.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.0.tar.gz
Algorithm Hash digest
SHA256 41383bb648611cfc34ee8a3f9caf3d9bc7917e2401798a816e79e8461533137d
MD5 37dc1dc02e2f82c4d28870452d56aa99
BLAKE2b-256 712cc3a3ed6b38266d723f7250556faea7f15771b70f4864218feab079b8cc86

See more details on using hashes here.

File details

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

File metadata

  • Download URL: filestore-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 29.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.5

File hashes

Hashes for filestore-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ff7b6f8a5682fe3fc08eb7954b862d54aa00fca382c74ea3fdba60b44dfe5e94
MD5 2ae1f5300524ad29916f0b59a1e750ea
BLAKE2b-256 a3c8c3c690585fc029a06dc7b5c1da8603443d7f6f92f545eeb307df5fddbf8c

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