From 9cb98a14e34162284e318073d3c13cac3e9587f7 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 7 Oct 2026 15:00:27 -0700 Subject: [PATCH 1/4] Add tests for keeping meta files out of the container listing cache --- tests/Assets/AssetContainerTest.php | 59 +++++++++++++++++++++++------ tests/Assets/AssetTest.php | 16 ++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/tests/Assets/AssetContainerTest.php b/tests/Assets/AssetContainerTest.php index 21256960450..8d1ccefa7b8 100644 --- a/tests/Assets/AssetContainerTest.php +++ b/tests/Assets/AssetContainerTest.php @@ -757,12 +757,8 @@ public function it_still_only_gets_the_folders_from_the_cache_and_blink_once_per #[Test] public function it_does_not_leak_stale_contents_state_across_calls_when_running_in_a_queue_worker() { - $cacheKey = 'asset-list-contents-test'; - - Cache::put($cacheKey, collect([ - 'a.txt' => ['type' => 'file', 'path' => 'a.txt', 'dirname' => ''], - '.meta/a.txt.yaml' => ['type' => 'file', 'path' => '.meta/a.txt.yaml', 'dirname' => '.meta'], - ])); + Storage::fake('test'); + Storage::disk('test')->put('.meta/a.txt.yaml', ''); $container = (new AssetContainer)->handle('test')->disk('test'); @@ -778,12 +774,7 @@ public function it_does_not_leak_stale_contents_state_across_calls_when_running_ ); // Simulate the next job seeing a different state on disk. - Cache::put($cacheKey, collect([ - 'a.txt' => ['type' => 'file', 'path' => 'a.txt', 'dirname' => ''], - '.meta/a.txt.yaml' => ['type' => 'file', 'path' => '.meta/a.txt.yaml', 'dirname' => '.meta'], - 'b.txt' => ['type' => 'file', 'path' => 'b.txt', 'dirname' => ''], - '.meta/b.txt.yaml' => ['type' => 'file', 'path' => '.meta/b.txt.yaml', 'dirname' => '.meta'], - ])); + Storage::disk('test')->put('.meta/b.txt.yaml', ''); // Simulate the job boundary: Laravel clears resolved facade instances // before every job. @@ -795,6 +786,50 @@ public function it_does_not_leak_stale_contents_state_across_calls_when_running_ ); } + #[Test] + public function it_does_not_cache_meta_files_or_directories_in_the_listing() + { + $disk = $this->mock(Filesystem::class); + $disk->shouldReceive('filesystem->getDriver->listContents') + ->with('/', true) + ->once() + ->andReturn(new DirectoryListing([ + new DirectoryAttributes('.meta'), + new FileAttributes('.meta/one.jpg.yaml'), + new FileAttributes('one.jpg'), + new DirectoryAttributes('one/.meta'), + new FileAttributes('one/.meta/two.jpg.yaml'), + new FileAttributes('one/two.jpg'), + ])); + + File::shouldReceive('disk')->with('test')->andReturn($disk); + + $container = (new AssetContainer)->handle('test')->disk('test'); + + $this->assertEquals(['one.jpg', 'one/two.jpg'], $container->files()->all()); + $this->assertEquals( + ['one', 'one.jpg', 'one/two.jpg'], + Cache::get('asset-list-contents-test')->keys()->all() + ); + } + + #[Test] + public function it_gets_meta_files_from_the_filesystem_when_the_listing_is_cached_without_them() + { + Cache::put('asset-list-contents-test', collect([ + 'a.txt' => ['type' => 'file', 'path' => 'a.txt', 'dirname' => ''], + ])); + + $this->assertEquals([ + '.meta/a.txt.yaml', + '.meta/b.txt.yaml', + 'nested/.meta/nested-a.txt.yaml', + 'nested/.meta/nested-b.txt.yaml', + 'nested/double-nested/.meta/double-nested-a.txt.yaml', + 'nested/double-nested/.meta/double-nested-b.txt.yaml', + ], $this->containerWithDisk()->metaFiles()->all()); + } + #[Test] public function it_gets_an_asset() { diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index c5fde7bc263..8c9219f4ebb 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -2904,6 +2904,22 @@ public function it_resolves_pending_original_meta_values_when_hydrating() ], $asset->getRawOriginal()); } + #[Test] + public function it_resolves_pending_original_meta_values_from_disk_when_the_listing_is_loaded() + { + Storage::disk('test')->put('path/to/test.txt', ''); + + // Warm the container listing before the meta file exists. + $this->container->files(); + + Storage::disk('test')->put('path/to/.meta/test.txt.yaml', "data:\n alfa: bravo"); + + $asset = (new Asset)->container($this->container)->path('path/to/test.txt'); + $asset->syncOriginal(); + + $this->assertEquals(['alfa' => 'bravo'], $asset->getOriginal('data')); + } + #[Test] #[DataProvider('warmPresetProvider')] public function it_gets_which_presets_to_warm($extension, $orientation, $cpEnabled, $expectedWarm) From d788c512ceda67fcad0cd37ed98389b7a9aff4f2 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 7 Oct 2026 15:01:35 -0700 Subject: [PATCH 2/4] Keep meta files out of the container listing cache --- src/Assets/Asset.php | 2 +- src/Assets/AssetContainerContents.php | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index a432150acbb..295b296dc27 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -315,7 +315,7 @@ public function metaPath() protected function metaExists() { - return $this->container()->metaFiles()->contains($this->metaPath()); + return $this->disk()->exists($this->metaPath()); } public function writeMeta($meta) diff --git a/src/Assets/AssetContainerContents.php b/src/Assets/AssetContainerContents.php index 9c32f2defba..e5a2e0aea92 100644 --- a/src/Assets/AssetContainerContents.php +++ b/src/Assets/AssetContainerContents.php @@ -37,6 +37,7 @@ public function all() return $this->files = $this->cacheStore()->remember($this->key(), $this->ttl(), function () { return collect($this->getRawFlysystemDirectoryListing()) ->keyBy('path') + ->reject(fn ($file, $path) => $this->isMetaPath($path)) ->map(fn ($file) => $this->normalizeFlysystemAttributes($file)) ->pipe(fn ($files) => $this->ensureMissingDirectoriesExist($files)) ->sortKeys(); @@ -182,12 +183,12 @@ public function metaFilesIn($folder, $recursive) return $this->metaFiles[$key]; } - $files = $this->files(); - - $files = $files->filter(function ($file, $path) { - return Str::startsWith($path, '.meta/') - || Str::contains($path, '/.meta/'); - }); + $files = collect($this->filesystem()->listContents($recursive ? $folder : $folder.'/.meta', $recursive)) + ->filter(fn ($attributes) => $attributes->isFile()) + ->keyBy('path') + ->map(fn ($file) => $this->normalizeFlysystemAttributes($file)) + ->filter(fn ($file, $path) => $this->isMetaPath($path)) + ->sortKeys(); // Filter by folder and recursiveness. But don't bother if we're // requesting the root recursively as it's already that way. @@ -268,6 +269,11 @@ private function filesystem() return $this->container->disk()->filesystem()->getDriver(); } + private function isMetaPath($path): bool + { + return (bool) preg_match('#(^|/)\.meta(/|$)#', $path); + } + public function save() { $this->cacheStore()->put($this->key(), $this->all(), $this->ttl()); From f0dfa1b745b527854b2cb118c0fe6195142b5557 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 15 Oct 2026 19:39:57 -0700 Subject: [PATCH 3/4] Drop redundant folder filter from metaFilesIn --- src/Assets/AssetContainerContents.php | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Assets/AssetContainerContents.php b/src/Assets/AssetContainerContents.php index e5a2e0aea92..9cea8fb0224 100644 --- a/src/Assets/AssetContainerContents.php +++ b/src/Assets/AssetContainerContents.php @@ -183,27 +183,15 @@ public function metaFilesIn($folder, $recursive) return $this->metaFiles[$key]; } - $files = collect($this->filesystem()->listContents($recursive ? $folder : $folder.'/.meta', $recursive)) + $path = $recursive ? $folder : trim($folder, '/').'/.meta'; + + $files = collect($this->filesystem()->listContents($path, $recursive)) ->filter(fn ($attributes) => $attributes->isFile()) ->keyBy('path') ->map(fn ($file) => $this->normalizeFlysystemAttributes($file)) ->filter(fn ($file, $path) => $this->isMetaPath($path)) ->sortKeys(); - // Filter by folder and recursiveness. But don't bother if we're - // requesting the root recursively as it's already that way. - if ($folder === '/' && $recursive) { - // - } else { - $files = $files->filter(function ($file) use ($folder, $recursive) { - $dir = $file['dirname']; - $dir = substr($dir, 0, -6); // remove .meta/ from the end - $dir = $dir ?: '/'; - - return $recursive ? Str::startsWith($dir, $folder) : $dir == $folder; - }); - } - return $this->metaFiles[$key] = $files; } From 278b2eb784875a6f0c9b176ecca87437dfaf9b78 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 15 Oct 2026 19:40:01 -0700 Subject: [PATCH 4/4] Fold duplicate meta files test into the default one --- tests/Assets/AssetContainerTest.php | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/tests/Assets/AssetContainerTest.php b/tests/Assets/AssetContainerTest.php index 8d1ccefa7b8..de56d0d4ac6 100644 --- a/tests/Assets/AssetContainerTest.php +++ b/tests/Assets/AssetContainerTest.php @@ -380,6 +380,11 @@ public function it_gets_all_files_by_default() #[Test] public function it_gets_all_meta_files_by_default() { + // The cached listing doesn't include meta files, so they must come from the filesystem. + Cache::put('asset-list-contents-test', collect([ + 'a.txt' => ['type' => 'file', 'path' => 'a.txt', 'dirname' => ''], + ])); + $this->assertEquals([ '.meta/a.txt.yaml', '.meta/b.txt.yaml', @@ -813,23 +818,6 @@ public function it_does_not_cache_meta_files_or_directories_in_the_listing() ); } - #[Test] - public function it_gets_meta_files_from_the_filesystem_when_the_listing_is_cached_without_them() - { - Cache::put('asset-list-contents-test', collect([ - 'a.txt' => ['type' => 'file', 'path' => 'a.txt', 'dirname' => ''], - ])); - - $this->assertEquals([ - '.meta/a.txt.yaml', - '.meta/b.txt.yaml', - 'nested/.meta/nested-a.txt.yaml', - 'nested/.meta/nested-b.txt.yaml', - 'nested/double-nested/.meta/double-nested-a.txt.yaml', - 'nested/double-nested/.meta/double-nested-b.txt.yaml', - ], $this->containerWithDisk()->metaFiles()->all()); - } - #[Test] public function it_gets_an_asset() {