From 352ad2ff714fed27a60e8d6191c4be853ebe09e7 Mon Sep 17 00:00:00 2001 From: czachor Date: Tue, 20 Oct 2026 13:55:03 +0200 Subject: [PATCH] perf(image-cache): let the browser cache image previews The cachebreaker appended time() to every image preview URL, so the URL changed on each page load and the browser downloaded every preview again. preview() now passes the file modification time, which keeps the URL stable until the file changes. fileIcon() accepts an int version; true still falls back to time() for Twig fileIcon() callers. The file route now answers revalidation requests (If-Modified-Since) with 304 instead of resending the file. Last-Modified was already set by BinaryFileResponse. --- Controller/ManagerController.php | 6 +++- Resources/doc/book/1-basic-configuration.md | 2 +- Service/FileTypeService.php | 10 ++++-- Tests/Controller/CachebreakerTest.php | 32 ++++++++++++++++++ .../App/config/config_cachebreaker.yml | 8 +++++ Tests/Fixtures/web/images/image.png | Bin 0 -> 69 bytes 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 Tests/Controller/CachebreakerTest.php create mode 100644 Tests/Fixtures/App/config/config_cachebreaker.yml create mode 100644 Tests/Fixtures/web/images/image.png diff --git a/Controller/ManagerController.php b/Controller/ManagerController.php index e57712c..5faabe2 100644 --- a/Controller/ManagerController.php +++ b/Controller/ManagerController.php @@ -289,7 +289,11 @@ public function binaryFileResponseAction(Request $request, string $fileName): Bi } $this->dispatch(FileManagerEvents::FILE_ACCESS, ['path' => $file]); - return new BinaryFileResponse($file); + $response = new BinaryFileResponse($file); + // Last-Modified is set by default; answer revalidation requests with 304 instead of resending the file + $response->isNotModified($request); + + return $response; } #[Route("/delete/", name: 'file_manager_delete')] diff --git a/Resources/doc/book/1-basic-configuration.md b/Resources/doc/book/1-basic-configuration.md index 7828cc6..56492d6 100644 --- a/Resources/doc/book/1-basic-configuration.md +++ b/Resources/doc/book/1-basic-configuration.md @@ -163,7 +163,7 @@ artgris_file_manager: | :--- |:--------:|:--------:|:-------------:| | `cachebreaker` | `Boolean` | False | True | -Adding a cachebreaker ?time=RANDOM_NUMBER or & &time=RANDOM_NUMBER at the end of the images (preview) url. It's removed if you have used "twig_extension" option. +Adding a cachebreaker ?time=FILE_MODIFICATION_TIME or &time=FILE_MODIFICATION_TIME at the end of the images (preview) url. The url changes only when the file changes, so the browser can cache previews between page loads. It's removed if you have used "twig_extension" option. ## `show_file_count` to show the number of files per directory in the file browser tree diff --git a/Service/FileTypeService.php b/Service/FileTypeService.php index b5a9948..3692634 100644 --- a/Service/FileTypeService.php +++ b/Service/FileTypeService.php @@ -34,7 +34,10 @@ public function preview(FileManager $fileManager, SplFileInfo $file) { if ('file' === $type) { $size = $this::IMAGE_SIZE[$fileManager->getView()]; - return $this->fileIcon($filePath, $extension, $size, true, $fileManager->getConfigurationParameter('twig_extension'), $fileManager->getConfigurationParameter('cachebreaker')); + // The modification time keeps the URL stable until the file changes, so the browser can cache it + $cachebreaker = $fileManager->getConfigurationParameter('cachebreaker') ? $file->getMTime() : null; + + return $this->fileIcon($filePath, $extension, $size, true, $fileManager->getConfigurationParameter('twig_extension'), $cachebreaker); } if ('dir' === $type) { @@ -68,7 +71,7 @@ public function accept($type): bool|string { return $accept; } - public function fileIcon(string $filePath,?string $extension = null, ?int $size = 75, ?bool $lazy = false, ?string $twigExtension = null, ?bool $cachebreaker = null): array { + public function fileIcon(string $filePath,?string $extension = null, ?int $size = 75, ?bool $lazy = false, ?string $twigExtension = null, bool|int|null $cachebreaker = null): array { $imageTemplate = null; @@ -89,7 +92,8 @@ public function fileIcon(string $filePath,?string $extension = null, ?int $size $fileName = $filePath; if ($cachebreaker) { $query = parse_url($filePath, PHP_URL_QUERY); - $time = 'time='.time(); + // An int is used as the version (file modification time), true falls back to the current time + $time = 'time='.(\is_int($cachebreaker) ? $cachebreaker : time()); $fileName = $query ? $filePath.'&'.$time : $filePath.'?'.$time; } diff --git a/Tests/Controller/CachebreakerTest.php b/Tests/Controller/CachebreakerTest.php new file mode 100644 index 0000000..6c9920e --- /dev/null +++ b/Tests/Controller/CachebreakerTest.php @@ -0,0 +1,32 @@ +initClient(['environment' => 'cachebreaker']); + } + + public function testImagePreviewCachebreakerIsFileModificationTime() + { + // A fixed date in the past, so a cachebreaker built from time() cannot match by accident + touch(__DIR__.'/../Fixtures/web/images/image.png', 1600000000); + + $crawler = $this->getManagerPage(); + $this->assertSame('/manager/file/image.png?conf=default&time=1600000000', $crawler->filter('img.lazy')->attr('data-src')); + } + + public function testFileIsNotResentWhenNotModified() + { + $image = __DIR__.'/../Fixtures/web/images/image.png'; + touch($image, 1600000000); + + // Only the 304 path is requested: a 200 guesses the MIME type, which needs ext-fileinfo (missing on the Windows CI) + $this->client->request('GET', '/manager/file/image.png?conf=default', [], [], ['HTTP_IF_MODIFIED_SINCE' => gmdate('D, d M Y H:i:s', 1600000000).' GMT']); + $this->assertSame(304, $this->client->getResponse()->getStatusCode()); + } +} diff --git a/Tests/Fixtures/App/config/config_cachebreaker.yml b/Tests/Fixtures/App/config/config_cachebreaker.yml new file mode 100644 index 0000000..d0e42da --- /dev/null +++ b/Tests/Fixtures/App/config/config_cachebreaker.yml @@ -0,0 +1,8 @@ +imports: + - { resource: config.yml } + +artgris_file_manager: + conf: + default: + dir: "%kernel.project_dir%/Tests/Fixtures/web/images" + cachebreaker: true diff --git a/Tests/Fixtures/web/images/image.png b/Tests/Fixtures/web/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..500c8899b9d86dfb5f9c5dc3e186c86077b09b03 GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9x<|P04|KFa0^&iu}