Skip to content

Checking uploads

The type of a file from its bytes, its size and pixels, safe file names, and signed URLs and upload forms for object storage. In fastapi_alcazaba.uploads.

In the guide: Checking uploads, Object storage. In an app: the example.

fastapi_alcazaba.uploads.FastAPIUploads

FastAPIUploads(types: str | Collection[str], *, max_size: int = 10 * 1024 * 1024, max_pixels: int | None = 40000000, metrics: Metrics | None = None)

Checks uploaded files: their type from their bytes, their size and, for images, their dimensions. Use it as a dependency for a form field named file, or call check():

uploads = FastAPIUploads(types={"application/pdf"}, max_size=20 * 1024 * 1024)

@app.post("/invoices")
async def invoice(upload: Annotated[CheckedFile, Depends(uploads)]) -> Invoice: ...

@app.post("/albums")
async def album(photos: list[UploadFile]) -> Album:
    checked = [await photo_uploads.check(photo) for photo in photos]

The upload is read in chunks and counted, whatever UploadFile.size says. By the time an endpoint runs, the multipart parser has already stored the whole request, so keep its size down with BodySizeLimitMiddleware on the route as well.

It doesn't look inside the files: a PDF can carry scripts, an Office document macros (the macro-enabled types are told apart from the others), ZIP and GZIP archives may expand to far more than their size, and the frames of a GIF aren't counted. HTML, SVG and XML run scripts in a browser: allow them only to serve them from another domain, as attachments.

Parameters:

Name Type Description Default
types str | Collection[str]

the types accepted, from KNOWN_TYPES (IMAGES, OFFICE_DOCUMENTS...).

required
max_size int

the largest file accepted, in bytes; larger ones get a 413.

10 * 1024 * 1024
max_pixels int | None

the most pixels (width × height) an image may have; more gets a 413. None for no limit. Images whose dimensions can't be read get a 415 either way.

40000000
metrics Metrics | None

where to count the outcomes (fastapi_alcazaba.uploads); FastAPICache's by default.

None

check async

check(file: UploadFile) -> CheckedFile

The upload, checked and read back to its start; an HTTPException otherwise: 413 when it's larger than max_size or has more pixels than max_pixels, 415 when its type isn't one of types or it's an image whose dimensions can't be read, 422 when it's empty.

fastapi_alcazaba.uploads.CheckedFile dataclass

CheckedFile(file: UploadFile, content_type: str, size: int, filename: str, width: int | None = None, height: int | None = None)

An upload that passed FastAPIUploads' checks, read back to its start.

extension property

extension: str

The usual extension of its type, with the dot (.png).

fastapi_alcazaba.uploads.sniff

sniff(data: bytes) -> str | None

The type of a file from its bytes, whatever its name or the type the client said; None when it isn't one of KNOWN_TYPES.

The first bytes are enough (a few dozen; 4096 to tell HTML and SVG from other XML), except to tell Office documents from other ZIP archives, which takes the whole file. Text must be UTF-8 (or ASCII) without control characters throughout data, which may end in a cut character. CSV is text: it can't be told apart from other text.

fastapi_alcazaba.uploads.image_size

image_size(data: bytes) -> tuple[int, int] | None

(width, height) of a PNG, JPEG, GIF or WebP image, read from its headers without decoding it; None for other files and when they can't be read. A JPEG may need more than its first bytes: its frame header comes after its metadata.

fastapi_alcazaba.uploads.safe_filename

safe_filename(name: str | None, *, default: str = 'file', max_length: int = 255) -> str

A version of a client's file name that's safe to show, and to use on any file system: NFKC-normalised, only what follows the last / or \, without control or invisible characters (such as the right-to-left override that disguises exe.txt as txt.exe), with <>:"|?* replaced by _, without leading dots, spaces or dashes (hidden files, command-line options) nor trailing dots and spaces (which Windows drops), _ before the names Windows reserves (CON, NUL, COM1...), and shortened to max_length bytes of UTF-8, keeping the extension. default when nothing is left.

It doesn't make the extension match the content: store files under a name of your own with CheckedFile.extension, and keep this one for display or Content-Disposition.

fastapi_alcazaba.uploads.S3Presigner

S3Presigner(*, bucket: str, region: str, access_key: str, secret_key: str | SecretStr, session_token: str | SecretStr | None = None, endpoint: str | None = None, path_style: bool | None = None, service: str = 's3')

Signs URLs and upload forms for one bucket (Signature Version 4, query or POST policy).

Parameters:

Name Type Description Default
bucket str

the bucket's name.

required
region str

its region (us-east-1, auto for R2...).

required
access_key str

the access key's ID, of a user that may only do what the URLs need on this bucket.

required
secret_key str | SecretStr

its secret (a SecretStr from your settings).

required
session_token str | SecretStr | None

the token of temporary credentials (STS), signed into each URL.

None
endpoint str | None

the service's URL, for other than AWS (https://<account>.r2.cloudflarestorage.com, http://localhost:9000 for MinIO); AWS's for the region by default.

None
path_style bool | None

the bucket in the path (endpoint/bucket/key) rather than in the host: what MinIO and most others want; AWS takes virtual hosts.

None
service str

the signing service's name: s3.

's3'

download_url

download_url(key: str, *, expires: int = 300, filename: str | None = None, content_type: str | None = None, now: datetime | None = None) -> str

A URL that GETs the object key for expires seconds. With filename, browsers download it as that file (Content-Disposition: attachment) rather than show it, so an uploaded HTML or SVG never runs on the bucket's origin; content_type overrides its type.

upload_url

upload_url(key: str, *, expires: int = 300, now: datetime | None = None) -> str

A URL that PUTs the object key for expires seconds, whatever its size: prefer upload_form, which bounds it.

upload_form

upload_form(key: str, *, max_size: int, min_size: int = 1, content_type: str | None = None, content_type_prefix: str | None = None, expires: int = 300, now: datetime | None = None) -> UploadForm

A form that POSTs one file as the object key for expires seconds, refused by the service when it's smaller than min_size or larger than max_size bytes, or declares another type than content_type (or one that doesn't start with content_type_prefix, such as "image/"). Overwrites what's under key: give each upload a key of its own.

fastapi_alcazaba.uploads.UploadForm dataclass

UploadForm(url: str, fields: dict[str, str])

What the browser posts the file with: fields first, then the file as file.

Parameters:

Name Type Description Default
url str

where to post it.

required
fields dict[str, str]

the form's fields, the signed policy included.

required