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]; } }