Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

File Manager Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  • Browsing — 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>
    
  • Form attachment — the picker, which is the real reason to adopt this over a plain 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.

Implementation Patterns

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.
  • A single string like 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.
  • An array like ['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.
  • Trash is filtered by 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.

Gotchas and Tips

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings