Configuration
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 key | Env | Default | Meaning |
|---|---|---|---|
disk | FILES_DISK | assets | File manager upload disk (assets local or s3 for S3/MinIO/R2). |
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. |
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.
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, 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 configure which file types are accepted. The File Manager uses a code-level extension denylist (ForbiddenUploadExtension, including svg / html / htm) plus PlainTextSanitizer on name/metadata/tags — see File manager — Upload security. Team chat uploads remain any-type until saved into Files.
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.