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); } } $this->info(sprintf( '%s cutoff=%s streaming batches of 500 (no pre-count)', $execute ? 'EXECUTE' : 'DRY-RUN', $date, )); $disk = Storage::disk('local'); $seen = 0; $deletedRows = 0; $deletedFiles = 0; $deviceIds = []; $this->eachPhotoBatch($q, 500, function (Collection $photos) use ( $execute, $disk, &$seen, &$deletedRows, &$deletedFiles, &$deviceIds, ): void { $ids = []; foreach ($photos as $photo) { $ids[] = $photo->id; $deviceIds[$photo->device_id] = true; $seen++; if (! $execute) { continue; } $path = trim((string) ($photo->path ?? '')); if ($path !== '' && $disk->delete($path)) { $deletedFiles++; } } if ($execute && $ids !== []) { DB::table('photo_reads')->whereIn('photo_id', $ids)->delete(); Photo::query()->whereIn('id', $ids)->delete(); $deletedRows += count($ids); } $this->info($execute ? sprintf(' deleted_rows=%d deleted_files=%d', $deletedRows, $deletedFiles) : sprintf(' matched=%d', $seen)); }); if ($seen === 0) { $this->info('nothing to prune'); return self::SUCCESS; } if (! $execute) { $this->info(sprintf('dry-run matched_photos=%d devices=%d; pass --execute to delete', $seen, count($deviceIds))); return self::SUCCESS; } foreach (array_keys($deviceIds) as $deviceId) { $device = Device::query()->find($deviceId); if ($device) { $previews->forgetForDevice((string) $device->device_id); } } $this->info(sprintf( 'done deleted_rows=%d deleted_files=%d devices=%d', $deletedRows, $deletedFiles, count($deviceIds), )); return self::SUCCESS; } /** * @param callable(Collection): void $fn */ private function eachPhotoBatch(Builder $q, int $size, callable $fn): void { $lastId = 0; while (true) { $photos = (clone $q) ->select(['photos.id', 'photos.device_id', 'photos.path']) ->where('photos.id', '>', $lastId) ->orderBy('photos.id') ->limit($size) ->get(); if ($photos->isEmpty()) { return; } $fn($photos); $lastId = (int) $photos->last()->id; } } }