Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions src/Assets/Asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public function exists()
return false;
}

return $this->container()->files()->contains($path);
return $this->container()->contents()->hasFile($path);
}

public function getRawMeta()
Expand Down Expand Up @@ -293,9 +293,11 @@ public function generateMeta()
if ($this->exists()) {
$attributes = Attributes::asset($this)->get();

// The listing already holds the size and timestamp of every file,
// so only ask the disk for them when it somehow doesn't.
$meta = array_merge($meta, [
'size' => $this->disk()->size($this->path()),
'last_modified' => $this->disk()->lastModified($this->path()),
'size' => $this->listingAttribute('size') ?? $this->disk()->size($this->path()),
'last_modified' => $this->listingAttribute('timestamp') ?? $this->disk()->lastModified($this->path()),
'width' => Arr::get($attributes, 'width'),
'height' => Arr::get($attributes, 'height'),
'mime_type' => $this->disk()->mimeType($this->path()),
Expand All @@ -315,7 +317,7 @@ public function metaPath()

protected function metaExists()
{
return $this->container()->metaFiles()->contains($this->metaPath());
return $this->container()->contents()->hasMetaFile($this->metaPath());
}

public function writeMeta($meta)
Expand Down Expand Up @@ -606,7 +608,25 @@ public function mimeType()
*/
public function lastModified()
{
return Carbon::createFromTimestamp($this->meta('last_modified') ?? 0, config('app.timezone'));
$timestamp = $this->listingAttribute('timestamp') ?? $this->meta('last_modified') ?? 0;

return Carbon::createFromTimestamp($timestamp, config('app.timezone'));
}

/**
* Get a value the container listing already knows about this file.
*
* The listing holds the size and timestamp of every file, so these can be
* answered without reading (or generating) the asset's meta file, which
* is a request per asset when the container is on a remote disk.
*/
private function listingAttribute($key)
{
if (! $path = $this->path()) {
return null;
}

return $this->container()->contents()->file($path)[$key] ?? null;
}

/**
Expand Down Expand Up @@ -925,7 +945,7 @@ public function ratio()
*/
public function size()
{
return $this->meta('size');
return $this->listingAttribute('size') ?? $this->meta('size');
}

/**
Expand Down Expand Up @@ -983,6 +1003,8 @@ public function reupload(ReplacementFile $file)
);
}

$this->container()->contents()->add($this->path())->save();

$this->clearCaches();
$this->writeMeta($this->generateMeta());

Expand Down
42 changes: 42 additions & 0 deletions src/Assets/AssetContainerContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,48 @@ public function directories()
return $this->all()->where('type', 'dir');
}

/**
* Whether an asset file exists at the given path.
*
* This looks the path up directly rather than filtering the whole listing,
* so checking one asset doesn't cost a pass over every file in the container.
*/
public function hasFile($path): bool
{
return $this->isFile($path) && ! $this->isMetaPath($path) && ! $this->isIgnoredPath($path);
}

/**
* Get the listing attributes (type, path, timestamp, size) of an asset file.
*/
public function file($path): ?array
{
return $this->hasFile($path) ? $this->all()->get($path) : null;
}

/**
* Whether a meta file exists at the given path.
*/
public function hasMetaFile($path): bool
{
return $this->isFile($path) && $this->isMetaPath($path);
}

private function isFile($path): bool
{
return ($this->all()->get($path)['type'] ?? null) === 'file';
}

private function isMetaPath($path): bool
{
return Str::startsWith($path, '.meta/') || Str::contains($path, '/.meta/');
}

private function isIgnoredPath($path): bool
{
return Str::endsWith($path, ['.DS_Store', '.gitkeep', '.gitignore']);
}

public function metaFilesIn($folder, $recursive)
{
if (isset($this->metaFiles[$key = $folder.($recursive ? '-recursive' : '')])) {
Expand Down
92 changes: 91 additions & 1 deletion tests/Assets/AssetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,96 @@ public function it_checks_if_it_exists()
$this->assertFalse($noPath->exists());
}

#[Test]
public function checking_existence_does_not_filter_the_whole_container_listing()
{
$disk = Storage::fake('test');
$disk->put('yes.txt', '');
$disk->put('.meta/yes.txt.yaml', '');
$disk->put('sub/.meta/nested.txt.yaml', '');
$disk->put('sub/.gitkeep', '');

$contents = new class extends \Statamic\Assets\AssetContainerContents
{
public $filtered = 0;

public function filteredFilesIn($folder, $recursive)
{
$this->filtered++;

return parent::filteredFilesIn($folder, $recursive);
}

public function metaFilesIn($folder, $recursive)
{
$this->filtered++;

return parent::metaFilesIn($folder, $recursive);
}
};

$this->app->bind(\Statamic\Assets\AssetContainerContents::class, fn () => $contents);

$asset = fn ($path) => (new Asset)->container($this->container)->path($path);

$this->assertTrue($asset('yes.txt')->exists());
$this->assertFalse($asset('no.txt')->exists());
$this->assertFalse($asset('sub')->exists());
$this->assertFalse($asset('.meta/yes.txt.yaml')->exists());
$this->assertFalse($asset('sub/.meta/nested.txt.yaml')->exists());
$this->assertFalse($asset('sub/.gitkeep')->exists());

$this->assertTrue($contents->hasMetaFile('.meta/yes.txt.yaml'));
$this->assertTrue($contents->hasMetaFile('sub/.meta/nested.txt.yaml'));
$this->assertFalse($contents->hasMetaFile('yes.txt'));
$this->assertFalse($contents->hasMetaFile('.meta/no.txt.yaml'));

$this->assertSame(0, $contents->filtered);
}

#[Test]
public function it_gets_size_and_last_modified_from_the_container_listing_without_touching_meta()
{
Carbon::setTestNow('2017-01-02 14:35:00');
$disk = Storage::fake('test');
$disk->put('foo/test.txt', 'four');
touch($disk->path('foo/test.txt'), Carbon::now()->timestamp);

$asset = (new Asset)->container($this->container)->path('foo/test.txt');

$this->assertSame(4, $asset->size());
$this->assertEquals(Carbon::now()->timestamp, $asset->lastModified()->timestamp);
$this->assertSame(4, $asset->getQueryableValue('size'));

// Neither needed the meta, so it was not read, generated or written.
$disk->assertMissing('foo/.meta/test.txt.yaml');
$this->assertNull($asset->getRawMeta());
}

#[Test]
public function generating_meta_uses_the_size_and_timestamp_the_listing_already_has()
{
Storage::fake('test');
$file = UploadedFile::fake()->image('image.jpg', 30, 60);
Storage::disk('test')->putFileAs('foo', $file, 'image.jpg');
touch(Storage::disk('test')->path('foo/image.jpg'), $listed = Carbon::parse('2021-02-22 09:41:42')->timestamp);

$container = Facades\AssetContainer::make('test')->disk('test');
$asset = (new Asset)->container($container)->path('foo/image.jpg');
$container->contents()->all();

// If the disk were asked again it would report this newer time.
touch(Storage::disk('test')->path('foo/image.jpg'), $listed + 100);
clearstatcache();

$meta = $asset->generateMeta();

$this->assertSame($listed, $meta['last_modified']);
$this->assertSame(Storage::disk('test')->size('foo/image.jpg'), $meta['size']);
$this->assertSame(30, $meta['width']);
$this->assertSame('image/jpeg', $meta['mime_type']);
}

#[Test]
public function it_gets_the_filename()
{
Expand Down Expand Up @@ -911,7 +1001,7 @@ public function it_generates_meta_on_demand_if_a_required_value_is_missing()

Storage::disk('test')->put('foo/.meta/image.jpg.yaml', YAML::dump($incompleteMeta));

$asset->size();
$asset->mimeType();

$this->assertEquals($completeMeta, YAML::parse(Storage::disk('test')->get('foo/.meta/image.jpg.yaml')));
}
Expand Down
Loading