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)!
)
- This filter runs after the global filter.
- 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"),
)