Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gotenberg output filename #137

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/Builder/AsyncBuilderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 11 additions & 4 deletions src/Builder/DefaultBuilderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually no. Because whatever we do Gotenberg will add the extension.

Copy link
Collaborator Author

@StevenRenaux StevenRenaux Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normally we guess the filename from the response. Maybe there are some edge cases @Jean-Beru ?

Copy link
Contributor

@Jean-Beru Jean-Beru Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can extract the filename from the response to pass it to the processor constructor. But it means that the request will be sent before calling GotenbergFileResult::process or GotenbergFileResult::stream to retrieve headers.

An other solution could be that a processor doesn't receive the filename in its constructor. It will have to retrieve it by itself from a chunk.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to dig a bit to understand more.

}

$this->fileName = $fileName;
$this->headerDisposition = $headerDisposition;

Expand Down Expand Up @@ -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,
);
}
}
22 changes: 13 additions & 9 deletions src/Builder/GotenbergFileResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, void, ChunkInterface, mixed> $processorGenerator
* @param ProcessorInterface<mixed> $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,
) {
}

Expand Down Expand Up @@ -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));
Jean-Beru marked this conversation as resolved.
Show resolved Hide resolved

if (null !== $this->getFileName()) {
$headers->set('Content-Disposition', HeaderUtils::makeDisposition($this->disposition, $this->getFileName()));
}

return new StreamedResponse(
Expand All @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Processor/FileProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}

Expand Down
43 changes: 27 additions & 16 deletions tests/Builder/GotenbergFileResultTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,43 +18,37 @@
final class GotenbergFileResultTest extends TestCase
{
private GotenbergResponse $response;
/** \Generator<int, void, ChunkInterface, string> */
private \Generator $processorGenerator;

/** @var ProcessorInterface<mixed> */
private ProcessorInterface $processor;

protected function setUp(): void
{
$client = new MockHttpClient(new MockResponse(['a', 'b', 'c']));
$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();
Expand All @@ -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<string>
*/
class TestProcessor implements ProcessorInterface
{
public function __invoke(string|null $fileName): \Generator
{
$content = '';
do {
$chunk = yield;
$content .= $chunk->getContent();
} while (!$chunk->isLast());

return $content;
}
}
10 changes: 7 additions & 3 deletions tests/Builder/Pdf/AbstractPdfBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion tests/Builder/Screenshot/AbstractScreenshotBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading