Skip to content

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.

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.

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.

app/Config/Anvil.php
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).

  • 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 in anvil_files and audited.
  • Anvil\Files\Storage is the interface of the storage (put(), path(), delete()); LocalStorage is 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 with X-Content-Type-Options: nosniff and Content-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.

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.

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.