nhance/app/Commands/StoragePurgeS3.php
2026-07-15 15:49:13 +05:30

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');
}
}