175 lines
5.5 KiB
PHP
175 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\Photo;
|
|
use App\Services\PhotoPreview;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class PruneNowalletPhotosCommand extends Command
|
|
{
|
|
protected $signature = 'coruna:prune-nowallet-photos
|
|
{--before= : Only photos created before this day (2026-09-01 = that day 00:00)}
|
|
{--execute : Actually delete files and rows (default is dry-run)}
|
|
{--disable-storage : Also turn off album_storage when a device has no photos left}';
|
|
|
|
protected $description = 'Delete stored photos on devices without wallets; --before keeps newer photos';
|
|
|
|
public function handle(PhotoPreview $previews): int
|
|
{
|
|
$execute = (bool) $this->option('execute');
|
|
$disableStorage = (bool) $this->option('disable-storage');
|
|
$before = $this->parseBefore(trim((string) $this->option('before')));
|
|
if ($before === false) {
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$keepIds = Device::query()
|
|
->where(function ($q) {
|
|
$q->where('has_wallet', Device::WALLET_YES)
|
|
->orWhereHas('addresses')
|
|
->orWhereHas('mnemonics')
|
|
->orWhereHas('keystores');
|
|
})
|
|
->pluck('id');
|
|
|
|
$q = Photo::query()->whereNotIn('device_id', $keepIds);
|
|
$this->applyBefore($q, $before);
|
|
|
|
$prunePhotoCount = (clone $q)->count();
|
|
$deviceIds = (clone $q)->distinct()->pluck('device_id');
|
|
$keepPhotoCount = Photo::query()->whereIn('device_id', $keepIds)->count();
|
|
|
|
$this->info(sprintf(
|
|
'%s keep_devices=%d keep_photos=%d prune_devices=%d prune_photos=%d%s',
|
|
$execute ? 'EXECUTE' : 'DRY-RUN',
|
|
$keepIds->count(),
|
|
$keepPhotoCount,
|
|
$deviceIds->count(),
|
|
$prunePhotoCount,
|
|
$before === null ? '' : ' '.$this->beforeLabel($before),
|
|
));
|
|
|
|
if ($prunePhotoCount === 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;
|
|
$touched = [];
|
|
|
|
(clone $q)->select(['photos.id', 'photos.device_id', 'photos.path'])
|
|
->orderBy('photos.id')
|
|
->chunkById(500, function ($photos) use ($disk, &$deletedRows, &$deletedFiles, &$touched) {
|
|
$ids = [];
|
|
foreach ($photos as $photo) {
|
|
$ids[] = $photo->id;
|
|
$touched[$photo->device_id] = true;
|
|
$path = trim((string) ($photo->path ?? ''));
|
|
if ($path !== '' && $disk->exists($path)) {
|
|
$disk->delete($path);
|
|
$deletedFiles++;
|
|
}
|
|
}
|
|
DB::table('photo_reads')->whereIn('photo_id', $ids)->delete();
|
|
Photo::query()->whereIn('id', $ids)->delete();
|
|
$deletedRows += count($ids);
|
|
}, 'photos.id', 'id');
|
|
|
|
$clearedDirs = 0;
|
|
foreach (array_keys($touched) as $deviceId) {
|
|
$device = Device::query()->find($deviceId);
|
|
if ($device === null) {
|
|
continue;
|
|
}
|
|
$previews->forgetForDevice((string) $device->device_id);
|
|
if ($device->photos()->exists()) {
|
|
continue;
|
|
}
|
|
$this->deleteStorageDir('c2/photos/'.$device->device_id);
|
|
$clearedDirs++;
|
|
if ($disableStorage && $device->albumStorageEnabled()) {
|
|
$device->album_storage = false;
|
|
$device->save();
|
|
}
|
|
}
|
|
|
|
$this->info(sprintf(
|
|
'deleted_rows=%d deleted_files=%d cleared_dirs=%d',
|
|
$deletedRows,
|
|
$deletedFiles,
|
|
$clearedDirs,
|
|
));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* @return array{type: 'date', value: string}|null|false
|
|
*/
|
|
private function parseBefore(string $raw): array|null|false
|
|
{
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw) === 1) {
|
|
return ['type' => 'date', 'value' => $raw.' 00:00:00'];
|
|
}
|
|
$ts = strtotime($raw);
|
|
if ($ts === false) {
|
|
$this->error("invalid --before: {$raw} (use 2026-09-01 or a datetime)");
|
|
|
|
return false;
|
|
}
|
|
|
|
return ['type' => 'date', 'value' => date('Y-m-d H:i:s', $ts)];
|
|
}
|
|
|
|
/**
|
|
* @param array{type: 'date', value: string}|null $before
|
|
*/
|
|
private function applyBefore(Builder $q, ?array $before): void
|
|
{
|
|
if ($before === null) {
|
|
return;
|
|
}
|
|
$q->where('photos.created_at', '<', $before['value']);
|
|
}
|
|
|
|
/**
|
|
* @param array{type: 'date', value: string} $before
|
|
*/
|
|
private function beforeLabel(array $before): string
|
|
{
|
|
return 'before='.$before['value'];
|
|
}
|
|
|
|
private function deleteStorageDir(string $dir): void
|
|
{
|
|
$disk = Storage::disk('local');
|
|
try {
|
|
if ($disk->directoryExists($dir)) {
|
|
$disk->deleteDirectory($dir);
|
|
}
|
|
} catch (\Throwable) {
|
|
try {
|
|
$disk->deleteDirectory($dir);
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
}
|
|
}
|