info('Starting Orphaned Media Watcher...'); $dryRun = $this->option('dry-run'); // 1. Cleanup JIT cache (safe to delete anytime) $cachePath = storage_path('app/public/media/cache'); if (is_dir($cachePath)) { $this->info('Cleaning up JIT cache...'); if ($dryRun) { $this->info('Dry-run: Would delete directory ' . $cachePath); } else { File::deleteDirectory($cachePath); mkdir($cachePath, 0755, true); } } // 2. Identification of orphaned originals $this->info('Scanning for orphaned media files in database...'); $mediaRecords = Media::all(); $referencedMediaIds = $this->getReferencedMediaIds(); $orphansCount = 0; foreach ($mediaRecords as $media) { if (!in_array($media->id, $referencedMediaIds)) { $orphansCount++; $this->warn("Orphaned media found: ID {$media->id}, Filename: {$media->filename}"); if ($dryRun) { $this->info("Dry-run: Would delete media ID {$media->id} and file {$media->path}"); } else { Storage::disk('public')->delete($media->path); $media->delete(); $this->info("Deleted media ID {$media->id}"); } } } if ($orphansCount === 0) { $this->info("\nNo orphaned media files found."); } else { $this->info("\nProcessed {$orphansCount} orphaned media files."); } $this->info('Media cleanup completed.'); return 0; } /** * Scans all pages for media IDs. */ protected function getReferencedMediaIds(): array { $ids = []; $pages = Page::all(); foreach ($pages as $page) { $content = $page->content; // Array due to cast if (is_array($content)) { $ids = array_merge($ids, $this->scanBlocksForMediaIds($content)); } } return array_unique($ids); } /** * Recursively scan blocks for media IDs. */ protected function scanBlocksForMediaIds(array $blocks): array { $ids = []; foreach ($blocks as $block) { if (isset($block['type']) && $block['type'] === 'media' && isset($block['media_id'])) { $ids[] = (int) $block['media_id']; } // In case of nested blocks in the future if (isset($block['children']) && is_array($block['children'])) { $ids = array_merge($ids, $this->scanBlocksForMediaIds($block['children'])); } } return $ids; } }