From 8fc49645b2cd57631c53db13d494211a1aa061d5 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Fri, 25 Jul 2025 10:47:37 -0400 Subject: [PATCH 1/4] Initial support for storing asset meta as content --- config/assets.php | 12 +++++++++ config/stache.php | 1 + src/Assets/Asset.php | 51 ++++++++++++++++++++++++++++++----- tests/Assets/AssetTest.php | 55 ++++++++++++++++++++++++++++++++++++++ tests/TestCase.php | 1 + 5 files changed, 113 insertions(+), 7 deletions(-) diff --git a/config/assets.php b/config/assets.php index e3587e20c58..e89601397cc 100644 --- a/config/assets.php +++ b/config/assets.php @@ -172,6 +172,18 @@ 'cache_meta' => true, + /* + |-------------------------------------------------------------------------- + | Metadata as Content + |-------------------------------------------------------------------------- + | + | Asset metadata will be saved as content alongside the rest of the content. + | This is useful when wanting to track metadata changes in git while using + | another storage location for assets (ie. S3). + | + */ + 'meta_as_content' => false, + /* |-------------------------------------------------------------------------- | Focal Point Editor diff --git a/config/stache.php b/config/stache.php index 2d2ec830d36..b7f8aa6125a 100644 --- a/config/stache.php +++ b/config/stache.php @@ -93,6 +93,7 @@ 'assets' => [ 'class' => Stores\AssetsStore::class, + 'directory' => base_path('content/assets'), ], 'users' => [ diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index 66bed47dc6e..ea6fbf3b0cf 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -35,8 +35,10 @@ use Statamic\Facades; use Statamic\Facades\AssetContainer as AssetContainerAPI; use Statamic\Facades\Blink; +use Statamic\Facades\File; use Statamic\Facades\Image; use Statamic\Facades\Path; +use Statamic\Facades\Stache; use Statamic\Facades\URL; use Statamic\Facades\YAML; use Statamic\GraphQL\ResolvesValues; @@ -255,8 +257,13 @@ public function meta($key = null) } return $this->meta = $this->cacheStore()->rememberForever($this->metaCacheKey(), function () { - if ($contents = $this->disk()->get($path = $this->metaPath())) { - return YAML::file($path)->parse($contents); + $contents = match (config('statamic.assets.meta_as_content')) { + true => File::get($this->metaPath()), + false => $this->disk()->get($this->metaPath()), + }; + + if ($contents) { + return YAML::parse($contents); } $this->writeMeta($meta = $this->generateMeta()); @@ -302,13 +309,29 @@ public function generateMeta() public function metaPath() { - $path = dirname($this->path()).'/.meta/'.$this->basename().'.yaml'; + $path = Path::resolve(match (config('statamic.assets.meta_as_content')) { + true => implode(DIRECTORY_SEPARATOR, [ + Stache::store('assets')->directory(), + $this->container()->handle(), + dirname($this->path()), + $this->basename().'.yaml', + ]), + false => implode(DIRECTORY_SEPARATOR, [ + dirname($this->path()), + '.meta', + $this->basename().'.yaml', + ]), + }); - return (string) Str::of($path)->replaceFirst('./', '')->ltrim('/'); + return Str::of($path)->replaceFirst('./', '')->ltrim('/')->value(); } protected function metaExists() { + if (config('statamic.assets.meta_as_content')) { + return File::exists($this->metaPath()); + } + return $this->container()->metaFiles()->contains($this->metaPath()); } @@ -318,7 +341,12 @@ public function writeMeta($meta) $contents = YAML::dump($meta); - $this->disk()->put($this->metaPath(), $contents); + if (config('statamic.assets.meta_as_content')) { + File::makeDirectory(dirname($this->metaPath()), 0755, true); + File::put($this->metaPath(), $contents); + } else { + $this->disk()->put($this->metaPath(), $contents); + } } public function metaCacheKey() @@ -676,7 +704,12 @@ public function delete() } $this->disk()->delete($this->path()); - $this->disk()->delete($this->metaPath()); + + if (config('statamic.assets.meta_as_content')) { + File::delete($this->metaPath()); + } else { + $this->disk()->delete($this->metaPath()); + } Facades\Asset::delete($this); @@ -770,7 +803,11 @@ public function move($folder, $filename = null) $this->path($newPath); $this->save(); - $this->disk()->rename($oldMetaPath, $this->metaPath()); + if (config('statamic.assets.meta_as_content')) { + File::move($oldMetaPath, $this->metaPath()); + } else { + $this->disk()->rename($oldMetaPath, $this->metaPath()); + } return $this; } diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index 2895b14514c..c1cc27a5831 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -33,6 +33,8 @@ use Statamic\Facades; use Statamic\Facades\Antlers; use Statamic\Facades\File; +use Statamic\Facades\Path; +use Statamic\Facades\Stache; use Statamic\Facades\YAML; use Statamic\Fields\Blueprint; use Statamic\Fields\Fieldtype; @@ -708,6 +710,29 @@ public function it_gets_existing_meta_data() $this->assertEquals($expected, Cache::get($asset->metaCacheKey())); } + #[Test] + public function it_gets_existing_meta_data_as_content() + { + config()->set('statamic.assets.meta_as_content', true); + $relativePath = 'foo/test.txt'; + $metaFilePath = Path::resolve(Stache::store('assets')->directory()."/test/{$relativePath}.yaml"); + + Storage::fake('test'); + Storage::disk('test')->put($relativePath, ''); + + File::makeDirectory(dirname($metaFilePath), 0755, true); + File::put($metaFilePath, YAML::dump($data = [ + 'data' => ['foo' => 'bar'], + 'size' => 123, + ])); + + $container = tap(Facades\AssetContainer::make('test')->disk('test'))->save(); + $asset = (new Asset)->container($container)->path($relativePath); + + $this->assertEquals($metaFilePath, $asset->metaPath()); + $this->assertEquals($data, $asset->meta()); + } + #[Test] public function it_properly_merges_new_unsaved_data_to_meta() { @@ -1237,6 +1262,36 @@ public function it_doesnt_lowercase_moved_files_when_configured() ], $container->assets('/', true)->map->path()->all()); } + #[Test] + public function it_can_be_moved_to_another_folder_and_renamed_when_meta_as_content() + { + config()->set('statamic.assets.meta_as_content', true); + + Storage::fake('test'); + $disk = Storage::disk('test'); + $disk->put('old/asset.txt', 'The asset contents'); + + $container = tap(Facades\AssetContainer::make('test')->disk('test'))->save(); + $asset = tap($container->makeAsset('old/asset.txt')->data(['foo' => 'bar']))->save(); + $meta = $asset->meta(); + + $metaPath = Stache::store('assets')->directory().'/'.$container->handle(); + + $this->assertFileExists("{$metaPath}/old/asset.txt.yaml"); + $this->assertEquals(YAML::dump($asset->meta()), File::get("{$metaPath}/old/asset.txt.yaml")); + $this->assertEquals(['old/asset.txt'], $container->files()->all()); + + $return = $asset->move('new', 'asset2'); + + $this->assertEquals($return, $asset); + $disk->assertMissing('old/asset.txt'); + $this->assertFileDoesNotExist("{$metaPath}/old/asset.txt.yaml"); + $disk->assertExists('new/asset2.txt'); + $this->assertFileExists($newMetaPath = "{$metaPath}/new/asset2.txt.yaml"); + $this->assertEquals(YAML::dump($meta), File::get($newMetaPath)); + $this->assertEquals(['new/asset2.txt'], $container->files()->all()); + } + #[Test] public function it_renames() { diff --git a/tests/TestCase.php b/tests/TestCase.php index e8c5e54d689..dde6bc96fea 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -103,6 +103,7 @@ protected function getEnvironmentSetUp($app) $app['config']->set('statamic.stache.stores.globals.directory', __DIR__.'/__fixtures__/content/globals'); $app['config']->set('statamic.stache.stores.global-variables.directory', __DIR__.'/__fixtures__/content/globals'); $app['config']->set('statamic.stache.stores.asset-containers.directory', __DIR__.'/__fixtures__/content/assets'); + $app['config']->set('statamic.stache.stores.assets.directory', __DIR__.'/__fixtures__/content/assets'); $app['config']->set('statamic.stache.stores.nav-trees.directory', __DIR__.'/__fixtures__/content/structures/navigation'); $app['config']->set('statamic.stache.stores.collection-trees.directory', __DIR__.'/__fixtures__/content/structures/collections'); $app['config']->set('statamic.stache.stores.form-submissions.directory', __DIR__.'/__fixtures__/content/submissions'); From 68ec19edebb4286dc13d83b3a12d677a092cf928 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Fri, 25 Jul 2025 11:12:33 -0400 Subject: [PATCH 2/4] Fixing paths --- src/Assets/Asset.php | 8 ++++---- tests/Assets/AssetTest.php | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index ea6fbf3b0cf..beb73a6d8b5 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -309,19 +309,19 @@ public function generateMeta() public function metaPath() { - $path = Path::resolve(match (config('statamic.assets.meta_as_content')) { - true => implode(DIRECTORY_SEPARATOR, [ + $path = match (config('statamic.assets.meta_as_content')) { + true => implode('/', [ Stache::store('assets')->directory(), $this->container()->handle(), dirname($this->path()), $this->basename().'.yaml', ]), - false => implode(DIRECTORY_SEPARATOR, [ + false => implode('/', [ dirname($this->path()), '.meta', $this->basename().'.yaml', ]), - }); + }; return Str::of($path)->replaceFirst('./', '')->ltrim('/')->value(); } diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index c1cc27a5831..d49fb57b5ba 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -33,7 +33,6 @@ use Statamic\Facades; use Statamic\Facades\Antlers; use Statamic\Facades\File; -use Statamic\Facades\Path; use Statamic\Facades\Stache; use Statamic\Facades\YAML; use Statamic\Fields\Blueprint; @@ -715,7 +714,7 @@ public function it_gets_existing_meta_data_as_content() { config()->set('statamic.assets.meta_as_content', true); $relativePath = 'foo/test.txt'; - $metaFilePath = Path::resolve(Stache::store('assets')->directory()."/test/{$relativePath}.yaml"); + $metaFilePath = Stache::store('assets')->directory()."/test/{$relativePath}.yaml"; Storage::fake('test'); Storage::disk('test')->put($relativePath, ''); From c9533dcca96f1f3393e251237022d6b26c88c8f6 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Sat, 2 Aug 2025 01:18:58 -0400 Subject: [PATCH 3/4] Fix metaPath for tests --- src/Assets/Asset.php | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index beb73a6d8b5..cfe48ce4ae8 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -309,21 +309,22 @@ public function generateMeta() public function metaPath() { - $path = match (config('statamic.assets.meta_as_content')) { - true => implode('/', [ - Stache::store('assets')->directory(), - $this->container()->handle(), - dirname($this->path()), - $this->basename().'.yaml', - ]), - false => implode('/', [ - dirname($this->path()), - '.meta', - $this->basename().'.yaml', - ]), - }; - - return Str::of($path)->replaceFirst('./', '')->ltrim('/')->value(); + return Str::of($this->path()) + ->dirname() + ->finish('/') // Sometimes the dirname is just '.', so we ensure it ends with a slash + ->replaceFirst('./', '') + ->explode('/') + ->when( + config('statamic.assets.meta_as_content'), + fn ($path) => $path->unshift( + Stache::store('assets')->directory(), + $this->container()->handle(), + ), + fn ($path) => $path->push('.meta'), + ) + ->push($this->basename().'.yaml') + ->filter() // Remove any empty segments + ->implode('/'); } protected function metaExists() From e75ee1043da5fc5bc9082aafa115ac4b0bca6b08 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Sat, 2 Aug 2025 01:47:33 -0400 Subject: [PATCH 4/4] Oops didn't fully test that did I --- src/Assets/Asset.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index cfe48ce4ae8..cd6825368df 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -316,10 +316,10 @@ public function metaPath() ->explode('/') ->when( config('statamic.assets.meta_as_content'), - fn ($path) => $path->unshift( + fn ($path) => collect([ Stache::store('assets')->directory(), $this->container()->handle(), - ), + ])->concat($path), fn ($path) => $path->push('.meta'), ) ->push($this->basename().'.yaml')