From 67f340d23d7aace0cb82b3815a91066cbde6a796 Mon Sep 17 00:00:00 2001 From: denkfabrik-li <274324701+denkfabrik-li@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:49:24 +0200 Subject: [PATCH] Keep preview renditions out of the orphan-file scan The orphan scanner skips derived artifacts by path prefix, but the list was a hard-coded ['thumbnails/', 'zips/'] that never learned about 'previews/'. ImageRendition::Preview caches under previews/ (and previews/external/) on the local files disk, so every cached preview was reported as an orphan: offered for import on the orphans screen, and deleted by the purge command once past the grace period. An imported preview also became a File row pointing at a path the rendition cache owns -- destroyed the moment its source file was deleted or the cache was flushed. Derive the rendition prefixes from ImageRendition::cases() rather than repeating them, so a future rendition can't be forgotten here the way previews were; 'zips/' (the download-bundle job's) stays as it was. --- app/Modules/Files/OrphanFileScanner.php | 33 +++++++++++++++++++------ tests/Feature/Files/OrphanFilesTest.php | 2 ++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/Modules/Files/OrphanFileScanner.php b/app/Modules/Files/OrphanFileScanner.php index 3712a11a..fddc6e80 100644 --- a/app/Modules/Files/OrphanFileScanner.php +++ b/app/Modules/Files/OrphanFileScanner.php @@ -6,6 +6,7 @@ namespace App\Modules\Files; use App\Models\User; use App\Modules\Files\Models\File; +use App\Modules\Files\Thumbnails\ImageRendition; use App\Modules\Files\Uploads\UploadExtensionPolicy; use App\Modules\Platform\Settings\ExternalStorageConfigApplier; use App\Modules\Platform\Settings\ExternalStorageSettings; @@ -20,13 +21,6 @@ use Illuminate\Support\Facades\Storage; */ class OrphanFileScanner { - // Derived artifacts written by FileThumbnailController and - // BuildZipDownloadJob respectively — never orphaned uploads, so - // never candidates regardless of what's in the files table. - // Thumbnails are always local; zips would be too if that job ever - // ran against 'files_external', so the exclusion applies per-disk. - private const EXCLUDED_PREFIXES = ['thumbnails/', 'zips/']; - public function __construct( private readonly UploadExtensionPolicy $extensionPolicy, private readonly ExternalStorageConfigApplier $externalStorage, @@ -160,9 +154,32 @@ class OrphanFileScanner )); } + /** + * Path prefixes that are derived artifacts, never orphaned uploads, so + * never candidates regardless of what's in the files table: every image + * rendition's cache directory (taken from ImageRendition so a new + * rendition can't be forgotten here — previews used to be) plus the + * download-bundle job's 'zips'. Thumbnails and previews are always local; + * zips would be too if that job ever ran against 'files_external', so the + * exclusion applies per-disk. + * + * @return list + */ + private function excludedPrefixes(): array + { + $prefixes = array_map( + static fn (ImageRendition $rendition): string => $rendition->directory().'/', + ImageRendition::cases(), + ); + + $prefixes[] = 'zips/'; + + return $prefixes; + } + private function isExcluded(string $path): bool { - foreach (self::EXCLUDED_PREFIXES as $prefix) { + foreach ($this->excludedPrefixes() as $prefix) { if (str_starts_with($path, $prefix)) { return true; } diff --git a/tests/Feature/Files/OrphanFilesTest.php b/tests/Feature/Files/OrphanFilesTest.php index c0ff2b48..0738673b 100644 --- a/tests/Feature/Files/OrphanFilesTest.php +++ b/tests/Feature/Files/OrphanFilesTest.php @@ -44,6 +44,8 @@ function orphanDelete(array $items): TestResponse test('scanning excludes derived-artifact prefixes and anything already claimed by a file row, including soft-deleted ones', function () { makeOrphanFile('2026/07/orphan.pdf'); makeOrphanFile('thumbnails/2026/07/some.jpg'); + makeOrphanFile('previews/2026/07/some.jpg'); + makeOrphanFile('previews/external/some.jpg'); makeOrphanFile('zips/some.zip'); $adopted = makeAdoptedFile($this->admin, '2026/07/adopted.pdf');