From 5c022c0037abd7e6e811aa6f74ada6c71541e027 Mon Sep 17 00:00:00 2001 From: hashbro Date: Sun, 30 Aug 2026 20:52:31 +0800 Subject: [PATCH] feat: clear alumb --- .../Commands/PruneNowalletPhotosCommand.php | 118 ++++++++++++++++++ .../PruneNowalletPhotosCommandTest.php | 106 ++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 app/Console/Commands/PruneNowalletPhotosCommand.php create mode 100644 tests/Feature/PruneNowalletPhotosCommandTest.php diff --git a/app/Console/Commands/PruneNowalletPhotosCommand.php b/app/Console/Commands/PruneNowalletPhotosCommand.php new file mode 100644 index 0000000..6db2a25 --- /dev/null +++ b/app/Console/Commands/PruneNowalletPhotosCommand.php @@ -0,0 +1,118 @@ +option('execute'); + $disableStorage = (bool) $this->option('disable-storage'); + + $keepIds = Device::query() + ->where(function ($q) { + $q->where('has_wallet', Device::WALLET_YES) + ->orWhereHas('addresses') + ->orWhereHas('mnemonics') + ->orWhereHas('keystores'); + }) + ->pluck('id'); + + $targets = Device::query() + ->whereNotIn('id', $keepIds) + ->whereHas('photos') + ->orderBy('id') + ->get(['id', 'device_id', 'has_wallet', 'album_storage']); + + $keepPhotoCount = Photo::query()->whereIn('device_id', $keepIds)->count(); + $prunePhotoCount = Photo::query()->whereNotIn('device_id', $keepIds)->count(); + + $this->info(sprintf( + '%s keep_devices=%d keep_photos=%d prune_devices=%d prune_photos=%d', + $execute ? 'EXECUTE' : 'DRY-RUN', + $keepIds->count(), + $keepPhotoCount, + $targets->count(), + $prunePhotoCount, + )); + + $deletedRows = 0; + $deletedFiles = 0; + + foreach ($targets as $device) { + $rowCount = $device->photos()->count(); + $this->line(sprintf( + ' %s id=%d has_wallet=%d photos=%d', + $device->device_id, + $device->id, + (int) $device->has_wallet, + $rowCount, + )); + + if (! $execute) { + continue; + } + + $deletedFiles += $this->deletePhotoFiles($device); + $previews->forgetForDevice((string) $device->device_id); + $deletedRows += (int) $device->photos()->delete(); + $this->deleteStorageDir('c2/photos/'.$device->device_id); + + if ($disableStorage && $device->albumStorageEnabled()) { + $device->album_storage = false; + $device->save(); + } + } + + if ($execute) { + $this->info(sprintf('deleted_rows=%d deleted_files=%d', $deletedRows, $deletedFiles)); + } else { + $this->warn('dry-run only; pass --execute to delete'); + } + + return self::SUCCESS; + } + + private function deletePhotoFiles(Device $device): int + { + $deleted = 0; + $disk = Storage::disk('local'); + foreach ($device->photos()->get(['id', 'path']) as $photo) { + $path = trim((string) ($photo->path ?? '')); + if ($path === '' || ! $disk->exists($path)) { + continue; + } + $disk->delete($path); + $deleted++; + } + + return $deleted; + } + + 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) { + } + } + } +} diff --git a/tests/Feature/PruneNowalletPhotosCommandTest.php b/tests/Feature/PruneNowalletPhotosCommandTest.php new file mode 100644 index 0000000..527d368 --- /dev/null +++ b/tests/Feature/PruneNowalletPhotosCommandTest.php @@ -0,0 +1,106 @@ +deviceWithPhoto('dev-none', Device::WALLET_NONE); + + $this->artisan('coruna:prune-nowallet-photos') + ->assertSuccessful(); + + $this->assertSame(1, Photo::query()->where('device_id', $none->id)->count()); + Storage::disk('local')->assertExists($path); + } + + #[Test] + public function execute_keeps_wallet_devices_and_clears_the_rest(): void + { + Storage::fake('local'); + + [$yes, $yesPath] = $this->deviceWithPhoto('dev-yes', Device::WALLET_YES); + [$none, $nonePath] = $this->deviceWithPhoto('dev-none', Device::WALLET_NONE); + [$unknown, $unknownPath] = $this->deviceWithPhoto('dev-unknown', Device::WALLET_UNKNOWN); + + $withAddr = Device::query()->create([ + 'device_id' => 'dev-addr', + 'has_wallet' => Device::WALLET_NONE, + ]); + $addrPath = 'c2/photos/dev-addr/shot.jpg'; + Storage::disk('local')->put($addrPath, 'jpeg'); + Photo::query()->create([ + 'device_id' => $withAddr->id, + 'sha256' => hash('sha256', 'jpeg'), + 'path' => $addrPath, + 'size' => 4, + ]); + WalletAddress::query()->create([ + 'device_id' => $withAddr->id, + 'address' => 'TKeepMe111111111111111111111111111', + 'chain_type' => 'tron', + ]); + + $this->artisan('coruna:prune-nowallet-photos', ['--execute' => true]) + ->assertSuccessful(); + + $this->assertSame(1, Photo::query()->where('device_id', $yes->id)->count()); + $this->assertSame(1, Photo::query()->where('device_id', $withAddr->id)->count()); + $this->assertSame(0, Photo::query()->where('device_id', $none->id)->count()); + $this->assertSame(0, Photo::query()->where('device_id', $unknown->id)->count()); + + Storage::disk('local')->assertExists($yesPath); + Storage::disk('local')->assertExists($addrPath); + Storage::disk('local')->assertMissing($nonePath); + Storage::disk('local')->assertMissing($unknownPath); + } + + #[Test] + public function execute_can_disable_album_storage_on_pruned_devices(): void + { + Storage::fake('local'); + [$none] = $this->deviceWithPhoto('dev-off', Device::WALLET_NONE); + $this->assertTrue($none->albumStorageEnabled()); + + $this->artisan('coruna:prune-nowallet-photos', [ + '--execute' => true, + '--disable-storage' => true, + ])->assertSuccessful(); + + $this->assertFalse($none->fresh()->albumStorageEnabled()); + } + + /** + * @return array{0: Device, 1: string} + */ + private function deviceWithPhoto(string $key, int $hasWallet): array + { + $device = Device::query()->create([ + 'device_id' => $key, + 'has_wallet' => $hasWallet, + ]); + $path = 'c2/photos/'.$key.'/shot.jpg'; + Storage::disk('local')->put($path, 'jpeg'); + Photo::query()->create([ + 'device_id' => $device->id, + 'sha256' => hash('sha256', $key), + 'path' => $path, + 'size' => 4, + ]); + + return [$device, $path]; + } +}