fix: storage preview cache misses and stale cache eviction

Three bugs causing storage preview cache to be ineffective:

1. Cache keys included the `token` auth parameter, so requests using
   resource tokens always generated unique keys and never hit cache.
   Introduced `cache.params` label for routes to opt-in specific params
   into the cache key; preview now declares only the transform params.

2. Cache hits never refreshed `accessedAt` in the DB or the filesystem
   file mtime, because `$response->send()` in the init hook skips the
   shutdown hook. After 30 days the maintenance job evicted still-active
   cache entries, and after the original 30-day filesystem TTL the cache
   file expired — causing periodic full re-renders. The cache-hit path
   now updates both on the APP_CACHE_UPDATE (24h) interval.

3. `updateDocument` in the preview action passed the full file document
   instead of a sparse one when updating `transformedAt`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
loks0n
2026-04-09 17:05:14 +01:00
co-authored by Claude Sonnet 4.6
parent f371237fd5
commit f2df9cb93a
3 changed files with 17 additions and 1 deletions
+9
View File
@@ -693,6 +693,15 @@ Http::init()
}
}
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
'accessedAt' => DateTime::now(),
])));
// Refresh the filesystem file's mtime so TTL-based expiry in cache->load() stays valid
$cache->save($key, $data);
}
$response
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('X-Appwrite-Cache', 'hit')
@@ -54,6 +54,7 @@ class Get extends Action
->label('cache', true)
->label('cache.resourceType', 'bucket/{request.bucketId}')
->label('cache.resource', 'file/{request.fileId}')
->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output'])
->label('sdk', new Method(
namespace: 'storage',
group: 'files',
@@ -277,7 +278,9 @@ class Get extends Action
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([
'transformedAt' => $file->getAttribute('transformedAt'),
])));
}
}
+4
View File
@@ -234,6 +234,10 @@ class Request extends UtopiaRequest
public function cacheIdentifier(): string
{
$params = $this->getParams();
$allowedParams = $this->getRoute()?->getLabel('cache.params', null);
if ($allowedParams !== null) {
$params = array_intersect_key($params, array_flip($allowedParams));
}
ksort($params);
return md5($this->getURI() . '*' . serialize($params) . '*' . APP_CACHE_BUSTER);
}