This commit is contained in:
hashbro
2026-09-12 03:53:21 +08:00
parent 8c5724ff3b
commit b4f0dde628
2 changed files with 178 additions and 4 deletions
@@ -0,0 +1,100 @@
<?php
namespace App\Console\Commands;
use App\Models\Photo;
use App\Services\PhotoPreview;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class PrunePhotosBeforeCommand extends Command
{
protected $signature = 'coruna:prune-photos-before
{date : Delete photos created before this date (e.g. 2026-09-01 or "2026-09-01 00:00:00")}
{--execute : Actually delete files and rows (default is dry-run)}
{--device= : Only prune photos for this device_id or numeric id}';
protected $description = 'Delete photo DB rows, file blobs, read markers, and preview caches created before a given date';
public function handle(PhotoPreview $previews): int
{
$execute = (bool) $this->option('execute');
$dateArg = trim((string) $this->argument('date'));
$date = date('Y-m-d H:i:s', strtotime($dateArg));
if ($date === false || $date === '1970-01-01 00:00:00') {
$this->error("invalid date: {$dateArg}");
return self::FAILURE;
}
$q = Photo::query()->where('photos.created_at', '<', $date);
$deviceFilter = trim((string) $this->option('device'));
if ($deviceFilter !== '') {
if (ctype_digit($deviceFilter)) {
$q->where('photos.device_id', (int) $deviceFilter);
} else {
$q->join('devices', 'devices.id', '=', 'photos.device_id')
->where('devices.device_id', $deviceFilter)
->select('photos.*');
}
}
$count = (clone $q)->count();
$this->info(sprintf(
'%s cutoff=%s photos=%d',
$execute ? 'EXECUTE' : 'DRY-RUN',
$date,
$count,
));
if ($count === 0) {
$this->info('nothing to prune');
return self::SUCCESS;
}
if (! $execute) {
$this->warn('dry-run only; pass --execute to delete');
return self::SUCCESS;
}
$disk = Storage::disk('local');
$deletedRows = 0;
$deletedFiles = 0;
$deviceIds = [];
// Process in chunks to avoid loading everything into memory.
$q->select(['photos.id', 'photos.device_id', 'photos.path'])
->orderBy('photos.id')
->chunk(500, function ($photos) use ($disk, $previews, &$deletedRows, &$deletedFiles, &$deviceIds) {
$ids = [];
foreach ($photos as $photo) {
$ids[] = $photo->id;
$path = trim((string) ($photo->path ?? ''));
if ($path !== '' && $disk->exists($path)) {
$disk->delete($path);
$deletedFiles++;
}
$deviceIds[$photo->device_id] = true;
}
// Delete PhotoRead markers first (foreign key), then photos.
DB::table('photo_reads')->whereIn('photo_id', $ids)->delete();
Photo::query()->whereIn('id', $ids)->delete();
$deletedRows += count($ids);
});
// Clear preview caches for affected devices.
foreach (array_keys($deviceIds) as $deviceId) {
$device = \App\Models\Device::find($deviceId);
if ($device) {
$previews->forgetForDevice((string) $device->device_id);
}
}
$this->info(sprintf('deleted_rows=%d deleted_files=%d devices=%d', $deletedRows, $deletedFiles, count($deviceIds)));
return self::SUCCESS;
}
}