Features

Markdown

File manager

The file manager is the admin UI and JSON API under /files/*. FileController delegates to FileService (and transform/jobs for thumbnails, zip, and duplication). Every request passes through can.manage.files, which maps the route (and bulk action) to a specific file permission.

File manager
Hierarchical file manager

Related

Tree / versions / uploads model: File tree model. Disks & limits: Files & storage. Named presets + focal point: Image transforms. HTTP tables: Admin & files API. Async outcomes land in the notifications bell.

Access & UI

ItemDetail
ControllerApp\Http\Controllers\Admin\FileController
ServiceApp\Services\FileService (plus FileTransformService for thumbnails)
Middleware aliascan.manage.files → App\Http\Middleware\EnsureCanManageFiles
Routesroutes/admin.php (prefix /files)
Frontendresources/js/pages/admin/files/index.tsx
Sidebar gatecan-show-files
UI i18nfiles.* keys in resources/js/locales/{en,it,de}.json (shell locale)

Grid and details panel (1.4.0)

  • File cards (core #592, #596): every card is square, files and folders alike. The preview fills the card edge to edge above a 40px name row (object-cover on the file's focal point); only the selection checkbox, the favourite star and the Private badge sit over it. Non-image files and folders show their type or folder icon centred in the same area.
  • Name row: the name on one line (full name in a tooltip); a tag button, only when the file has tags, that lists them in a hover card on hover or keyboard focus; and a ⋮ button with the same actions as the right-click menu (opening it selects the card).
  • Columns: 2 on phones, then as many as fit at a minimum of 15rem (4 at 1440 px, 6 at 1920 px).
  • Focal point (core #583): for images, the details panel shows the preview; click it to set the focal point (a crosshair marks it), or use the arrow keys (1%, Shift 10%) and Enter. Reset to centre clears it. The point drives the crop of every cover transform — Image transforms — Focal point.
  • Transforms fields removed: the Translate X / Translate Y / Scale fields are gone from the panel because nothing read them. The database columns stay until a later migration.
  • Folders and non-image files show no focal point.

The single Inertia page handles browse, selection, uploads, and bulk actions; many operations also expose JSON endpoints for the same UI. Labels follow the signed-in user’s admin UI locale (en / it / de) — see Account settings — Admin UI locale.

Upload security

Uploads run two gates (from core 1.3.0, #118 / PR #267):

  1. Extension denylist — App\Support\Uploads\ForbiddenUploadExtension (last path extension wins) on direct upload / replace, chunked init / complete (file_name), and rename.
  2. MIME allowlist + magic bytes — App\Support\Uploads\MimeTypeValidator against config/files.php upload_allowed_mimes / upload_magic_signatures (first 16 bytes). Applied on File Manager upload/replace, chunked assembly, Public API POST /api/v1/files, chat→Files save, and AI chat attachments.

Forbidden extensions include executable / script families such as php, phtml, phar, exe, bat, cmd, js, mjs, vbs, asp/aspx, jsp, sh, ps1, dll, so, and related aliases, plus svg / html / htm, and from 1.3.0 SSI / markup extensions shtml / shtm / xml / xhtml / xht / mht / xsl / svgz (core PR #314). Rejection is a validation error (This file type is not allowed.).

Breaking (1.3.0): SVG, JSON, and XML are not on the default MIME allowlist (SVG/HTML/SSI/markup also stay extension-denied). From 1.4.0, operators can widen the allowlist and opt into SVG under Settings → Files & Media — HTML/SSI/executables remain on a non-disableable hard floor (core#327).

Docker nginx: official images sandbox public /storage/ with X-Content-Type-Options: nosniff and a restrictive CSP (sandbox; default-src 'none'; …) so a public-disk asset cannot execute as a document — Deploy with Docker.

Plain-text fields (display name, metadata string values, tags) pass through App\Support\Security\PlainTextSanitizer (trim + strip_tags) before persist — stored XSS mitigation for the Files UI, not a full HTML sanitizer.

Other upload surfaces

Team chat attachments still accept any file type at upload time (Chat). Saving a chat attachment into the Files pool goes through FileService::uploadFile, so both gates (including SVG/HTML deny + MIME allowlist) apply then. AI chat attachments also go through MimeTypeValidator (CSV/TXT/XLSX/PDF remain the practical set) — AI chat.

Public vs private visibility

Each file/folder has an access field (files.access): public, private, or null (inherit from the nearest ancestor). The detail panel Visibility select sets Inherit / Public / Private; folders marked private apply to children unless a child overrides. Admin users always see every file.

Bytes: effective-private files on the local assets family live on the private_assets disk (storage/app/private/assets) — not under storage:link / /storage/assets/.... FileTransformService::publicUrl() returns null for private files; the admin File Manager uses the session-auth GET /files/{file}/download (and thumbnail) path. Public files keep {FILE_PUBLIC_URL_BASE}/storage/assets/.... After upgrading, run php artisan files:migrate-private-disk once to move any legacy private bytes still on the public disk — Files configuration.

Public CMS API (role Files access): read covers public files; read_private unlocks effective-private files. Without read_private, get/content/transform return 403, list omits private rows, and item expanders return null / omit (no id leak). Without read on a public file field expand (include=files), the payload is { "id": N, "access": "denied" } — grant Files → Read on the role. Typical public role: Read ✓, Read private ✕ — see Public CMS API and Roles & permissions.

Leave guard during upload

While any upload is pending / uploading, the files page registers with the admin leave guard. Navigating away shows Upload in progress — Leave anyway lets uploads finish in the background (global in-memory store); Keep editing stays on the page. Soft/force delete and bulk delete open a confirm dialog before running.

Open a file’s detail panel via ?file={id} on /files (synced when opening/closing the panel). See Routing — Admin UI deep-links.

Where used (collection references)

The detail panel Where used section calls GET /files/{file}/where-used (files.where-used). FileWhereUsedScanner scans collections_items_values for image / file / files / blocks fields that store this file id (on-demand; ~30s cache; no dedicated DB index in v1).

Each hit links to the item editor (/collections/{id}/items/{item_id}) and shows the field name. Soft-deleted items are ignored.

Soft/force delete confirms still allow Delete anyway when references exist — the dialog shows a reference count when the selected files can be scanned quickly (≤10 files). Open the detail panel for the full list of links.

Limits

Where-used is an on-demand LIKE prefilter + exact id match. Nested block file fields are supported; very large catalogs may feel slow until a dedicated index lands.

Capabilities

CapabilityHow
BrowseGET /files/{folder?} (Inertia) and GET /files/list (JSON, page size 50)
FoldersPOST /files/folders → FileService::createFolder
Direct uploadPOST /files/upload → uploadFile
Chunked uploadPOST /files/uploads/init, …/chunk, …/complete; GET …/status
Replace / versionsPOST /files/{file}/replace → replaceFile / version resolution
CopyPOST /files/{file}/copy — sync or queued (see below)
Move / renamePATCH …/move, PATCH …/rename
MetadataPATCH /files/{file} → updateMetadata (includes visibility access)
TagsGET /files/tags, PUT /files/{file}/tags (Spatie tags)
FavoritesPOST/DELETE /files/{file}/favorite
Soft / force deleteDELETE /files/{file}, POST …/restore, DELETE …/force
Where usedGET /files/{file}/where-used → collection item references
Attach / detachPOST …/attach, POST …/detach (polymorphic links)
ThumbnailsGET /files/{file}/thumbnail → FileTransformService::ensureThumbnail (images)
BulkPOST /files/bulk — move, delete, restore, force_delete, favorite/unfavorite, tag/untag, copy
Single downloadGET /files/{file}/download
Zip (async)POST /files/download → PrepareFilesZipJob; download GET /files/zips/{jobId}

Duplicate: sync vs queue

FileController copy behavior:

  • Sync when the source is not a folder and size <= config('files.duplicate_sync_max_bytes', 52428800) (~50 MiB).
  • Queue DuplicateFilesJob otherwise (folders, larger files, or bulk action=copy) → HTTP 202 with { queued: true, job_id }.

Completed/failed duplication notifies via FileDuplicationCompletedNotification / FileDuplicationFailedNotification.

Zip async + notifications

PrepareFilesZipJob builds the archive; the user is notified with:

  • FileZipReadyNotification (type: file_zip_ready) — download via files.zips.download
  • FileZipFailedNotification (type: file_zip_failed)

can.manage.files mapping

EnsureCanManageFiles resolves the required permission with EffectivePermissionResolver, then checks it.

Named route overrides

Route name(s)Permission
files.restorecan-restore-files
files.force-deletecan-force-delete-files
files.download, files.download-many, files.zips.downloadcan-download-files
files.favorite, files.unfavoritecan-favorite-files
files.copycan-copy-files
files.replacecan-replace-files
files.tagscan-tag-files
files.updatecan-update-file-metadata

Bulk action → permission

actionPermission
restorecan-restore-files
force_deletecan-force-delete-files
favorite / unfavoritecan-favorite-files
tag / untagcan-tag-files
copycan-copy-files
deletecan-delete-files
move / defaultcan-edit-files

Default by HTTP method

If no override matches:

MethodPermission
GET / HEADcan-show-files
POSTcan-create-files
PUT / PATCHcan-edit-files
DELETEcan-delete-files

File PermissionEnum values

can-show-files
can-create-files
can-edit-files
can-delete-files
can-restore-files
can-force-delete-files
can-download-files
can-favorite-files
can-copy-files
can-replace-files
can-tag-files
can-update-file-metadata

Route checklist

GET    /files/list
GET    /files/tags
POST   /files/folders
POST   /files/upload
POST   /files/bulk
POST   /files/download
GET    /files/zips/{jobId}
PATCH  /files/{file}
POST   /files/{file}/replace
POST   /files/{file}/copy
POST   /files/{file}/favorite
DELETE /files/{file}/favorite
PUT    /files/{file}/tags
GET    /files/{file}/download
GET    /files/{file}/thumbnail
PATCH  /files/{file}/move
PATCH  /files/{file}/rename
DELETE /files/{file}
POST   /files/{file}/restore
DELETE /files/{file}/force
POST   /files/{file}/attach
POST   /files/{file}/detach
POST   /files/uploads/init
POST   /files/uploads/chunk
POST   /files/uploads/complete
GET    /files/uploads/status
GET    /files/{folder?}          # Inertia index (whereNumber folder)

Jobs

JobNotifications
App\Jobs\PrepareFilesZipJobFileZipReadyNotification, FileZipFailedNotification
App\Jobs\DuplicateFilesJobFileDuplicationCompletedNotification, FileDuplicationFailedNotification

Source map

ConcernLocation
Controllerapp/Http/Controllers/Admin/FileController.php
Middlewareapp/Http/Middleware/EnsureCanManageFiles.php
Serviceapp/Services/FileService.php
Extension denyapp/Support/Uploads/ForbiddenUploadExtension.php
MIME / magicapp/Support/Uploads/MimeTypeValidator.php + config/files.php
Plain-text stripapp/Support/Security/PlainTextSanitizer.php
Aliasbootstrap/app.php → can.manage.files
Pageresources/js/pages/admin/files/index.tsx
Locale stringsresources/js/locales/{en,it,de}.json → files.*
Previous
Live Preview