proside/file-manager
Reusable file manager for Laravel 10/11/12 built in Livewire v4: directory tree, grid/list views, uploads, drag & drop, trash with restore, previews, search, multi-disk support and a file picker component. No iframe or external service.
Four steps get a working UI, and the third is the one that silently bites.
composer require proside/file-manager
php artisan storage:link
php artisan vendor:publish --tag=file-manager-config
Package discovery registers the service provider, the GET /file-manager route, the livewire:file-manager component, and the x-file-manager:: Blade namespace — there is no providers entry to add and no route file to import. The symlink matters only because the default disk is public.
Read the published config before writing any integration code. The defaults are real assumptions about your filesystem, not placeholders:
| Key | Default | What it actually decides |
|---|---|---|
disk |
public |
Which Filesystem disk everything resolves against |
root |
conteudos |
The only navigable base folder |
trash |
apagados |
Where deletions land |
trash_retention_days |
30 |
Age before permanent deletion — only enforced by the scheduler |
root_resolver |
null |
Per-user scoping; null = full access |
uploads.max_size / mimes |
51200 KB / null |
Upload guardrails |
media_url |
route |
How file URLs are generated |
route.* |
prefix + middleware | The full-page route |
Wire Tailwind next. If your app compiles Tailwind, the package's utility classes (a proximo palette) must be in the JIT scan path or the entire UI renders as unstyled block markup:
content: [
'./vendor/proside/file-manager/resources/views/**/*.blade.php',
],
If you do not compile Tailwind at all, publish the prebuilt stylesheet instead — php artisan vendor:publish --tag=file-manager-assets. Do one or the other; doing neither is the single most common "the package is broken" report.
Then schedule pruning, or trash_retention_days is decorative and apagados grows forever:
// Laravel 10: app/Console/Kernel.php
$schedule->command('file-manager:prune-trash')->daily();
// Laravel 11/12: routes/console.php
Schedule::command('file-manager:prune-trash')->daily();
Two entry points exist, and most teams actually want the second:
GET /file-manager works immediately, or embed it. The height wrapper is mandatory: the component fills its container and collapses to zero height without one.
<div style="height: 80vh"><livewire:file-manager /></div>
file input:
<x-file-manager::picker input-name="gfqueue_image" :value="$queue->image ?? ''" />
The picker renders a <input type="hidden" name="gfqueue_image">, so your FormRequest, $model->update(), and existing validation rules keep working untouched. The canonical first real task is swapping an ad-hoc upload widget for this and storing only the relative disk path on the model.
Where to look when you need to go past the surface: config/file-manager.php first, then resources/views/livewire/file-manager.blade.php (the layout and toolbar), then the FileManagerService methods (listing(), tree(), createFolder(), rename(), move(), trash()). The README's Next.js → Livewire mapping table is a genuinely useful Rosetta stone if you are migrating from the old version — it tells you which React component each Blade partial replaced, so you know where a given behaviour now lives.
Picker + your own validation. The picker manages selection, nothing more. filter="images" narrows what is clickable in the browser, but it is a UI affordance, not a security boundary — the hidden input is just a string in a form post and can be tampered with. Keep the rule on the request:
<x-file-manager::picker input-name="imagens[]" :value="$existing" multiple filter="images" />
'imagens.*' => ['string', 'regex:/^conteudos\/.*\.(jpg|jpeg|png|webp)$/i'],
Store the relative path (relative to the configured disk), never a full URL. That value is exactly what Storage::disk($disk)->url($path) consumes, and it survives a later switch of media_url mode or a move to S3.
Reacting to selection inside Livewire. When a plain form post is not enough — show a thumbnail immediately, read dimensions, populate a second field — listen for the emitted event:
#[On('file-manager-selected')]
public function onFileSelected(array $paths): void
{
$this->photo = $paths[0] ?? null;
}
The payload is always { paths: [...] }, even for single selection. Index [0]; do not assume a scalar.
Per-user scoping via root_resolver. This is the multi-tenant story and the piece worth designing deliberately. Point the config at an invokable class returning null, a string, or an array:
// config/file-manager.php
'root_resolver' => \App\FileManager\LevelRootResolver::class,
final class LevelRootResolver
{
public function __invoke(): null|string|array
{
$user = auth()->user();
if ($user?->isAdmin()) {
return null; // full access
}
return $user->file_root ?: 'conteudos/'.$user->team_slug;
}
}
Semantics, as documented:
null, '', or a value equal to the configured root → full access.conteudos/optivisao → the user sees that folder and everything below it; the tree, breadcrumbs, navigation and all operations are confined, and PathGuard blocks attempts to walk above it.['conteudos/a', 'conteudos/b'] → multiple roots, each with its own tree in the sidebar; the first one opens by default; navigation is confined to any of them. Paths outside the configured root are ignored — this is deliberate anti-privilege-escalation, and it means a typo in a resolver silently narrows access rather than widening it.originalPath: a scoped user only sees what they deleted from their own roots. With multiple roots, the trash is shared across those roots.The docs/BACKOFFICE-INTEGRATION.md file has a worked example (gflevel / gfleveluser / gflevel_ctvdir) if your tenancy model looks like a backoffice profile system.
Multi-disk / S3. Since everything goes through Laravel's Filesystem, switching disk to an s3 entry is a config change — with one caveat on media_url, covered in Gotchas below. Do the switch early in your integration; discovering the storage mode limitation after building URLs into a dozen views is annoying.
Embedding vs. the full-page route. Prefer embedding. Drop the component into your existing admin layout so it inherits your chrome, and leave the standalone route as a debugging aid until you have decided whether to expose it. If you do expose it, set route.middleware to include auth — it ships with whatever prefix and middleware the config declares, and the package reuses your session, so an unprotected route is an unprotected file browser.
Extensions worth knowing about. Everything under resources/views/vendor/file-manager is publishable, so the grid item, list item, tree node, context menu, modal and lightbox partials are all replaceable without forking. Translations are publishable per locale (file-manager-lang), which matters because the defaults are PT/EN and the trash folder is literally named apagados in the UI string as well as on disk.
Livewire 4 is a hard requirement. The package targets ^4.0. If your app is on Livewire 3 — which describes most Laravel apps in this window — you cannot install this without a Livewire major upgrade first, and that is a much bigger project than adopting a file manager. Check composer show livewire/livewire before you do anything else; this constraint, not the file manager itself, will decide whether you adopt it this quarter.
The Tailwind content path is the #1 support issue. No error, no warning — just an unstyled page. If you see raw HTML with no spacing, the classes were never generated. And if you switch from the published asset to the compiled path (or vice versa), run php artisan view:clear and rebuild; stale published CSS after a package update is the second variant of the same bug.
config:cache breaks closures. A root_resolver defined as a closure works in development and blows up (or silently fails) once you cache config in production. Use an invokable class-string — that is explicitly why the README recommends it.
media_url => 'route' vs 'storage'. route is the safe default: a MediaController streams the file through file-manager.media, which works for any disk, any visibility setting, and any filename. storage generates a direct Storage::url() link — faster, but it will fail for private S3 buckets and can produce broken URLs for filenames with spaces, accents or non-ASCII characters. auto picks per disk. If thumbnails or the lightbox 404 while the file clearly exists, this config key is the first thing to inspect.
filter is not validation. Repeated because it is the classic mistake: filter="images" restricts the browser UI only. The submitted value is a string, and your FormRequest has no idea it came from the picker.
Scheduling is not optional, and retention is measured in days. Without file-manager:prune-trash, the trash never empties. And note this is a retention policy, not a recovery guarantee — do not tell users "deleted files stay for 30 days" unless you have verified the scheduler actually runs in that environment.
Performance on large directories is undocumented. The README describes listing() and tree(), filters, sorting and search, and does not mention pagination. A directory with thousands of entries is a real risk — the tree lazy-loads, but a single flat folder may not. If you have large media libraries, benchmark a realistic worst-case folder before committing, and consider sharding by date or category folder.
PathGuard failures are quiet. When a scoped user hits a blocked path, they get a block — not an explanatory error. When debugging "the user can't see their files", log the resolver's return value and the configured root side by side. The most common cause is a resolver returning a path that does not actually exist on the disk, or that lies outside root and is therefore ignored.
Test the picker's multiple + value round-trip before you build on it. :value="$existing" with input-name="imagens[]" and multiple is documented as the drop-in replacement for the old <x-file-manager-modal>, same prop API — but round-tripping an existing array of paths back into the picker's selected state is exactly the kind of thing that works for one item and subtly does not for many. Verify with real data, including paths whose files have since been deleted.
Vet the dependency. Zero stars, zero dependents, single maintainer, and a relatively young release line. It is a Livewire-based package so the blast radius is smaller than a Next.js sidecar, but you are taking a real supply-chain dependency inside an authenticated admin surface. Pin the version in composer.json, skim the tag you install, and confirm what test coverage exists before it touches production files. The upside — no iframe, no JWT, no external service, no .pem key rotation — is genuinely a large architectural improvement over the Next.js predecessor, and worth the vetting.
How can I help you explore Laravel packages today?