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..9cea8fb0224 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,26 +183,14 @@ public function metaFilesIn($folder, $recursive) return $this->metaFiles[$key]; } - $files = $this->files(); + $path = $recursive ? $folder : trim($folder, '/').'/.meta'; - $files = $files->filter(function ($file, $path) { - return Str::startsWith($path, '.meta/') - || Str::contains($path, '/.meta/'); - }); - - // 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; - }); - } + $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(); return $this->metaFiles[$key] = $files; } @@ -268,6 +257,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()); diff --git a/tests/Assets/AssetContainerTest.php b/tests/Assets/AssetContainerTest.php index 21256960450..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', @@ -757,12 +762,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 +779,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 +791,33 @@ 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_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)