bbs-lab/nova-file-manager
A file manager for Laravel Nova that lets you browse, upload, organize, and manage files and folders directly inside your Nova admin panel. Provides a clean interface for common file operations, making media and document management easy without leaving Nova.
The package slots into Nova as both a Tool (a full-page file browser under the sidebar) and a Field (an uploader you drop onto resource forms). You almost always want both.
Install
composer require bbs-lab/nova-file-manager
Publish config and assets
php artisan vendor:publish --provider="BbsLab\NovaFileManager\NovaFileManagerServiceProvider"
You should end up with config/nova-file-manager.php and published JS/CSS under public/vendor/nova-file-manager. Publish assets again after every composer update — this is the single most common "it looks broken" cause.
Register the tool in app/Providers/NovaServiceProvider.php:
public function tools()
{
return [
\BbsLab\NovaFileManager\NovaFileManager::make(),
];
}
Make storage reachable — the manager operates on a Laravel disk, so the disk must actually be browsable by the web process. For local disks:
php artisan storage:link
Then confirm the disk in config/filesystems.php matches the disk value in the package config.
First use case: add the field to a model form and upload a cover image.
use BbsLab\NovaFileManager\FileManager;
public function fields(Request $request)
{
return [
FileManager::make('Cover')->disk('public')->path('covers'),
];
}
The value stored is the relative path on the disk — treat it like any other string column and resolve URLs with Storage::disk('public')->url($model->cover).
Where to look first
config/nova-file-manager.php — disk, base path, allowed extensions, max size, preview behaviour. This is where 90% of "why can't I upload X" lives.disk/path keys before you touch any Vue or views.Tool as the shared media library. Register one tool with a broad base path (e.g. media) and let editors browse/reuse assets across resources. Keep the tool read-heavy and restricted by policy so it doesn't become a dumping ground.
Field per model, scoped path. Give each resource its own namespace so uploads don't collide:
FileManager::make('Gallery')
->disk('s3')
->path("models/{$this->resource->id}/gallery")
->multiple()
->acceptedTypes(['image/*']);
Storing under an ID-scoped path makes cleanup on delete trivial — hook Model::deleted and Storage::disk(...)->deleteDirectory(...).
Multi-disk in one app. Config the tool to a public disk for editor assets and point specific fields at S3/private disks. The field-level disk()/path() overrides the global config, so you rarely need multiple tool registrations.
Multi-tenancy. Derive the base path from the tenant rather than publishing a separate config per tenant: a small service provider or the field's path() closure reading tenant()->slug keeps tenant files isolated while reusing one tool.
Integration with rich-text editors. The tool exposes a browse/upload URL that TinyMCE/CKEditor file pickers can be pointed at. Wire the editor's file browser callback to the tool route so "Insert image" opens the Nova manager instead of a bare upload input — this is the highest-leverage integration for content teams.
Authorization. Don't rely on the tool's canSee alone if fields are also mounted. Add a Nova policy/gate check in the field's canSee/readonly closures so per-role upload restrictions hold everywhere.
Post-upload processing. Use the package's upload events (or a queued listener on file creation) to generate thumbnails, strip EXIF, or push to a CDN. Keep this in listeners, not in the field definition, so both tool and field uploads get the same treatment.
Testing. Point the test disk at Storage::fake('public') and assert the stored path rather than the rendered Vue component. For the UI, keep a couple of Dusk tests on the tool page only — the field is better covered by form-submission tests.
vendor/nova-file-manager/js/... is almost always an unpublished asset, not a PHP error.visibility: private will upload fine but show broken previews. Either set visibility: public, or use signed/temporary URLs if the package config supports URL generation — check the config before assuming a bug.upload_max_filesize/post_max_size/max_execution_time, not in Laravel. If chunked uploads are configured, verify the chunk endpoint is reachable behind your proxy (nginx body limits, load balancer timeouts).path() from raw user input. Validate that any dynamic path is inside the intended root; a permissive base path plus ../ is a real security issue, not just a cosmetic one.config/nova-file-manager.php, run php artisan config:cache (or config:clear in dev). Nova also has its own cache — nova:clear if a newly registered tool doesn't appear.storage/app/public and the published vendor directory must be writable/readable by the web user. A 403 on thumbnails is usually ownership, not the package.disk() call, check you're not accidentally relying on the global tool config — field overrides are explicit.Storage::disk(...)->allFiles($path) in tinker to confirm what's actually on disk. That sequence isolates "Nova problem vs. storage problem vs. package problem" in about two minutes.filesystems.php, reference by name in config), overriding published views for tool chrome, and the upload events. Prefer these over forking — the package moves fast (last release mid-2025), and forks rot.How can I help you explore Laravel packages today?