Configuration

Markdown

Files configuration

File manager storage and async thresholds are configured in config/files.php and config/filesystems.php. Env keys are documented under Environment variables; this page explains how disks, URLs, and cleanup interact.

Thresholds (config/files.php)

Config keyEnvDefaultMeaning
diskFILES_DISKassetsFile manager upload disk: assets (local), s3 (S3/MinIO/R2), gcs (Google Cloud Storage, 1.4.0), or azure (Azure Blob, 1.4.0).
duplicate_sync_max_bytesFILES_DUPLICATE_SYNC_MAX_BYTES52428800 (50 MB)Single non-folder files at or below this size are duplicated inline. Folders, bulk selections, and larger files are queued.
zip_ttl_minutesFILES_ZIP_TTL_MINUTES60Prepared zip archives under storage/app/zips expire after this many minutes.
zip_max_bytesFILES_ZIP_MAX_BYTES104857600 (100 MB)Maximum zip payload size enforced by the application.
FILES_DISK=assets
FILES_DUPLICATE_SYNC_MAX_BYTES=52428800
FILES_ZIP_TTL_MINUTES=60
FILES_ZIP_MAX_BYTES=104857600

Queue dependency

Queued duplicates and zip builds require a running queue worker (queue:listen / queue:work). See Operations.

Filesystem disks (config/filesystems.php)

DiskDriverRoot / notes
locallocalstorage/app/private — default private storage (FILESYSTEM_DISK defaults to local). serve enabled for authenticated streaming patterns.
publiclocalstorage/app/public — URL {APP_URL}/storage. Visibility public.
assetslocalstorage/app/public/assets — public file-manager bytes. URL uses FILE_PUBLIC_URL_BASE (see below).
private_assetslocalstorage/app/private/assets — effective-private file-manager bytes. Not in storage:link; no public URL.
s3s3Optional cloud disk; scaffolded with standard AWS_* env vars.
gcsgcsGoogle Cloud Storage (1.4.0) via spatie/laravel-google-cloud-storage; GOOGLE_CLOUD_* env vars.
azureazure-storage-blobAzure Blob Storage (1.4.0) via azure-oss/storage-blob-laravel; AZURE_STORAGE_* env vars.

Default disk:

FILESYSTEM_DISK=local
'links' => [
    public_path('storage') => storage_path('app/public'),
],

Create the link once per environment:

php artisan storage:link

Without this link, browsers cannot resolve /storage/... URLs for the public / assets disks. The private_assets disk is intentionally not linked.

Private vs public local bytes

When FILES_DISK=assets (default), Externa maps effective privacy onto the local pair:

Effective accessDiskBrowser URL
publicassets{FILE_PUBLIC_URL_BASE}/storage/assets/{storage_path}
privateprivate_assetsnone — admin download / API /api/v1/files/{id}/content

Visibility changes (updateMetadata / move into a private folder) move bytes between the two disks. One-shot migration for legacy private rows still on assets:

php artisan files:migrate-private-disk

When FILES_DISK=s3, objects stay on S3; private files still omit a static public URL and use auth'd paths (bucket ACL is the operator's responsibility).

FILE_PUBLIC_URL_BASE

At the top of config/filesystems.php:

$filePublicBase = rtrim((string) env('FILE_PUBLIC_URL_BASE', env('APP_URL', 'http://localhost')), '/');

The assets disk URL is:

{FILE_PUBLIC_URL_BASE}/storage/assets
VariableFallbackWhen to set
FILE_PUBLIC_URL_BASEAPP_URLCDN or different public origin for asset URLs while the app still runs on another host.
# Optional — omit to use APP_URL
FILE_PUBLIC_URL_BASE=https://cdn.example.com
APP_URL=https://app.example.com

The public disk URL still uses APP_URL directly ({APP_URL}/storage). Keep both consistent unless you intentionally split origins.

Optional S3 / MinIO

Set the file manager disk with FILES_DISK (default assets). Framework default FILESYSTEM_DISK stays local for private/local attachments.

FILES_DISK=s3
FILESYSTEM_DISK=local

AWS_ACCESS_KEY_ID=externa
AWS_SECRET_ACCESS_KEY=externasecret
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=externa
AWS_ENDPOINT=http://minio:9000
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_URL=http://localhost:9000/externa

Local Compose MinIO: docker compose --profile minio up (Installation — Docker).

Upload, download, where-used, thumbnails/transforms, and zip source reads go through the Storage disk API (S3-safe). Chat/AI temporary attachments default to the local disk; absolutePath() materializes remote objects to a temp file when needed.

Google Cloud Storage (core 1.4.0)

From externa-core 1.4.0 (#188). The driver ships with core (spatie/laravel-google-cloud-storage).

FILES_DISK=gcs
GOOGLE_CLOUD_PROJECT_ID=my-project
GOOGLE_CLOUD_KEY_FILE=/run/secrets/gcs-service-account.json
GOOGLE_CLOUD_STORAGE_BUCKET=externa-files
# GOOGLE_CLOUD_STORAGE_PATH_PREFIX=
# GOOGLE_CLOUD_STORAGE_API_URI=
# GOOGLE_CLOUD_STORAGE_API_ENDPOINT=
# GOOGLE_CLOUD_STORAGE_VISIBILITY=private
EnvDefaultNotes
GOOGLE_CLOUD_PROJECT_IDemptyGCP project id
GOOGLE_CLOUD_KEY_FILEemptyService-account JSON path. Leave empty to use Application Default Credentials / GOOGLE_APPLICATION_CREDENTIALS
GOOGLE_CLOUD_STORAGE_BUCKETemptyBucket name
GOOGLE_CLOUD_STORAGE_PATH_PREFIXemptyOptional key prefix inside the bucket
GOOGLE_CLOUD_STORAGE_API_URIemptyOptional public URI base for object URLs
GOOGLE_CLOUD_STORAGE_API_ENDPOINTemptyOptional API endpoint (emulators)
GOOGLE_CLOUD_STORAGE_VISIBILITYprivateDefault object visibility

Objects are written with Cache-Control: public,max-age=86400.

Azure Blob Storage (core 1.4.0)

From externa-core 1.4.0 (#188). The driver ships with core (azure-oss/storage-blob-laravel).

FILES_DISK=azure
AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=…;AccountKey=…;EndpointSuffix=core.windows.net"
AZURE_STORAGE_CONTAINER=externa
# AZURE_STORAGE_PREFIX=
# AZURE_STORAGE_URL=
EnvDefaultNotes
AZURE_STORAGE_CONNECTION_STRINGemptyShared-key connection string (simplest auth path)
AZURE_STORAGE_CONTAINERexternaContainer name
AZURE_STORAGE_PREFIXemptyOptional blob prefix
AZURE_STORAGE_URLemptyOptional public base URL (CDN / custom domain)

Entra ID / managed identity options are documented by the package.

Admin CSP and remote disks

The admin security headers add the origin of the configured disk's url (AWS_URL for s3, AZURE_STORAGE_URL for azure) to img-src and media-src, so previews from the bucket load. The gcs disk has no url key, so image previews rely on the default img-src https:; if video/audio previews from GCS are blocked, set a full SECURITY_CSP. Files still stored on a previous disk after a FILES_DISK switch also need a SECURITY_CSP override.

Remaining limits (precise)

Still local / ephemeralWhy
Prepared zip archives under storage/app/zipsBuilt on a shared volume between app and Horizon; not sticky to one host bind for user files, but zips themselves are local ephemeral artifacts (TTL + files:cleanup-zips)
Chunk upload scratch on the chosen diskChunks live on FILES_DISK (S3 OK); session metadata is in DB
storage:link for assetsOnly required when FILES_DISK=assets (local public assets)

Multi-container

With FILES_DISK=s3 (or gcs / azure), app replicas do not need a sticky host bind for CMS user files. Still share a storage volume (or accept zip loss) so prepared downloads and local chat temps work across Horizon workers.

Cleanup commands

Scheduled in routes/console.php (see Operations):

CommandSchedulePurpose
files:cleanup-uploadshourlyRemoves expired partial/chunked upload sessions and their chunk storage (CleanupStaleFileUploadsCommand → FileService::cleanupStaleUploads()).
files:cleanup-zipshourlyRemoves prepared zip downloads older than zip_ttl_minutes (CleanupExpiredFileZipsCommand → FileService::cleanupExpiredZips()).
files:migrate-private-diskmanualMoves effective-private local files from assets → private_assets (upgrade / one-shot).

Run manually:

php artisan files:cleanup-uploads
php artisan files:cleanup-zips
php artisan files:migrate-private-disk

Upload policy (app code, not env)

Thresholds above do not alone configure which file types are accepted. From core 1.3.0, uploads use an extension denylist (ForbiddenUploadExtension) plus a MIME allowlist + magic-byte check (MimeTypeValidator, config/files.php). From 1.4.0, Settings → Files & Media can override the MIME/extension allowlist and opt into SVG; the hard floor (executables, HTML, SSI) cannot be disabled. Defaults still reject SVG/JSON/XML until opted in. See File manager — Upload security.

Operator-tunable byte caps, upload type policy, and image transform presets live under Settings → Files & Media (/settings/project/files) — Project settings — Files & Media · Image transforms.

JSON and Inertia file-manager endpoints live under /files/* with the can.manage.files middleware. Full route table: Admin API. Data model notes: File tree model.