From 3944a701aa7d02887c6fd3a425ace9784e9d9be8 Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Wed, 8 Jan 2025 15:57:24 +0100 Subject: [PATCH] Add gotenberg output filename --- src/Builder/AsyncBuilderTrait.php | 1 - src/Builder/DefaultBuilderTrait.php | 15 +++++-- src/Builder/GotenbergFileResult.php | 22 ++++++---- src/Processor/FileProcessor.php | 2 +- tests/Builder/GotenbergFileResultTest.php | 43 ++++++++++++------- tests/Builder/Pdf/AbstractPdfBuilderTest.php | 10 +++-- .../AbstractScreenshotBuilderTest.php | 6 ++- 7 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/Builder/AsyncBuilderTrait.php b/src/Builder/AsyncBuilderTrait.php index b71646ef..abee1465 100644 --- a/src/Builder/AsyncBuilderTrait.php +++ b/src/Builder/AsyncBuilderTrait.php @@ -56,7 +56,6 @@ public function generateAsync(): void } if (null !== $this->fileName) { - // Gotenberg will add the extension to the file name (e.g. filename : "file.pdf" => generated file : "file.pdf.pdf"). $headers['Gotenberg-Output-Filename'] = $this->fileName; } $this->client->call($this->getEndpoint(), $this->getMultipartFormData(), $headers); diff --git a/src/Builder/DefaultBuilderTrait.php b/src/Builder/DefaultBuilderTrait.php index d125a2b4..932798f7 100644 --- a/src/Builder/DefaultBuilderTrait.php +++ b/src/Builder/DefaultBuilderTrait.php @@ -4,6 +4,7 @@ use Psr\Log\LoggerInterface; use Sensiolabs\GotenbergBundle\Client\GotenbergClientInterface; +use Sensiolabs\GotenbergBundle\Exception\InvalidBuilderConfiguration; use Sensiolabs\GotenbergBundle\Exception\JsonEncodingException; use Sensiolabs\GotenbergBundle\Formatter\AssetBaseDirFormatter; use Sensiolabs\GotenbergBundle\Processor\NullProcessor; @@ -69,6 +70,10 @@ protected function encodeData(string $key, mixed $value): array */ public function fileName(string $fileName, string $headerDisposition = HeaderUtils::DISPOSITION_INLINE): static { + if (!preg_match('/\.[^.]+$/', $fileName)) { + throw new InvalidBuilderConfiguration(\sprintf('File name "%s" needs to get extension as ".pdf", ".png" or any other valid extension.', $fileName)); + } + $this->fileName = $fileName; $this->headerDisposition = $headerDisposition; @@ -214,13 +219,15 @@ public function generate(): GotenbergFileResult 'sensiolabs_gotenberg.builder' => $this::class, ]); - $processor = $this->processor ?? new NullProcessor(); + $headers = []; + if (null !== $this->fileName) { + $headers['Gotenberg-Output-Filename'] = $this->fileName; + } return new GotenbergFileResult( - $this->client->call($this->getEndpoint(), $this->getMultipartFormData()), - $processor($this->fileName), + $this->client->call($this->getEndpoint(), $this->getMultipartFormData(), $headers), + $this->processor ?? new NullProcessor(), $this->headerDisposition, - $this->fileName, ); } } diff --git a/src/Builder/GotenbergFileResult.php b/src/Builder/GotenbergFileResult.php index d80094de..880ba5eb 100644 --- a/src/Builder/GotenbergFileResult.php +++ b/src/Builder/GotenbergFileResult.php @@ -4,21 +4,20 @@ use Sensiolabs\GotenbergBundle\Client\GotenbergResponse; use Sensiolabs\GotenbergBundle\Exception\ProcessorException; +use Sensiolabs\GotenbergBundle\Processor\ProcessorInterface; use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\StreamedResponse; -use Symfony\Contracts\HttpClient\ChunkInterface; class GotenbergFileResult { /** - * @param \Generator $processorGenerator + * @param ProcessorInterface $processor */ public function __construct( protected readonly GotenbergResponse $response, - protected readonly \Generator $processorGenerator, + protected readonly ProcessorInterface $processor, protected readonly string $disposition, - protected readonly string|null $fileName = null, ) { } @@ -51,19 +50,22 @@ public function process(): mixed throw new ProcessorException('Already processed query.'); } + $processorGenerator = ($this->processor)($this->getFileName()); + foreach ($this->response->getStream() as $chunk) { - $this->processorGenerator->send($chunk); + $processorGenerator->send($chunk); } - return $this->processorGenerator->getReturn(); + return $processorGenerator->getReturn(); } public function stream(): StreamedResponse { $headers = $this->getHeaders(); $headers->set('X-Accel-Buffering', 'no'); // See https://symfony.com/doc/current/components/http_foundation.html#streaming-a-json-response - if (null !== $this->fileName) { - $headers->set('Content-Disposition', HeaderUtils::makeDisposition($this->disposition, $this->fileName)); + + if (null !== $this->getFileName()) { + $headers->set('Content-Disposition', HeaderUtils::makeDisposition($this->disposition, $this->getFileName())); } return new StreamedResponse( @@ -72,8 +74,10 @@ function (): void { throw new ProcessorException('Already processed query.'); } + $processorGenerator = ($this->processor)($this->getFileName()); + foreach ($this->response->getStream() as $chunk) { - $this->processorGenerator->send($chunk); + $processorGenerator->send($chunk); echo $chunk->getContent(); flush(); } diff --git a/src/Processor/FileProcessor.php b/src/Processor/FileProcessor.php index 7ded3aed..29d2317e 100644 --- a/src/Processor/FileProcessor.php +++ b/src/Processor/FileProcessor.php @@ -21,7 +21,7 @@ public function __construct( public function __invoke(string|null $fileName): \Generator { if (null === $fileName) { - $fileName = uniqid('gotenberg_', true).'.pdf'; + $fileName = uniqid('gotenberg_', true); $this->logger?->debug('{processor}: no filename given. Content will be dumped to "{file}".', ['processor' => self::class, 'file' => $fileName]); } diff --git a/tests/Builder/GotenbergFileResultTest.php b/tests/Builder/GotenbergFileResultTest.php index 96e3e01b..befd50ce 100644 --- a/tests/Builder/GotenbergFileResultTest.php +++ b/tests/Builder/GotenbergFileResultTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Sensiolabs\GotenbergBundle\Builder\GotenbergFileResult; use Sensiolabs\GotenbergBundle\Client\GotenbergResponse; +use Sensiolabs\GotenbergBundle\Processor\ProcessorInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\ResponseHeaderBag; @@ -17,8 +18,9 @@ final class GotenbergFileResultTest extends TestCase { private GotenbergResponse $response; - /** \Generator */ - private \Generator $processorGenerator; + + /** @var ProcessorInterface */ + private ProcessorInterface $processor; protected function setUp(): void { @@ -26,34 +28,27 @@ protected function setUp(): void $this->response = new GotenbergResponse( $client->stream($client->request('GET', '/')), 200, - new ResponseHeaderBag(), + new ResponseHeaderBag([ + 'Content-Disposition' => 'inline; filename="file.pdf"', + ]), ); - $this->processorGenerator = (function () { - $content = ''; - do { - $chunk = yield; - $content .= $chunk->getContent(); - } while (!$chunk->isLast()); - - return $content; - })(); + $this->processor = new TestProcessor(); } #[TestDox('Response is processed')] public function testProcess(): void { - $result = new GotenbergFileResult($this->response, $this->processorGenerator, 'inline', 'file.pdf'); + $result = new GotenbergFileResult($this->response, $this->processor, 'inline'); $process = $result->process(); self::assertSame('abc', $process); - self::assertSame('abc', $this->processorGenerator->getReturn()); } #[TestDox('Response is streamed')] public function testStream(): void { - $result = new GotenbergFileResult($this->response, $this->processorGenerator, 'inline', 'file.pdf'); + $result = new GotenbergFileResult($this->response, $this->processor, 'inline'); $stream = $result->stream(); ob_start(); @@ -63,6 +58,22 @@ public function testStream(): void self::assertSame('inline; filename=file.pdf', $stream->headers->get('Content-Disposition')); self::assertSame('no', $stream->headers->get('X-Accel-Buffering')); - self::assertSame('abc', $this->processorGenerator->getReturn()); + } +} + +/** + * @implements ProcessorInterface + */ +class TestProcessor implements ProcessorInterface +{ + public function __invoke(string|null $fileName): \Generator + { + $content = ''; + do { + $chunk = yield; + $content .= $chunk->getContent(); + } while (!$chunk->isLast()); + + return $content; } } diff --git a/tests/Builder/Pdf/AbstractPdfBuilderTest.php b/tests/Builder/Pdf/AbstractPdfBuilderTest.php index d9a63641..1246f89a 100644 --- a/tests/Builder/Pdf/AbstractPdfBuilderTest.php +++ b/tests/Builder/Pdf/AbstractPdfBuilderTest.php @@ -30,16 +30,20 @@ public function testFilenameIsCorrectlySetOnResponse(): void { // @phpstan-ignore-next-line $this->gotenbergClient = new GotenbergClient(new MockHttpClient([ - new MockResponse(), + new MockResponse(info: [ + 'response_headers' => [ + 'Content-Disposition' => 'attachment; filename="some_file.pdf"', + ], + ]), ])); $response = $this->getPdfBuilder() - ->fileName('some_file.png', HeaderUtils::DISPOSITION_ATTACHMENT) + ->fileName('some_file.pdf', HeaderUtils::DISPOSITION_ATTACHMENT) ->generate() ->stream() ; - self::assertSame('attachment; filename=some_file.png', $response->headers->get('Content-Disposition')); + self::assertSame('attachment; filename=some_file.pdf', $response->headers->get('Content-Disposition')); } public static function nativeNormalizersProvider(): \Generator diff --git a/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php b/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php index c47f2911..f0a775d8 100644 --- a/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php +++ b/tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php @@ -30,7 +30,11 @@ public function testFilenameIsCorrectlySetOnResponse(): void { // @phpstan-ignore-next-line $this->gotenbergClient = new GotenbergClient(new MockHttpClient([ - new MockResponse(), + new MockResponse(info: [ + 'response_headers' => [ + 'Content-Disposition' => 'attachment; filename="some_file.png"', + ], + ]), ])); $response = $this->getScreenshotBuilder()