Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete orphaned media in clean command #3419

Merged
merged 9 commits into from
Oct 25, 2023
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,
]);
});