mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02f1231e7d | ||
|
|
6846e7941f | ||
|
|
719287c3a6 | ||
|
|
194670c3bf | ||
|
|
c4222e0981 | ||
|
|
61dc4b8bb8 |
@@ -13,7 +13,7 @@ At TheCodingMachine, we build a lot of web applications (intranets, extranets an
|
|||||||
|
|
||||||
* HTML and Markdown conversions using Google Chrome headless
|
* HTML and Markdown conversions using Google Chrome headless
|
||||||
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
||||||
* Performance :zap:: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
* Performance :zap:: Google Chrome and LibreOffice (unoconv) started once in the background thanks to PM2
|
||||||
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
|
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
|
||||||
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
||||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||||
@@ -23,7 +23,7 @@ At TheCodingMachine, we build a lot of web applications (intranets, extranets an
|
|||||||
Open a terminal and run the following command:
|
Open a terminal and run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:4
|
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
The API is now available on your host at `http://localhost:3000`.
|
The API is now available on your host at `http://localhost:3000`.
|
||||||
@@ -33,7 +33,7 @@ to learn how to interact with it!
|
|||||||
|
|
||||||
## Badges
|
## Badges
|
||||||
|
|
||||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:4)
|
[](https://microbadger.com/images/thecodingmachine/gotenberg:5)
|
||||||
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
||||||
[](https://godoc.org/github.com/thecodingmachine/gotenberg)
|
[](https://godoc.org/github.com/thecodingmachine/gotenberg)
|
||||||
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
FROM thecodingmachine/gotenberg:3.2.0 AS hack
|
|
||||||
FROM debian:9.5-slim
|
FROM debian:9.5-slim
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
@@ -20,37 +19,27 @@ RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free"
|
|||||||
# | recovering. In our case: Chrome (headless) and Office (headless).
|
# | recovering. In our case: Chrome (headless) and Office (headless).
|
||||||
# |
|
# |
|
||||||
|
|
||||||
# Yep, this is dirty. The following script does not work anymore (see https://github.com/Unitech/pm2/issues/4127).
|
|
||||||
#RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
|
|
||||||
|
|
||||||
# Installing PM2 with Node.js and npm breaks something which prevents Google Chrome to work as expected.
|
|
||||||
# So we just copy all PM2 related files from a previous version of Gotenberg.
|
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
||||||
apt-get install -y nodejs
|
apt-get install -y nodejs &&\
|
||||||
|
npm install -g pm2
|
||||||
COPY --from=hack /usr/bin/pm2 /usr/bin/pm2
|
|
||||||
COPY --from=hack /usr/share/pm2 /usr/share/pm2
|
|
||||||
COPY --from=hack /etc/default/pm2 /etc/default/pm2
|
|
||||||
COPY --from=hack /etc/systemd/system/pm2.service /etc/systemd/system/pm2.service
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Chrome
|
# | Chrome
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# |
|
# |
|
||||||
# | Installs Chrome for HTML and Markdown conversions.
|
# | Installs Chrome.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
||||||
apt-get update &&\
|
apt-get update &&\
|
||||||
apt-get -y install google-chrome-stable
|
apt-get -y --allow-unauthenticated install google-chrome-stable
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Unoconv
|
# | Unoconv
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# |
|
# |
|
||||||
# | Installs unoconv and LibreOffice for Office documents conversions.
|
# | Installs unoconv and LibreOffice.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
RUN pip3 install unoconv &&\
|
RUN pip3 install unoconv &&\
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ title: Introduction
|
|||||||
|
|
||||||
* HTML and Markdown conversions using Google Chrome headless
|
* HTML and Markdown conversions using Google Chrome headless
|
||||||
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
||||||
* Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
* Performance: Google Chrome and LibreOffice (unoconv) started once in the background thanks to PM2
|
||||||
* Failure prevention: PM2 automatically restarts previous processes if they fail
|
* Failure prevention: PM2 automatically restarts previous processes if they fail
|
||||||
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
||||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||||
@@ -7,12 +7,14 @@ Gotenberg is shipped within a Docker image.
|
|||||||
You may start it with:
|
You may start it with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:4
|
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
Or add it in your Docker Compose stack:
|
## Docker Compose
|
||||||
|
|
||||||
|
You may also add it in your Docker Compose stack:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3'
|
version: '3'
|
||||||
@@ -22,32 +24,19 @@ services:
|
|||||||
# your others services
|
# your others services
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: thecodingmachine/gotenberg:4
|
image: thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
||||||
|
|
||||||
|
## Kubernetes
|
||||||
|
|
||||||
It may also be deployed with Kubernetes.
|
It may also be deployed with Kubernetes.
|
||||||
|
|
||||||
|
Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0.2` CPU).
|
||||||
|
Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoconv).
|
||||||
|
|
||||||
|
> The more resources are granted, the quicker will be the conversions.
|
||||||
|
|
||||||
In the following examples, we will assume your
|
In the following examples, we will assume your
|
||||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
## Go client
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v4
|
|
||||||
```
|
|
||||||
|
|
||||||
## PHP client
|
|
||||||
|
|
||||||
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require php-http/guzzle6-adapter
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the PHP client:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require thecodingmachine/gotenberg-php-client
|
|
||||||
```
|
|
||||||
25
build/docs/content/02-clients.md
Normal file
25
build/docs/content/02-clients.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: Clients
|
||||||
|
---
|
||||||
|
|
||||||
|
We provide clients in various languages for easing the interactions with the API.
|
||||||
|
|
||||||
|
## Go client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v5
|
||||||
|
```
|
||||||
|
|
||||||
|
## PHP client
|
||||||
|
|
||||||
|
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ composer require php-http/guzzle6-adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the PHP client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ composer require thecodingmachine/gotenberg-php-client
|
||||||
|
```
|
||||||
48
build/docs/content/03-environment-variables.md
Normal file
48
build/docs/content/03-environment-variables.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: Environment variables
|
||||||
|
---
|
||||||
|
|
||||||
|
You may customize the API behaviour thanks to environment variables.
|
||||||
|
|
||||||
|
## Disable Google Chrome
|
||||||
|
|
||||||
|
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`.
|
||||||
|
|
||||||
|
It takes the strings `"0"` or `"1"` as value.
|
||||||
|
|
||||||
|
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
|
||||||
|
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
||||||
|
|
||||||
|
## Disable LibreOffice (unoconv)
|
||||||
|
|
||||||
|
You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`.
|
||||||
|
|
||||||
|
> If LibreOffice (unoconv) is disabled, the following conversion will **not** be available anymore:
|
||||||
|
> [Office](#office)
|
||||||
|
|
||||||
|
## Default wait timeout
|
||||||
|
|
||||||
|
By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful.
|
||||||
|
|
||||||
|
You may customize this timeout thanks to the environment variable `DEFAULT_WAIT_TIMEOUT`.
|
||||||
|
|
||||||
|
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||||
|
|
||||||
|
> The default timeout may also be overridden per request thanks to the form field `waitTimeout`.
|
||||||
|
> See the [timeout section](#timeout).
|
||||||
|
|
||||||
|
## Disable logging on healthcheck
|
||||||
|
|
||||||
|
By default, the API will add a log entry when the [healthcheck endpoint](#ping) is called.
|
||||||
|
|
||||||
|
You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable `DISABLE_HEALTHCHECK_LOGGING`.
|
||||||
|
|
||||||
|
This environment variable operates in the same manner as the `DISABLE_GOOGLE_CHROME` and `DISABLE_UNOCONV` variables operate in that it accepts the strings `"0"` or `"1"` as values.
|
||||||
|
|
||||||
|
## Default listen port
|
||||||
|
|
||||||
|
By default, the API will listen on port `3000`. For most use cases this is perfectly fine, but at times there may be cases where you need to change this due to port conflicts.
|
||||||
|
|
||||||
|
You may customize this port location with the environment variable `DEFAULT_LISTEN_PORT`.
|
||||||
|
|
||||||
|
This environment variable accepts any string that can be turned into a port number (e.g., the string `"0"` up to the string `"65535"`).
|
||||||
@@ -39,7 +39,7 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
@@ -113,13 +113,13 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
req.SetHeader("header.html")
|
req.Header("header.html")
|
||||||
req.SetFooter("footer.html")
|
req.Footer("footer.html")
|
||||||
dest := "result.pdf"
|
dest := "result.pdf"
|
||||||
c.Store(req, dest)
|
c.Store(req, dest)
|
||||||
}
|
}
|
||||||
@@ -186,9 +186,6 @@ You may also use *remote* paths for Google fonts, images and so on.
|
|||||||
|
|
||||||
> If you want to install fonts directly in the Gotenberg Docker image,
|
> If you want to install fonts directly in the Gotenberg Docker image,
|
||||||
> see to the [fonts section](#fonts).
|
> see to the [fonts section](#fonts).
|
||||||
>
|
|
||||||
> For web fonts (Google fonts), there is a timeout of 500ms by default. You may update
|
|
||||||
> this value thanks to the form field `webFontsTimeout`.
|
|
||||||
|
|
||||||
### cURL
|
### cURL
|
||||||
|
|
||||||
@@ -206,12 +203,12 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
req.SetAssets("font.woff", "img.gif", "style.css")
|
req.Assets("font.woff", "img.gif", "style.css")
|
||||||
dest := "result.pdf"
|
dest := "result.pdf"
|
||||||
c.Store(req, dest)
|
c.Store(req, dest)
|
||||||
}
|
}
|
||||||
@@ -243,8 +240,7 @@ You may also customize the resulting PDF format.
|
|||||||
|
|
||||||
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
|
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
|
||||||
|
|
||||||
> Paper size and margins have to be provided in `inches`.
|
> Paper size and margins have to be provided in `inches`. Same for margins.
|
||||||
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
|
|
||||||
|
|
||||||
### cURL
|
### cURL
|
||||||
|
|
||||||
@@ -266,14 +262,14 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
req.SetPaperSize(gotenberg.A4)
|
req.PaperSize(gotenberg.A4)
|
||||||
req.SetMargins(gotenberg.NoMargins)
|
req.Margins(gotenberg.NoMargins)
|
||||||
req.SetLandscape(true)
|
req.Landscape(true)
|
||||||
dest := "result.pdf"
|
dest := "result.pdf"
|
||||||
c.Store(req, dest)
|
c.Store(req, dest)
|
||||||
}
|
}
|
||||||
@@ -295,4 +291,52 @@ $request->setMargins(Request::NO_MARGINS);
|
|||||||
$request->setLandscape(true);
|
$request->setLandscape(true);
|
||||||
$dest = "result.pdf";
|
$dest = "result.pdf";
|
||||||
$client->store($request, $dest);
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wait delay
|
||||||
|
|
||||||
|
In some cases, you may want to wait a certain amount of time to make sure the
|
||||||
|
page you're trying to generate is fully rendered.
|
||||||
|
|
||||||
|
> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds).
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form waitDelay=5.5 \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.WaitDelay(5.5)
|
||||||
|
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->setWaitDelay(5.5);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
```
|
```
|
||||||
@@ -20,7 +20,7 @@ named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conv
|
|||||||
$ curl --request POST \
|
$ curl --request POST \
|
||||||
--url http://localhost:3000/convert/url \
|
--url http://localhost:3000/convert/url \
|
||||||
--header 'Content-Type: multipart/form-data' \
|
--header 'Content-Type: multipart/form-data' \
|
||||||
--form remoteURL=https://google.com
|
--form remoteURL=https://google.com \
|
||||||
--form marginTop=0 \
|
--form marginTop=0 \
|
||||||
--form marginBottom=0 \
|
--form marginBottom=0 \
|
||||||
--form marginLeft=0 \
|
--form marginLeft=0 \
|
||||||
@@ -31,12 +31,12 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
req := gotenberg.NewURLRequest("https://google.com")
|
req := gotenberg.NewURLRequest("https://google.com")
|
||||||
req.SetMargins(gotenberg.NoMargins)
|
req.Margins(gotenberg.NoMargins)
|
||||||
dest := "result.pdf"
|
dest := "result.pdf"
|
||||||
c.Store(req, dest)
|
c.Store(req, dest)
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
@@ -43,7 +43,7 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
@@ -90,12 +90,12 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
req, _ := gotenberg.NewOfficeRequest("document.docx")
|
req, _ := gotenberg.NewOfficeRequest("document.docx")
|
||||||
req.SetLandscape(true)
|
req.Landscape(true)
|
||||||
dest := "result.pdf"
|
dest := "result.pdf"
|
||||||
c.Store(req, dest)
|
c.Store(req, dest)
|
||||||
}
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: Filename
|
|
||||||
---
|
|
||||||
|
|
||||||
All endpoints accept a form field named `filename`.
|
|
||||||
|
|
||||||
If provided, the API will return the resulting PDF file with the given filename.
|
|
||||||
Otherwise a random filename is used.
|
|
||||||
|
|
||||||
> The Go and PHP libraries do not provide a way to set this form field.
|
|
||||||
> However, you may hijack the response from the API or store the resulting PDF
|
|
||||||
> using a custom filename.
|
|
||||||
>
|
|
||||||
> **Attention:** this feature does not work if the form field `webhookURL` is given.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### cURL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/html \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html \
|
|
||||||
--form filename='result.pdf' \
|
|
||||||
-o result.pdf
|
|
||||||
```
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
title: Merge
|
title: Merge
|
||||||
---
|
---
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/merge` for merging PDFs.
|
Gotenberg provides the endpoint `/convert/merge` for merging PDFs.
|
||||||
|
|
||||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ will merge them and return the resulting PDF file.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ curl --request POST \
|
$ curl --request POST \
|
||||||
--url http://localhost:3000/merge \
|
--url http://localhost:3000/convert/merge \
|
||||||
--header 'Content-Type: multipart/form-data' \
|
--header 'Content-Type: multipart/form-data' \
|
||||||
--form files=@file.pdf \
|
--form files=@file.pdf \
|
||||||
--form files=@file2.pdf \
|
--form files=@file2.pdf \
|
||||||
@@ -27,7 +27,7 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
52
build/docs/content/09-timeout.md
Normal file
52
build/docs/content/09-timeout.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: Timeout
|
||||||
|
---
|
||||||
|
|
||||||
|
All endpoints accept a form field named `waitTimeout`.
|
||||||
|
|
||||||
|
The API will wait the given **seconds** before it considers the conversion to be unsucessful.
|
||||||
|
|
||||||
|
It takes a float as value (e.g `2.5` for 2.5 seconds).
|
||||||
|
|
||||||
|
> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form waitTimeout=2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.WaitTimeout(2.5)
|
||||||
|
resp, _ := c.Post(req)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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->setWaitTimeout(2.5);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
@@ -24,13 +24,12 @@ $ curl --request POST \
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v4"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
req.SetWebhookURL("http://myapp.com/webhook/")
|
req.WebhookURL("http://myapp.com/webhook/")
|
||||||
dest := "result.pdf"
|
|
||||||
resp, _ := c.Post(req)
|
resp, _ := c.Post(req)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
50
build/docs/content/11-result-filename.md
Normal file
50
build/docs/content/11-result-filename.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Result filename
|
||||||
|
---
|
||||||
|
|
||||||
|
All endpoints accept a form field named `resultFilename`.
|
||||||
|
|
||||||
|
If provided, the API will return the resulting PDF file with the given filename.
|
||||||
|
Otherwise a random filename is used.
|
||||||
|
|
||||||
|
> **Attention:** this feature does not work if the form field `webhookURL` is given.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form resultFilename='foo.pdf'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.ResultFilename("foo.pdf")
|
||||||
|
resp, _ := c.Post(req)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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->setResultFilename('foo.pdf');
|
||||||
|
$resp = $client->post($request);
|
||||||
|
```
|
||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
# your others services
|
# your others services
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: thecodingmachine/gotenberg:4
|
image: thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
You may now launch your services using:
|
You may now launch your services using:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Liveness
|
title: Ping
|
||||||
---
|
---
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/ping` for checking the API availability with
|
Gotenberg provides the endpoint `/ping` for checking the API availability with
|
||||||
@@ -7,7 +7,7 @@ By default, a handful of fonts are installed. Asian characters are also supporte
|
|||||||
If you wish to use more fonts, you will have to create your own image:
|
If you wish to use more fonts, you will have to create your own image:
|
||||||
|
|
||||||
```Dockerfile
|
```Dockerfile
|
||||||
FROM thecodingmachine/gotenberg:4
|
FROM thecodingmachine/gotenberg:5
|
||||||
|
|
||||||
RUN apt-get -y install yourfonts
|
RUN apt-get -y install yourfonts
|
||||||
```
|
```
|
||||||
@@ -10,7 +10,7 @@ FROM golang:${GOLANG_VERSION}-stretch
|
|||||||
# | than gometalinter.
|
# | than gometalinter.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
ENV GOLANGCI_LINT_VERSION 1.15.0
|
ENV GOLANGCI_LINT_VERSION 1.16.0
|
||||||
|
|
||||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
||||||
golangci-lint --version
|
golangci-lint --version
|
||||||
@@ -32,4 +32,4 @@ COPY go.sum .
|
|||||||
# Install module dependencies.
|
# Install module dependencies.
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=lll", "--disable=errcheck", "--disable=gosec", "--disable=gochecknoglobals", "--disable=gochecknoinits" ]
|
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
||||||
@@ -5,12 +5,11 @@ set -xe
|
|||||||
# Testing PM2 processes launch separatly for avoiding
|
# Testing PM2 processes launch separatly for avoiding
|
||||||
# spending to much time on each tests depending on
|
# spending to much time on each tests depending on
|
||||||
# them.
|
# them.
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeLaunch
|
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvLaunch
|
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
||||||
|
|
||||||
# Running others tests.
|
# Running others tests.
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/printer
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
||||||
|
|
||||||
# Finally testing processes shutdown.
|
# Finally testing processes shutdown.
|
||||||
|
|||||||
@@ -1,21 +1,146 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set on build time.
|
// version will be set on build time.
|
||||||
|
// nolint: gochecknoglobals
|
||||||
var version = "snapshot"
|
var version = "snapshot"
|
||||||
|
|
||||||
func main() {
|
const (
|
||||||
notify.Println(fmt.Sprintf("Gotenberg %s", version))
|
defaultWaitTimeoutEnvVar = "DEFAULT_WAIT_TIMEOUT"
|
||||||
if err := api.Start(); err != nil {
|
defaultListenPortEnvVar = "DEFAULT_LISTEN_PORT"
|
||||||
notify.ErrPrintln(err)
|
disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME"
|
||||||
|
disableUnoconvEnvVar = "DISABLE_UNOCONV"
|
||||||
|
disableHealthcheckLoggingEnvVar = "DISABLE_HEALTHCHECK_LOGGING"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustParseEnvVar() *api.Options {
|
||||||
|
opts := api.DefaultOptions()
|
||||||
|
if os.Getenv(defaultWaitTimeoutEnvVar) != "" {
|
||||||
|
defaultWaitTimeout, err := strconv.ParseFloat(os.Getenv(defaultWaitTimeoutEnvVar), 64)
|
||||||
|
if err != nil {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want float got %v", defaultWaitTimeoutEnvVar, err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.DefaultWaitTimeout = defaultWaitTimeout
|
||||||
|
}
|
||||||
|
if v, ok := os.LookupEnv(defaultListenPortEnvVar); ok {
|
||||||
|
defaultListener, err := strconv.ParseUint(os.Getenv(defaultListenPortEnvVar), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want uint got %v", defaultListenPortEnvVar, err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if defaultListener > 65535 {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want uint < 65535 got %v", defaultListenPortEnvVar, defaultListener))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.DefaultListenPort = v
|
||||||
|
}
|
||||||
|
if v, ok := os.LookupEnv(disableGoogleChromeEnvVar); ok {
|
||||||
|
if v != "1" && v != "0" {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableGoogleChromeEnvVar, v))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.EnableChromeEndpoints = v != "1"
|
||||||
|
}
|
||||||
|
if v, ok := os.LookupEnv(disableUnoconvEnvVar); ok {
|
||||||
|
if v != "1" && v != "0" {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableUnoconvEnvVar, v))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.EnableUnoconvEndpoints = v != "1"
|
||||||
|
}
|
||||||
|
if v, ok := os.LookupEnv(disableHealthcheckLoggingEnvVar); ok {
|
||||||
|
if v != "1" && v != "0" {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableHealthcheckLoggingEnvVar, v))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.EnableHealthcheckLogging = v != "1"
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustStartProcesses(opts *api.Options) []pm2.Process {
|
||||||
|
var processes []pm2.Process
|
||||||
|
if opts.EnableChromeEndpoints {
|
||||||
|
processes = append(processes, pm2.NewChrome())
|
||||||
|
}
|
||||||
|
if opts.EnableUnoconvEndpoints {
|
||||||
|
processes = append(processes, pm2.NewUnoconv())
|
||||||
|
}
|
||||||
|
for _, p := range processes {
|
||||||
|
notify.Printf("starting %s with PM2...", p.Fullname())
|
||||||
|
if err := p.Start(); err != nil {
|
||||||
|
notify.ErrPrint(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processes
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustStartAPI(srv *echo.Echo, port string) {
|
||||||
|
notify.Printf("http server started on port %v", port)
|
||||||
|
if err := srv.Start(fmt.Sprintf(":%v", port)); err != nil {
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
notify.ErrPrint(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustShutdownProcesses(processes []pm2.Process) {
|
||||||
|
for _, p := range processes {
|
||||||
|
notify.Printf("shutting down %s with PM2... (Ctrl+C to force)", p.Fullname())
|
||||||
|
if err := p.Shutdown(); err != nil {
|
||||||
|
notify.ErrPrint(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustShutdownAPI(srv *echo.Echo) {
|
||||||
|
// create a deadline to wait for.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
// doesn't block if no connections, but will otherwise wait
|
||||||
|
// until the timeout deadline.
|
||||||
|
notify.Print("shutting down http server... (Ctrl+C to force)")
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
notify.ErrPrint(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
notify.Printf("Gotenberg %s", version)
|
||||||
|
opts := mustParseEnvVar()
|
||||||
|
srv := api.New(opts)
|
||||||
|
processes := mustStartProcesses(opts)
|
||||||
|
// run our API in a goroutine so that it doesn't block.s
|
||||||
|
go func() {
|
||||||
|
mustStartAPI(srv, opts.DefaultListenPort)
|
||||||
|
}()
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||||
|
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||||
|
signal.Notify(quit, os.Interrupt)
|
||||||
|
// block until we receive our signal.
|
||||||
|
<-quit
|
||||||
|
mustShutdownAPI(srv)
|
||||||
|
mustShutdownProcesses(processes)
|
||||||
|
notify.Print("bye!")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
349
docs/index.html
349
docs/index.html
@@ -50,6 +50,14 @@
|
|||||||
<a href="#install">Install</a>
|
<a href="#install">Install</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<a href="#clients">Clients</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<a href="#environment_variables">Environment variables</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a href="#html">HTML</a>
|
<a href="#html">HTML</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,12 +78,16 @@
|
|||||||
<a href="#merge">Merge</a>
|
<a href="#merge">Merge</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<a href="#timeout">Timeout</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a href="#webhook">Webhook</a>
|
<a href="#webhook">Webhook</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a href="#filename">Filename</a>
|
<a href="#result_filename">Result filename</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
@@ -83,7 +95,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a href="#liveness">Liveness</a>
|
<a href="#ping">Ping</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
@@ -108,7 +120,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>HTML and Markdown conversions using Google Chrome headless</li>
|
<li>HTML and Markdown conversions using Google Chrome headless</li>
|
||||||
<li>Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using <a href="https://github.com/dagwieers/unoconv">unoconv</a></li>
|
<li>Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using <a href="https://github.com/dagwieers/unoconv">unoconv</a></li>
|
||||||
<li>Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2</li>
|
<li>Performance: Google Chrome and LibreOffice (unoconv) started once in the background thanks to PM2</li>
|
||||||
<li>Failure prevention: PM2 automatically restarts previous processes if they fail</li>
|
<li>Failure prevention: PM2 automatically restarts previous processes if they fail</li>
|
||||||
<li>Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!</li>
|
<li>Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!</li>
|
||||||
<li>Easily interact with the API using our <a href="https://github.com/thecodingmachine/gotenberg-go-client">Go</a> and <a href="https://github.com/thecodingmachine/gotenberg-php-client">PHP</a> libraries</li>
|
<li>Easily interact with the API using our <a href="https://github.com/thecodingmachine/gotenberg-go-client">Go</a> and <a href="https://github.com/thecodingmachine/gotenberg-php-client">PHP</a> libraries</li>
|
||||||
@@ -124,14 +136,18 @@
|
|||||||
|
|
||||||
<p>You may start it with:</p>
|
<p>You may start it with:</p>
|
||||||
|
|
||||||
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:4
|
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:5
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<p>Or add it in your Docker Compose stack:</p>
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.docker_compose" href="#install.docker_compose">
|
||||||
|
<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>Docker Compose</h2>
|
||||||
|
|
||||||
|
<p>You may also add it in your Docker Compose stack:</p>
|
||||||
|
|
||||||
<pre class="chroma">version<span class="p">:</span><span class="w"> </span><span class="s1">'3'</span><span class="w">
|
<pre class="chroma">version<span class="p">:</span><span class="w"> </span><span class="s1">'3'</span><span class="w">
|
||||||
</span><span class="w">
|
</span><span class="w">
|
||||||
@@ -140,26 +156,45 @@
|
|||||||
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
|
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
|
||||||
</span><span class="w">
|
</span><span class="w">
|
||||||
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
|
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
|
||||||
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">4</span><span class="w">
|
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">5</span><span class="w">
|
||||||
</span></pre>
|
</span></pre>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>The API will be available under <code>gotenberg:3000</code> in your Docker Compose network.</p>
|
<p>The API will be available under <code>gotenberg:3000</code> in your Docker Compose network.</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.kubernetes" href="#install.kubernetes">
|
||||||
|
<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>Kubernetes</h2>
|
||||||
|
|
||||||
<p>It may also be deployed with Kubernetes.</p>
|
<p>It may also be deployed with Kubernetes.</p>
|
||||||
|
|
||||||
|
<p>Make sure to provide enough memory and CPU requests (for instance <code>512Mi</code> and <code>0.2</code> CPU).
|
||||||
|
Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoconv).</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>The more resources are granted, the quicker will be the conversions.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
<p>In the following examples, we will assume your
|
<p>In the following examples, we will assume your
|
||||||
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.go_client" href="#install.go_client">
|
</div>
|
||||||
|
|
||||||
|
<div class="Page" id="clients">
|
||||||
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="clients" href="#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>Clients</h1>
|
||||||
|
<p>We provide clients in various languages for easing the interactions with the API.</p>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="clients.go_client" href="#clients.go_client">
|
||||||
<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>
|
<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 client</h2>
|
</a>Go client</h2>
|
||||||
|
|
||||||
<pre class="chroma">$ go get -u github.com/thecodingmachine/gotenberg-go-client/v4
|
<pre class="chroma">$ go get -u github.com/thecodingmachine/gotenberg-go-client/v5
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.php_client" href="#install.php_client">
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="clients.php_client" href="#clients.php_client">
|
||||||
<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>
|
<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 client</h2>
|
</a>PHP client</h2>
|
||||||
|
|
||||||
@@ -175,6 +210,73 @@ Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="Page" id="environment_variables">
|
||||||
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables" href="#environment_variables">
|
||||||
|
<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>Environment variables</h1>
|
||||||
|
<p>You may customize the API behaviour thanks to environment variables.</p>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.disable_google_chrome" href="#environment_variables.disable_google_chrome">
|
||||||
|
<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 Google Chrome</h2>
|
||||||
|
|
||||||
|
<p>In order to save some resources, the Gotenberg image accepts the environment variable <code>DISABLE_GOOGLE_CHROME</code>.</p>
|
||||||
|
|
||||||
|
<p>It takes the strings <code>"0"</code> or <code>"1"</code> as value.</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>If Google Chrome is disabled, the following conversions will <strong>not</strong> be available anymore:
|
||||||
|
<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.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>
|
||||||
|
|
||||||
|
<p>You may also disable LibreOffice (unoconv) with <code>DISABLE_UNOCONV</code>.</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>If LibreOffice (unoconv) is disabled, the following conversion will <strong>not</strong> be available anymore:
|
||||||
|
<a href="#office">Office</a></p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.default_wait_timeout" href="#environment_variables.default_wait_timeout">
|
||||||
|
<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 wait timeout</h2>
|
||||||
|
|
||||||
|
<p>By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful.</p>
|
||||||
|
|
||||||
|
<p>You may customize this timeout thanks to the environment variable <code>DEFAULT_WAIT_TIMEOUT</code>.</p>
|
||||||
|
|
||||||
|
<p>It takes a string representation of a float as value (e.g <code>"2.5"</code> for 2.5 seconds).</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>The default timeout may also be overridden per request thanks to the form field <code>waitTimeout</code>.
|
||||||
|
See the <a href="#timeout">timeout section</a>.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.disable_logging_on_healthcheck" href="#environment_variables.disable_logging_on_healthcheck">
|
||||||
|
<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 logging on healthcheck</h2>
|
||||||
|
|
||||||
|
<p>By default, the API will add a log entry when the <a href="#ping">healthcheck endpoint</a> is called.</p>
|
||||||
|
|
||||||
|
<p>You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable <code>DISABLE_HEALTHCHECK_LOGGING</code>.</p>
|
||||||
|
|
||||||
|
<p>This environment variable operates in the same manner as the <code>DISABLE_GOOGLE_CHROME</code> and <code>DISABLE_UNOCONV</code> variables operate in that it accepts the strings <code>"0"</code> or <code>"1"</code> as values.</p>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.default_listen_port" href="#environment_variables.default_listen_port">
|
||||||
|
<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 listen port</h2>
|
||||||
|
|
||||||
|
<p>By default, the API will listen on port <code>3000</code>. For most use cases this is perfectly fine, but at times there may be cases where you need to change this due to port conflicts.</p>
|
||||||
|
|
||||||
|
<p>You may customize this port location with the environment variable <code>DEFAULT_LISTEN_PORT</code>.</p>
|
||||||
|
|
||||||
|
<p>This environment variable accepts any string that can be turned into a port number (e.g., the string <code>"0"</code> up to the string <code>"65535"</code>).</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="Page" id="html">
|
<div class="Page" id="html">
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="html" href="#html">
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="html" href="#html">
|
||||||
<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>
|
<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>
|
||||||
@@ -219,7 +321,7 @@ which will be converted to PDF.</p>
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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>
|
||||||
@@ -301,13 +403,13 @@ Also, <code>footer.html</code> CSS properties override the ones from <code>heade
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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="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">SetHeader</span><span class="p">(</span><span class="s">"header.html"</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">Header</span><span class="p">(</span><span class="s">"header.html"</span><span class="p">)</span>
|
||||||
<span class="nx">req</span><span class="p">.</span><span class="nf">SetFooter</span><span class="p">(</span><span class="s">"footer.html"</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">Footer</span><span class="p">(</span><span class="s">"footer.html"</span><span class="p">)</span>
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</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="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>
|
<span class="p">}</span>
|
||||||
@@ -376,9 +478,6 @@ are on the same level as the <code>index.html</code> file.</p>
|
|||||||
<blockquote>
|
<blockquote>
|
||||||
<p>If you want to install fonts directly in the Gotenberg Docker image,
|
<p>If you want to install fonts directly in the Gotenberg Docker image,
|
||||||
see to the <a href="#fonts">fonts section</a>.</p>
|
see to the <a href="#fonts">fonts section</a>.</p>
|
||||||
|
|
||||||
<p>For web fonts (Google fonts), there is a timeout of 500ms by default. You may update
|
|
||||||
this value thanks to the form field <code>webFontsTimeout</code>.</p>
|
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.c_url" href="#html.assets.c_url">
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.c_url" href="#html.assets.c_url">
|
||||||
@@ -399,12 +498,12 @@ this value thanks to the form field <code>webFontsTimeout</code>.</p>
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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="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">SetAssets</span><span class="p">(</span><span class="s">"font.woff"</span><span class="p">,</span> <span class="s">"img.gif"</span><span class="p">,</span> <span class="s">"style.css"</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">Assets</span><span class="p">(</span><span class="s">"font.woff"</span><span class="p">,</span> <span class="s">"img.gif"</span><span class="p">,</span> <span class="s">"style.css"</span><span class="p">)</span>
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</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="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>
|
<span class="p">}</span>
|
||||||
@@ -440,8 +539,7 @@ $client->store($request, $dest);
|
|||||||
<p>By default, it will be rendered with <code>A4</code> size, <code>1 inch</code> margins and <code>portrait</code> orientation.</p>
|
<p>By default, it will be rendered with <code>A4</code> size, <code>1 inch</code> margins and <code>portrait</code> orientation.</p>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>Paper size and margins have to be provided in <code>inches</code>.
|
<p>Paper size and margins have to be provided in <code>inches</code>. Same for margins.</p>
|
||||||
Also, you have to set both <code>paperWidth</code> and <code>paperHeight</code>. Same for margins.</p>
|
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.c_url" href="#html.paper_size_margins_orientation.c_url">
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.c_url" href="#html.paper_size_margins_orientation.c_url">
|
||||||
@@ -466,14 +564,14 @@ Also, you have to set both <code>paperWidth</code> and <code>paperHeight</code>.
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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="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">SetPaperSize</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">A4</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">PaperSize</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">A4</span><span class="p">)</span>
|
||||||
<span class="nx">req</span><span class="p">.</span><span class="nf">SetMargins</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">NoMargins</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">Margins</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">NoMargins</span><span class="p">)</span>
|
||||||
<span class="nx">req</span><span class="p">.</span><span class="nf">SetLandscape</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">Landscape</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</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="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>
|
<span class="p">}</span>
|
||||||
@@ -496,6 +594,61 @@ $request->setMargins(Request::NO_MARGINS);
|
|||||||
$request->setLandscape(true);
|
$request->setLandscape(true);
|
||||||
$dest = "result.pdf";
|
$dest = "result.pdf";
|
||||||
$client->store($request, $dest);
|
$client->store($request, $dest);
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.wait_delay" href="#html.wait_delay">
|
||||||
|
<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>Wait delay</h2>
|
||||||
|
|
||||||
|
<p>In some cases, you may want to wait a certain amount of time to make sure the
|
||||||
|
page you’re trying to generate is fully rendered.</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>The wait delay is a duration in <strong>seconds</strong> (e.g <code>2.5</code> for 2.5 seconds).</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.wait_delay.c_url" href="#html.wait_delay.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">waitDelay</span><span class="o">=</span><span class="m">5</span>.5 <span class="se">\
|
||||||
|
</span><span class="se"></span> -o result.pdf
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.wait_delay.go" href="#html.wait_delay.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/v5"</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">WaitDelay</span><span class="p">(</span><span class="mf">5.5</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.wait_delay.php" href="#html.wait_delay.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->setWaitDelay(5.5);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -527,8 +680,8 @@ If not, some of the content of the page might be hidden.</p>
|
|||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
<pre class="chroma">$ curl --request POST <span class="se">\
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/url <span class="se">\
|
</span><span class="se"></span> --url http://localhost:3000/convert/url <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> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
||||||
</span><span class="se"></span> --form <span class="nv">remoteURL</span><span class="o">=</span>https://google.com
|
</span><span class="se"></span> --form <span class="nv">remoteURL</span><span class="o">=</span>https://google.com <span class="se">\
|
||||||
--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">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">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">\
|
</span><span class="se"></span> --form <span class="nv">marginLeft</span><span class="o">=</span><span class="m">0</span> <span class="se">\
|
||||||
</span><span class="se"></span> --form <span class="nv">marginRight</span><span class="o">=</span><span class="m">0</span> <span class="se">\
|
</span><span class="se"></span> --form <span class="nv">marginRight</span><span class="o">=</span><span class="m">0</span> <span class="se">\
|
||||||
@@ -539,12 +692,12 @@ If not, some of the content of the page might be hidden.</p>
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewURLRequest</span><span class="p">(</span><span class="s">"https://google.com"</span><span class="p">)</span>
|
<span class="nx">req</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewURLRequest</span><span class="p">(</span><span class="s">"https://google.com"</span><span class="p">)</span>
|
||||||
<span class="nx">req</span><span class="p">.</span><span class="nf">SetMargins</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">NoMargins</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">Margins</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">NoMargins</span><span class="p">)</span>
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</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="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>
|
<span class="p">}</span>
|
||||||
@@ -613,7 +766,7 @@ in the file <code>index.html</code>. This function will convert a given markdown
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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>
|
||||||
@@ -696,7 +849,7 @@ See the <a href="#scalability">scalability section</a> to find how to mitigate t
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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>
|
||||||
@@ -748,12 +901,12 @@ $client->store($request, $dest);
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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">NewOfficeRequest</span><span class="p">(</span><span class="s">"document.docx"</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">NewOfficeRequest</span><span class="p">(</span><span class="s">"document.docx"</span><span class="p">)</span>
|
||||||
<span class="nx">req</span><span class="p">.</span><span class="nf">SetLandscape</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">Landscape</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</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="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>
|
<span class="p">}</span>
|
||||||
@@ -783,7 +936,7 @@ $client->store($request, $dest);
|
|||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="merge" href="#merge">
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="merge" href="#merge">
|
||||||
<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>
|
<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>Merge</h1>
|
</a>Merge</h1>
|
||||||
<p>Gotenberg provides the endpoint <code>/merge</code> for merging PDFs.</p>
|
<p>Gotenberg provides the endpoint <code>/convert/merge</code> for merging PDFs.</p>
|
||||||
|
|
||||||
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
|
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
|
||||||
|
|
||||||
@@ -803,7 +956,7 @@ will merge them and return the resulting PDF file.</p>
|
|||||||
</a>cURL</h3>
|
</a>cURL</h3>
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
<pre class="chroma">$ curl --request POST <span class="se">\
|
||||||
</span><span class="se"></span> --url http://localhost:3000/merge <span class="se">\
|
</span><span class="se"></span> --url http://localhost:3000/convert/merge <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> --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>@file.pdf <span class="se">\
|
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@file.pdf <span class="se">\
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@file2.pdf <span class="se">\
|
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@file2.pdf <span class="se">\
|
||||||
@@ -814,7 +967,7 @@ will merge them and return the resulting PDF file.</p>
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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>
|
||||||
@@ -844,6 +997,68 @@ $client->store($request, $dest);
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="Page" id="timeout">
|
||||||
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="timeout" href="#timeout">
|
||||||
|
<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>Timeout</h1>
|
||||||
|
<p>All endpoints accept a form field named <code>waitTimeout</code>.</p>
|
||||||
|
|
||||||
|
<p>The API will wait the given <strong>seconds</strong> before it considers the conversion to be unsucessful.</p>
|
||||||
|
|
||||||
|
<p>It takes a float as value (e.g <code>2.5</code> for 2.5 seconds).</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>You may also define this value globally: see the <a href="#environment_variables.default_wait_timeout">environment variables</a> section.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="timeout.examples" href="#timeout.examples">
|
||||||
|
<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>Examples</h2>
|
||||||
|
|
||||||
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="timeout.examples.c_url" href="#timeout.examples.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">waitTimeout</span><span class="o">=</span><span class="m">2</span>.5
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="timeout.examples.go" href="#timeout.examples.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/v5"</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">WaitTimeout</span><span class="p">(</span><span class="mf">2.5</span><span class="p">)</span>
|
||||||
|
<span class="nx">resp</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nf">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
|
||||||
|
<span class="p">}</span>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="timeout.examples.php" href="#timeout.examples.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->setWaitTimeout(2.5);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="Page" id="webhook">
|
<div class="Page" id="webhook">
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook" href="#webhook">
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook" href="#webhook">
|
||||||
<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>
|
<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>
|
||||||
@@ -874,13 +1089,12 @@ to given URL.</p>
|
|||||||
<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>
|
<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>
|
</a>Go</h3>
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v4"</span>
|
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg-go-client/v5"</span>
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</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">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="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">SetWebhookURL</span><span class="p">(</span><span class="s">"http://myapp.com/webhook/"</span><span class="p">)</span>
|
<span class="nx">req</span><span class="p">.</span><span class="nf">WebhookURL</span><span class="p">(</span><span class="s">"http://myapp.com/webhook/"</span><span class="p">)</span>
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">resp</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nf">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
|
<span class="nx">resp</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nf">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
|
||||||
<span class="p">}</span>
|
<span class="p">}</span>
|
||||||
</pre>
|
</pre>
|
||||||
@@ -902,28 +1116,24 @@ $resp = $client->post($request);
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="Page" id="filename">
|
<div class="Page" id="result_filename">
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="filename" href="#filename">
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="result_filename" href="#result_filename">
|
||||||
<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>
|
<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>Filename</h1>
|
</a>Result filename</h1>
|
||||||
<p>All endpoints accept a form field named <code>filename</code>.</p>
|
<p>All endpoints accept a form field named <code>resultFilename</code>.</p>
|
||||||
|
|
||||||
<p>If provided, the API will return the resulting PDF file with the given filename.
|
<p>If provided, the API will return the resulting PDF file with the given filename.
|
||||||
Otherwise a random filename is used.</p>
|
Otherwise a random filename is used.</p>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>The Go and PHP libraries do not provide a way to set this form field.
|
|
||||||
However, you may hijack the response from the API or store the resulting PDF
|
|
||||||
using a custom filename.</p>
|
|
||||||
|
|
||||||
<p><strong>Attention:</strong> this feature does not work if the form field <code>webhookURL</code> is given.</p>
|
<p><strong>Attention:</strong> this feature does not work if the form field <code>webhookURL</code> is given.</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="filename.examples" href="#filename.examples">
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="result_filename.examples" href="#result_filename.examples">
|
||||||
<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>
|
<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>Examples</h2>
|
</a>Examples</h2>
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="filename.examples.c_url" href="#filename.examples.c_url">
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="result_filename.examples.c_url" href="#result_filename.examples.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>
|
<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>
|
</a>cURL</h3>
|
||||||
|
|
||||||
@@ -931,8 +1141,37 @@ using a custom filename.</p>
|
|||||||
</span><span class="se"></span> --url http://localhost:3000/convert/html <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> --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">files</span><span class="o">=</span>@index.html <span class="se">\
|
||||||
</span><span class="se"></span> --form <span class="nv">filename</span><span class="o">=</span><span class="s1">'result.pdf'</span> <span class="se">\
|
</span><span class="se"></span> --form <span class="nv">resultFilename</span><span class="o">=</span><span class="s1">'foo.pdf'</span>
|
||||||
</span><span class="se"></span> -o result.pdf
|
</pre>
|
||||||
|
|
||||||
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="result_filename.examples.go" href="#result_filename.examples.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/v5"</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">ResultFilename</span><span class="p">(</span><span class="s">"foo.pdf"</span><span class="p">)</span>
|
||||||
|
<span class="nx">resp</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nf">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
|
||||||
|
<span class="p">}</span>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="result_filename.examples.php" href="#result_filename.examples.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->setResultFilename('foo.pdf');
|
||||||
|
$resp = $client->post($request);
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -952,7 +1191,7 @@ using a custom filename.</p>
|
|||||||
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
|
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
|
||||||
</span><span class="w">
|
</span><span class="w">
|
||||||
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
|
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
|
||||||
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">4</span><span class="w">
|
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">5</span><span class="w">
|
||||||
</span></pre>
|
</span></pre>
|
||||||
|
|
||||||
<p>You may now launch your services using:</p>
|
<p>You may now launch your services using:</p>
|
||||||
@@ -965,10 +1204,10 @@ redirect a request to a Gotenberg container according to the round-robin strateg
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="Page" id="liveness">
|
<div class="Page" id="ping">
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="liveness" href="#liveness">
|
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="ping" href="#ping">
|
||||||
<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>
|
<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>Liveness</h1>
|
</a>Ping</h1>
|
||||||
<p>Gotenberg provides the endpoint <code>/ping</code> for checking the API availability with
|
<p>Gotenberg provides the endpoint <code>/ping</code> for checking the API availability with
|
||||||
a simple <code>GET</code> request.</p>
|
a simple <code>GET</code> request.</p>
|
||||||
|
|
||||||
@@ -989,7 +1228,7 @@ a simple <code>GET</code> request.</p>
|
|||||||
|
|
||||||
<p>If you wish to use more fonts, you will have to create your own image:</p>
|
<p>If you wish to use more fonts, you will have to create your own image:</p>
|
||||||
|
|
||||||
<pre class="chroma"><span class="k">FROM</span><span class="s"> thecodingmachine/gotenberg:4</span><span class="err">
|
<pre class="chroma"><span class="k">FROM</span><span class="s"> thecodingmachine/gotenberg:5</span><span class="err">
|
||||||
</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="k">RUN</span> apt-get -y install yourfonts<span class="err">
|
||||||
</span></pre>
|
</span></pre>
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -6,20 +6,18 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.2.0 // indirect
|
github.com/google/go-cmp v0.2.0 // indirect
|
||||||
github.com/gorilla/websocket v1.4.0 // indirect
|
github.com/gorilla/websocket v1.4.0 // indirect
|
||||||
github.com/hhrutter/pdfcpu v0.1.19
|
|
||||||
github.com/labstack/echo/v4 v4.0.0
|
github.com/labstack/echo/v4 v4.0.0
|
||||||
github.com/labstack/gommon v0.2.8
|
github.com/labstack/gommon v0.2.8
|
||||||
github.com/mafredri/cdp v0.20.0
|
github.com/mafredri/cdp v0.22.0
|
||||||
github.com/mattn/go-colorable v0.1.1 // indirect
|
github.com/mattn/go-colorable v0.1.1 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.7 // indirect
|
github.com/mattn/go-isatty v0.0.7 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1
|
github.com/microcosm-cc/bluemonday v1.0.1
|
||||||
github.com/pkg/errors v0.8.0 // indirect
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/russross/blackfriday/v2 v2.0.1
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
github.com/valyala/fasttemplate v1.0.1 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect
|
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -7,14 +7,12 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
|||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/hhrutter/pdfcpu v0.1.19 h1:J6GjNFDPjurQWallPW9gmz3CcNqBfvgB7Dad9T3e8Hc=
|
|
||||||
github.com/hhrutter/pdfcpu v0.1.19/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
|
||||||
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
|
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
|
||||||
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
||||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||||
github.com/mafredri/cdp v0.20.0 h1:dRq16FwXCAAbolR4W2iyRNsL14Bp6jFHLbRp2EkICVs=
|
github.com/mafredri/cdp v0.22.0 h1:BV17j8hXLDWczo2SZIAFuOjMpQMIOq5DOcd9sgB2hv0=
|
||||||
github.com/mafredri/cdp v0.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
github.com/mafredri/cdp v0.22.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||||
@@ -26,8 +24,6 @@ github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc
|
|||||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
|
|||||||
@@ -1,139 +1,53 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start starts the API server on port 3000.
|
const pingEndpoint = "/ping"
|
||||||
func Start() error {
|
|
||||||
e := setup()
|
// Options allows to customize the behaviour
|
||||||
// start Chrome headless and
|
// of the API.
|
||||||
// unoconv listener with PM2.
|
type Options struct {
|
||||||
chrome := &pm2.Chrome{}
|
DefaultWaitTimeout float64
|
||||||
unoconv := &pm2.Unoconv{}
|
DefaultListenPort string
|
||||||
if err := chrome.Launch(); err != nil {
|
EnableChromeEndpoints bool
|
||||||
return err
|
EnableUnoconvEndpoints bool
|
||||||
}
|
EnableHealthcheckLogging bool
|
||||||
if err := unoconv.Launch(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// run our API in a goroutine so that it doesn't block.
|
|
||||||
go func() {
|
|
||||||
notify.Println("http server started on port 3000")
|
|
||||||
if err := e.Start(":3000"); err != nil {
|
|
||||||
e.Logger.Fatalf("%v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
|
||||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
|
||||||
signal.Notify(quit, os.Interrupt)
|
|
||||||
// block until we receive our signal.
|
|
||||||
<-quit
|
|
||||||
// create a deadline to wait for.
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
// doesn't block if no connections, but will otherwise wait
|
|
||||||
// until the timeout deadline.
|
|
||||||
notify.Println("shutting down http server... (Ctrl+C to force)")
|
|
||||||
return e.Shutdown(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup() *echo.Echo {
|
// DefaultOptions returns default options.
|
||||||
e := echo.New()
|
func DefaultOptions() *Options {
|
||||||
e.HideBanner = true
|
return &Options{
|
||||||
e.HidePort = true
|
DefaultWaitTimeout: 10,
|
||||||
e.Use(middleware.Logger())
|
DefaultListenPort: "3000",
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
EnableChromeEndpoints: true,
|
||||||
return func(c echo.Context) error {
|
EnableUnoconvEndpoints: true,
|
||||||
if err := next(c); err != nil {
|
EnableHealthcheckLogging: true,
|
||||||
// TODO should return a better HTTP status code
|
}
|
||||||
// than 500 for some cases.
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%v", err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
e.GET("/ping", func(c echo.Context) error { return nil })
|
|
||||||
e.POST("/merge", merge)
|
|
||||||
g := e.Group("/convert")
|
|
||||||
g.POST("/html", convertHTML)
|
|
||||||
g.POST("/url", convertURL)
|
|
||||||
g.POST("/markdown", convertMarkdown)
|
|
||||||
g.POST("/office", convertOffice)
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContext(r *resource) (context.Context, context.CancelFunc) {
|
// New returns an API.
|
||||||
webhookURL := r.webhookURL()
|
func New(opts *Options) *echo.Echo {
|
||||||
if webhookURL == "" {
|
api := echo.New()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
api.HideBanner = true
|
||||||
return ctx, cancel
|
api.HidePort = true
|
||||||
|
api.Use(handleLogging(opts.EnableHealthcheckLogging))
|
||||||
|
api.GET(pingEndpoint, func(c echo.Context) error { return nil })
|
||||||
|
g := api.Group("/convert")
|
||||||
|
g.Use(handleContext(opts))
|
||||||
|
g.Use(handleError())
|
||||||
|
g.POST("/merge", merge)
|
||||||
|
if !opts.EnableChromeEndpoints && !opts.EnableUnoconvEndpoints {
|
||||||
|
return api
|
||||||
}
|
}
|
||||||
return context.Background(), nil
|
if opts.EnableChromeEndpoints {
|
||||||
}
|
g.POST("/html", convertHTML)
|
||||||
|
g.POST("/url", convertURL)
|
||||||
func print(c echo.Context, p printer.Printer, r *resource) error {
|
g.POST("/markdown", convertMarkdown)
|
||||||
baseFilename, err := rand.Get()
|
}
|
||||||
if err != nil {
|
if opts.EnableUnoconvEndpoints {
|
||||||
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
|
g.POST("/office", convertOffice)
|
||||||
}
|
}
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
return api
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
// if no webhook URL given, run conversion
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or an error.
|
|
||||||
if r.webhookURL() == "" {
|
|
||||||
defer r.removeAll()
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if r.filename() != "" {
|
|
||||||
filename = r.filename()
|
|
||||||
}
|
|
||||||
return c.Attachment(fpath, filename)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
defer r.removeAll()
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hijackErr(err error, r *resource) error {
|
|
||||||
if r != nil {
|
|
||||||
defer r.removeAll()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,123 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/thecodingmachine/gotenberg/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPing(t *testing.T) {
|
func TestDefaultWaitTimeout(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
opts := DefaultOptions()
|
||||||
rec := httptest.NewRecorder()
|
opts.DefaultWaitTimeout = 0
|
||||||
e := echo.New()
|
srv := New(opts)
|
||||||
_ = e.NewContext(req, rec)
|
// testing if timeout.
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
// testing if no timeout.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "10"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisableChromeEndpoints(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.EnableChromeEndpoints = false
|
||||||
|
srv := New(opts)
|
||||||
|
// Ping.
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Merge.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// HTML.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Markdown.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// URL.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Office.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisableUnoconvEndpoints(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.EnableUnoconvEndpoints = false
|
||||||
|
srv := New(opts)
|
||||||
|
// Ping.
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Merge.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// HTML.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Markdown.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// URL.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Office.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
}
|
||||||
|
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.EnableChromeEndpoints = false
|
||||||
|
opts.EnableUnoconvEndpoints = false
|
||||||
|
srv := New(opts)
|
||||||
|
// Ping.
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Merge.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// HTML.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Markdown.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// URL.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Office.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,2 @@
|
|||||||
/*
|
// Package api helps managing the HTTP server behind Gotenberg.
|
||||||
Package api starts a HTTP server on port 3000.
|
|
||||||
|
|
||||||
It accepts POST requests with a multipart/form-data Content-Type
|
|
||||||
for converting HTML, Markdown and Office documents to PDF.
|
|
||||||
|
|
||||||
It is also able to merge a list of PDF files.
|
|
||||||
*/
|
|
||||||
package api
|
package api
|
||||||
|
|||||||
157
internal/app/api/handler.go
Normal file
157
internal/app/api/handler.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errBadRequest struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errBadRequest) Error() string {
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.mergePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpaths, err := ctx.resource.fpaths(".pdf")
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewMerge(fpaths, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertHTML(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.chromePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpath, err := ctx.resource.fpath("index.html")
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewHTML(fpath, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMarkdown(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.chromePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpath, err := ctx.resource.fpath("index.html")
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p, err := printer.NewMarkdown(fpath, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertURL(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.chromePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
remote, err := ctx.resource.get(remoteURL)
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewURL(remote, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertOffice(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.officePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpaths, err := ctx.resource.fpaths(
|
||||||
|
".txt",
|
||||||
|
".rtf",
|
||||||
|
".fodt",
|
||||||
|
".doc",
|
||||||
|
".docx",
|
||||||
|
".odt",
|
||||||
|
".xls",
|
||||||
|
".xlsx",
|
||||||
|
".ods",
|
||||||
|
".ppt",
|
||||||
|
".pptx",
|
||||||
|
".odp",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewOffice(fpaths, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert(ctx *resourceContext, p printer.Printer) error {
|
||||||
|
baseFilename, err := rand.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||||
|
fpath := fmt.Sprintf("%s/%s", ctx.resource.formFilesDirPath, filename)
|
||||||
|
// if no webhook URL given, run conversion
|
||||||
|
// and directly return the resulting PDF file
|
||||||
|
// or an error.
|
||||||
|
if !ctx.resource.has(webhookURL) {
|
||||||
|
if err := p.Print(fpath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ctx.resource.has(resultFilename) {
|
||||||
|
filename, err = ctx.resource.get(resultFilename)
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Attachment(fpath, filename)
|
||||||
|
}
|
||||||
|
// as a webhook URL has been given, we
|
||||||
|
// run the following lines in a goroutine so that
|
||||||
|
// it doesn't block.
|
||||||
|
go func() {
|
||||||
|
defer ctx.resource.close() // nolint: errcheck
|
||||||
|
if err := p.Print(fpath); err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := os.Open(fpath)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close() // nolint: errcheck
|
||||||
|
webhook, err := ctx.resource.get(webhookURL)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := http.Post(webhook, "application/pdf", f) /* #nosec */
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close() // nolint: errcheck
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
360
internal/app/api/handler_test.go
Normal file
360
internal/app/api/handler_test.go
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMerge(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTML(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkdown(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURL(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOffice(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrent(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.DefaultWaitTimeout = 30
|
||||||
|
srv := New(opts)
|
||||||
|
// Merge.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// HTML.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// Markdown.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// URL.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// Office.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhook(t *testing.T) {
|
||||||
|
status := make(chan error, 2)
|
||||||
|
rcv := echo.New()
|
||||||
|
rcv.POST("/foo", func(c echo.Context) error {
|
||||||
|
if c.Request().Header.Get("Content-type") != "application/pdf" {
|
||||||
|
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(c.Request().Body)
|
||||||
|
if err != nil {
|
||||||
|
status <- err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if body == nil || len(body) == 0 {
|
||||||
|
status <- errors.New("empty body")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
status <- nil
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
rcv.Start(":3001")
|
||||||
|
}()
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, map[string]string{webhookURL: "http://localhost:3001/foo"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
err := <-status
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultFilename(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resultFilename: "foo.pdf"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertHTML(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
p := &printer.HTML{Context: ctx}
|
|
||||||
indexPath, err := r.filePath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.WithLocalURL(indexPath)
|
|
||||||
headerPath, _ := r.filePath("header.html")
|
|
||||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
footerPath, _ := r.filePath("footer.html")
|
|
||||||
if err := p.WithFooterFile(footerPath); err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
paperSize, err := r.paperSize()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.PaperWidth = paperSize[0]
|
|
||||||
p.PaperHeight = paperSize[1]
|
|
||||||
paperMargins, err := r.paperMargins()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.MarginTop = paperMargins[0]
|
|
||||||
p.MarginBottom = paperMargins[1]
|
|
||||||
p.MarginLeft = paperMargins[2]
|
|
||||||
p.MarginRight = paperMargins[3]
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
chromeWebFontsTimeout, err := r.webFontsTimeout()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.WebFontsTimeout = chromeWebFontsTimeout
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
body, contentType := test.HTMLTestMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
if assert.NoError(t, convertHTML(c)) {
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertMarkdown(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
indexPath, err := r.filePath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
|
|
||||||
headerPath, _ := r.filePath("header.html")
|
|
||||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
footerPath, _ := r.filePath("footer.html")
|
|
||||||
if err := p.WithFooterFile(footerPath); err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
paperSize, err := r.paperSize()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.PaperWidth = paperSize[0]
|
|
||||||
p.PaperHeight = paperSize[1]
|
|
||||||
paperMargins, err := r.paperMargins()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.MarginTop = paperMargins[0]
|
|
||||||
p.MarginBottom = paperMargins[1]
|
|
||||||
p.MarginLeft = paperMargins[2]
|
|
||||||
p.MarginRight = paperMargins[3]
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
chromeWebFontsTimeout, err := r.webFontsTimeout()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.WebFontsTimeout = chromeWebFontsTimeout
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
if assert.NoError(t, convertMarkdown(c)) {
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
func merge(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
fpaths, err := r.filePaths([]string{".pdf"})
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
if len(fpaths) == 0 {
|
|
||||||
return hijackErr(errors.New("no suitable PDF files to merge"), r)
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
|
|
||||||
}
|
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
// if no webhook URL given, run merge
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or an error.
|
|
||||||
if r.webhookURL() == "" {
|
|
||||||
defer r.removeAll()
|
|
||||||
if err := printer.Merge(fpaths, fpath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if r.filename() != "" {
|
|
||||||
filename = r.filename()
|
|
||||||
}
|
|
||||||
return c.Attachment(fpath, filename)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
defer r.removeAll()
|
|
||||||
if err := printer.Merge(fpaths, fpath); err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
if assert.NoError(t, merge(c)) {
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
75
internal/app/api/middleware.go
Normal file
75
internal/app/api/middleware.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleLogging(enableHealthcheckLogging bool) echo.MiddlewareFunc {
|
||||||
|
if enableHealthcheckLogging {
|
||||||
|
// default logging middleware.
|
||||||
|
return middleware.Logger()
|
||||||
|
}
|
||||||
|
// middleware for skipping logging when the ping endpoint is called.
|
||||||
|
return middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
return c.Request().URL.Path == pingEndpoint
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleContext(opts *Options) echo.MiddlewareFunc {
|
||||||
|
// middleware for extending default context with our
|
||||||
|
// custom constext.
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := &resourceContext{c, opts, nil}
|
||||||
|
r, err := newResource(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if resourceErr := r.close(); resourceErr != nil {
|
||||||
|
c.Logger().Error(resourceErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.resource = r
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError() echo.MiddlewareFunc {
|
||||||
|
// middleware for handling errors and removing resources
|
||||||
|
// once the request has been handled.
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
err := next(c)
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
// if a webhookURL has been given,
|
||||||
|
// do not remove the resources here because
|
||||||
|
// we don't know if the result file has been
|
||||||
|
// generated or sent.
|
||||||
|
if !ctx.resource.has(webhookURL) {
|
||||||
|
if resourceErr := ctx.resource.close(); resourceErr != nil {
|
||||||
|
c.Logger().Error(resourceErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*echo.HTTPError); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := err.(*errBadRequest); ok {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
||||||
|
return echo.NewHTTPError(http.StatusRequestTimeout)
|
||||||
|
}
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var officeExts = []string{
|
|
||||||
".txt",
|
|
||||||
".rtf",
|
|
||||||
".fodt",
|
|
||||||
".doc",
|
|
||||||
".docx",
|
|
||||||
".odt",
|
|
||||||
".xls",
|
|
||||||
".xlsx",
|
|
||||||
".ods",
|
|
||||||
".ppt",
|
|
||||||
".pptx",
|
|
||||||
".odp",
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertOffice(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
fpaths, err := r.filePaths(officeExts)
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
if len(fpaths) == 0 {
|
|
||||||
return hijackErr(errors.New("no suitable office documents to convert"), r)
|
|
||||||
}
|
|
||||||
p := &printer.Office{Context: ctx, FilePaths: fpaths}
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
body, contentType := test.OfficeTestMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
if assert.NoError(t, convertOffice(c)) {
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +1,264 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
remoteURL string = "remoteURL"
|
resultFilename string = "resultFilename"
|
||||||
webhookURL string = "webhookURL"
|
waitTimeout string = "waitTimeout"
|
||||||
paperWidth string = "paperWidth"
|
webhookURL string = "webhookURL"
|
||||||
paperHeight string = "paperHeight"
|
remoteURL string = "remoteURL"
|
||||||
marginTop string = "marginTop"
|
waitDelay string = "waitDelay"
|
||||||
marginBottom string = "marginBottom"
|
paperWidth string = "paperWidth"
|
||||||
marginLeft string = "marginLeft"
|
paperHeight string = "paperHeight"
|
||||||
marginRight string = "marginRight"
|
marginTop string = "marginTop"
|
||||||
landscape string = "landscape"
|
marginBottom string = "marginBottom"
|
||||||
webFontsTimeout string = "webFontsTimeout"
|
marginLeft string = "marginLeft"
|
||||||
filename string = "filename"
|
marginRight string = "marginRight"
|
||||||
|
landscape string = "landscape"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resource facilitates storing and accessing
|
|
||||||
// data from a multipart/form-data request.
|
|
||||||
type resource struct {
|
type resource struct {
|
||||||
values map[string]string
|
formValues map[string]string
|
||||||
dirPath string
|
formFilesDirPath string
|
||||||
|
opts *Options
|
||||||
}
|
}
|
||||||
|
|
||||||
func newResource(c echo.Context) (*resource, error) {
|
type resourceContext struct {
|
||||||
|
echo.Context
|
||||||
|
opts *Options
|
||||||
|
resource *resource
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResource(ctx *resourceContext) (*resource, error) {
|
||||||
|
r := &resource{
|
||||||
|
formValues: formValues(ctx),
|
||||||
|
opts: ctx.opts,
|
||||||
|
}
|
||||||
dirPath, err := rand.Get()
|
dirPath, err := rand.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
r.formFilesDirPath = dirPath
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
||||||
}
|
}
|
||||||
r := &resource{values: values(c), dirPath: dirPath}
|
if err := formFiles(ctx, dirPath); err != nil {
|
||||||
form, err := c.MultipartForm()
|
return r, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formValues(ctx *resourceContext) map[string]string {
|
||||||
|
v := make(map[string]string)
|
||||||
|
v[resultFilename] = ctx.FormValue(resultFilename)
|
||||||
|
v[waitTimeout] = ctx.FormValue(waitTimeout)
|
||||||
|
v[webhookURL] = ctx.FormValue(webhookURL)
|
||||||
|
v[remoteURL] = ctx.FormValue(remoteURL)
|
||||||
|
v[waitDelay] = ctx.FormValue(waitDelay)
|
||||||
|
v[paperWidth] = ctx.FormValue(paperWidth)
|
||||||
|
v[paperHeight] = ctx.FormValue(paperHeight)
|
||||||
|
v[marginTop] = ctx.FormValue(marginTop)
|
||||||
|
v[marginBottom] = ctx.FormValue(marginBottom)
|
||||||
|
v[marginLeft] = ctx.FormValue(marginLeft)
|
||||||
|
v[marginRight] = ctx.FormValue(marginRight)
|
||||||
|
v[landscape] = ctx.FormValue(landscape)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func formFiles(ctx *resourceContext, dirPath string) error {
|
||||||
|
form, err := ctx.MultipartForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r, fmt.Errorf("getting multipart form: %v", err)
|
return fmt.Errorf("getting multipart form: %v", err)
|
||||||
}
|
}
|
||||||
for _, files := range form.File {
|
for _, files := range form.File {
|
||||||
for _, fh := range files {
|
for _, fh := range files {
|
||||||
in, err := fh.Open()
|
in, err := fh.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
return fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
||||||
}
|
}
|
||||||
defer in.Close()
|
defer in.Close() // nolint: errcheck
|
||||||
if err := r.writeFile(fh.Filename, in); err != nil {
|
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
||||||
return r, err
|
out, err := os.Create(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
||||||
|
}
|
||||||
|
defer out.Close() // nolint: errcheck
|
||||||
|
if err := out.Chmod(0644); err != nil {
|
||||||
|
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
||||||
|
}
|
||||||
|
if _, err := out.Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func values(c echo.Context) map[string]string {
|
|
||||||
v := make(map[string]string)
|
|
||||||
v[remoteURL] = c.FormValue(remoteURL)
|
|
||||||
v[webhookURL] = c.FormValue(webhookURL)
|
|
||||||
v[paperWidth] = c.FormValue(paperWidth)
|
|
||||||
v[paperHeight] = c.FormValue(paperHeight)
|
|
||||||
v[marginTop] = c.FormValue(marginTop)
|
|
||||||
v[marginBottom] = c.FormValue(marginBottom)
|
|
||||||
v[marginLeft] = c.FormValue(marginLeft)
|
|
||||||
v[marginRight] = c.FormValue(marginRight)
|
|
||||||
v[landscape] = c.FormValue(landscape)
|
|
||||||
v[webFontsTimeout] = c.FormValue(webFontsTimeout)
|
|
||||||
v[filename] = c.FormValue(filename)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) writeFile(filename string, in io.Reader) error {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
if err := out.Chmod(0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := out.Seek(0, 0); err != nil {
|
|
||||||
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resource) filePath(filename string) (string, error) {
|
func (r *resource) close() error {
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
if _, err := os.Stat(r.formFilesDirPath); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.RemoveAll(r.formFilesDirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||||
|
|
||||||
|
func (r *resource) chromePrinterOptions() (*printer.ChromeOptions, error) {
|
||||||
|
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
delay, err := r.float64(waitDelay, 0.0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
header, err := r.content("header.html", defaultHeaderFooterHTML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
footer, err := r.content("footer.html", defaultHeaderFooterHTML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
width, err := r.float64(paperWidth, 8.27)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
height, err := r.float64(paperHeight, 11.7)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
top, err := r.float64(marginTop, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bottom, err := r.float64(marginBottom, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
left, err := r.float64(marginLeft, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
right, err := r.float64(marginRight, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
landscape, err := r.bool(landscape, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &printer.ChromeOptions{
|
||||||
|
WaitTimeout: timeout,
|
||||||
|
WaitDelay: delay,
|
||||||
|
HeaderHTML: header,
|
||||||
|
FooterHTML: footer,
|
||||||
|
PaperWidth: width,
|
||||||
|
PaperHeight: height,
|
||||||
|
MarginTop: top,
|
||||||
|
MarginBottom: bottom,
|
||||||
|
MarginLeft: left,
|
||||||
|
MarginRight: right,
|
||||||
|
Landscape: landscape,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) officePrinterOptions() (*printer.OfficeOptions, error) {
|
||||||
|
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
landscape, err := r.bool(landscape, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &printer.OfficeOptions{
|
||||||
|
WaitTimeout: timeout,
|
||||||
|
Landscape: landscape,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) mergePrinterOptions() (*printer.MergeOptions, error) {
|
||||||
|
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &printer.MergeOptions{
|
||||||
|
WaitTimeout: timeout,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) has(key string) bool {
|
||||||
|
v, ok := r.formValues[key]
|
||||||
|
if ok {
|
||||||
|
ok = v != ""
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) hasFile(filename string) bool {
|
||||||
|
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||||
|
_, err := os.Stat(fpath)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) get(key string) (string, error) {
|
||||||
|
v, ok := r.formValues[key]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("form value %s does not exist", key)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) float64(key string, defaultValue float64) (float64, error) {
|
||||||
|
if !r.has(key) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
v, err := r.get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, err
|
||||||
|
}
|
||||||
|
f, err := strconv.ParseFloat(v, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf("form value %s: %v", key, err)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) bool(key string, defaultValue bool) (bool, error) {
|
||||||
|
if !r.has(key) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
v, err := r.get(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
b, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("form value %s: %v", key, err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) fpath(filename string) (string, error) {
|
||||||
|
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||||
_, err := os.Stat(fpath)
|
_, err := os.Stat(fpath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return "", fmt.Errorf("%s: file does not exist", filename)
|
return "", fmt.Errorf("%s: form file does not exist", filename)
|
||||||
}
|
}
|
||||||
absPath, err := filepath.Abs(fpath)
|
absPath, err := filepath.Abs(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -109,13 +267,28 @@ func (r *resource) filePath(filename string) (string, error) {
|
|||||||
return absPath, nil
|
return absPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resource) filePaths(exts []string) ([]string, error) {
|
func (r *resource) content(filename string, defaultValue string) (string, error) {
|
||||||
|
if !r.hasFile(filename) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
fpath, err := r.fpath(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b, err := ioutil.ReadFile(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s: reading form file: %v", fpath, err)
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) fpaths(exts ...string) ([]string, error) {
|
||||||
var fpaths []string
|
var fpaths []string
|
||||||
err := filepath.Walk(r.dirPath, func(path string, info os.FileInfo, _ error) error {
|
err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fpath, err := r.filePath(info.Name())
|
fpath, err := r.fpath(info.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -130,86 +303,8 @@ func (r *resource) filePaths(exts []string) ([]string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(fpaths) == 0 {
|
||||||
|
return nil, fmt.Errorf("no form files found for extensions: %v", exts)
|
||||||
|
}
|
||||||
return fpaths, nil
|
return fpaths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resource) paperSize() ([2]float64, error) {
|
|
||||||
defaultSize := [2]float64{8.27, 11.7}
|
|
||||||
widthStr := r.values[paperWidth]
|
|
||||||
heightStr := r.values[paperHeight]
|
|
||||||
if widthStr == "" || heightStr == "" {
|
|
||||||
return defaultSize, nil
|
|
||||||
}
|
|
||||||
width, err := strconv.ParseFloat(widthStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultSize, fmt.Errorf("paper width: %v", err)
|
|
||||||
}
|
|
||||||
height, err := strconv.ParseFloat(heightStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultSize, fmt.Errorf("paper height: %v", err)
|
|
||||||
}
|
|
||||||
return [2]float64{width, height}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) paperMargins() ([4]float64, error) {
|
|
||||||
defaultMargins := [4]float64{1, 1, 1, 1}
|
|
||||||
topStr := r.values[marginTop]
|
|
||||||
bottomStr := r.values[marginBottom]
|
|
||||||
leftStr := r.values[marginLeft]
|
|
||||||
rightStr := r.values[marginRight]
|
|
||||||
if topStr == "" || bottomStr == "" || leftStr == "" || rightStr == "" {
|
|
||||||
return defaultMargins, nil
|
|
||||||
}
|
|
||||||
top, err := strconv.ParseFloat(topStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin top: %v", err)
|
|
||||||
}
|
|
||||||
bottom, err := strconv.ParseFloat(bottomStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin bottom: %v", err)
|
|
||||||
}
|
|
||||||
left, err := strconv.ParseFloat(leftStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin left: %v", err)
|
|
||||||
}
|
|
||||||
right, err := strconv.ParseFloat(rightStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin right: %v", err)
|
|
||||||
}
|
|
||||||
return [4]float64{top, bottom, left, right}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) landscape() (bool, error) {
|
|
||||||
landscapeStr := r.values[landscape]
|
|
||||||
if landscapeStr == "" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
landscape, err := strconv.ParseBool(landscapeStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("landscape: %v", err)
|
|
||||||
}
|
|
||||||
return landscape, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) remoteURL() (string, error) {
|
|
||||||
if r.values[remoteURL] == "" {
|
|
||||||
return "", errors.New("no remote URL")
|
|
||||||
}
|
|
||||||
return r.values[remoteURL], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) webFontsTimeout() (int64, error) {
|
|
||||||
webFontsTimeoutStr := r.values[webFontsTimeout]
|
|
||||||
if webFontsTimeoutStr == "" {
|
|
||||||
return 500, nil
|
|
||||||
}
|
|
||||||
timeout, err := strconv.ParseInt(webFontsTimeoutStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 500, fmt.Errorf("web fonts timeout: %v", err)
|
|
||||||
}
|
|
||||||
return timeout, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) webhookURL() string { return r.values[webhookURL] }
|
|
||||||
func (r *resource) filename() string { return r.values[filename] }
|
|
||||||
func (r *resource) removeAll() error { return os.RemoveAll(r.dirPath) }
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertURL(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
p := &printer.HTML{Context: ctx}
|
|
||||||
URL, err := r.remoteURL()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.URL = URL
|
|
||||||
headerPath, _ := r.filePath("header.html")
|
|
||||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
footerPath, _ := r.filePath("footer.html")
|
|
||||||
if err := p.WithFooterFile(footerPath); err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
paperSize, err := r.paperSize()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.PaperWidth = paperSize[0]
|
|
||||||
p.PaperHeight = paperSize[1]
|
|
||||||
paperMargins, err := r.paperMargins()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.MarginTop = paperMargins[0]
|
|
||||||
p.MarginBottom = paperMargins[1]
|
|
||||||
p.MarginLeft = paperMargins[2]
|
|
||||||
p.MarginRight = paperMargins[3]
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
chromeWebFontsTimeout, err := r.webFontsTimeout()
|
|
||||||
if err != nil {
|
|
||||||
return hijackErr(err, r)
|
|
||||||
}
|
|
||||||
p.WebFontsTimeout = chromeWebFontsTimeout
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestURL(t *testing.T) {
|
|
||||||
body, contentType := test.URLTestMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
if assert.NoError(t, convertURL(c)) {
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Package notify is used across the application
|
Package notify helps displaying nice outputs
|
||||||
to display nice outputs to the user.
|
to the user.
|
||||||
*/
|
*/
|
||||||
package notify
|
package notify
|
||||||
|
|||||||
@@ -7,24 +7,29 @@ import (
|
|||||||
"github.com/labstack/gommon/color"
|
"github.com/labstack/gommon/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// Print prints a message to stdout.
|
||||||
stdout *color.Color
|
func Print(message string) {
|
||||||
stderr *color.Color
|
stdout := color.New()
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
stdout = color.New()
|
|
||||||
stdout.SetOutput(os.Stdout)
|
stdout.SetOutput(os.Stdout)
|
||||||
stderr = color.New()
|
|
||||||
stderr.SetOutput(os.Stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Println prints a message to stdout.
|
|
||||||
func Println(message string) {
|
|
||||||
stdout.Printf("⇨ %s\n", message)
|
stdout.Printf("⇨ %s\n", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrPrintln prints an error to stderr.
|
// Printf prints a formatted message to stdout.
|
||||||
func ErrPrintln(err error) {
|
func Printf(format string, a ...interface{}) {
|
||||||
|
message := fmt.Sprintf(format, a...)
|
||||||
|
Print(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarnPrint prints a warning to stderr.
|
||||||
|
func WarnPrint(err error) {
|
||||||
|
stderr := color.New()
|
||||||
|
stderr.SetOutput(os.Stderr)
|
||||||
|
stderr.Printf("%s\n", color.Yellow(fmt.Sprintf("⇨ warn: %v", err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrPrint prints an error to stderr.
|
||||||
|
func ErrPrint(err error) {
|
||||||
|
stderr := color.New()
|
||||||
|
stderr.SetOutput(os.Stderr)
|
||||||
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,36 @@ package pm2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
"github.com/mafredri/cdp/devtool"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chrome facilitates starting or shutting down
|
type chrome struct {
|
||||||
// Chrome headless with PM2.
|
manager *processManager
|
||||||
type Chrome struct{}
|
|
||||||
|
|
||||||
// Launch starts Chrome headless with PM2.
|
|
||||||
func (c *Chrome) Launch() error {
|
|
||||||
return launch(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown stops Chrome headless and
|
// NewChrome retruns a Google Chrome
|
||||||
// removes it from the list of PM2
|
// headless process.
|
||||||
// processes.
|
func NewChrome() Process {
|
||||||
func (c *Chrome) Shutdown() error {
|
return &chrome{
|
||||||
return shutdown(c)
|
manager: &processManager{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) getArgs() []string {
|
func (p *chrome) Fullname() string {
|
||||||
|
return "Google Chrome headless"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) Start() error {
|
||||||
|
return p.manager.start(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) Shutdown() error {
|
||||||
|
return p.manager.shutdown(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) args() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--headless",
|
"--headless",
|
||||||
@@ -44,28 +50,23 @@ func (c *Chrome) getArgs() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) getName() string {
|
func (p *chrome) name() string {
|
||||||
return "google-chrome-stable"
|
return "google-chrome-stable"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) getFullname() string {
|
func (p *chrome) viable() bool {
|
||||||
return "Chrome headless"
|
// check if Google Chrome is correctly running.
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) isViable() bool {
|
|
||||||
// check if Chrome is correctly running.
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, err := devtool.New("http://localhost:9222").Version(ctx)
|
_, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) warmup() {
|
func (p *chrome) warmup() {
|
||||||
notify.Println(fmt.Sprintf("warming-up %s", c.getFullname()))
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(Chrome))
|
_ = Process(new(chrome))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestChromeLaunch(t *testing.T) {
|
func TestChromeStart(t *testing.T) {
|
||||||
p := &Chrome{}
|
p := NewChrome()
|
||||||
err := p.Launch()
|
err := p.Start()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChromeShutdown(t *testing.T) {
|
func TestChromeShutdown(t *testing.T) {
|
||||||
p := &Chrome{}
|
p := NewChrome()
|
||||||
err := p.Shutdown()
|
err := p.Shutdown()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Package pm2 facilitates starting external
|
Package pm2 facilitates starting external
|
||||||
processes on which our API depends.
|
processes on which our API depends.
|
||||||
|
|
||||||
For instance, it starts Chrome headless and
|
For instance, it may start Google Chrome headless and
|
||||||
unoconv listener with PM2.
|
unoconv listener with PM2.
|
||||||
|
|
||||||
The PM2 process manager launch those processes and keep
|
The PM2 process manager launch those processes and keep
|
||||||
|
|||||||
@@ -3,69 +3,81 @@ package pm2
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process is a type that can launch or
|
const (
|
||||||
|
stoppedState = iota
|
||||||
|
runningState
|
||||||
|
errorState
|
||||||
|
)
|
||||||
|
|
||||||
|
// Process is a type that can start or
|
||||||
// shutdown a process with PM2.
|
// shutdown a process with PM2.
|
||||||
type Process interface {
|
type Process interface {
|
||||||
Launch() error
|
Fullname() string
|
||||||
|
Start() error
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
getArgs() []string
|
args() []string
|
||||||
getName() string
|
name() string
|
||||||
getFullname() string
|
viable() bool
|
||||||
isViable() bool
|
|
||||||
warmup()
|
warmup()
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxRestartAttempts int = 5
|
type processManager struct {
|
||||||
|
heuristicState int32
|
||||||
var humanNames = map[string]string{
|
|
||||||
"start": "started",
|
|
||||||
"restart": "restarted",
|
|
||||||
"stop": "stopped",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func launch(p Process) error {
|
func (m *processManager) start(p Process) error {
|
||||||
if err := run(p, "start"); err != nil {
|
if err := m.pm2(p, "start"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.warmup()
|
p.warmup()
|
||||||
if !p.isViable() {
|
if !p.viable() {
|
||||||
attempts := 0
|
attempts := 0
|
||||||
for attempts < maxRestartAttempts && !p.isViable() {
|
for attempts < 5 && !p.viable() {
|
||||||
run(p, "restart")
|
if err := m.pm2(p, "restart"); err != nil {
|
||||||
|
m.heuristicState = errorState
|
||||||
|
return err
|
||||||
|
}
|
||||||
p.warmup()
|
p.warmup()
|
||||||
attempts++
|
attempts++
|
||||||
}
|
}
|
||||||
if !p.isViable() {
|
if !p.viable() {
|
||||||
return fmt.Errorf("failed to launch %s", p.getFullname())
|
m.heuristicState = errorState
|
||||||
|
return fmt.Errorf("failed to launch %s", p.Fullname())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.heuristicState = runningState
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdown(p Process) error {
|
func (m *processManager) shutdown(p Process) error {
|
||||||
return run(p, "stop")
|
if m.heuristicState != runningState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := m.pm2(p, "stop"); err != nil {
|
||||||
|
m.heuristicState = errorState
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.heuristicState = stoppedState
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(p Process, cmdName string) error {
|
func (m *processManager) pm2(p Process, cmdName string) error {
|
||||||
cmdArgs := []string{
|
cmdArgs := []string{
|
||||||
cmdName,
|
cmdName,
|
||||||
p.getName(),
|
p.name(),
|
||||||
}
|
}
|
||||||
if cmdName == "start" {
|
if cmdName == "start" {
|
||||||
cmdArgs = append(cmdArgs, "--interpreter none", "--")
|
cmdArgs = append(cmdArgs, "--interpreter=none", "--")
|
||||||
cmdArgs = append(cmdArgs, p.getArgs()...)
|
cmdArgs = append(cmdArgs, p.args()...)
|
||||||
}
|
}
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"pm2",
|
"pm2",
|
||||||
cmdArgs...,
|
cmdArgs...,
|
||||||
)
|
)
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.getFullname(), err)
|
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err)
|
||||||
}
|
}
|
||||||
notify.Println(fmt.Sprintf("%s %s with PM2", p.getFullname(), humanNames[cmdName]))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,52 @@
|
|||||||
package pm2
|
package pm2
|
||||||
|
|
||||||
// Unoconv facilitates starting or shutting down
|
type unoconv struct {
|
||||||
// unoconv listener with PM2.
|
manager *processManager
|
||||||
type Unoconv struct{}
|
|
||||||
|
|
||||||
// Launch starts unoconv listener with PM2.
|
|
||||||
func (u *Unoconv) Launch() error {
|
|
||||||
return launch(u)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown stops unoconv listener and
|
// NewUnoconv retruns a unoconv listener
|
||||||
// removes it from the list of PM2
|
// process.
|
||||||
// processes.
|
func NewUnoconv() Process {
|
||||||
func (u *Unoconv) Shutdown() error {
|
return &unoconv{
|
||||||
return shutdown(u)
|
manager: &processManager{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) getArgs() []string {
|
func (p *unoconv) Fullname() string {
|
||||||
|
return "unoconv listener"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *unoconv) Start() error {
|
||||||
|
return p.manager.start(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *unoconv) Shutdown() error {
|
||||||
|
return p.manager.shutdown(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *unoconv) args() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"--listener",
|
"--listener",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) getName() string {
|
func (p *unoconv) name() string {
|
||||||
return "unoconv"
|
return "unoconv"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) getFullname() string {
|
func (p *unoconv) viable() bool {
|
||||||
return "unoconv listener"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unoconv) isViable() bool {
|
|
||||||
// TODO find a way to check if
|
// TODO find a way to check if
|
||||||
// unoconv is correctly started?
|
// the unoconv listener
|
||||||
|
// is correctly started?
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) warmup() {
|
func (p *unoconv) warmup() {
|
||||||
// let's do nothing.
|
// let's do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(Unoconv))
|
_ = Process(new(unoconv))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnoconvLaunch(t *testing.T) {
|
func TestUnoconvStart(t *testing.T) {
|
||||||
p := &Unoconv{}
|
p := NewUnoconv()
|
||||||
err := p.Launch()
|
err := p.Start()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvShutdown(t *testing.T) {
|
func TestUnoconvShutdown(t *testing.T) {
|
||||||
p := &Unoconv{}
|
p := NewUnoconv()
|
||||||
err := p.Shutdown()
|
err := p.Shutdown()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
169
internal/pkg/printer/chrome.go
Normal file
169
internal/pkg/printer/chrome.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mafredri/cdp"
|
||||||
|
"github.com/mafredri/cdp/devtool"
|
||||||
|
"github.com/mafredri/cdp/protocol/network"
|
||||||
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
|
"github.com/mafredri/cdp/protocol/target"
|
||||||
|
"github.com/mafredri/cdp/rpcc"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chrome struct {
|
||||||
|
url string
|
||||||
|
opts *ChromeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChromeOptions helps customizing the
|
||||||
|
// Google Chrome printer behaviour.
|
||||||
|
type ChromeOptions struct {
|
||||||
|
WaitTimeout float64
|
||||||
|
WaitDelay float64
|
||||||
|
HeaderHTML string
|
||||||
|
FooterHTML string
|
||||||
|
PaperWidth float64
|
||||||
|
PaperHeight float64
|
||||||
|
MarginTop float64
|
||||||
|
MarginBottom float64
|
||||||
|
MarginLeft float64
|
||||||
|
MarginRight float64
|
||||||
|
Landscape bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) Print(destination string) error {
|
||||||
|
duration := time.Duration(p.opts.WaitTimeout+p.opts.WaitDelay) * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), duration)
|
||||||
|
defer cancel()
|
||||||
|
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
||||||
|
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer devtConn.Close() // nolint: errcheck
|
||||||
|
// create a new CDP Client that uses conn.
|
||||||
|
devtClient := cdp.NewClient(devtConn)
|
||||||
|
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new browser context: %v", err)
|
||||||
|
}
|
||||||
|
// create a new blank target with the new browser context.
|
||||||
|
createTargetArgs := target.
|
||||||
|
NewCreateTargetArgs("about:blank").
|
||||||
|
SetBrowserContextID(newContextTarget.BrowserContextID)
|
||||||
|
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new blank target: %v", err)
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting client to blank target: %v", err)
|
||||||
|
}
|
||||||
|
defer newContextConn.Close() // nolint: errcheck
|
||||||
|
// create a new CDP Client that uses newContextConn.
|
||||||
|
targetClient := cdp.NewClient(newContextConn)
|
||||||
|
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
||||||
|
// close the target when done.
|
||||||
|
defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck
|
||||||
|
if err := runBatch(
|
||||||
|
// enable all the domain events that we're interested in.
|
||||||
|
func() error { return targetClient.DOM.Enable(ctx) },
|
||||||
|
func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) },
|
||||||
|
func() error { return targetClient.Page.Enable(ctx) },
|
||||||
|
func() error { return targetClient.Runtime.Enable(ctx) },
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.navigate(ctx, targetClient); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
print, err := targetClient.Page.PrintToPDF(
|
||||||
|
ctx,
|
||||||
|
page.NewPrintToPDFArgs().
|
||||||
|
SetPaperWidth(p.opts.PaperWidth).
|
||||||
|
SetPaperHeight(p.opts.PaperHeight).
|
||||||
|
SetMarginTop(p.opts.MarginTop).
|
||||||
|
SetMarginBottom(p.opts.MarginBottom).
|
||||||
|
SetMarginLeft(p.opts.MarginLeft).
|
||||||
|
SetMarginRight(p.opts.MarginRight).
|
||||||
|
SetLandscape(p.opts.Landscape).
|
||||||
|
SetDisplayHeaderFooter(true).
|
||||||
|
SetHeaderTemplate(p.opts.HeaderHTML).
|
||||||
|
SetFooterTemplate(p.opts.FooterHTML).
|
||||||
|
SetPrintBackground(true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("printing page to PDF: %v", err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("%s: writing file: %v", destination, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error {
|
||||||
|
// make sure Page events are enabled.
|
||||||
|
if err := client.Page.Enable(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// make sure Network events are enabled.
|
||||||
|
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// create all clients for events.
|
||||||
|
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer domContentEventFired.Close() // nolint: errcheck
|
||||||
|
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadEventFired.Close() // nolint: errcheck
|
||||||
|
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadingFinished.Close() // nolint: errcheck
|
||||||
|
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runBatch(
|
||||||
|
// wait for all events.
|
||||||
|
func() error { _, err := domContentEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadingFinished.Recv(); return err },
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// wait for a given amount of time (useful for javascript delay).
|
||||||
|
time.Sleep(time.Duration(p.opts.WaitDelay) * time.Second)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBatch(fn ...func() error) error {
|
||||||
|
// run all functions simultaneously and wait until
|
||||||
|
// execution has completed or an error is encountered.
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
for _, f := range fn {
|
||||||
|
eg.Go(f)
|
||||||
|
}
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = Printer(new(chrome))
|
||||||
|
)
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Package printer contains structs which convert
|
Package printer contains structs which convert
|
||||||
a specific file type to PDF.
|
a specific file type to PDF.
|
||||||
|
|
||||||
It is also able to merge a list of PDF files.
|
|
||||||
*/
|
*/
|
||||||
package printer
|
package printer
|
||||||
|
|||||||
@@ -1,193 +1,14 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mafredri/cdp"
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
|
||||||
"github.com/mafredri/cdp/protocol/network"
|
|
||||||
"github.com/mafredri/cdp/protocol/page"
|
|
||||||
"github.com/mafredri/cdp/protocol/runtime"
|
|
||||||
"github.com/mafredri/cdp/protocol/target"
|
|
||||||
"github.com/mafredri/cdp/rpcc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTML facilitates HTML to PDF conversion.
|
// NewHTML returns an HTML printer.
|
||||||
type HTML struct {
|
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
||||||
Context context.Context
|
URL := fmt.Sprintf("file://%s", fpath)
|
||||||
URL string
|
return &chrome{
|
||||||
HeaderHTML string
|
url: URL,
|
||||||
FooterHTML string
|
opts: opts,
|
||||||
PaperWidth float64
|
}
|
||||||
PaperHeight float64
|
|
||||||
MarginTop float64
|
|
||||||
MarginBottom float64
|
|
||||||
MarginLeft float64
|
|
||||||
MarginRight float64
|
|
||||||
Landscape bool
|
|
||||||
WebFontsTimeout int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
|
||||||
|
|
||||||
// Print converts HTML to PDF.
|
|
||||||
// Credits: https://medium.com/compass-true-north/go-service-to-convert-web-pages-to-pdf-using-headless-chrome-5fd9ffbae1af
|
|
||||||
func (html *HTML) Print(destination string) error {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
// use the DevTools HTTP/JSON API to manage targets (e.g. pages, webworkers).
|
|
||||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating DevTools target: %v", err)
|
|
||||||
}
|
|
||||||
// open a new RPC connection to the Chrome Debugging Protocol target.
|
|
||||||
conn, err := rpcc.DialContext(html.Context, devt.WebSocketDebuggerURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating RPC connection: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
// create new browser context.
|
|
||||||
baseBrowser := cdp.NewClient(conn)
|
|
||||||
newContextTarget, err := baseBrowser.Target.CreateBrowserContext(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new browser context: %v", err)
|
|
||||||
}
|
|
||||||
// create a new blank target with the new browser context.
|
|
||||||
newTargetArgs := target.NewCreateTargetArgs("about:blank").
|
|
||||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
|
||||||
newTarget, err := baseBrowser.Target.CreateTarget(html.Context, newTargetArgs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new blank target: %v", err)
|
|
||||||
}
|
|
||||||
// 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(html.Context, newTargetWsURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connecting client to blank target: %v", err)
|
|
||||||
}
|
|
||||||
defer newContextConn.Close()
|
|
||||||
// close the target when done.
|
|
||||||
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
|
||||||
defer baseBrowser.Target.CloseTarget(html.Context, closeTargetArgs)
|
|
||||||
c := cdp.NewClient(newContextConn)
|
|
||||||
// enable the runtime.
|
|
||||||
if err := c.Runtime.Enable(html.Context); err != nil {
|
|
||||||
return fmt.Errorf("enabling runtime: %v", err)
|
|
||||||
}
|
|
||||||
// enable the network.
|
|
||||||
if err := c.Network.Enable(html.Context, network.NewEnableArgs()); err != nil {
|
|
||||||
return fmt.Errorf("enabling network: %v", err)
|
|
||||||
}
|
|
||||||
// enable events on the page domain.
|
|
||||||
if err := c.Page.Enable(html.Context); err != nil {
|
|
||||||
return fmt.Errorf("enabling events on page domain: %v", err)
|
|
||||||
}
|
|
||||||
// create a client to listen for the load event to be fired.
|
|
||||||
loadEventFiredClient, err := c.Page.LoadEventFired(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating client listening for load event: %v", err)
|
|
||||||
}
|
|
||||||
defer loadEventFiredClient.Close()
|
|
||||||
// tell the page to navigate to the URL.
|
|
||||||
navArgs := page.NewNavigateArgs(html.URL)
|
|
||||||
_, err = c.Page.Navigate(html.Context, navArgs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: navigating to URL: %v", html.URL, err)
|
|
||||||
}
|
|
||||||
// wait for the page to finish loading.
|
|
||||||
_, err = loadEventFiredClient.Recv()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("waiting for page loading: %v", err)
|
|
||||||
}
|
|
||||||
// inject a script to make sure web fonts are loaded.
|
|
||||||
script := fmt.Sprintf(`new Promise((resolve, reject) => {
|
|
||||||
document.fonts.ready.then(function () {
|
|
||||||
resolve('fonts loaded');
|
|
||||||
});
|
|
||||||
setTimeout(resolve.bind(resolve, 'timeout'), %d);
|
|
||||||
});`, html.WebFontsTimeout)
|
|
||||||
scriptArg := runtime.NewEvaluateArgs(script).SetAwaitPromise(true)
|
|
||||||
returnObj, _ := c.Runtime.Evaluate(html.Context, scriptArg)
|
|
||||||
if returnObj.ExceptionDetails != nil {
|
|
||||||
return fmt.Errorf("script evaluated with exception: %+v", returnObj.ExceptionDetails)
|
|
||||||
}
|
|
||||||
loadFontsResult := string(returnObj.Result.Value)
|
|
||||||
if strings.Contains(loadFontsResult, "timeout") {
|
|
||||||
return errors.New("timed out loading fonts")
|
|
||||||
}
|
|
||||||
// if no header or footer, use the default template
|
|
||||||
// for avoiding displaying default Chrome templates.
|
|
||||||
if html.HeaderHTML == "" {
|
|
||||||
html.HeaderHTML = defaultHeaderFooterHTML
|
|
||||||
}
|
|
||||||
if html.FooterHTML == "" {
|
|
||||||
html.FooterHTML = defaultHeaderFooterHTML
|
|
||||||
}
|
|
||||||
print, err := c.Page.PrintToPDF(
|
|
||||||
html.Context,
|
|
||||||
page.NewPrintToPDFArgs().
|
|
||||||
SetPaperWidth(html.PaperWidth).
|
|
||||||
SetPaperHeight(html.PaperHeight).
|
|
||||||
SetMarginTop(html.MarginTop).
|
|
||||||
SetMarginBottom(html.MarginBottom).
|
|
||||||
SetMarginLeft(html.MarginLeft).
|
|
||||||
SetMarginRight(html.MarginRight).
|
|
||||||
SetLandscape(html.Landscape).
|
|
||||||
SetDisplayHeaderFooter(true).
|
|
||||||
SetHeaderTemplate(html.HeaderHTML).
|
|
||||||
SetFooterTemplate(html.FooterHTML).
|
|
||||||
SetPrintBackground(true),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("printing page to PDF: %v", err)
|
|
||||||
}
|
|
||||||
return writeBytesToFile(destination, print.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLocalURL sets a local URL from a file path.
|
|
||||||
func (html *HTML) WithLocalURL(fpath string) {
|
|
||||||
html.URL = fmt.Sprintf("file://%s", fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHeaderFile sets header content from a file.
|
|
||||||
func (html *HTML) WithHeaderFile(fpath string) error {
|
|
||||||
if fpath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
contentHTML, err := fileContentToString(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
html.HeaderHTML = contentHTML
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFooterFile sets footer content from a file.
|
|
||||||
func (html *HTML) WithFooterFile(fpath string) error {
|
|
||||||
if fpath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
contentHTML, err := fileContentToString(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
html.FooterHTML = contentHTML
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileContentToString(fpath string) (string, error) {
|
|
||||||
b, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(HTML))
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
dirPath := test.HTMLTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
html := &HTML{
|
|
||||||
Context: ctx,
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
MarginTop: 1,
|
|
||||||
MarginBottom: 1,
|
|
||||||
MarginLeft: 1,
|
|
||||||
MarginRight: 1,
|
|
||||||
}
|
|
||||||
html.WithLocalURL(fmt.Sprintf("%s/%s", dirPath, "index.html"))
|
|
||||||
err := html.WithHeaderFile(fmt.Sprintf("%s/%s", dirPath, "header.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
err = html.WithFooterFile(fmt.Sprintf("%s/%s", dirPath, "footer.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err = html.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -13,101 +12,48 @@ import (
|
|||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Markdown facilitates Markdown to PDF conversion.
|
// NewMarkdown returns a Markdown printer.
|
||||||
type Markdown struct {
|
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
||||||
Context context.Context
|
tmpl, err := template.
|
||||||
TemplatePath string
|
New(filepath.Base(fpath)).
|
||||||
HeaderHTML string
|
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||||
FooterHTML string
|
ParseFiles(fpath)
|
||||||
PaperWidth float64
|
if err != nil {
|
||||||
PaperHeight float64
|
return nil, fmt.Errorf("%s: parsing template: %v", fpath, err)
|
||||||
MarginTop float64
|
}
|
||||||
MarginBottom float64
|
dirPath := filepath.Dir(fpath)
|
||||||
MarginLeft float64
|
data := &templateData{DirPath: dirPath}
|
||||||
MarginRight float64
|
var buffer bytes.Buffer
|
||||||
Landscape bool
|
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||||
WebFontsTimeout int64
|
return nil, fmt.Errorf("%s: executing template: %v", fpath, err)
|
||||||
|
}
|
||||||
html *HTML
|
baseFilename, err := rand.Get()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
||||||
|
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: writing file: %v", dst, err)
|
||||||
|
}
|
||||||
|
URL := fmt.Sprintf("file://%s", dst)
|
||||||
|
return &chrome{
|
||||||
|
url: URL,
|
||||||
|
opts: opts,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type templateData struct {
|
type templateData struct {
|
||||||
DirPath string
|
DirPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print converts markdown to PDF.
|
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
||||||
func (md *Markdown) Print(destination string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
if md.HeaderHTML != "" {
|
|
||||||
md.html.HeaderHTML = md.HeaderHTML
|
|
||||||
}
|
|
||||||
if md.FooterHTML != "" {
|
|
||||||
md.html.FooterHTML = md.FooterHTML
|
|
||||||
}
|
|
||||||
md.html.PaperWidth = md.PaperWidth
|
|
||||||
md.html.PaperHeight = md.PaperHeight
|
|
||||||
md.html.MarginTop = md.MarginTop
|
|
||||||
md.html.MarginBottom = md.MarginBottom
|
|
||||||
md.html.MarginLeft = md.MarginLeft
|
|
||||||
md.html.MarginRight = md.MarginRight
|
|
||||||
md.html.Landscape = md.Landscape
|
|
||||||
md.html.WebFontsTimeout = md.WebFontsTimeout
|
|
||||||
tmpl, err := template.
|
|
||||||
New(filepath.Base(md.TemplatePath)).
|
|
||||||
Funcs(template.FuncMap{"toHTML": toHTML}).
|
|
||||||
ParseFiles(md.TemplatePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: parsing template: %v", md.TemplatePath, err)
|
|
||||||
}
|
|
||||||
dirPath := filepath.Dir(md.TemplatePath)
|
|
||||||
data := &templateData{DirPath: dirPath}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
|
||||||
return fmt.Errorf("%s: executing template: %v", md.TemplatePath, err)
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
|
||||||
if err := writeBytesToFile(dst, buffer.Bytes()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
md.html.WithLocalURL(dst)
|
|
||||||
return md.html.Print(destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toHTML(dirPath, filename string) (template.HTML, error) {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||||
b, err := ioutil.ReadFile(fpath)
|
b, err := ioutil.ReadFile(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
||||||
}
|
}
|
||||||
unsafe := blackfriday.Run(b)
|
unsafe := blackfriday.Run(b)
|
||||||
contentHTML := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||||
return template.HTML(contentHTML), nil
|
/* #nosec */
|
||||||
|
return template.HTML(content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHeaderFile sets header content from a file.
|
|
||||||
func (md *Markdown) WithHeaderFile(fpath string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
return md.html.WithHeaderFile(fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFooterFile sets footer content from a file.
|
|
||||||
func (md *Markdown) WithFooterFile(fpath string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
return md.html.WithFooterFile(fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(Markdown))
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
dirPath := test.MarkdownTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
markdown := &Markdown{
|
|
||||||
Context: ctx,
|
|
||||||
TemplatePath: fmt.Sprintf("%s/%s", dirPath, "index.html"),
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
MarginTop: 1,
|
|
||||||
MarginBottom: 1,
|
|
||||||
MarginLeft: 1,
|
|
||||||
MarginRight: 1,
|
|
||||||
}
|
|
||||||
err := markdown.WithHeaderFile(fmt.Sprintf("%s/%s", dirPath, "header.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
err = markdown.WithFooterFile(fmt.Sprintf("%s/%s", dirPath, "footer.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err = markdown.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
50
internal/pkg/printer/merge.go
Normal file
50
internal/pkg/printer/merge.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type merge struct {
|
||||||
|
ctx context.Context
|
||||||
|
fpaths []string
|
||||||
|
opts *MergeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeOptions helps customizing the
|
||||||
|
// merge printer behaviour.
|
||||||
|
type MergeOptions struct {
|
||||||
|
WaitTimeout float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMerge returns a merge printer.
|
||||||
|
func NewMerge(fpaths []string, opts *MergeOptions) Printer {
|
||||||
|
return &merge{
|
||||||
|
fpaths: fpaths,
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *merge) Print(destination string) error {
|
||||||
|
if p.ctx == nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
p.ctx = ctx
|
||||||
|
}
|
||||||
|
var cmdArgs []string
|
||||||
|
cmdArgs = append(cmdArgs, p.fpaths...)
|
||||||
|
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
||||||
|
cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...)
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pdtk: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = Printer(new(merge))
|
||||||
|
)
|
||||||
@@ -2,66 +2,89 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mu sync.Mutex
|
type office struct {
|
||||||
|
fpaths []string
|
||||||
// Office facilitates Office documents to PDF conversion.
|
opts *OfficeOptions
|
||||||
type Office struct {
|
|
||||||
Context context.Context
|
|
||||||
FilePaths []string
|
|
||||||
Landscape bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print converts Office documents to PDF.
|
// OfficeOptions helps customizing the
|
||||||
func (o *Office) Print(destination string) error {
|
// Office printer behaviour.
|
||||||
mu.Lock()
|
type OfficeOptions struct {
|
||||||
defer mu.Unlock()
|
WaitTimeout float64
|
||||||
fpaths := make([]string, len(o.FilePaths))
|
Landscape bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOffice returns an Office printer.
|
||||||
|
func NewOffice(fpaths []string, opts *OfficeOptions) Printer {
|
||||||
|
return &office{
|
||||||
|
fpaths: fpaths,
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *office) Print(destination string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
fpaths := make([]string, len(p.fpaths))
|
||||||
dirPath := filepath.Dir(destination)
|
dirPath := filepath.Dir(destination)
|
||||||
for i, fpath := range o.FilePaths {
|
for i, fpath := range p.fpaths {
|
||||||
baseFilename, err := rand.Get()
|
baseFilename, err := rand.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tmpDest := fmt.Sprintf("%s/%s.pdf", dirPath, baseFilename)
|
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
||||||
cmdArgs := []string{
|
if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
||||||
"--format",
|
return err
|
||||||
"pdf",
|
|
||||||
}
|
|
||||||
if o.Landscape {
|
|
||||||
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "--output", tmpDest, fpath)
|
|
||||||
cmd := exec.CommandContext(
|
|
||||||
o.Context,
|
|
||||||
"unoconv",
|
|
||||||
cmdArgs...,
|
|
||||||
)
|
|
||||||
_, err = cmd.Output()
|
|
||||||
if o.Context.Err() == context.DeadlineExceeded {
|
|
||||||
return errors.New("unoconv: command timed out")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unoconv: non-zero exit code: %v", err)
|
|
||||||
}
|
}
|
||||||
fpaths[i] = tmpDest
|
fpaths[i] = tmpDest
|
||||||
}
|
}
|
||||||
if len(fpaths) == 1 {
|
if len(fpaths) == 1 {
|
||||||
return os.Rename(fpaths[0], destination)
|
return os.Rename(fpaths[0], destination)
|
||||||
}
|
}
|
||||||
return Merge(fpaths, destination)
|
m := &merge{
|
||||||
|
ctx: ctx,
|
||||||
|
fpaths: fpaths,
|
||||||
|
}
|
||||||
|
return m.Print(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: gochecknoglobals
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
cmdArgs := []string{
|
||||||
|
"--format",
|
||||||
|
"pdf",
|
||||||
|
}
|
||||||
|
if opts.Landscape {
|
||||||
|
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "--output", destination, fpath)
|
||||||
|
cmd := exec.CommandContext(
|
||||||
|
ctx,
|
||||||
|
"unoconv",
|
||||||
|
cmdArgs...,
|
||||||
|
)
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unoconv: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(Office))
|
_ = Printer(new(office))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
dirPath := test.OfficeTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
office := &Office{
|
|
||||||
Context: ctx,
|
|
||||||
FilePaths: []string{
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "document.docx"),
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "document.txt"),
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "document.rtf"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err := office.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,46 +1,7 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
pdfcpuAPI "github.com/hhrutter/pdfcpu/pkg/api"
|
|
||||||
pdfcpuLog "github.com/hhrutter/pdfcpu/pkg/log"
|
|
||||||
pdfcpuConfig "github.com/hhrutter/pdfcpu/pkg/pdfcpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// disable loggers when merging
|
|
||||||
// PDFs.
|
|
||||||
pdfcpuLog.DisableLoggers()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printer is a type that can create a PDF file from a source.
|
// Printer is a type that can create a PDF file from a source.
|
||||||
// The source is defined in the underlying implementation.
|
// The source is defined in the underlying implementation.
|
||||||
type Printer interface {
|
type Printer interface {
|
||||||
Print(destination string) error
|
Print(destination string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge merges PDF files.
|
|
||||||
func Merge(fpaths []string, destination string) error {
|
|
||||||
cmdcpu := pdfcpuAPI.MergeCommand(fpaths, destination, pdfcpuConfig.NewDefaultConfiguration())
|
|
||||||
_, err := pdfcpuAPI.Merge(cmdcpu)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// if pdfcpu failed to merge PDF files...
|
|
||||||
// https://github.com/thecodingmachine/gotenberg/issues/29
|
|
||||||
var cmdArgs []string
|
|
||||||
cmdArgs = append(cmdArgs, fpaths...)
|
|
||||||
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
|
||||||
cmd := exec.Command("pdftk", cmdArgs...)
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeBytesToFile(dst string, b []byte) error {
|
|
||||||
if err := ioutil.WriteFile(dst, b, 0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", dst, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
dirPath := test.PDFTestDirPath(t)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err := Merge(
|
|
||||||
[]string{
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "gotenberg.pdf"),
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "gotenberg_bis.pdf"),
|
|
||||||
},
|
|
||||||
dst,
|
|
||||||
)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
9
internal/pkg/printer/url.go
Normal file
9
internal/pkg/printer/url.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
// NewURL returns a URL printer.
|
||||||
|
func NewURL(url string, opts *ChromeOptions) Printer {
|
||||||
|
return &chrome{
|
||||||
|
url: url,
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
113
test/testfunc.go
113
test/testfunc.go
@@ -6,64 +6,88 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AssertStatusCode checks if the given request
|
||||||
|
// returns the expected status code.
|
||||||
|
func AssertStatusCode(t *testing.T, expectedStatusCode int, srv http.Handler, req *http.Request) {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
assert.Equal(t, expectedStatusCode, rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertConcurrent runs all functions simultaneously
|
||||||
|
// and wait until execution has completed
|
||||||
|
// or an error is encountered.
|
||||||
|
func AssertConcurrent(t *testing.T, fn func() error, amount int) {
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
for i := 0; i < amount; i++ {
|
||||||
|
eg.Go(fn)
|
||||||
|
}
|
||||||
|
err := eg.Wait()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
// HTMLTestMultipartForm returns the body
|
// HTMLTestMultipartForm returns the body
|
||||||
// for a multipate/form-data request with all
|
// for a multipate/form-data request with all
|
||||||
// files under "html" folder.
|
// files under "html" folder.
|
||||||
func HTMLTestMultipartForm(t *testing.T) (*bytes.Buffer, string) {
|
func HTMLTestMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) {
|
||||||
return multipartForm(t, "html")
|
return multipartForm(t, "html", formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLTestMultipartForm returns the body
|
// URLTestMultipartForm returns the body
|
||||||
// for a multipate/form-data request with all
|
// for a multipate/form-data request with all
|
||||||
// files under "url" folder.
|
// files under "url" folder.
|
||||||
func URLTestMultipartForm(t *testing.T) (*bytes.Buffer, string) {
|
func URLTestMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) {
|
||||||
return multipartForm(t, "url")
|
return multipartForm(t, "url", formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkdownTestMultipartForm returns the body
|
// MarkdownTestMultipartForm returns the body
|
||||||
// for a multipate/form-data request with all
|
// for a multipate/form-data request with all
|
||||||
// files under "markdown" folder.
|
// files under "markdown" folder.
|
||||||
func MarkdownTestMultipartForm(t *testing.T) (*bytes.Buffer, string) {
|
func MarkdownTestMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) {
|
||||||
return multipartForm(t, "markdown")
|
return multipartForm(t, "markdown", formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OfficeTestMultipartForm returns the body
|
// OfficeTestMultipartForm returns the body
|
||||||
// for a multipate/form-data request with all
|
// for a multipate/form-data request with all
|
||||||
// files under "office" folder.
|
// files under "office" folder.
|
||||||
func OfficeTestMultipartForm(t *testing.T) (*bytes.Buffer, string) {
|
func OfficeTestMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) {
|
||||||
return multipartForm(t, "office")
|
return multipartForm(t, "office", formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDFTestMultipartForm returns the body
|
// PDFTestMultipartForm returns the body
|
||||||
// for a multipate/form-data request with all
|
// for a multipate/form-data request with all
|
||||||
// files under "pdf" folder.
|
// files under "pdf" folder.
|
||||||
func PDFTestMultipartForm(t *testing.T) (*bytes.Buffer, string) {
|
func PDFTestMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) {
|
||||||
return multipartForm(t, "pdf")
|
return multipartForm(t, "pdf", formValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
func multipartForm(t *testing.T, kind string) (*bytes.Buffer, string) {
|
func multipartForm(t *testing.T, kind string, formValues map[string]string) (*bytes.Buffer, string) {
|
||||||
body := &bytes.Buffer{}
|
body := &bytes.Buffer{}
|
||||||
writer := multipart.NewWriter(body)
|
writer := multipart.NewWriter(body)
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
dirPath := abs(t, kind, "")
|
dirPath := abs(t, kind, "")
|
||||||
fpaths := make(map[string]string)
|
fpaths := make(map[string]string)
|
||||||
filepath.Walk(dirPath, func(path string, info os.FileInfo, _ error) error {
|
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, _ error) error {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fpaths[info.Name()] = abs(t, kind, info.Name())
|
fpaths[info.Name()] = abs(t, kind, info.Name())
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
for filename, fpath := range fpaths {
|
for filename, fpath := range fpaths {
|
||||||
file, err := os.Open(fpath)
|
file, err := os.Open(fpath)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@@ -76,68 +100,13 @@ func multipartForm(t *testing.T, kind string) (*bytes.Buffer, string) {
|
|||||||
err := writer.WriteField("remoteURL", "http://google.com")
|
err := writer.WriteField("remoteURL", "http://google.com")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
for k, v := range formValues {
|
||||||
|
err := writer.WriteField(k, v)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
return body, writer.FormDataContentType()
|
return body, writer.FormDataContentType()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTMLTestDirPath creates a copy
|
|
||||||
// of "html" folder in test/testdata.
|
|
||||||
func HTMLTestDirPath(t *testing.T) string {
|
|
||||||
return copyDir(t, "html")
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLTestDirPath creates a copy
|
|
||||||
// of "url" folder in test/testdata.
|
|
||||||
func URLTestDirPath(t *testing.T) string {
|
|
||||||
return copyDir(t, "url")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkdownTestDirPath creates a copy
|
|
||||||
// of "markdown" folder in test/testdata.
|
|
||||||
func MarkdownTestDirPath(t *testing.T) string {
|
|
||||||
return copyDir(t, "markdown")
|
|
||||||
}
|
|
||||||
|
|
||||||
// OfficeTestDirPath creates a copy
|
|
||||||
// of "office" folder in test/testdata.
|
|
||||||
func OfficeTestDirPath(t *testing.T) string {
|
|
||||||
return copyDir(t, "office")
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDFTestDirPath creates a copy
|
|
||||||
// of "pdf" folder in test/testdata.
|
|
||||||
func PDFTestDirPath(t *testing.T) string {
|
|
||||||
return copyDir(t, "pdf")
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyDir(t *testing.T, kind string) string {
|
|
||||||
tmpDirPath, err := rand.Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
err = os.MkdirAll(tmpDirPath, 0755)
|
|
||||||
require.Nil(t, err)
|
|
||||||
dirPath := abs(t, kind, "")
|
|
||||||
filepath.Walk(dirPath, func(path string, info os.FileInfo, _ error) error {
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fpath := abs(t, kind, info.Name())
|
|
||||||
in, err := os.Open(fpath)
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer in.Close()
|
|
||||||
tmpFpath := fmt.Sprintf("%s/%s", tmpDirPath, info.Name())
|
|
||||||
out, err := os.Create(tmpFpath)
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer out.Close()
|
|
||||||
err = out.Chmod(0644)
|
|
||||||
require.Nil(t, err)
|
|
||||||
_, err = io.Copy(out, in)
|
|
||||||
require.Nil(t, err)
|
|
||||||
_, err = out.Seek(0, 0)
|
|
||||||
require.Nil(t, err)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return tmpDirPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func abs(t *testing.T, kind, filename string) string {
|
func abs(t *testing.T, kind, filename string) string {
|
||||||
_, gofilename, _, ok := runtime.Caller(0)
|
_, gofilename, _, ok := runtime.Caller(0)
|
||||||
require.Equal(t, ok, true, "got no caller information")
|
require.Equal(t, ok, true, "got no caller information")
|
||||||
|
|||||||
Reference in New Issue
Block a user