Files and attachments
Anvil stores uploaded files outside of the web root, checks their type against the content and can scan them for viruses. Files are attached to a record, either as a list of attachments or in a form field.
Attachments
Section titled “Attachments”final class DealResource extends Resource{ protected bool $attachments = true;}The detail page of a record then shows its attachments. Users who may see
the record see and download them; users who may update it (permission
<slug>.update and policy) upload and delete them. Up to 10 files per
upload.
What is accepted
Section titled “What is accepted”| Check | Default (Config\Anvil) |
|---|---|
| Extension and MIME type detected from the content | $fileTypes: PDF, PNG, JPEG, GIF, WebP, TXT, CSV, DOCX, XLSX, PPTX, ODT, ODS |
| Size | $fileMaxSize = 10 MB, see Maximum file size |
| Virus scan | $fileScanner = none; ClamAvScanner::class scans with clamd ($clamAvSocket) |
SVG and HTML are not allowed by default because browsers run their
scripts. A renamed file (e.g. a PDF named .png) is rejected, because the
detected type does not match the extension. If the virus scanner finds
something or cannot be reached, the upload is rejected (fail closed); an
infected file is recorded as file.rejected in the audit log.
public array $fileTypes = [ ..., // keep the defaults you need 'dwg' => ['image/vnd.dwg', 'application/acad'],];public ?string $fileScanner = \Anvil\Files\ClamAvScanner::class;public string $clamAvSocket = 'tcp://127.0.0.1:3310';Each extension lists the MIME types accepted for it, as finfo detects
them from the content.
File names are cleaned (no paths, no control characters, at most 200 characters).
Storage and downloads
Section titled “Storage and downloads”- Files are stored outside of the web root (
WRITEPATH/anvil/files,$filePath) under random keys; the metadata (name, MIME type, size, SHA-256, uploader) is inanvil_filesand audited. Anvil\Files\Storageis the interface of the storage (put(),path(),delete());LocalStorageis the default. Another storage (e.g. S3) or scanner (Anvil\Files\Scanner::isClean()) can be used, see Extending.- Files belong to a resource class and a record: resources with the same slug in different panels never share files. Deleting a record deletes its files (soft deleted records keep them for a restore). Stored files are removed only when the deletion is committed; a rolled back upload leaves no file behind.
- Downloads go through
/{panel}/_file/{uuid}: the file must belong to a record of a resource of this panel that the user may see (tenant,scopeQuery(), policy), otherwise 404. Downloads are rate limited like the endpoint. Files are always sent as downloads withX-Content-Type-Options: nosniffandContent-Security-Policy: sandbox, never shown inline. Every download is written to the audit log (file.downloaded).
Run the migrations of Anvil to create anvil_files.
File fields
Section titled “File fields”A form can hold one file per field, e.g. a contract:
FileUpload::make('contract')->label('Contract (PDF)')->accept(['pdf'])->required()- The file is stored like an attachment (same checks, virus scan and downloads) and belongs to the record and the field; there is no column.
accept()restricts the types to some of$fileTypes.- Forms with a file field are sent as
multipart/form-data. The file is stored after the record, in the same transaction: if it is rejected, the record is not saved and the error is shown at the field. - A new file replaces the old one. Optional fields offer Remove file;
required()fields need a file when created and cannot be emptied. - File fields are not part of the API.
In own code
Section titled “In own code”service('files') (Anvil\Files\FileStore) runs the same checks for own
uploads:
$uuid = service('files')->store($this->request->getFile('scan'), 'contracts', $contractId);$files = service('files')->forOwner('contracts', $contractId);store() throws an EndpointException (422) with the reason at the field
files when the file is rejected. The panel’s download route only serves
files whose owner is a resource of the panel (Resource::fileOwner());
files with an own owner type must be served by your own controller, with
the same checks (permission, tenant, policy) and headers. Tests can upload files with
Anvil\Test\TestUploadedFile, see Testing.