From f2df9cb93aae32963c5d15ae1af9df22cb776f49 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:05:14 +0100 Subject: [PATCH] fix: storage preview cache misses and stale cache eviction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/controllers/shared/api.php | 9 +++++++++ .../Modules/Storage/Http/Buckets/Files/Preview/Get.php | 5 ++++- src/Appwrite/Utopia/Request.php | 4 ++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 11ac345fca..24744c501f 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -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') diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php index a5e48be478..22e55e672e 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php @@ -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'), + ]))); } } diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index ed602ecdd5..3f1ea794ab 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -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); }