[,folder...] [--limit 0] [--offset 0] [--execute] [--list-modules]'; protected $options = [ '--module' => 'Required. Module folder name(s), comma-separated (e.g. claim_files).', '--limit' => 'Max objects to delete per module (0 = all, default: 0).', '--offset' => 'Skip first N keys per module (default: 0).', '--execute' => 'Actually delete. Without this flag, dry-run only.', '--list-modules' => 'List known local upload module folder names and exit.', ]; public function run(array $params) { if (CLI::getOption('list-modules') !== null) { $this->printKnownModules(); return EXIT_SUCCESS; } $moduleOpt = trim((string) (CLI::getOption('module') ?? ($params[0] ?? ''))); if ($moduleOpt === '') { CLI::error('Missing --module. Example: php spark storage:purge-s3 --module claim_files'); CLI::write('Tip: php spark storage:purge-s3 --list-modules'); return EXIT_ERROR; } $execute = CLI::getOption('execute') !== null; $dryRun = ! $execute; $limit = max(0, (int) (CLI::getOption('limit') ?? 0)); $offset = max(0, (int) (CLI::getOption('offset') ?? 0)); $modules = $this->parseModules($moduleOpt); if ($modules === []) { CLI::error('No valid module names after parsing. Use basename-only folder names (no path).'); return EXIT_ERROR; } $storage = \Config\Services::getFileStorageService(false); if (! $storage->usesS3()) { CLI::error('FileStorageService is not in S3 mode. Set FILE_STORAGE_DRIVER=s3 and AWS_FILE_UPLOAD_BUCKET.'); return EXIT_ERROR; } $s3 = \Config\Services::getS3Service(false); $bucket = $storage->getBucket(); CLI::write($dryRun ? 'Running in DRY-RUN mode (no deletes).' : 'Running in EXECUTE mode (deletes enabled).', 'yellow'); CLI::write('Bucket: ' . ($bucket ?? 'N/A'), 'cyan'); CLI::write('Modules: ' . implode(', ', $modules)); CLI::newLine(); $totals = [ 'listed' => 0, 'deleted' => 0, 'failed' => 0, ]; foreach ($modules as $module) { $prefix = $module . '/'; CLI::write("Module: {$module}", 'green'); CLI::write(" Prefix: {$prefix}"); $listed = $s3->listFiles($prefix, $bucket); if (! ($listed['success'] ?? false)) { CLI::error(' List failed: ' . ($listed['message'] ?? 'unknown')); $totals['failed']++; CLI::newLine(); continue; } $keys = []; foreach ($listed['files'] ?? [] as $file) { $key = (string) ($file['key'] ?? ''); if ($key === '' || substr($key, -1) === '/') { continue; // skip empty / folder markers } $keys[] = $key; } sort($keys); $batch = array_slice($keys, $offset, $limit > 0 ? $limit : null); CLI::write(' Objects under prefix: ' . count($keys)); CLI::write(' In this batch: ' . count($batch)); if ($batch === []) { CLI::write(' Nothing to purge.', 'yellow'); CLI::newLine(); continue; } foreach ($batch as $key) { $totals['listed']++; if ($dryRun) { CLI::write(" - [dry-run] Would delete: {$key}"); continue; } $result = $s3->delete($key, $bucket); if ($result['success'] ?? false) { $totals['deleted']++; CLI::write(" - Deleted: {$key}", 'green'); } else { $totals['failed']++; CLI::write(' - Failed: ' . $key . ' | ' . ($result['message'] ?? ''), 'red'); } } CLI::newLine(); } CLI::write('Purge summary', 'cyan'); CLI::write(' Listed (batch): ' . $totals['listed']); CLI::write(' Deleted: ' . $totals['deleted'], $totals['deleted'] > 0 ? 'green' : 'white'); CLI::write(' Failed: ' . $totals['failed'], $totals['failed'] > 0 ? 'red' : 'white'); if ($dryRun) { CLI::newLine(); CLI::write('Dry-run complete. Re-run with --execute to delete these objects.', 'yellow'); } return $totals['failed'] > 0 ? EXIT_ERROR : EXIT_SUCCESS; } /** * @return list */ private function parseModules(string $moduleOpt): array { $raw = array_filter(array_map('trim', explode(',', strtolower($moduleOpt)))); $out = []; foreach ($raw as $module) { $module = basename(str_replace('\\', '/', $module)); if ($module === '' || $module === '.' || $module === '..') { continue; } if (strpos($module, '/') !== false || preg_match('/[^a-z0-9_\-]/i', $module)) { CLI::write(" Skipping invalid module name: {$module}", 'yellow'); continue; } $out[] = $module; } return array_values(array_unique($out)); } private function printKnownModules(): void { $uploadRoot = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads'; CLI::write('Known local upload folders (S3 prefix = folder name):', 'cyan'); $entries = @scandir($uploadRoot); if (! is_array($entries)) { CLI::write(' (uploads root missing)'); return; } $names = []; foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } if (is_dir($uploadRoot . DIRECTORY_SEPARATOR . $entry)) { $names[] = $entry; } } sort($names); foreach ($names as $name) { CLI::write(' ' . $name); } CLI::newLine(); CLI::write('Example: php spark storage:purge-s3 --module claim_files'); } }