Skip to content
Ichinga-SamuelPublic

About

File Storage Dependency for FastAPI

Resources

Stars

6 stars

Watchers

0 watching

Forks

Repository files navigation

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

About

File Storage Dependency for FastAPI

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages