Skip to content

Commit

Permalink
Delete orphaned media in clean command (#3419)
Browse files Browse the repository at this point in the history
* Delete orphaned media in clean command

* Add 'getOrphans' to the media repository

* Make delete orphaned opt-in, add test

* Add collection name option, fix phpstan, add extra test

* Fix tests

* Fix test for L9

* Typo

* Respect rate limit in remove orphaned media item method
  • Loading branch information
mbardelmeijer authored Oct 25, 2023
1 parent 0d4b30f commit fde9246
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 1 deletion.
38 changes: 37 additions & 1 deletion src/MediaCollections/Commands/CleanCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +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 },
{--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.';
Expand Down Expand Up @@ -54,6 +55,10 @@ public function handle(
$this->isDryRun = $this->option('dry-run');
$this->rateLimit = (int) $this->option('rate-limit');

if ($this->option('delete-orphaned')) {
$this->deleteOrphanedMediaItems();
}

if (! $this->option('skip-conversions')) {
$this->deleteFilesGeneratedForDeprecatedConversions();
}
Expand Down Expand Up @@ -87,6 +92,37 @@ public function getMediaItems(): Collection
return $this->mediaRepository->all();
}

protected function deleteOrphanedMediaItems(): void
{
$this->getOrphanedMediaItems()->each(function (Media $media): void {
if ($this->isDryRun) {
$this->info("Orphaned Media[id={$media->id}] found");

return;
}

$media->delete();

if ($this->rateLimit) {
usleep((1 / $this->rateLimit) * 1_000_000);
}

$this->info("Orphaned Media[id={$media->id}] has been removed");
});
}

/** @return Collection<int, Media> */
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) {
Expand Down
15 changes: 15 additions & 0 deletions src/MediaCollections/MediaRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ public function getByCollectionName(string $collectionName): DbCollection
->get();
}

public function getOrphans(): DbCollection
{
return $this->query()
->whereDoesntHave('model')
->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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,81 @@
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->deletePreservingMedia();

$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 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->deletePreservingMedia();
$mediaToKeep->model->deletePreservingMedia();

$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('will not 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->deletePreservingMedia();

// 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,
]);
});

0 comments on commit fde9246

Please sign in to comment.