Skip to content

Multi-Field Uploads

filestore supports handling multiple upload fields in a single request, each with its own configuration, validation rules — and even its own storage engine.

Defining Multiple Fields

Use FileField to declare each upload field:

from filestore import FileField, FileStore, LocalEngine, StoreConfig

storage = FileStore(
    fields=[
        FileField(
            name="avatar",
            required=True,
            max_count=1,
            config=StoreConfig(
                destination="uploads/avatars",
                allowed_extensions=[".jpg", ".png"],
                max_file_size=2 * 1024 * 1024,
            ),
        ),
        FileField(
            name="documents",
            required=False,
            max_count=5,
            config=StoreConfig(
                destination="uploads/docs",
                allowed_extensions=[".pdf", ".docx"],
                max_file_size=10 * 1024 * 1024,
            ),
        ),
    ],
    engine=LocalEngine(base_dir="uploads"),
)

FileField is a Pydantic model — invalid definitions (empty name, max_count < 1) fail immediately, and duplicate field names raise ConfigurationError when the FileStore is constructed.

Reading Multi-Field Results

@app.post("/profile")
async def update_profile(store: Store = Depends(storage)):
    # Access files by field name
    avatar = store.first("avatar")
    documents = store.files.get("documents", [])

    return {
        "avatar": avatar.filename if avatar else None,
        "documents": [doc.filename for doc in documents if doc.status],
        "status": store.status,
        "errors": store.errors,
    }

Config Inheritance

Per-field config overrides the store-level config key by key. Filters are concatenated (both run):

def global_size_check(ctx):
    return True

def avatar_dimension_check(ctx):
    return True

storage = FileStore(
    fields=[
        FileField(
            name="avatar",
            config={"filters": [avatar_dimension_check]},  # (1)!
        ),
    ],
    config={"filters": [global_size_check]},  # (2)!
)
  1. This filter runs after the global filter.
  2. This filter runs first for all fields.

Both global_size_check and avatar_dimension_check will run for the avatar field. The merge happens once, when the FileStore is constructed.

Max Count Enforcement

When more files are submitted than max_count allows, the extras are rejected:

field = FileField(name="photos", max_count=3)
# If 5 photos are submitted, 3 are processed and 2 are rejected

Rejected files appear in store.failed_files with a clear error message.

Mixed Engines

Different fields can use different storage engines — the field's engine wins over the store's:

from filestore import FileField, FileStore, LocalEngine, MemoryEngine, S3Engine

storage = FileStore(
    fields=[
        FileField(
            name="original",
            engine=S3Engine(bucket="originals"),   # Archive to S3
        ),
        FileField(
            name="thumbnail",
            engine=MemoryEngine(),                 # Keep in memory for processing
        ),
        FileField(name="report"),                  # Uses the store engine below
    ],
    engine=LocalEngine(base_dir="uploads"),
)