Skip to content

Commit

Permalink
fix(common): perf issues with etag behoind apache with deflate mod (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
theus77 authored Nov 29, 2024
1 parent 98dc5c0 commit 473c43f
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 29 deletions.
25 changes: 23 additions & 2 deletions EMS/common-bundle/src/Controller/FileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class FileController extends AbstractController
{
Expand Down Expand Up @@ -88,11 +90,30 @@ public function generateLocalImage(Request $request, string $filename, string $c
return $response;
}

public function assetInArchive(Request $request, string $hash, string $path, int $maxAge = 604800, bool $extract = true, string $indexResource = null): Response
public function assetInArchive(Request $request, string $hash, string $path, int $maxAge = 604800, bool $extract = true, string $indexResource = null, string $notFoundTemplate = null): Response
{
$this->closeSession($request);

return $this->processor->getResponseFromArchive($request, $hash, $path, $maxAge, $extract, $indexResource);
try {
return $this->processor->getResponseFromArchive($request, $hash, $path, $maxAge, $extract, $indexResource);
} catch (NotFoundHttpException $e) {
if (null === $notFoundTemplate) {
throw $e;
}
}

try {
return $this->render($notFoundTemplate, [
'error' => $e,
'hash' => $hash,
'path' => $path,
'maxAge' => $maxAge,
'extract' => $extract,
'indexResource' => $indexResource,
]);
} catch (\Throwable $e) {
throw $e->getPrevious() instanceof HttpException ? $e->getPrevious() : $e;
}
}

private function getFile(Request $request, string $hash, string $disposition): Response
Expand Down
13 changes: 10 additions & 3 deletions EMS/common-bundle/src/Helper/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace EMS\CommonBundle\Helper;

use EMS\Helpers\Html\Headers;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class Cache
Expand All @@ -20,12 +22,17 @@ public function generateEtag(Response $response): ?string
return \hash($this->hashAlgo, $content);
}

public function makeResponseCacheable(Response $response, string $etag, ?\DateTime $lastUpdateDate, bool $immutableRoute): void
public function makeResponseCacheable(Request $request, Response $response, string $etag, ?\DateTime $lastUpdateDate, bool $immutableRoute, int $maxAge = 600): void
{
$rewritedEtags = [];
foreach ($request->getETags() as $requestEtag) {
$rewritedEtags[] = \preg_replace('/\-gzip"$/i', '"', $requestEtag);
}
$request->headers->replace([Headers::IF_NONE_MATCH => $rewritedEtags]);
$response->setCache([
'etag' => $etag,
'max_age' => $immutableRoute ? 604800 : 600,
's_maxage' => $immutableRoute ? 2_678_400 : 3600,
'max_age' => $immutableRoute ? 604800 : $maxAge,
's_maxage' => $immutableRoute ? 2_678_400 : ($maxAge * 6),
'public' => true,
'private' => false,
'immutable' => $immutableRoute,
Expand Down
21 changes: 11 additions & 10 deletions EMS/common-bundle/src/Storage/Processor/Processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function getStreamedResponse(Request $request, Config $config, string $fi
$cacheKey = $config->getCacheKey();

$cacheResponse = new Response();
$this->cacheHelper->makeResponseCacheable($cacheResponse, $cacheKey, $config->getLastUpdateDate(), $immutableRoute);
$this->cacheHelper->makeResponseCacheable($request, $cacheResponse, $cacheKey, $config->getLastUpdateDate(), $immutableRoute);
if ($cacheResponse->isNotModified($request)) {
return $cacheResponse;
}
Expand All @@ -98,7 +98,7 @@ public function getStreamedResponse(Request $request, Config $config, string $fi
]);
}

$this->cacheHelper->makeResponseCacheable($response, $cacheKey, $config->getLastUpdateDate(), $immutableRoute);
$this->cacheHelper->makeResponseCacheable($request, $response, $cacheKey, $config->getLastUpdateDate(), $immutableRoute);

return $response;
}
Expand Down Expand Up @@ -341,18 +341,19 @@ private function locate(string $filename): string

public function getResponseFromArchive(Request $request, string $hash, string $path, int $maxAge, bool $extract, ?string $indexResource): Response
{
$etag = \hash('sha1', \sprintf('Asset in archive: %s:%s', $hash, $path));
$cacheResponse = new Response();
$this->cacheHelper->makeResponseCacheable($request, $cacheResponse, $etag, null, false, $maxAge);
if ($cacheResponse->isNotModified($request)) {
return $cacheResponse;
}

$streamWrapper = $this->storageManager->getStreamFromArchive($hash, $path, $extract, $indexResource);
$response = $this->getResponseFromStreamInterface($streamWrapper->getStream(), $request);
$response->headers->add([
$response->headers->replace(\array_merge($cacheResponse->headers->all(), [
Headers::CONTENT_DISPOSITION => HeaderUtils::DISPOSITION_INLINE.'; '.HeaderUtils::toString(['filename' => \basename($path)], ';'),
Headers::CONTENT_TYPE => $streamWrapper->getMimetype(),
]);
$response->setCache([
'etag' => \hash('sha1', \sprintf('Asset in archive: %s:%s', $hash, $path)),
'max_age' => $maxAge,
'public' => true,
'private' => false,
]);
]));

return $response;
}
Expand Down
7 changes: 5 additions & 2 deletions EMS/common-bundle/src/Storage/Service/S3Storage.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
class S3Storage extends AbstractUrlStorage
{
private ?S3Client $s3Client = null;
private bool $streamWrapperRegistered = false;

/**
* @param array{version?: string, credentials?: array{key: string, secret: string}, region?: string} $credentials
Expand All @@ -33,7 +34,10 @@ public function __construct(LoggerInterface $logger, private readonly Cache $cac

protected function getBaseUrl(): string
{
$this->getS3Client();
if (!$this->streamWrapperRegistered) {
$this->getS3Client()->registerStreamWrapper();
$this->streamWrapperRegistered = true;
}

return "s3://$this->bucket";
}
Expand Down Expand Up @@ -222,7 +226,6 @@ private function getS3Client(): S3Client
{
if (null === $this->s3Client) {
$this->s3Client = new S3Client($this->credentials);
$this->s3Client->registerStreamWrapper();
}

return $this->s3Client;
Expand Down
25 changes: 20 additions & 5 deletions EMS/common-bundle/src/Twig/AssetRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
use EMS\Helpers\File\TempDirectory;
use EMS\Helpers\File\TempFile;
use EMS\Helpers\Standard\Json;
use EMS\Helpers\Standard\Type;
use Psr\Log\LoggerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class AssetRuntime
Expand Down Expand Up @@ -197,12 +199,25 @@ public function hash(string $input, ?string $hashAlgo = null, bool $binary = fal
return $this->storageManager->computeStringHash($input, $hashAlgo, $binary);
}

public function fileFromArchive(string $hash, string $path): string
/**
* @param mixed[] $options
*/
public function fileFromArchive(string $hash, string $path, array $options = []): null|string|TempFile
{
$streamWrapper = $this->storageManager->getStreamFromArchive($hash, $path);
$tempFile = TempFile::create();
$tempFile->loadFromStream($streamWrapper->getStream());
$extract = Type::bool($options['extract'] ?? true);
$asTempFile = Type::bool($options['asTempFile'] ?? false);
try {
$streamWrapper = $this->storageManager->getStreamFromArchive($hash, $path, $extract);
$tempFile = TempFile::create();
$tempFile->loadFromStream($streamWrapper->getStream());
} catch (NotFoundHttpException $e) {
if ($extract) {
throw $e;
}

return null;
}

return $tempFile->path;
return $asTempFile ? $tempFile : $tempFile->path;
}
}
7 changes: 5 additions & 2 deletions EMS/common-bundle/tests/Unit/Helper/CacheAiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use EMS\CommonBundle\Helper\Cache;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class CacheAiTest extends TestCase
Expand Down Expand Up @@ -37,12 +38,13 @@ public function testGenerateEtagWithNonStringContent(): void

public function testMakeResponseCacheable(): void
{
$request = new Request();
$response = new Response();
$etag = '"test-etag"';
$lastUpdateDate = new \DateTime('2023-01-01');
$immutableRoute = true;

$this->cache->makeResponseCacheable($response, $etag, $lastUpdateDate, $immutableRoute);
$this->cache->makeResponseCacheable($request, $response, $etag, $lastUpdateDate, $immutableRoute);

$this->assertEquals($etag, $response->getEtag());
$this->assertEquals(2_678_400, $response->getMaxAge());
Expand All @@ -52,11 +54,12 @@ public function testMakeResponseCacheable(): void

public function testMakeResponseCacheableWithoutLastUpdateDate(): void
{
$request = new Request();
$response = new Response();
$etag = '"test-etag"';
$immutableRoute = false;

$this->cache->makeResponseCacheable($response, $etag, null, $immutableRoute);
$this->cache->makeResponseCacheable($request, $response, $etag, null, $immutableRoute);

$this->assertEquals($etag, $response->getEtag());
$this->assertEquals(3600, $response->getMaxAge());
Expand Down
37 changes: 32 additions & 5 deletions EMS/common-bundle/tests/Unit/Helper/CacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace EMS\CommonBundle\Tests\Unit\Helper;

use EMS\CommonBundle\Helper\Cache;
use EMS\Helpers\Html\Headers;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class CacheTest extends TestCase
Expand Down Expand Up @@ -36,31 +38,56 @@ public function testGenerateEtagShouldReturnHash(): void

public function testMakeResponseCacheableReturnSameEtag(): void
{
$this->cache->makeResponseCacheable($this->response, 'test', null, false);
$request = new Request();
$this->cache->makeResponseCacheable($request, $this->response, 'test', null, false);
self::assertSame('"test"', $this->response->getEtag());
}

public function testMakeResponseCacheableReturnSameMaxAgeFalse(): void
{
$this->cache->makeResponseCacheable($this->response, 'test', null, false);
$request = new Request();
$this->cache->makeResponseCacheable($request, $this->response, 'test', null, false);
self::assertSame(3600, $this->response->getMaxAge());
}

public function testMakeResponseCacheableReturnSameMaxAgeTrue(): void
{
$this->cache->makeResponseCacheable($this->response, 'test', null, true);
$request = new Request();
$this->cache->makeResponseCacheable($request, $this->response, 'test', null, true);
self::assertSame(2_678_400, $this->response->getMaxAge());
}

public function testMakeResponseCacheableReturnSameLastUpdateDateNotNull(): void
{
$this->cache->makeResponseCacheable($this->response, 'test', null, false);
$request = new Request();
$this->cache->makeResponseCacheable($request, $this->response, 'test', null, false);
self::assertSame(null, $this->response->getLastModified());
}

public function testMakeResponseCacheableReturnSameImmutableRoute(): void
{
$this->cache->makeResponseCacheable($this->response, 'test', null, false);
$request = new Request();
$this->cache->makeResponseCacheable($request, $this->response, 'test', null, false);
self::assertSame(true, !$this->response->isImmutable());
}

public function testEtagIsNotModified(): void
{
$request = new Request();
$request->headers->replace([
Headers::IF_NONE_MATCH => '"test"',
]);
$this->cache->makeResponseCacheable($request, $this->response, 'test', null, false);
self::assertTrue($this->response->isNotModified($request));
}

public function testEtagIsNotModifiedBehindApacheWithDeflateMod(): void
{
$request = new Request();
$request->headers->replace([
Headers::IF_NONE_MATCH => '"test-gzip"',
]);
$this->cache->makeResponseCacheable($request, $this->response, 'test', null, false);
self::assertTrue($this->response->isNotModified($request));
}
}
1 change: 1 addition & 0 deletions EMS/helpers/src/Html/Headers.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ class Headers
public const WWW_AUTHENTICATE = 'WWW-Authenticate';
public const SET_COOKIE = 'set-cookie';
public const COOKIE = 'cookie';
public const IF_NONE_MATCH = 'if-none-match';
}
20 changes: 20 additions & 0 deletions docs/dev/client-helper-bundle/twig.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,23 @@ Some repetitive computes can also be cached. For that you may call the `cacheabl
```

The `cacheType` parameter is the content type's name used to invalidate the response in cache (if something has been updated)


## emsch_http_error

Cancel the curent rendering an return the specified HTTP return code. Useful to redirect to another URL.

Return a 404:

```twig
{% do emsch_http_error(404, 'Page not found') %}
```

Redirect to Symfony:

```twig
{% do emsch_http_error(307, "Temporary Redirect to #{url}", {
location: url,
}) %}
```

14 changes: 14 additions & 0 deletions docs/dev/common-bundle/twig.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,20 @@ Returns a path to a temporary asset extracted from an archive (a zip file). Usef
{% set path = ems_file_from_archive('253b903b1fb3ac30975ae9844a0352a65cdcfa3d', 'img/logos/full-logo.svg') %}
````

This function also accept extra options:

* `extract`: Specify that the function can not try to re-extract the archive if the searched path is missing. If disabled the function won't throw an error but `null`. Default value: `true`.
* `asTempFile`: Returns a [EMS\Helpers\File\TempFile](https://github.com/ems-project/elasticms/blob/5.x/EMS/helpers/src/File/TempFile.php) object when activated, a path to the temporary file instead. A TempFile is useful to get file's contents with the member function `getContents()`. Default value `false`

Example:

```twig
{% set tempFile = ems_file_from_archive(hash, "#{path}/index.php", {
extract: false,
asTempFile: true,
}) %}
```

## ems_link

Return the [EMSLink](https://github.com/ems-project/elasticms/blob/HEAD/EMS/common-bundle/src/Common/EMSLink.php) object
Expand Down

0 comments on commit 473c43f

Please sign in to comment.