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
13 changes: 10 additions & 3 deletions config/assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,16 @@
| Save Cached Images
|--------------------------------------------------------------------------
|
| Enabling this will make Glide save publicly accessible images. It will
| increase performance at the cost of the dynamic nature of HTTP based
| image manipulation. You will need to invalidate images manually.
| This controls how manipulated images are cached and served.
|
| false - Images are generated on each HTTP request via Glide routes.
| true - Images are eagerly generated during template rendering and
| saved to a publicly accessible location.
| 'hybrid' - Images are generated on-demand on the first HTTP request,
| then saved to a publicly accessible location so the web
| server can serve them directly on subsequent requests.
|
| When using true or 'hybrid', you should configure the cache_path below.
|
*/

Expand Down
2 changes: 1 addition & 1 deletion routes/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
});
}

if (Glide::shouldServeByHttp()) {
if (Glide::shouldServeByHttp() || Glide::isUsingHybridCaching()) {
require __DIR__.'/glide.php';
}

Expand Down
1 change: 1 addition & 0 deletions src/Facades/Glide.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* @method static \Illuminate\Contracts\Filesystem\Filesystem cacheDisk()
* @method static bool shouldServeDirectly()
* @method static bool shouldServeByHttp()
* @method static bool isUsingHybridCaching()
* @method static string route()
* @method static string url()
* @method static \Illuminate\Contracts\Cache\Repository cacheStore()
Expand Down
54 changes: 52 additions & 2 deletions src/Http/Controllers/GlideController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Statamic\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnableToReadFile;
use League\Glide\Server;
use League\Glide\Signatures\SignatureException;
Expand All @@ -12,6 +13,7 @@
use Statamic\Facades\Asset;
use Statamic\Facades\AssetContainer;
use Statamic\Facades\Config;
use Statamic\Facades\Glide;
use Statamic\Facades\Site;
use Statamic\Imaging\ImageGenerator;
use Statamic\Support\Str;
Expand Down Expand Up @@ -51,6 +53,10 @@ public function __construct(Server $server, Request $request, ImageGenerator $ge
*/
public function generateByPath($path)
{
if (Glide::isUsingHybridCaching()) {
return $this->generateOnDemand($path);
}

$this->validateSignature();

// If the auto crop setting is enabled, we will attempt to resolve an asset from the
Expand Down Expand Up @@ -79,6 +85,50 @@ public function generateByUrl($url)
return $this->createResponse($this->generateBy('url', $url));
}

/**
* Generate an on-demand image for the hybrid caching strategy.
*
* The URL path is the predicted cache path. A mapping stored in the
* Glide cache store links it back to the source and manipulation params.
*/
private function generateOnDemand(string $path)
{
if (Glide::cacheDisk()->exists($path)) {
Log::debug('Glide hybrid cache loaded ['.$path.'] If you are seeing this, your server rewrite rules have not been set up correctly.');

return $this->createResponse($path);
}

$mapping = Glide::cacheStore()->get('hybrid::'.$path);

throw_unless($mapping, new NotFoundHttpException);

$type = $mapping['type'];
$params = $mapping['params'];

$item = match ($type) {
'asset' => Asset::find($mapping['id']) ?? throw new NotFoundHttpException,
'url' => $mapping['url'],
'path' => $mapping['path'],
};

return $this->createResponse($this->ensureGenerated($type, $item, $params));
}

/**
* Forget any stale cache store entry, then generate the image.
*
* In hybrid mode, the file on disk is the source of truth.
* If we're here, the file doesn't exist, so the cache store
* entry (if any) is stale and should be cleared first.
*/
private function ensureGenerated(string $type, $item, array $params)
{
Glide::cacheStore()->forget(ImageGenerator::manipulationCacheKey($type, $item, $params));

return $this->generateBy($type, $item, $params);
}

/**
* Generate a manipulated image by an asset reference.
*
Expand Down Expand Up @@ -108,12 +158,12 @@ public function generateByAsset($encoded)
*
* @return mixed
*/
private function generateBy($type, $item)
private function generateBy($type, $item, ?array $params = null)
{
$method = 'generateBy'.ucfirst($type);

try {
return $this->generator->$method($item, $this->request->all());
return $this->generator->$method($item, $params ?? $this->request->all());
} catch (InvalidRemoteUrlException $e) {
abort(400, $e->getMessage());
} catch (UnableToReadFile $e) {
Expand Down
91 changes: 91 additions & 0 deletions src/Imaging/GlideCachePathResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace Statamic\Imaging;

use League\Glide\Server;
use Statamic\Contracts\Assets\Asset;
use Statamic\Facades\Asset as Assets;
use Statamic\Facades\URL;
use Statamic\Support\Str;

class GlideCachePathResolver
{
public function __construct(private Server $server)
{
}

public function resolveForAsset(Asset $asset, array $params): string
{
return $this->resolve(
$asset->basename(),
$params,
sourcePathPrefix: $asset->folder(),
cachePathPrefix: ImageGenerator::assetCachePathPrefix($asset).'/'.$asset->folder(),
asset: $asset,
);
}

public function resolveForPath(string $path, array $params): string
{
return $this->resolve(
$path,
$params,
sourcePathPrefix: '/',
cachePathPrefix: 'paths',
);
}

public function resolveForUrl(string $url, array $params): string
{
$parsed = app(RemoteUrlValidator::class)->parse($url);
$qs = $parsed['query'];
$path = $parsed['path'].($qs ? '?'.$qs : '');

return $this->resolve(
$path,
$params,
sourcePathPrefix: '/',
cachePathPrefix: 'http',
);
}

public function resolveForItem($item, array $params): string
{
if ($item instanceof Asset) {
return $this->resolveForAsset($item, $params);
}

if (is_string($item) && Str::contains($item, '::')) {
$asset = Assets::find($item);

if ($asset) {
return $this->resolveForAsset($asset, $params);
}
}

if (is_string($item) && URL::isAbsolute($item)) {
return $this->resolveForUrl($item, $params);
}

return $this->resolveForPath($item, $params);
}

private function resolve(string $image, array $params, string $sourcePathPrefix, string $cachePathPrefix, ?Asset $asset = null): string
{
$origSourcePrefix = $this->server->getSourcePathPrefix();
$origCachePrefix = $this->server->getCachePathPrefix();
$origDefaults = $this->server->getDefaults();

$this->server->setSourcePathPrefix($sourcePathPrefix);
$this->server->setCachePathPrefix($cachePathPrefix);
$this->server->setDefaults(ImageGenerator::getDefaultManipulations($asset));

try {
return $this->server->getCachePath($image, $params);
} finally {
$this->server->setSourcePathPrefix($origSourcePrefix);
$this->server->setCachePathPrefix($origCachePrefix);
$this->server->setDefaults($origDefaults);
}
}
}
17 changes: 13 additions & 4 deletions src/Imaging/GlideManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ public function cacheDisk()

private function wantsCustomFilesystem()
{
return is_string(Config::get('statamic.assets.image_manipulation.cache'));
$cache = Config::get('statamic.assets.image_manipulation.cache');

return is_string($cache) && $cache !== 'hybrid';
}

private function localCacheFilesystem()
Expand Down Expand Up @@ -77,19 +79,26 @@ private function customCacheFilesystem()
*/
private function cachePath()
{
return $this->shouldServeDirectly()
return ($this->shouldServeDirectly() || $this->isUsingHybridCaching())
? Config::get('statamic.assets.image_manipulation.cache_path')
: storage_path('statamic/glide');
}

public function shouldServeDirectly()
{
return (bool) Config::get('statamic.assets.image_manipulation.cache');
$cache = Config::get('statamic.assets.image_manipulation.cache');

return $cache === true || $this->wantsCustomFilesystem();
}

public function shouldServeByHttp()
{
return ! $this->shouldServeDirectly();
return ! $this->shouldServeDirectly() && ! $this->isUsingHybridCaching();
}

public function isUsingHybridCaching()
{
return Config::get('statamic.assets.image_manipulation.cache') === 'hybrid';
}

public function route()
Expand Down
76 changes: 76 additions & 0 deletions src/Imaging/HybridUrlBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Statamic\Imaging;

use Exception;
use Statamic\Contracts\Assets\Asset;
use Statamic\Facades\Asset as Assets;
use Statamic\Facades\Glide;
use Statamic\Facades\URL;
use Statamic\Support\Str;

class HybridUrlBuilder extends ImageUrlBuilder
{
protected GlideCachePathResolver $resolver;

protected array $options;

public function __construct(GlideCachePathResolver $resolver, array $options = [])
{
$this->resolver = $resolver;
$this->options = $options;
}

/**
* Build the URL.
*
* @param \Statamic\Contracts\Assets\Asset|string $item
* @param array $params
* @return string
*
* @throws \Exception
*/
public function build($item, $params)
{
$this->item = $item;

if (isset($params['mark']) && $params['mark'] instanceof Asset) {
$asset = $params['mark'];
$params['mark'] = 'asset::'.Str::toBase64Url($asset->containerId().'/'.$asset->path());
}

$cachePath = $this->resolver->resolveForItem($item, $params);

$this->cacheSource($cachePath, $params);

$urlPath = URL::tidy($this->options['route'].'/'.$cachePath, withTrailingSlash: false);

return URL::makeRelative(URL::prependSiteUrl($urlPath));
}

private function cacheSource(string $cachePath, array $params): void
{
$mapping = match ($this->itemType()) {
'asset' => ['type' => 'asset', 'id' => $this->item->id(), 'params' => $params],
'url' => ['type' => 'url', 'url' => $this->item, 'params' => $params],
'id' => ['type' => 'asset', 'id' => str_replace('/', '::', $this->item), 'params' => $params],
'path' => ['type' => 'path', 'path' => $this->item, 'params' => $params],
default => throw new Exception('Cannot build a hybrid Glide URL without a URL, path, or asset.'),
};

$mappingKey = 'hybrid::'.$cachePath;

Glide::cacheStore()->forever($mappingKey, $mapping);

// Add to the asset manifest so clearAsset() cleans up the mapping too.
if ($mapping['type'] === 'asset') {
$manifestKey = ImageGenerator::assetCacheManifestKey(
Assets::find($mapping['id'])
);

$manifest = Glide::cacheStore()->get($manifestKey, []);
$manifest[] = $mappingKey;
Glide::cacheStore()->forever($manifestKey, array_unique($manifest));
}
}
}
Loading
Loading