Skip to main content

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

Metadata

Release files for filestore 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for filestore 2.0.0
File Size Uploaded Provenance Actions
filestore-2.0.0.tar.gz 2.7 MB Without provenance Details for filestore-2.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for filestore 2.0.0
File Interpreter ABI Platform Actions
filestore-2.0.0-py3-none-any.whl Python 3 none any Details for filestore-2.0.0-py3-none-any.whl

Total release size: 2.8 MB

Release files / filestore-2.0.0.tar.gz

Download URL filestore-2.0.0.tar.gz
Size 2.7 MB
Tags Source
SHA-256 checksum
How to use checksums
41383bb648611cfc34ee8a3f9caf3d9bc7917e2401798a816e79e8461533137d
BLAKE2b-256 checksum
How to use checksums
712cc3a3ed6b38266d723f7250556faea7f15771b70f4864218feab079b8cc86
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.5

Release files / filestore-2.0.0-py3-none-any.whl

Download URL filestore-2.0.0-py3-none-any.whl
Size 29.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ff7b6f8a5682fe3fc08eb7954b862d54aa00fca382c74ea3fdba60b44dfe5e94
BLAKE2b-256 checksum
How to use checksums
a3c8c3c690585fc029a06dc7b5c1da8603443d7f6f92f545eeb307df5fddbf8c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.5

Security for filestore 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page