|insurer_claim_form|all] [--limit 500] [--offset 0] [--execute]'; protected $options = [ '--module' => 'Target module/folder (default: writable_uploads). Use writable_uploads for all writable/uploads/* folders.', '--limit' => 'Max files per run (default: 500).', '--offset' => 'Start offset for phased batches (default: 0).', '--execute' => 'Actually upload files. Without this flag, command runs in dry-run mode.', ]; /** * @var array */ private array $staticPaths = [ 'insurer_claim_form' => WRITEPATH . 'insurer_claim_form', ]; public function run(array $params) { $moduleOpt = strtolower((string) (CLI::getOption('module') ?? 'writable_uploads')); $execute = CLI::getOption('execute') !== null; $dryRun = !$execute; $limit = max(1, (int) (CLI::getOption('limit') ?? 500)); $offset = max(0, (int) (CLI::getOption('offset') ?? 0)); $modulePaths = $this->buildModulePaths(); $selectedModules = $this->resolveModules($moduleOpt, $modulePaths); if ($selectedModules === []) { CLI::error('Invalid --module value. Allowed: writable_uploads, ' . implode(', ', array_keys($modulePaths)) . ', all'); return; } $storage = \Config\Services::getFileStorageService(false); if (!$storage->usesS3()) { CLI::error('FileStorageService is not in S3 mode. Set FILE_STORAGE_DRIVER=s3 before migration.'); return; } CLI::write($dryRun ? 'Running in DRY-RUN mode (no uploads).' : 'Running in EXECUTE mode (uploads enabled).', 'yellow'); CLI::write('Target bucket: ' . ($storage->getBucket() ?? 'N/A'), 'cyan'); CLI::newLine(); $totals = [ 'seen' => 0, 'uploaded' => 0, 'skipped_exists' => 0, 'skipped_missing' => 0, 'failed' => 0, ]; foreach ($selectedModules as $module) { $path = $modulePaths[$module]; CLI::write("Module: {$module}", 'green'); CLI::write("Path: {$path}"); if (!is_dir($path)) { CLI::write(' - Skipped: folder does not exist', 'yellow'); $totals['skipped_missing']++; CLI::newLine(); continue; } $files = $this->listFiles($path); $files = array_slice($files, $offset, $limit); CLI::write(' - Files in batch: ' . count($files)); foreach ($files as $fullPath) { $totals['seen']++; $fileName = basename($fullPath); if (!is_file($fullPath)) { $totals['skipped_missing']++; CLI::write(" - Missing: {$fileName}", 'yellow'); continue; } $alreadyExists = $storage->exists($path, $fileName); if ($alreadyExists) { $totals['skipped_exists']++; CLI::write(" - Exists in S3, skipped: {$fileName}", 'yellow'); continue; } if ($dryRun) { CLI::write(" - [dry-run] Would upload: {$fileName}"); continue; } $result = $storage->upload($fullPath, $path, $fileName); if ($result['success'] ?? false) { $totals['uploaded']++; CLI::write(" - Uploaded: {$fileName}", 'green'); } else { $totals['failed']++; CLI::write(" - Failed: {$fileName} | " . ($result['message'] ?? 'unknown error'), 'red'); } } CLI::newLine(); } CLI::write('Migration summary', 'cyan'); CLI::write(' Seen: ' . $totals['seen']); CLI::write(' Uploaded: ' . $totals['uploaded'], $totals['uploaded'] > 0 ? 'green' : 'white'); CLI::write(' Skipped (already in S3): ' . $totals['skipped_exists'], 'yellow'); CLI::write(' Skipped (missing/invalid): ' . $totals['skipped_missing'], 'yellow'); CLI::write(' Failed: ' . $totals['failed'], $totals['failed'] > 0 ? 'red' : 'white'); if ($dryRun) { CLI::newLine(); CLI::write('Dry-run complete. Re-run with --execute to perform uploads.', 'yellow'); } } /** * @return array */ private function resolveModules(string $moduleOpt, array $modulePaths): array { if ($moduleOpt === 'all') { return array_keys($modulePaths); } if ($moduleOpt === 'writable_uploads') { $uploadRoot = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR; $modules = []; foreach ($modulePaths as $module => $path) { $normalized = str_replace('\\', '/', $path); $normalizedRoot = str_replace('\\', '/', $uploadRoot); if (strpos($normalized, rtrim($normalizedRoot, '/')) === 0) { $modules[] = $module; } } return $modules; } $raw = array_filter(array_map('trim', explode(',', $moduleOpt))); $valid = []; foreach ($raw as $module) { if (array_key_exists($module, $modulePaths)) { $valid[] = $module; } } return array_values(array_unique($valid)); } /** * Build migratable paths: * - all direct subfolders in writable/uploads * - additional static paths (outside writable/uploads) * * @return array */ private function buildModulePaths(): array { $paths = []; $uploadRoot = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads'; $entries = @scandir($uploadRoot); if (is_array($entries)) { foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } $fullPath = $uploadRoot . DIRECTORY_SEPARATOR . $entry; if (is_dir($fullPath)) { $paths[strtolower($entry)] = $fullPath; } } } foreach ($this->staticPaths as $key => $path) { $paths[$key] = $path; } ksort($paths); return $paths; } /** * @return array */ private function listFiles(string $path): array { $entries = @scandir($path); if (!is_array($entries)) { return []; } $files = []; foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } $fullPath = rtrim($path, '/\\') . DIRECTORY_SEPARATOR . $entry; if (is_file($fullPath)) { $files[] = $fullPath; } } sort($files); return $files; } }