Configuration
MarkdownFiles 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 key | Env | Default | Meaning |
|---|---|---|---|
disk | FILES_DISK | assets | File 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_bytes | FILES_DUPLICATE_SYNC_MAX_BYTES | 52428800 (50 MB) | Single non-folder files at or below this size are duplicated inline. Folders, bulk selections, and larger files are queued. |
zip_ttl_minutes | FILES_ZIP_TTL_MINUTES | 60 | Prepared zip archives under storage/app/zips expire after this many minutes. |
zip_max_bytes | FILES_ZIP_MAX_BYTES | 104857600 (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)
| Disk | Driver | Root / notes |
|---|---|---|
local | local | storage/app/private — default private storage (FILESYSTEM_DISK defaults to local). serve enabled for authenticated streaming patterns. |
public | local | storage/app/public — URL {APP_URL}/storage. Visibility public. |
assets | local | storage/app/public/assets — public file-manager bytes. URL uses FILE_PUBLIC_URL_BASE (see below). |
private_assets | local | storage/app/private/assets — effective-private file-manager bytes. Not in storage:link; no public URL. |
s3 | s3 | Optional cloud disk; scaffolded with standard AWS_* env vars. |
gcs | gcs | Google Cloud Storage (1.4.0) via spatie/laravel-google-cloud-storage; GOOGLE_CLOUD_* env vars. |
azure | azure-storage-blob | Azure Blob Storage (1.4.0) via azure-oss/storage-blob-laravel; AZURE_STORAGE_* env vars. |
Default disk:
FILESYSTEM_DISK=local
Symbolic link
'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 access | Disk | Browser URL |
|---|---|---|
| public | assets | {FILE_PUBLIC_URL_BASE}/storage/assets/{storage_path} |
| private | private_assets | none — 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
| Variable | Fallback | When to set |
|---|---|---|
FILE_PUBLIC_URL_BASE | APP_URL | CDN 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
| Env | Default | Notes |
|---|---|---|
GOOGLE_CLOUD_PROJECT_ID | empty | GCP project id |
GOOGLE_CLOUD_KEY_FILE | empty | Service-account JSON path. Leave empty to use Application Default Credentials / GOOGLE_APPLICATION_CREDENTIALS |
GOOGLE_CLOUD_STORAGE_BUCKET | empty | Bucket name |
GOOGLE_CLOUD_STORAGE_PATH_PREFIX | empty | Optional key prefix inside the bucket |
GOOGLE_CLOUD_STORAGE_API_URI | empty | Optional public URI base for object URLs |
GOOGLE_CLOUD_STORAGE_API_ENDPOINT | empty | Optional API endpoint (emulators) |
GOOGLE_CLOUD_STORAGE_VISIBILITY | private | Default 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=
| Env | Default | Notes |
|---|---|---|
AZURE_STORAGE_CONNECTION_STRING | empty | Shared-key connection string (simplest auth path) |
AZURE_STORAGE_CONTAINER | externa | Container name |
AZURE_STORAGE_PREFIX | empty | Optional blob prefix |
AZURE_STORAGE_URL | empty | Optional 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 / ephemeral | Why |
|---|---|
Prepared zip archives under storage/app/zips | Built 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 disk | Chunks live on FILES_DISK (S3 OK); session metadata is in DB |
storage:link for assets | Only 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):
| Command | Schedule | Purpose |
|---|---|---|
files:cleanup-uploads | hourly | Removes expired partial/chunked upload sessions and their chunk storage (CleanupStaleFileUploadsCommand → FileService::cleanupStaleUploads()). |
files:cleanup-zips | hourly | Removes prepared zip downloads older than zip_ttl_minutes (CleanupExpiredFileZipsCommand → FileService::cleanupExpiredZips()). |
files:migrate-private-disk | manual | Moves 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.
Related HTTP surface
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.