|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Statamic\Console\Commands; |
| 4 | + |
| 5 | +use Illuminate\Console\Command; |
| 6 | +use Illuminate\Support\Collection; |
| 7 | +use Illuminate\Support\Str; |
| 8 | +use Statamic\Assets\AssetContainer as AssetsContainer; |
| 9 | +use Statamic\Console\RunsInPlease; |
| 10 | +use Statamic\Facades\AssetContainer; |
| 11 | + |
| 12 | +use function Laravel\Prompts\progress; |
| 13 | + |
| 14 | +class AssetsMetaClean extends Command |
| 15 | +{ |
| 16 | + use RunsInPlease; |
| 17 | + |
| 18 | + protected $signature = 'statamic:assets:meta-clean |
| 19 | + { container? : Handle of a container } |
| 20 | + { --dry-run : List orphaned files without deleting }'; |
| 21 | + |
| 22 | + protected $description = 'Clean orphaned asset metadata files'; |
| 23 | + |
| 24 | + public function handle(): int |
| 25 | + { |
| 26 | + $containers = $this->getContainers()->keyBy->handle(); |
| 27 | + |
| 28 | + $orphanedMetaFilesByContainer = $containers->map(fn ($container) => $this->getOrphanedMetaFiles($container)); |
| 29 | + $orphanedMetaFilesCount = $orphanedMetaFilesByContainer->sum->count(); |
| 30 | + |
| 31 | + if ($orphanedMetaFilesCount === 0) { |
| 32 | + $this->components->info('No orphaned metadata files were found.'); |
| 33 | + |
| 34 | + return self::SUCCESS; |
| 35 | + } |
| 36 | + |
| 37 | + $flatOrphanedMetaFiles = $orphanedMetaFilesByContainer |
| 38 | + ->flatMap(fn (Collection $paths, string $container) => $paths->map(fn ($path) => [ |
| 39 | + 'container' => $container, |
| 40 | + 'path' => $path, |
| 41 | + ])) |
| 42 | + ->values(); |
| 43 | + |
| 44 | + if ($this->option('dry-run')) { |
| 45 | + $this->components->warn("Found {$orphanedMetaFilesCount} orphaned metadata ".Str::plural('file', $orphanedMetaFilesCount)); |
| 46 | + |
| 47 | + $flatOrphanedMetaFiles->each(function (array $metaFile) { |
| 48 | + $this->line("[{$metaFile['container']}] {$metaFile['path']}"); |
| 49 | + }); |
| 50 | + |
| 51 | + return self::SUCCESS; |
| 52 | + } |
| 53 | + |
| 54 | + progress( |
| 55 | + label: 'Deleting orphaned asset metadata...', |
| 56 | + steps: $flatOrphanedMetaFiles, |
| 57 | + callback: function (array $metaFile, $progress) use ($containers) { |
| 58 | + $containers->get($metaFile['container'])->disk()->delete($metaFile['path']); |
| 59 | + $progress->advance(); |
| 60 | + } |
| 61 | + ); |
| 62 | + |
| 63 | + $orphanedMetaFilesByContainer->each(function (Collection $metaFiles, string $container) use ($containers) { |
| 64 | + $this->deleteEmptyMetaDirectories($containers->get($container), $metaFiles); |
| 65 | + }); |
| 66 | + |
| 67 | + $this->components->warn("Deleted {$orphanedMetaFilesCount} orphaned metadata ".Str::plural('file', $orphanedMetaFilesCount)); |
| 68 | + |
| 69 | + return self::SUCCESS; |
| 70 | + } |
| 71 | + |
| 72 | + private function getContainers(): Collection |
| 73 | + { |
| 74 | + if (! $container = $this->argument('container')) { |
| 75 | + return AssetContainer::all(); |
| 76 | + } |
| 77 | + |
| 78 | + return collect([AssetContainer::findOrFail($container)]); |
| 79 | + } |
| 80 | + |
| 81 | + private function getOrphanedMetaFiles(AssetsContainer $container): Collection |
| 82 | + { |
| 83 | + $assetPaths = $container->files()->flip(); |
| 84 | + |
| 85 | + return $container->metaFiles() |
| 86 | + ->filter(fn (string $path) => Str::endsWith($path, '.yaml')) |
| 87 | + ->reject(fn (string $path) => $assetPaths->has($this->metaPathToAssetPath($path))) |
| 88 | + ->values(); |
| 89 | + } |
| 90 | + |
| 91 | + private function metaPathToAssetPath(string $metaPath): string |
| 92 | + { |
| 93 | + $pathWithoutYamlExtension = Str::endsWith($metaPath, '.yaml') |
| 94 | + ? substr($metaPath, 0, -5) |
| 95 | + : $metaPath; |
| 96 | + |
| 97 | + $pathWithoutMetaDirectory = str_replace('/.meta/', '/', $pathWithoutYamlExtension); |
| 98 | + |
| 99 | + if (Str::startsWith($pathWithoutMetaDirectory, '.meta/')) { |
| 100 | + $pathWithoutMetaDirectory = Str::replaceFirst('.meta/', '', $pathWithoutMetaDirectory); |
| 101 | + } |
| 102 | + |
| 103 | + return ltrim($pathWithoutMetaDirectory, '/'); |
| 104 | + } |
| 105 | + |
| 106 | + private function deleteEmptyMetaDirectories(AssetsContainer $container, Collection $metaFiles): void |
| 107 | + { |
| 108 | + $metaDirectories = $metaFiles |
| 109 | + ->map(fn (string $metaFile) => dirname($metaFile)) |
| 110 | + ->unique() |
| 111 | + ->sortByDesc(fn (string $directory) => substr_count($directory, '/')); |
| 112 | + |
| 113 | + $metaDirectories->each(function (string $metaDirectory) use ($container) { |
| 114 | + $disk = $container->disk(); |
| 115 | + |
| 116 | + if (! $disk->exists($metaDirectory)) { |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + if ($disk->isEmpty($metaDirectory)) { |
| 121 | + $disk->filesystem()->deleteDirectory($metaDirectory); |
| 122 | + } |
| 123 | + }); |
| 124 | + } |
| 125 | +} |
0 commit comments