mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e69ec4367 | ||
|
|
4623c01896 | ||
|
|
e85322de62 | ||
|
|
090ff3da13 | ||
|
|
fe068b1bfb | ||
|
|
d3b0a685b9 | ||
|
|
245c04fc5a | ||
|
|
94acb0c6ae | ||
|
|
ad4a16cc4e | ||
|
|
dae39113f6 | ||
|
|
1e1cb8fe57 | ||
|
|
606254d941 | ||
|
|
8fd51b9e9e | ||
|
|
f90442cfb1 | ||
|
|
2a44c0f65a | ||
|
|
cd8d2b5e95 | ||
|
|
8d89b2e80a | ||
|
|
ab475d65ee | ||
|
|
fa9963a737 | ||
|
|
d65d9c9a4c | ||
|
|
6f8c7a613f |
4
Makefile
4
Makefile
@@ -15,6 +15,7 @@ DEFAULT_LISTEN_PORT=3000
|
||||
DISABLE_GOOGLE_CHROME=0
|
||||
DISABLE_UNOCONV=0
|
||||
LOG_LEVEL=INFO
|
||||
DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576
|
||||
|
||||
# build the base Docker image.
|
||||
base:
|
||||
@@ -43,6 +44,7 @@ tests:
|
||||
|
||||
# generate documentation.
|
||||
doc:
|
||||
make workspace
|
||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REPOSITORY)/gotenberg:docs
|
||||
|
||||
@@ -53,7 +55,7 @@ image:
|
||||
|
||||
# start the API using previously built Docker image.
|
||||
gotenberg:
|
||||
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||
|
||||
# publish Gotenberg images according to version.
|
||||
publish:
|
||||
|
||||
@@ -47,5 +47,9 @@ securityContext:
|
||||
runAsUser: 1001
|
||||
```
|
||||
|
||||
## Cloud Run (Google Cloud)
|
||||
|
||||
If you're looking for cost savings, you might be interested by [Cloud Run](https://cloud.google.com/run).
|
||||
|
||||
In the following examples, we will assume your
|
||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
@@ -23,3 +23,7 @@ Then the PHP client:
|
||||
```bash
|
||||
$ composer require thecodingmachine/gotenberg-php-client
|
||||
```
|
||||
|
||||
## Community clients
|
||||
|
||||
* [JavaScript/TypeScript client](https://github.com/yumauri/gotenberg-js-client) by [yumauri](https://github.com/yumauri)
|
||||
|
||||
@@ -33,6 +33,19 @@ It takes the strings `"0"` or `"1"` as value where `1` means `true`
|
||||
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
|
||||
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
||||
|
||||
## Default Google Chrome rpcc buffer size
|
||||
|
||||
When performing a [HTML](#html), [URL](#url) or [Markdown](#markdown) conversion, the API might return
|
||||
a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
|
||||
|
||||
If so, you may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`.
|
||||
|
||||
It takes a string representation of an int as value (e.g. `"1048576"` for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.
|
||||
|
||||
> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`.
|
||||
> See the [rpcc buffer size section](#html.rpcc_buffer_size).
|
||||
|
||||
## Disable LibreOffice (unoconv)
|
||||
|
||||
You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`.
|
||||
|
||||
@@ -95,14 +95,15 @@ The following classes allow you to inject printing values:
|
||||
* `pageNumber`: current page number
|
||||
* `totalPage`: total pages in the document
|
||||
|
||||
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
||||
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
||||
There are some limitations:
|
||||
|
||||
For images, the only solution currently is to use a `base64` encoded source:
|
||||
|
||||
```html
|
||||
<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO 9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
|
||||
```
|
||||
* JavaScript is not executed
|
||||
* external resources are not loaded
|
||||
* the CSS properties are independant of the ones used in the `index.html` file
|
||||
* `footer.html` CSS properties override the ones from `header.html`
|
||||
* only fonts installed in the Docker image are loaded (see the [fonts section](#fonts))
|
||||
* images only work using a `base64` encoded source (`<img src="data:image/png;base64, iVBORw0K... />`)
|
||||
* `background-color` and `color` CSS properties require an additional `-webkit-print-color-adjust: exact` CSS property in order to work
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -256,7 +257,7 @@ $ curl --request POST \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form paperWidth=8.27 \
|
||||
--form paperHeight=11.27 \
|
||||
--form paperHeight=11.69 \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
@@ -347,3 +348,55 @@ $request->setWaitDelay(5.5);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Rpcc buffer size
|
||||
|
||||
The API might return a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
|
||||
|
||||
If so, you may increase this buffer size with a form field named `googleChromeRpccBufferSize`.
|
||||
|
||||
It takes an int as value (e.g. `1048576` for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_google_chrome_rpcc_buffer_size) section.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form googleChromeRpccBufferSize=1048576 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.GoogleChromeRpccBufferSize(1048576)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setGoogleChromeRpccBufferSize(1048576);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -5,17 +5,17 @@ title: Scalability
|
||||
The API uses under the hood intricate programs.
|
||||
|
||||
Gotenberg tries to abstract as much complexity as possible but it can
|
||||
only do it to a certain extend.
|
||||
only do it to a certain extent.
|
||||
|
||||
For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk
|
||||
instances are there are requests. The limitation here is the available memory and CPU usage.
|
||||
instances as there are requests. The limitation here is the available memory and CPU usage.
|
||||
|
||||
On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel.
|
||||
Indeed, Google Chrome misbehaves if there are too many concurrent conversions.
|
||||
|
||||
**The more concurrent requests, the more `504` HTTP codes the API will return.**
|
||||
|
||||
> See our [load testing use case](../loadtesting) for more details about the API behaviour under heavy load.
|
||||
> See our [load testing use case](https://github.com/thecodingmachine/gotenberg/tree/master/loadtesting) for more details about the API behaviour under heavy load.
|
||||
|
||||
## Strategies
|
||||
|
||||
|
||||
@@ -9,5 +9,9 @@ If you wish to use more fonts, you will have to create your own image:
|
||||
```Dockerfile
|
||||
FROM thecodingmachine/gotenberg:6
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get -y install yourfonts
|
||||
|
||||
USER gotenberg
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# | Binary
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Buils Gotenberg binary.
|
||||
# | Builds Gotenberg binary.
|
||||
# |
|
||||
|
||||
FROM thecodingmachine/gotenberg:workspace AS workspace
|
||||
@@ -42,7 +42,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
ARG TINI_VERSION
|
||||
|
||||
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
|
||||
ADD --chown=gotenberg https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static /tini
|
||||
RUN chmod +x /tini
|
||||
ENTRYPOINT [ "/tini", "--" ]
|
||||
|
||||
@@ -59,4 +59,4 @@ USER gotenberg
|
||||
WORKDIR /gotenberg
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "gotenberg" ]
|
||||
CMD [ "gotenberg" ]
|
||||
|
||||
120
docs/index.html
120
docs/index.html
@@ -184,6 +184,12 @@
|
||||
runAsUser: 1001
|
||||
</pre>
|
||||
|
||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.cloud_run_google_cloud" href="#install.cloud_run_google_cloud">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>Cloud Run (Google Cloud)</h2>
|
||||
|
||||
<p>If you’re looking for cost savings, you might be interested by <a href="https://cloud.google.com/run">Cloud Run</a>.</p>
|
||||
|
||||
<p>In the following examples, we will assume your
|
||||
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
||||
|
||||
@@ -216,6 +222,14 @@ Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3
|
||||
<pre class="chroma">$ composer require thecodingmachine/gotenberg-php-client
|
||||
</pre>
|
||||
|
||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="clients.community_clients" href="#clients.community_clients">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>Community clients</h2>
|
||||
|
||||
<ul>
|
||||
<li><a href="https://github.com/yumauri/gotenberg-js-client">JavaScript/TypeScript client</a> by <a href="https://github.com/yumauri">yumauri</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="Page" id="environment_variables">
|
||||
@@ -263,6 +277,23 @@ for disabling Google Chrome.</p>
|
||||
<a href="#html">HTML</a>, <a href="#url">URL</a> and <a href="#markdown">Markdown</a></p>
|
||||
</blockquote>
|
||||
|
||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.default_google_chrome_rpcc_buffer_size" href="#environment_variables.default_google_chrome_rpcc_buffer_size">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>Default Google Chrome rpcc buffer size</h2>
|
||||
|
||||
<p>When performing a <a href="#html">HTML</a>, <a href="#url">URL</a> or <a href="#markdown">Markdown</a> conversion, the API might return
|
||||
a <code>400</code> HTTP code with the message <code>increase the Google Chrome rpcc buffer size</code>.</p>
|
||||
|
||||
<p>If so, you may increase this buffer size with the environment variable <code>DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE</code>.</p>
|
||||
|
||||
<p>It takes a string representation of an int as value (e.g. <code>"1048576"</code> for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.</p>
|
||||
|
||||
<blockquote>
|
||||
<p>The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field <code>googleChromeRpccBufferSize</code>.
|
||||
See the <a href="#html.rpcc_buffer_size">rpcc buffer size section</a>.</p>
|
||||
</blockquote>
|
||||
|
||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.disable_libre_office_unoconv" href="#environment_variables.disable_libre_office_unoconv">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>Disable LibreOffice (unoconv)</h2>
|
||||
@@ -449,15 +480,17 @@ Respectively, a file named <code>header.html</code> and <code>footer.html</code>
|
||||
<li><code>totalPage</code>: total pages in the document</li>
|
||||
</ul>
|
||||
|
||||
<blockquote>
|
||||
<p><strong>Attention:</strong> the CSS properties are independant of the ones used in the <code>index.html</code> file.
|
||||
Also, <code>footer.html</code> CSS properties override the ones from <code>header.html</code>.</p>
|
||||
</blockquote>
|
||||
<p>There are some limitations:</p>
|
||||
|
||||
<p>For images, the only solution currently is to use a <code>base64</code> encoded source:</p>
|
||||
|
||||
<pre class="chroma"><span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO 9TXL0Y4OHwAAAABJRU5ErkJggg=="</span> <span class="na">alt</span><span class="o">=</span><span class="s">"Red dot"</span> <span class="p">/></span>
|
||||
</pre>
|
||||
<ul>
|
||||
<li>JavaScript is not executed</li>
|
||||
<li>external resources are not loaded</li>
|
||||
<li>the CSS properties are independant of the ones used in the <code>index.html</code> file</li>
|
||||
<li><code>footer.html</code> CSS properties override the ones from <code>header.html</code></li>
|
||||
<li>only fonts installed in the Docker image are loaded (see the <a href="#fonts">fonts section</a>)</li>
|
||||
<li>images only work using a <code>base64</code> encoded source (<code><img src="data:image/png;base64, iVBORw0K... /></code>)</li>
|
||||
<li><code>background-color</code> and <code>color</code> CSS properties require an additional <code>-webkit-print-color-adjust: exact</code> CSS property in order to work</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer.c_url" href="#html.header_and_footer.c_url">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
@@ -624,7 +657,7 @@ $client->store($request, $dest);
|
||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">paperWidth</span><span class="o">=</span><span class="m">8</span>.27 <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">paperHeight</span><span class="o">=</span><span class="m">11</span>.27 <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">paperHeight</span><span class="o">=</span><span class="m">11</span>.69 <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">marginTop</span><span class="o">=</span><span class="m">0</span> <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">marginBottom</span><span class="o">=</span><span class="m">0</span> <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">marginLeft</span><span class="o">=</span><span class="m">0</span> <span class="se">\
|
||||
@@ -723,6 +756,65 @@ $request = new HTMLRequest($index);
|
||||
$request->setWaitDelay(5.5);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
</pre>
|
||||
|
||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.rpcc_buffer_size" href="#html.rpcc_buffer_size">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>Rpcc buffer size</h2>
|
||||
|
||||
<p>The API might return a <code>400</code> HTTP code with the message <code>increase the Google Chrome rpcc buffer size</code>.</p>
|
||||
|
||||
<p>If so, you may increase this buffer size with a form field named <code>googleChromeRpccBufferSize</code>.</p>
|
||||
|
||||
<p>It takes an int as value (e.g. <code>1048576</code> for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.</p>
|
||||
|
||||
<blockquote>
|
||||
<p>You may also define this value globally: see the <a href="#environment_variables.default_google_chrome_rpcc_buffer_size">environment variables</a> section.</p>
|
||||
</blockquote>
|
||||
|
||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.rpcc_buffer_size.c_url" href="#html.rpcc_buffer_size.c_url">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>cURL</h3>
|
||||
|
||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
||||
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
|
||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
|
||||
</span><span class="se"></span> --form <span class="nv">googleChromeRpccBufferSize</span><span class="o">=</span><span class="m">1048576</span> <span class="se">\
|
||||
</span><span class="se"></span> -o result.pdf
|
||||
</pre>
|
||||
|
||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.rpcc_buffer_size.go" href="#html.rpcc_buffer_size.go">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>Go</h3>
|
||||
|
||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v6"</span>
|
||||
|
||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
|
||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
||||
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewHTMLRequest</span><span class="p">(</span><span class="s">"index.html"</span><span class="p">)</span>
|
||||
<span class="nx">req</span><span class="p">.</span><span class="nf">GoogleChromeRpccBufferSize</span><span class="p">(</span><span class="mi">1048576</span><span class="p">)</span>
|
||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
||||
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
||||
<span class="p">}</span>
|
||||
</pre>
|
||||
|
||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.rpcc_buffer_size.php" href="#html.rpcc_buffer_size.php">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
</a>PHP</h3>
|
||||
|
||||
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setGoogleChromeRpccBufferSize(1048576);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
</pre>
|
||||
|
||||
</div>
|
||||
@@ -1317,10 +1409,10 @@ $resp = $client->post($request);
|
||||
<p>The API uses under the hood intricate programs.</p>
|
||||
|
||||
<p>Gotenberg tries to abstract as much complexity as possible but it can
|
||||
only do it to a certain extend.</p>
|
||||
only do it to a certain extent.</p>
|
||||
|
||||
<p>For instance, <a href="#office">Office</a> and <a href="#merge">Merge</a> endpoints will start respectively as many LibreOffice (unoconv) and PDTk
|
||||
instances are there are requests. The limitation here is the available memory and CPU usage.</p>
|
||||
instances as there are requests. The limitation here is the available memory and CPU usage.</p>
|
||||
|
||||
<p>On another hand, for the <a href="#html">HTML</a>, <a href="#url">URL</a> and <a href="#markdown">Markdown</a> endpoints, the API does only 6 conversions in parallel.
|
||||
Indeed, Google Chrome misbehaves if there are too many concurrent conversions.</p>
|
||||
@@ -1328,7 +1420,7 @@ Indeed, Google Chrome misbehaves if there are too many concurrent conversions.</
|
||||
<p><strong>The more concurrent requests, the more <code>504</code> HTTP codes the API will return.</strong></p>
|
||||
|
||||
<blockquote>
|
||||
<p>See our <a href="../loadtesting">load testing use case</a> for more details about the API behaviour under heavy load.</p>
|
||||
<p>See our <a href="https://github.com/thecodingmachine/gotenberg/tree/master/loadtesting">load testing use case</a> for more details about the API behaviour under heavy load.</p>
|
||||
</blockquote>
|
||||
|
||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="scalability.strategies" href="#scalability.strategies">
|
||||
@@ -1399,7 +1491,11 @@ restart your Gotenberg instances from time to time to ensure a nominal behaviour
|
||||
|
||||
<pre class="chroma"><span class="k">FROM</span><span class="s"> thecodingmachine/gotenberg:6</span><span class="err">
|
||||
</span><span class="err">
|
||||
</span><span class="err"></span><span class="k">USER</span><span class="s"> root</span><span class="err">
|
||||
</span><span class="err">
|
||||
</span><span class="err"></span><span class="k">RUN</span> apt-get -y install yourfonts<span class="err">
|
||||
</span><span class="err">
|
||||
</span><span class="err"></span><span class="k">USER</span><span class="s"> gotenberg</span><span class="err">
|
||||
</span></pre>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
@@ -24,6 +25,31 @@ const (
|
||||
officeEndpoint string = "/office"
|
||||
)
|
||||
|
||||
func isMultipartFormDataEndpoint(config conf.Config, path string) bool {
|
||||
var multipartFormDataEndpoints []string
|
||||
multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint)
|
||||
if !config.DisableGoogleChrome() {
|
||||
multipartFormDataEndpoints = append(
|
||||
multipartFormDataEndpoints,
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint),
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint),
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint),
|
||||
)
|
||||
}
|
||||
if !config.DisableUnoconv() {
|
||||
multipartFormDataEndpoints = append(
|
||||
multipartFormDataEndpoints,
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint),
|
||||
)
|
||||
}
|
||||
for _, endpoint := range multipartFormDataEndpoints {
|
||||
if endpoint == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pingHandler is the handler for healthcheck.
|
||||
func pingHandler(c echo.Context) error {
|
||||
const op string = "xhttp.pingHandler"
|
||||
|
||||
@@ -19,9 +19,11 @@ func TestPingHandler(t *testing.T) {
|
||||
// should return 200.
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
srv = New(config)
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodPost, pingEndpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
}
|
||||
|
||||
func TestMergeHandler(t *testing.T) {
|
||||
@@ -32,6 +34,13 @@ func TestMergeHandler(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, mergeEndpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
@@ -66,6 +75,13 @@ func TestHTMLHandler(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
@@ -185,6 +201,24 @@ func TestHTMLHandler(t *testing.T) {
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestURLHandler(t *testing.T) {
|
||||
@@ -196,6 +230,13 @@ func TestURLHandler(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
@@ -315,6 +356,24 @@ func TestURLHandler(t *testing.T) {
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestMarkdownHandler(t *testing.T) {
|
||||
@@ -326,6 +385,13 @@ func TestMarkdownHandler(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
@@ -445,6 +511,24 @@ func TestMarkdownHandler(t *testing.T) {
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestOfficeHandler(t *testing.T) {
|
||||
@@ -456,6 +540,13 @@ func TestOfficeHandler(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
|
||||
@@ -2,6 +2,7 @@ package xhttp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
|
||||
@@ -25,12 +26,28 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
|
||||
// extend the current echo context with our custom
|
||||
// context.
|
||||
ctx := context.New(c, logger, config)
|
||||
// if its an healthcheck request, there
|
||||
// is no need to create a Resource.
|
||||
if ctx.Path() == pingEndpoint {
|
||||
// if it's not a multipart/form-data request,
|
||||
// there is no need to create a Resource.
|
||||
if !isMultipartFormDataEndpoint(config, ctx.Path()) {
|
||||
// validate method for healthcheck endpoint.
|
||||
if ctx.Path() == pingEndpoint && ctx.Request().Method != http.MethodGet {
|
||||
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
// if the endpoint is not for healthcheck, create a
|
||||
// validate method.
|
||||
if ctx.Request().Method != http.MethodPost {
|
||||
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
// validate Content-Type.
|
||||
contentType := ctx.Request().Header.Get("Content-Type")
|
||||
if !strings.Contains(contentType, "multipart/form-data") {
|
||||
err := doErr(ctx, echo.NewHTTPError(http.StatusUnsupportedMediaType))
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
// it's a multipart/form-data request, create a
|
||||
// Resource.
|
||||
if err := ctx.WithResource(trace); err != nil {
|
||||
err = doCleanup(ctx, err)
|
||||
|
||||
@@ -48,18 +48,23 @@ func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.Chro
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
return printer.ChromePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
RpccBufferSize: googleChromeRpccBufferSize,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
|
||||
@@ -51,6 +51,9 @@ const (
|
||||
// LandscapeArgKey is the key
|
||||
// of the argument "landscape".
|
||||
LandscapeArgKey ArgKey = "landscape"
|
||||
// GoogleChromeRpccBufferSizeArgKey is the key
|
||||
// of the argument "googleChromeRpccBufferSize".
|
||||
GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -73,6 +76,7 @@ func ArgKeys() []ArgKey {
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,3 +269,24 @@ func MarginArgs(r Resource, config conf.Config) (float64, float64, float64, floa
|
||||
marginRight,
|
||||
nil
|
||||
}
|
||||
|
||||
/*
|
||||
GoogleChromeRpccBufferSizeArg is a helper for retrieving
|
||||
the "googleChromeRpccBufferSize" argument as int64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func GoogleChromeRpccBufferSizeArg(r Resource, config conf.Config) (int64, error) {
|
||||
const op string = "resource.GoogleChromeRpccBufferSizeArg"
|
||||
result, err := r.Int64Arg(
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
config.DefaultGoogleChromeRpccBufferSize(),
|
||||
xassert.Int64NotInferiorTo(0.0),
|
||||
xassert.Int64NotSuperiorTo(config.MaximumGoogleChromeRpccBufferSize()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestArgKeys(t *testing.T) {
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
}
|
||||
assert.Equal(t, expected, ArgKeys())
|
||||
}
|
||||
@@ -300,3 +301,47 @@ func TestMarginArgs(t *testing.T) {
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleChromeRpccBufferSizeArg(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected int64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
v, err := GoogleChromeRpccBufferSizeArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 10
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "10")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "-1")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "104857601")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "foo")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ func TestHeaderFooterContents(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
defer f2.Close() // nolint: errcheck
|
||||
err = r.WithFile("footer.html", f2)
|
||||
assert.Nil(t, err)
|
||||
header, footer, err = HeaderFooterContents(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, header, expected)
|
||||
|
||||
@@ -261,6 +261,7 @@ func TestFcontent(t *testing.T) {
|
||||
err = r.WithFile(filename, f)
|
||||
assert.Nil(t, err)
|
||||
v, err = r.Fcontent(filename, defaultValue)
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, v, defaultValue)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
|
||||
@@ -13,6 +13,15 @@ import (
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestNonExistingEndpoint(t *testing.T) {
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// "/" endpoint should return 404.
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
}
|
||||
|
||||
func TestDisableChromeEndpoints(t *testing.T) {
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
|
||||
config, err := conf.FromEnv()
|
||||
@@ -73,7 +82,7 @@ func TestDisableUnoconvEndpoints(t *testing.T) {
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
// Markdown endpoint should return 200.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
|
||||
@@ -34,35 +34,42 @@ const (
|
||||
// LogLevelEnvVar contains the name
|
||||
// of the environment variable "LOG_LEVEL".
|
||||
LogLevelEnvVar string = "LOG_LEVEL"
|
||||
// DefaultGoogleChromeRpccBufferSizeEnvVar contains the name
|
||||
// of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE".
|
||||
DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE"
|
||||
)
|
||||
|
||||
// Config contains the application
|
||||
// configuration.
|
||||
type Config struct {
|
||||
maximumWaitTimeout float64
|
||||
maximumWaitDelay float64
|
||||
maximumWebhookURLTimeout float64
|
||||
defaultWaitTimeout float64
|
||||
defaultWebhookURLTimeout float64
|
||||
defaultListenPort int64
|
||||
disableGoogleChrome bool
|
||||
disableUnoconv bool
|
||||
logLevel xlog.Level
|
||||
maximumWaitTimeout float64
|
||||
maximumWaitDelay float64
|
||||
maximumWebhookURLTimeout float64
|
||||
defaultWaitTimeout float64
|
||||
defaultWebhookURLTimeout float64
|
||||
defaultListenPort int64
|
||||
disableGoogleChrome bool
|
||||
disableUnoconv bool
|
||||
logLevel xlog.Level
|
||||
maximumGoogleChromeRpccBufferSize int64
|
||||
defaultGoogleChromeRpccBufferSize int64
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default
|
||||
// configuration.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
maximumWaitTimeout: 30.0,
|
||||
maximumWaitDelay: 10.0,
|
||||
maximumWebhookURLTimeout: 30.0,
|
||||
defaultWaitTimeout: 10.0,
|
||||
defaultWebhookURLTimeout: 10.0,
|
||||
defaultListenPort: 3000,
|
||||
disableGoogleChrome: false,
|
||||
disableUnoconv: false,
|
||||
logLevel: xlog.InfoLevel,
|
||||
maximumWaitTimeout: 30.0,
|
||||
maximumWaitDelay: 10.0,
|
||||
maximumWebhookURLTimeout: 30.0,
|
||||
defaultWaitTimeout: 10.0,
|
||||
defaultWebhookURLTimeout: 10.0,
|
||||
defaultListenPort: 3000,
|
||||
disableGoogleChrome: false,
|
||||
disableUnoconv: false,
|
||||
logLevel: xlog.InfoLevel,
|
||||
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
|
||||
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +163,16 @@ func FromEnv() (Config, error) {
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv(
|
||||
DefaultGoogleChromeRpccBufferSizeEnvVar,
|
||||
c.defaultGoogleChromeRpccBufferSize,
|
||||
xassert.Int64NotInferiorTo(0),
|
||||
xassert.Int64NotSuperiorTo(c.MaximumGoogleChromeRpccBufferSize()),
|
||||
)
|
||||
c.defaultGoogleChromeRpccBufferSize = defaultGoogleChromeRpccBufferSize
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
result, err := resolver()
|
||||
@@ -224,3 +241,15 @@ func (c Config) DisableUnoconv() bool {
|
||||
func (c Config) LogLevel() xlog.Level {
|
||||
return c.logLevel
|
||||
}
|
||||
|
||||
// MaximumGoogleChromeRpccBufferSize returns the maximum
|
||||
// Google Chrome rpcc buffer size from the configuration.
|
||||
func (c Config) MaximumGoogleChromeRpccBufferSize() int64 {
|
||||
return c.maximumGoogleChromeRpccBufferSize
|
||||
}
|
||||
|
||||
// DefaultGoogleChromeRpccBufferSize returns the default
|
||||
// Google Chrome rpcc buffer size from the configuration.
|
||||
func (c Config) DefaultGoogleChromeRpccBufferSize() int64 {
|
||||
return c.defaultGoogleChromeRpccBufferSize
|
||||
}
|
||||
|
||||
@@ -320,6 +320,43 @@ func TestLogLevelFromEnv(t *testing.T) {
|
||||
os.Unsetenv(LogLevelEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE correctly set.
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "100")
|
||||
expected = DefaultConfig()
|
||||
expected.defaultGoogleChromeRpccBufferSize = 100
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE wrongly set.
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE < 0.
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "-1")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE > 100 MB (maximumGoogleChromeRpccBufferSize).
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "104857601")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
}
|
||||
|
||||
func TestGetters(t *testing.T) {
|
||||
result := DefaultConfig()
|
||||
assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout())
|
||||
@@ -331,4 +368,6 @@ func TestGetters(t *testing.T) {
|
||||
assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome())
|
||||
assert.Equal(t, result.disableUnoconv, result.DisableUnoconv())
|
||||
assert.Equal(t, result.logLevel, result.LogLevel())
|
||||
assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize())
|
||||
assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mafredri/cdp"
|
||||
@@ -29,17 +30,18 @@ type chromePrinter struct {
|
||||
// ChromePrinterOptions helps customizing the
|
||||
// Google Chrome Printer behaviour.
|
||||
type ChromePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
WaitDelay float64
|
||||
HeaderHTML string
|
||||
FooterHTML string
|
||||
PaperWidth float64
|
||||
PaperHeight float64
|
||||
MarginTop float64
|
||||
MarginBottom float64
|
||||
MarginLeft float64
|
||||
MarginRight float64
|
||||
Landscape bool
|
||||
WaitTimeout float64
|
||||
WaitDelay float64
|
||||
HeaderHTML string
|
||||
FooterHTML string
|
||||
PaperWidth float64
|
||||
PaperHeight float64
|
||||
MarginTop float64
|
||||
MarginBottom float64
|
||||
MarginLeft float64
|
||||
MarginRight float64
|
||||
Landscape bool
|
||||
RpccBufferSize int64
|
||||
}
|
||||
|
||||
// DefaultChromePrinterOptions returns the default
|
||||
@@ -47,17 +49,18 @@ type ChromePrinterOptions struct {
|
||||
func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions {
|
||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||
return ChromePrinterOptions{
|
||||
WaitTimeout: config.DefaultWaitTimeout(),
|
||||
WaitDelay: 0.0,
|
||||
HeaderHTML: defaultHeaderFooterHTML,
|
||||
FooterHTML: defaultHeaderFooterHTML,
|
||||
PaperWidth: 8.27,
|
||||
PaperHeight: 11.7,
|
||||
MarginTop: 1.0,
|
||||
MarginBottom: 1.0,
|
||||
MarginLeft: 1.0,
|
||||
MarginRight: 1.0,
|
||||
Landscape: false,
|
||||
WaitTimeout: config.DefaultWaitTimeout(),
|
||||
WaitDelay: 0.0,
|
||||
HeaderHTML: defaultHeaderFooterHTML,
|
||||
FooterHTML: defaultHeaderFooterHTML,
|
||||
PaperWidth: 8.27,
|
||||
PaperHeight: 11.7,
|
||||
MarginTop: 1.0,
|
||||
MarginBottom: 1.0,
|
||||
MarginLeft: 1.0,
|
||||
MarginRight: 1.0,
|
||||
Landscape: false,
|
||||
RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +113,18 @@ func (p chromePrinter) Print(destination string) error {
|
||||
}
|
||||
// connect the client to the new target.
|
||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
||||
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
||||
newContextConn, err := rpcc.DialContext(
|
||||
ctx,
|
||||
newTargetWsURL,
|
||||
/*
|
||||
see:
|
||||
https://github.com/thecodingmachine/gotenberg/issues/108
|
||||
https://github.com/mafredri/cdp/issues/4
|
||||
https://github.com/ChromeDevTools/devtools-protocol/issues/24
|
||||
*/
|
||||
rpcc.WithWriteBufferSize(int(p.opts.RpccBufferSize)),
|
||||
rpcc.WithCompression(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -159,6 +173,16 @@ func (p chromePrinter) Print(destination string) error {
|
||||
SetPrintBackground(true),
|
||||
)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "rpcc: message too large") {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf(
|
||||
"'%d' bytes are not enough: increase the Google Chrome rpcc buffer size (up to 100 MB)",
|
||||
p.opts.RpccBufferSize,
|
||||
),
|
||||
err,
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package printer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||
@@ -54,6 +55,8 @@ func (p mergePrinter) Print(destination string) error {
|
||||
defer cancel()
|
||||
p.ctx = ctx
|
||||
}
|
||||
// see https://github.com/thecodingmachine/gotenberg/issues/139.
|
||||
sort.Strings(p.fpaths)
|
||||
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
|
||||
resolver := func() error {
|
||||
var args []string
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/phayes/freeport"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
@@ -53,6 +54,8 @@ func (p officePrinter) Print(destination string) error {
|
||||
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||
defer cancel()
|
||||
resolver := func() error {
|
||||
// see https://github.com/thecodingmachine/gotenberg/issues/139.
|
||||
sort.Strings(p.fpaths)
|
||||
fpaths := make([]string, len(p.fpaths))
|
||||
dirPath := filepath.Dir(destination)
|
||||
for i, fpath := range p.fpaths {
|
||||
|
||||
Reference in New Issue
Block a user