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 |
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 ( |
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.
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 ( |
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
( |
None
|
path_style
|
bool | None
|
the bucket in the path ( |
None
|
service
|
str
|
the signing service's name: |
'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 |