Add 100+ automated unit tests from .expect file specifications Add session system test Add rsx:constants:regenerate command test Add rsx:logrotate command test Add rsx:clean command test Add rsx:manifest:stats command test Add model enum system test Add model mass assignment prevention test Add rsx:check command test Add migrate:status command test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
206 lines
6.7 KiB
PHP
206 lines
6.7 KiB
PHP
<?php
|
|
/**
|
|
* CODING CONVENTION:
|
|
* This file follows the coding convention where variable_names and function_names
|
|
* use snake_case (underscore_wherever_possible).
|
|
*/
|
|
|
|
namespace App\RSpade\Commands\Rsx;
|
|
|
|
use App\Console\Commands\FrameworkDeveloperCommand;
|
|
use App\Models\File_Storage_Model;
|
|
use Rsx\Models\File_Attachment_Model;
|
|
use App\RSpade\Core\Locks\RsxLocks;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Storage Cleanup Command
|
|
* ========================
|
|
*
|
|
* PURPOSE:
|
|
* Force cleanup of orphaned storage and attachments.
|
|
*
|
|
* FRAMEWORK DEVELOPER ONLY:
|
|
* This command is hidden unless IS_FRAMEWORK_DEVELOPER=true in .env
|
|
*
|
|
* CLEANUP OPERATIONS:
|
|
* 1. Delete orphaned attachments (no fileable, older than orphan-age hours)
|
|
* 2. Delete orphaned storage (no attachments)
|
|
* 3. Delete physical files for deleted storage
|
|
*
|
|
* SAFETY:
|
|
* - Supports --dry-run to preview operations
|
|
* - Requires --force to skip confirmation
|
|
* - Uses file write lock to prevent race conditions
|
|
*/
|
|
class RsxStorageCleanupCommand extends FrameworkDeveloperCommand
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'rsx:storage:cleanup
|
|
{--dry-run : Show what would be deleted without deleting}
|
|
{--force : Skip confirmation prompts}
|
|
{--orphan-age=24 : Hours before orphaned attachment is deleted}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Force cleanup of orphaned storage and attachments';
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*/
|
|
public function handle()
|
|
{
|
|
$dry_run = $this->option('dry-run');
|
|
$force = $this->option('force');
|
|
$orphan_age = (int)$this->option('orphan-age');
|
|
|
|
$this->info('');
|
|
$this->info('File Storage Cleanup');
|
|
$this->info('====================');
|
|
$this->info('');
|
|
|
|
if ($dry_run) {
|
|
$this->warn('DRY RUN MODE - No files will be deleted');
|
|
$this->info('');
|
|
}
|
|
|
|
// Find orphaned attachments
|
|
$orphaned_attachments = File_Attachment_Model::whereNull('fileable_type')
|
|
->whereNull('fileable_id')
|
|
->where('created_at', '<', now()->subHours($orphan_age))
|
|
->get();
|
|
|
|
// Find orphaned storage
|
|
$orphaned_storage = DB::table('_file_storage')
|
|
->leftJoin('file_attachments', 'file_storage.id', '=', 'file_attachments.file_storage_id')
|
|
->whereNull('file_attachments.id')
|
|
->select('file_storage.*')
|
|
->get();
|
|
|
|
$this->info("Found {$orphaned_attachments->count()} orphaned attachments (>{$orphan_age}h)");
|
|
$this->info("Found {$orphaned_storage->count()} orphaned storage records");
|
|
$this->info('');
|
|
|
|
if ($orphaned_attachments->isEmpty() && $orphaned_storage->isEmpty()) {
|
|
$this->info('Nothing to clean up');
|
|
return 0;
|
|
}
|
|
|
|
// Show details
|
|
if ($orphaned_attachments->isNotEmpty()) {
|
|
$this->info('Orphaned Attachments:');
|
|
foreach ($orphaned_attachments->take(10) as $attachment) {
|
|
$age_hours = round($attachment->created_at->diffInHours(now()), 1);
|
|
$this->info(" - {$attachment->file_name} (key: " . substr($attachment->key, 0, 16) . "..., age: {$age_hours}h)");
|
|
}
|
|
if ($orphaned_attachments->count() > 10) {
|
|
$remaining = $orphaned_attachments->count() - 10;
|
|
$this->info(" ... and {$remaining} more");
|
|
}
|
|
$this->info('');
|
|
}
|
|
|
|
if ($orphaned_storage->isNotEmpty()) {
|
|
$this->info('Orphaned Storage:');
|
|
foreach ($orphaned_storage->take(10) as $storage) {
|
|
$storage_obj = File_Storage_Model::find($storage->id);
|
|
$size = $storage_obj ? $storage_obj->get_human_size() : 'unknown';
|
|
$this->info(" - " . substr($storage->hash, 0, 24) . "... ({$size})");
|
|
}
|
|
if ($orphaned_storage->count() > 10) {
|
|
$remaining = $orphaned_storage->count() - 10;
|
|
$this->info(" ... and {$remaining} more");
|
|
}
|
|
$this->info('');
|
|
}
|
|
|
|
// Confirmation
|
|
if (!$dry_run && !$force) {
|
|
if (!$this->confirm('Proceed with cleanup?', false)) {
|
|
$this->info('Cleanup cancelled');
|
|
return 0;
|
|
}
|
|
$this->info('');
|
|
}
|
|
|
|
if ($dry_run) {
|
|
$this->info('[DRY RUN] Would delete:');
|
|
$this->info(" - {$orphaned_attachments->count()} orphaned attachments");
|
|
$this->info(" - {$orphaned_storage->count()} orphaned storage records");
|
|
$this->info('');
|
|
return 0;
|
|
}
|
|
|
|
// Acquire file write lock
|
|
$lock = RsxLocks::get_lock(
|
|
RsxLocks::SERVER_LOCK,
|
|
RsxLocks::LOCK_FILE_WRITE,
|
|
RsxLocks::WRITE_LOCK,
|
|
60
|
|
);
|
|
|
|
try {
|
|
$deleted_attachments = 0;
|
|
$deleted_storage = 0;
|
|
$deleted_bytes = 0;
|
|
|
|
// Delete orphaned attachments
|
|
foreach ($orphaned_attachments as $attachment) {
|
|
$attachment->delete();
|
|
$deleted_attachments++;
|
|
}
|
|
|
|
// Delete orphaned storage
|
|
foreach ($orphaned_storage as $storage_data) {
|
|
$storage = File_Storage_Model::find($storage_data->id);
|
|
if ($storage) {
|
|
$deleted_bytes += $storage->size;
|
|
|
|
// Delete physical file
|
|
$file_path = $storage->get_full_path();
|
|
if (file_exists($file_path)) {
|
|
unlink($file_path);
|
|
}
|
|
|
|
// Delete record
|
|
$storage->delete();
|
|
$deleted_storage++;
|
|
}
|
|
}
|
|
|
|
$this->info('[OK] Cleanup completed');
|
|
$this->info('');
|
|
$this->info(" Deleted Attachments: {$deleted_attachments}");
|
|
$this->info(" Deleted Storage: {$deleted_storage}");
|
|
$this->info(" Freed Disk Space: " . $this->format_bytes($deleted_bytes));
|
|
$this->info('');
|
|
|
|
return 0;
|
|
|
|
} finally {
|
|
RsxLocks::release_lock($lock);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format bytes to human-readable size
|
|
*/
|
|
protected function format_bytes($bytes)
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
|
$bytes /= 1024;
|
|
}
|
|
|
|
return round($bytes, 2) . ' ' . $units[$i];
|
|
}
|
|
}
|