In Filament, while using the File Upload field, attached files are not automatically deleted when you delete the Model record itself. How to fix this? I will show two ways.
In the Filament docs for file upload we have this note:
Please note, it is the responsibility of the developer to delete these files from the disk if they are removed, as Filament is unaware if they are depended on elsewhere. One way to do this automatically is by observing a model event.
It's fair enough, as Filament doesn't "know" about those files, so we need to take care of them ourselves.
In this example, we will have a Task
Model with title
and attachment
fields.
database/migrations/xxxx_create_tasks_table.php:
return new class extends Migration { public function up(): void { Schema::create('tasks', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string('attachment')->nullable(); $table->timestamps(); }); }};
Our goal is to delete the physical file from the server if the Task is deleted in Filament adminpanel.
In fact, this tutorial is not even that much about Filament, as we will show two core Laravel methods to handle the non-deleted files. So you can use those methods outside Filament, it's just that Filament has this situation out-of-the-box.
Method 1: Observer
The first method is going to be Eloquent Observer. First, we need to create Observer and register it in EventServiceProvider.
php artisan make:observer TaskObserver --model=Task
app/Providers/EventServiceProvider.php:
use App\Models\Task;use App\Observers\TaskObserver; class EventServiceProvider extends ServiceProvider{ // ... public function boot(): void { Task::observe(TaskObserver::class); } // ...}
Next, in the Observer we will need two methods:
-
saved()
: will delete the old file on the edit page if it was replaced by a new file -
deleted()
: will delete the file when the Task is deleted
app/Observers/TaskObserver.php:
use App\Models\Task;use Illuminate\Support\Facades\Storage; class TaskObserver{ public function saved(Task $task): void { if ($task->isDirty('attachment')) { Storage::disk('public')->delete($task->getOriginal('attachment')); } } public function deleted(Task $task): void { if (! is_null($task->attachment)) { Storage::disk('public')->delete($task->attachment); } }}
As you can see, $task->isDirty('attachment')
checks if the attachment was replaced by a new different file, in that case we need to delete the older one.
Method 2: Scheduled Artisan Command
If you don't want to delete a specific unused file right away, you can schedule an automatic cleanup of all unused files.
For that, we will create an Artisan command and schedule it for every night:
php artisan make:command DeleteUnusedFiles
First, we need to set $signature
so that created command would have a proper command for running it. We'll name it delete:unused-files
.
app/Console/Commands.php:
class DeleteUnusedFiles extends Command{ protected $signature = 'delete:unused-files'; // ...}
Next, in the handle()
method we need to add all the logic of this command.
app/Console/Commands.php:
use App\Models\Task;use Illuminate\Console\Command;use Illuminate\Support\Facades\Storage; class DeleteUnusedFilesCommand extends Command{ protected $signature = 'delete:unused-files'; protected $description = 'Command description'; public function handle(): void { $tasks = Task::pluck('attachment')->toArray(); collect(Storage::disk('public')->allFiles()) ->reject(fn (string $file) => $file === '.gitignore') ->reject(fn (string $file) => in_array($file, $tasks)) ->each(fn ($file) => Storage::disk('public')->delete($file)); }}
What we're doing here?
- First, we get all the tasks
attachment
values and make an array of it - Then, we get all files from Storage into a Collection
- Then, the first
reject()
method, we remove the.gitignore
file, we don't want it to be removed - Then, the second
reject()
checks if the file is in the$tasks
array. If it is, we remove it from the Collection. - Then, we have the final Collection and use
each()
to delete each file.
Now, we just need to add this command to the schedule.
app/Console/Kernel.php:
class Kernel extends ConsoleKernel{ protected function schedule(Schedule $schedule): void { $schedule->command('delete:unused-files')->daily(); }}
That's it for this quick short tutorial. Which method would you prefer?
Thanks for the tutoria its a nice on.
Thank you for these fantastic mini tutorials that make grasping Laravel (and Filament) principles so much easier!
Note that the logic becomes much more complicated when using the
multiple()
feature of the Filament file upload field. In that case, the file paths will be stored as a JSON collection, and it becomes necessary to walk through each array item.I've left my code based on your tutorial's first approach here: https://gist.github.com/pekka/c9c9503b1ddd2c4ed6b762711d2e59af
It is not super duper tested, but on manual inspection it can successfully handle all the cases I could think of: deleting all files; deleting only one file; and deleting all files and replacing them with one file with the same name as one of the previous ones.