From 89526913e5e9c21980ce546f451896ca1a1b2e3c Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Mon, 23 Oct 2023 12:38:27 +0200 Subject: [PATCH 1/8] Delete orphaned media in clean command --- .../Commands/CleanCommand.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/MediaCollections/Commands/CleanCommand.php b/src/MediaCollections/Commands/CleanCommand.php index fc79d9640..26e69819b 100644 --- a/src/MediaCollections/Commands/CleanCommand.php +++ b/src/MediaCollections/Commands/CleanCommand.php @@ -24,6 +24,7 @@ class CleanCommand extends Command {--dry-run : List files that will be removed without removing them}, {--force : Force the operation to run when in production}, {--rate-limit= : Limit the number of requests per second }, + {--skip-orphaned : Do not remove orphaned media items}, {--skip-conversions : Do not remove deprecated conversions}'; protected $description = 'Clean deprecated conversions and files without related model.'; @@ -54,6 +55,10 @@ public function handle( $this->isDryRun = $this->option('dry-run'); $this->rateLimit = (int) $this->option('rate-limit'); + if (! $this->option('skip-orphaned')) { + $this->deleteOrphanedMediaItems(); + } + if (! $this->option('skip-conversions')) { $this->deleteFilesGeneratedForDeprecatedConversions(); } @@ -87,6 +92,21 @@ public function getMediaItems(): Collection return $this->mediaRepository->all(); } + protected function deleteOrphanedMediaItems(): void + { + $this->mediaRepository->getOrphans()->each(function (Media $media) { + if ($this->isDryRun) { + $this->info("Orphaned Media[id={$media->id}] found"); + + return; + } + + $media->delete(); + + $this->info("Orphaned Media[id={$media->id}] has been removed"); + }); + } + protected function deleteFilesGeneratedForDeprecatedConversions(): void { $this->getMediaItems()->each(function (Media $media) { From d8f92871abbdba0e6ea92bcef037174279572363 Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Mon, 23 Oct 2023 12:47:44 +0200 Subject: [PATCH 2/8] Add 'getOrphans' to the media repository --- src/MediaCollections/MediaRepository.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/MediaCollections/MediaRepository.php b/src/MediaCollections/MediaRepository.php index f0fcf3b79..770396599 100644 --- a/src/MediaCollections/MediaRepository.php +++ b/src/MediaCollections/MediaRepository.php @@ -88,6 +88,13 @@ public function getByCollectionName(string $collectionName): DbCollection ->get(); } + public function getOrphans(): DbCollection + { + return $this->query() + ->whereDoesntHave('model') + ->get(); + } + protected function query(): Builder { return $this->model->newQuery(); From f6a74f23bc9dcf4bfaefaec1916fc4f3b5119b0f Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Tue, 24 Oct 2023 12:06:41 +0200 Subject: [PATCH 3/8] Make delete orphaned opt-in, add test --- .../Commands/CleanCommand.php | 6 +-- ...nversionsTest.php => CleanCommandTest.php} | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) rename tests/Conversions/Commands/{CleanConversionsTest.php => CleanCommandTest.php} (88%) diff --git a/src/MediaCollections/Commands/CleanCommand.php b/src/MediaCollections/Commands/CleanCommand.php index 26e69819b..c6213de39 100644 --- a/src/MediaCollections/Commands/CleanCommand.php +++ b/src/MediaCollections/Commands/CleanCommand.php @@ -23,8 +23,8 @@ class CleanCommand extends Command protected $signature = 'media-library:clean {modelType?} {collectionName?} {disk?} {--dry-run : List files that will be removed without removing them}, {--force : Force the operation to run when in production}, - {--rate-limit= : Limit the number of requests per second }, - {--skip-orphaned : Do not remove orphaned media items}, + {--rate-limit= : Limit the number of requests per second}, + {--delete-orphaned : Delete orphaned media items}, {--skip-conversions : Do not remove deprecated conversions}'; protected $description = 'Clean deprecated conversions and files without related model.'; @@ -55,7 +55,7 @@ public function handle( $this->isDryRun = $this->option('dry-run'); $this->rateLimit = (int) $this->option('rate-limit'); - if (! $this->option('skip-orphaned')) { + if ($this->option('delete-orphaned')) { $this->deleteOrphanedMediaItems(); } diff --git a/tests/Conversions/Commands/CleanConversionsTest.php b/tests/Conversions/Commands/CleanCommandTest.php similarity index 88% rename from tests/Conversions/Commands/CleanConversionsTest.php rename to tests/Conversions/Commands/CleanCommandTest.php index 8262bc185..66a76cf44 100644 --- a/tests/Conversions/Commands/CleanConversionsTest.php +++ b/tests/Conversions/Commands/CleanCommandTest.php @@ -274,3 +274,50 @@ expect($this->getMediaDirectory("{$media->id}/test-thumb.jpg"))->toBeFile(); expect($this->getMediaDirectory("{$media->id}/test.jpg"))->toBeFile(); }); + +it('can clean orphaned media items when enabled', function () { + $mediaToDelete = TestModel::create(['name' => 'test.jpg']) + ->addMedia($this->getTestJpg()) + ->preservingOriginal() + ->toMediaCollection('collection1'); + + $mediaToKeep = TestModel::create(['name' => 'test.jpg']) + ->addMedia($this->getTestJpg()) + ->preservingOriginal() + ->toMediaCollection('collection1'); + + // Delete quietly to avoid deleting the related media file. + $mediaToDelete->model->deleteQuietly(); + + $this->artisan('media-library:clean', [ + '--delete-orphaned' => 'true', + ]); + + // Media should be deleted from the database. + $this->assertDatabaseMissing('media', [ + 'id' => $mediaToDelete->id, + ]); + + // This media should still exist. + $this->assertDatabaseHas('media', [ + 'id' => $mediaToKeep->id, + ]); +}); + +it('can won\'t clean orphaned media items when disabled', function () { + $media = TestModel::create(['name' => 'test.jpg']) + ->addMedia($this->getTestJpg()) + ->preservingOriginal() + ->toMediaCollection('collection1'); + + // Delete quietly to avoid deleting the related media file. + $media->model->deleteQuietly(); + + // Without the `--delete-orphaned` flag, the orphaned media should remain. + $this->artisan('media-library:clean'); + + // This media should still exist. + $this->assertDatabaseHas('media', [ + 'id' => $media->id, + ]); +}); From fb6c24ff6cb7d476276ea74ae8039e5dfdda89d8 Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Tue, 24 Oct 2023 12:16:38 +0200 Subject: [PATCH 4/8] Add collection name option, fix phpstan, add extra test --- .../Commands/CleanCommand.php | 14 ++++++++- src/MediaCollections/MediaRepository.php | 8 +++++ .../Conversions/Commands/CleanCommandTest.php | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/MediaCollections/Commands/CleanCommand.php b/src/MediaCollections/Commands/CleanCommand.php index c6213de39..5441dd751 100644 --- a/src/MediaCollections/Commands/CleanCommand.php +++ b/src/MediaCollections/Commands/CleanCommand.php @@ -94,7 +94,7 @@ public function getMediaItems(): Collection protected function deleteOrphanedMediaItems(): void { - $this->mediaRepository->getOrphans()->each(function (Media $media) { + $this->getOrphanedMediaItems()->each(function (Media $media): void { if ($this->isDryRun) { $this->info("Orphaned Media[id={$media->id}] found"); @@ -107,6 +107,18 @@ protected function deleteOrphanedMediaItems(): void }); } + /** @return Collection */ + protected function getOrphanedMediaItems(): Collection + { + $collectionName = $this->argument('collectionName'); + + if (is_string($collectionName)) { + return $this->mediaRepository->getOrphansByCollectionName($collectionName); + } + + return $this->mediaRepository->getOrphans(); + } + protected function deleteFilesGeneratedForDeprecatedConversions(): void { $this->getMediaItems()->each(function (Media $media) { diff --git a/src/MediaCollections/MediaRepository.php b/src/MediaCollections/MediaRepository.php index 770396599..cc499eb96 100644 --- a/src/MediaCollections/MediaRepository.php +++ b/src/MediaCollections/MediaRepository.php @@ -95,6 +95,14 @@ public function getOrphans(): DbCollection ->get(); } + public function getOrphansByCollectionName(string $collectionName): DbCollection + { + return $this->query() + ->whereDoesntHave('model') + ->where('collection_name', $collectionName) + ->get(); + } + protected function query(): Builder { return $this->model->newQuery(); diff --git a/tests/Conversions/Commands/CleanCommandTest.php b/tests/Conversions/Commands/CleanCommandTest.php index 66a76cf44..9f1f162d4 100644 --- a/tests/Conversions/Commands/CleanCommandTest.php +++ b/tests/Conversions/Commands/CleanCommandTest.php @@ -304,6 +304,37 @@ ]); }); +it('can clean orphaned media items when enabled for specific collections', function () { + $mediaToClean = TestModel::create(['name' => 'test.jpg']) + ->addMedia($this->getTestJpg()) + ->preservingOriginal() + ->toMediaCollection('collection-to-clean'); + + $mediaToKeep = TestModel::create(['name' => 'test.jpg']) + ->addMedia($this->getTestJpg()) + ->preservingOriginal() + ->toMediaCollection('collection-to-keep'); + + // Delete quietly to avoid deleting the related media file. + $mediaToClean->model->deleteQuietly(); + $mediaToKeep->model->deleteQuietly(); + + $this->artisan('media-library:clean', [ + '--delete-orphaned' => 'true', + 'collectionName' => 'collection-to-clean', + ]); + + // Media should be deleted from the database. + $this->assertDatabaseMissing('media', [ + 'id' => $mediaToClean->id, + ]); + + // This media should still exist. + $this->assertDatabaseHas('media', [ + 'id' => $mediaToKeep->id, + ]); +}); + it('can won\'t clean orphaned media items when disabled', function () { $media = TestModel::create(['name' => 'test.jpg']) ->addMedia($this->getTestJpg()) From 5bf3cf969a2de7c5f034df69a8cb371a8ac0d6be Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Tue, 24 Oct 2023 12:21:25 +0200 Subject: [PATCH 5/8] Fix tests --- tests/Conversions/Commands/CleanCommandTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Conversions/Commands/CleanCommandTest.php b/tests/Conversions/Commands/CleanCommandTest.php index 9f1f162d4..25b78bade 100644 --- a/tests/Conversions/Commands/CleanCommandTest.php +++ b/tests/Conversions/Commands/CleanCommandTest.php @@ -316,8 +316,8 @@ ->toMediaCollection('collection-to-keep'); // Delete quietly to avoid deleting the related media file. - $mediaToClean->model->deleteQuietly(); - $mediaToKeep->model->deleteQuietly(); + $mediaToClean->model->deletePreservingMedia(); + $mediaToKeep->model->deletePreservingMedia(); $this->artisan('media-library:clean', [ '--delete-orphaned' => 'true', @@ -342,7 +342,7 @@ ->toMediaCollection('collection1'); // Delete quietly to avoid deleting the related media file. - $media->model->deleteQuietly(); + $media->model->deletePreservingMedia(); // Without the `--delete-orphaned` flag, the orphaned media should remain. $this->artisan('media-library:clean'); From a6b6f17b33d4f561f64242067256dbdd1373350d Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Tue, 24 Oct 2023 12:26:10 +0200 Subject: [PATCH 6/8] Fix test for L9 --- tests/Conversions/Commands/CleanCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Conversions/Commands/CleanCommandTest.php b/tests/Conversions/Commands/CleanCommandTest.php index 25b78bade..015f4aac8 100644 --- a/tests/Conversions/Commands/CleanCommandTest.php +++ b/tests/Conversions/Commands/CleanCommandTest.php @@ -287,7 +287,7 @@ ->toMediaCollection('collection1'); // Delete quietly to avoid deleting the related media file. - $mediaToDelete->model->deleteQuietly(); + $mediaToDelete->model->deletePreservingMedia(); $this->artisan('media-library:clean', [ '--delete-orphaned' => 'true', From 517d7079bc74f335c03234b9f4e25a73ecb8e1e6 Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Tue, 24 Oct 2023 17:01:16 +0200 Subject: [PATCH 7/8] Typo --- tests/Conversions/Commands/CleanCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Conversions/Commands/CleanCommandTest.php b/tests/Conversions/Commands/CleanCommandTest.php index 015f4aac8..fd8fb7430 100644 --- a/tests/Conversions/Commands/CleanCommandTest.php +++ b/tests/Conversions/Commands/CleanCommandTest.php @@ -335,7 +335,7 @@ ]); }); -it('can won\'t clean orphaned media items when disabled', function () { +it('will not clean orphaned media items when disabled', function () { $media = TestModel::create(['name' => 'test.jpg']) ->addMedia($this->getTestJpg()) ->preservingOriginal() From b2ed5bd41eed80bf2b734a0f2ce9b855e1a5834e Mon Sep 17 00:00:00 2001 From: Michel Bardelmeijer Date: Tue, 24 Oct 2023 17:09:02 +0200 Subject: [PATCH 8/8] Respect rate limit in remove orphaned media item method --- src/MediaCollections/Commands/CleanCommand.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/MediaCollections/Commands/CleanCommand.php b/src/MediaCollections/Commands/CleanCommand.php index 5441dd751..48fa28304 100644 --- a/src/MediaCollections/Commands/CleanCommand.php +++ b/src/MediaCollections/Commands/CleanCommand.php @@ -103,6 +103,10 @@ protected function deleteOrphanedMediaItems(): void $media->delete(); + if ($this->rateLimit) { + usleep((1 / $this->rateLimit) * 1_000_000); + } + $this->info("Orphaned Media[id={$media->id}] has been removed"); }); }