190 lines
6.9 KiB
PHP
190 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Commands;
|
|
|
|
use CodeIgniter\CLI\BaseCommand;
|
|
use CodeIgniter\CLI\CLI;
|
|
|
|
/**
|
|
* Purge objects under an S3 prefix that matches a storage module folder
|
|
* (e.g. claim_files → keys claim_files/* in AWS_FILE_UPLOAD_BUCKET).
|
|
*
|
|
* Usage:
|
|
* php spark storage:purge-s3 --module claim_files
|
|
* php spark storage:purge-s3 --module claim_files --execute
|
|
* php spark storage:purge-s3 --module claim_files,excel --limit 100 --execute
|
|
*/
|
|
class StoragePurgeS3 extends BaseCommand
|
|
{
|
|
protected $group = 'Storage';
|
|
protected $name = 'storage:purge-s3';
|
|
protected $description = 'List/delete S3 objects under a module prefix (dry-run by default).';
|
|
protected $usage = 'storage:purge-s3 --module <folder>[,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<string>
|
|
*/
|
|
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');
|
|
}
|
|
}
|