218 lines
7.4 KiB
PHP
218 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Commands;
|
|
|
|
use CodeIgniter\CLI\BaseCommand;
|
|
use CodeIgniter\CLI\CLI;
|
|
|
|
class MigrateLocalFilesToS3 extends BaseCommand
|
|
{
|
|
protected $group = 'Storage';
|
|
protected $name = 'storage:migrate-local-to-s3';
|
|
protected $description = 'One-time phased migration of local uploaded files to S3 using FileStorageService.';
|
|
protected $usage = 'storage:migrate-local-to-s3 [--module writable_uploads|<folder_name>|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<string, string>
|
|
*/
|
|
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<int, string>
|
|
*/
|
|
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<string, string>
|
|
*/
|
|
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<int, string>
|
|
*/
|
|
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;
|
|
}
|
|
}
|