diff --git a/config/assets.php b/config/assets.php index ddeab7d3b3..29849e21cb 100644 --- a/config/assets.php +++ b/config/assets.php @@ -169,6 +169,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 6cca12baba..ba2250480c 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 9e815ff445..e8a20c15d7 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; @@ -261,8 +263,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()); @@ -308,13 +315,30 @@ public function generateMeta() public function metaPath() { - $path = dirname($this->path()).'/.meta/'.$this->basename().'.yaml'; - - return (string) Str::of($path)->replaceFirst('./', '')->ltrim('/'); + 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) => collect([ + Stache::store('assets')->directory(), + $this->container()->handle(), + ])->concat($path), + fn ($path) => $path->push('.meta'), + ) + ->push($this->basename().'.yaml') + ->filter() // Remove any empty segments + ->implode('/'); } protected function metaExists() { + if (config('statamic.assets.meta_as_content')) { + return File::exists($this->metaPath()); + } + return $this->container()->metaFiles()->contains($this->metaPath()); } @@ -324,7 +348,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() @@ -683,7 +712,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); @@ -779,7 +813,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 516df9febd..88fe1e7440 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -34,6 +34,7 @@ use Statamic\Facades; use Statamic\Facades\Antlers; use Statamic\Facades\File; +use Statamic\Facades\Stache; use Statamic\Facades\YAML; use Statamic\Fields\Blueprint; use Statamic\Fields\Fieldtype; @@ -59,10 +60,12 @@ public function setUp(): void config(['cache.default' => 'file']); Cache::clear(); - config(['filesystems.disks.test' => [ - 'driver' => 'local', - 'root' => __DIR__.'/tmp', - ]]); + config([ + 'filesystems.disks.test' => [ + 'driver' => 'local', + 'root' => __DIR__.'/tmp', + ], + ]); $this->container = (new AssetContainer) ->handle('test_container') @@ -352,11 +355,13 @@ public static function reAddRemovedDataProvider() 'by calling set method' => [fn ($asset) => $asset->set('one', 'new-foo')], 'by calling data method' => [fn ($asset) => $asset->data(['one' => 'new-foo', 'two' => 'bar', 'three' => 'qux'])], 'by calling merge method' => [fn ($asset) => $asset->merge(['one' => 'new-foo', 'three' => 'qux'])], - 'by calling __set() magically via property' => [function ($asset) { - $asset->one = 'new-foo'; + 'by calling __set() magically via property' => [ + function ($asset) { + $asset->one = 'new-foo'; - return $asset; - }], + return $asset; + }, + ], ]; } @@ -635,8 +640,33 @@ public function it_checks_if_its_an_image_file() public function it_checks_if_it_can_be_previewed_in_google_docs_previewer() { $extensions = [ - 'doc', 'docx', 'pages', 'txt', 'ai', 'psd', 'eps', 'ps', 'css', 'html', 'php', 'c', 'cpp', 'h', 'hpp', 'js', - 'ppt', 'pptx', 'flv', 'tiff', 'ttf', 'dxf', 'xps', 'zip', 'rar', 'xls', 'xlsx', + 'doc', + 'docx', + 'pages', + 'txt', + 'ai', + 'psd', + 'eps', + 'ps', + 'css', + 'html', + 'php', + 'c', + 'cpp', + 'h', + 'hpp', + 'js', + 'ppt', + 'pptx', + 'flv', + 'tiff', + 'ttf', + 'dxf', + 'xps', + 'zip', + 'rar', + 'xls', + 'xlsx', ]; foreach ($extensions as $ext) { @@ -725,6 +755,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 = 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() { @@ -1275,6 +1328,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_can_be_moved_uniquely_to_another_folder_when_conflict_exists() { @@ -1936,10 +2019,12 @@ public function it_can_upload_an_image_into_a_container_with_glide_config() { Event::fake(); - config(['statamic.assets.image_manipulation.presets.small' => [ - 'w' => '15', - 'h' => '15', - ]]); + config([ + 'statamic.assets.image_manipulation.presets.small' => [ + 'w' => '15', + 'h' => '15', + ], + ]); $this->container->sourcePreset('small'); @@ -1982,9 +2067,11 @@ public function it_can_upload_an_image_into_a_container_with_new_extension_forma { Event::fake(); - config(['statamic.assets.image_manipulation.presets.enforce_png' => [ - $formatParam => 'png', - ]]); + config([ + 'statamic.assets.image_manipulation.presets.enforce_png' => [ + $formatParam => 'png', + ], + ]); $this->container->sourcePreset('enforce_png'); @@ -2018,9 +2105,11 @@ public function it_normalizes_pjpg_format_to_jpg_extension_on_upload() { Event::fake(); - config(['statamic.assets.image_manipulation.presets.progressive' => [ - 'fm' => 'pjpg', - ]]); + config([ + 'statamic.assets.image_manipulation.presets.progressive' => [ + 'fm' => 'pjpg', + ], + ]); $this->container->sourcePreset('progressive'); @@ -2105,10 +2194,12 @@ public function it_doesnt_process_or_error_when_uploading_non_glideable_file_wit { Event::fake(); - config(['statamic.assets.image_manipulation.presets.small' => [ - 'w' => '15', - 'h' => '15', - ]]); + config([ + 'statamic.assets.image_manipulation.presets.small' => [ + 'w' => '15', + 'h' => '15', + ], + ]); $this->container->sourcePreset('small'); @@ -2418,10 +2509,21 @@ public function it_sends_a_download_response_with_a_different_name_and_custom_he private function toArrayKeysWhenFileExists() { return [ - 'size', 'size_bytes', 'size_kilobytes', 'size_megabytes', 'size_gigabytes', - 'size_b', 'size_kb', 'size_mb', 'size_gb', - 'last_modified', 'last_modified_timestamp', 'last_modified_instance', - 'focus', 'focus_css', 'mime_type', + 'size', + 'size_bytes', + 'size_kilobytes', + 'size_megabytes', + 'size_gigabytes', + 'size_b', + 'size_kb', + 'size_mb', + 'size_gb', + 'last_modified', + 'last_modified_timestamp', + 'last_modified_instance', + 'focus', + 'focus_css', + 'mime_type', ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index e219e3695e..ca96c15215 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -111,6 +111,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');