mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca81b382d3 | ||
|
|
a60d7976f5 | ||
|
|
46cf63da37 | ||
|
|
099079b962 | ||
|
|
5b8121516f | ||
|
|
23eacaa78a | ||
|
|
659292a3f1 | ||
|
|
f88756d3a8 | ||
|
|
81e7bab22a | ||
|
|
1eeb5fc2b0 | ||
|
|
126cdd73e4 | ||
|
|
2251267ae4 | ||
|
|
f3e4b6d9a2 | ||
|
|
b22379537b | ||
|
|
feb5bb0653 | ||
|
|
ebfe99be79 | ||
|
|
49c1b939d5 | ||
|
|
84a37f124b | ||
|
|
f0f48f4ddf | ||
|
|
cf15e3a9a1 | ||
|
|
927f98c66b | ||
|
|
782f6ac27e | ||
|
|
5140e4ec9a | ||
|
|
e255f9bfc5 | ||
|
|
765dc734a4 | ||
|
|
3abe8a1103 | ||
|
|
73333074fd | ||
|
|
a415786e1c | ||
|
|
a3a1077dea | ||
|
|
58a812c2c9 | ||
|
|
7e69ec4367 | ||
|
|
4623c01896 | ||
|
|
e85322de62 | ||
|
|
090ff3da13 | ||
|
|
fe068b1bfb | ||
|
|
d3b0a685b9 | ||
|
|
245c04fc5a | ||
|
|
94acb0c6ae | ||
|
|
ad4a16cc4e | ||
|
|
dae39113f6 | ||
|
|
1e1cb8fe57 | ||
|
|
606254d941 | ||
|
|
8fd51b9e9e | ||
|
|
3255f8af0c | ||
|
|
ca0784fa59 | ||
|
|
827060b6ce | ||
|
|
60c069cb1f | ||
|
|
70dfea5876 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -40,6 +40,6 @@ Fixes #
|
||||
- [ ] Have you lint your code locally prior to submission (`make lint`)?
|
||||
- [ ] Have you written new tests for your core changes, as applicable?
|
||||
- [ ] Have you successfully ran tests with your changes locally (`make tests`)?
|
||||
- [ ] Have you updated the documentation (`make doc`)?
|
||||
- [ ] Have you updated the documentation (Markdown files under `build > docs > content` and then `make doc`)?
|
||||
- [ ] I have squashed any insignificant commits
|
||||
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code
|
||||
|
||||
27
Makefile
27
Makefile
@@ -2,8 +2,10 @@ GOLANG_VERSION=1.13
|
||||
VERSION=snapshot
|
||||
DOCKER_USER=
|
||||
DOCKER_PASSWORD=
|
||||
DOCKER_REPOSITORY=thecodingmachine
|
||||
GOLANGCI_LINT_VERSION=1.19.1
|
||||
DOCKER_REGISTRY=thecodingmachine
|
||||
GOTENBERG_USER_GID=1001
|
||||
GOTENBERG_USER_UID=1001
|
||||
GOLANGCI_LINT_VERSION=1.20.1
|
||||
CODE_COVERAGE=0
|
||||
TINI_VERSION=0.18.0
|
||||
MAXIMUM_WAIT_TIMEOUT=30.0
|
||||
@@ -15,16 +17,17 @@ DEFAULT_LISTEN_PORT=3000
|
||||
DISABLE_GOOGLE_CHROME=0
|
||||
DISABLE_UNOCONV=0
|
||||
LOG_LEVEL=INFO
|
||||
ROOT_PATH=/
|
||||
DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576
|
||||
|
||||
# build the base Docker image.
|
||||
base:
|
||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build --build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) --build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) -t $(DOCKER_REGISTRY)/gotenberg:base -f build/base/Dockerfile .
|
||||
|
||||
# build the workspace Docker image.
|
||||
workspace:
|
||||
make base
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:workspace -f build/workspace/Dockerfile .
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:workspace -f build/workspace/Dockerfile .
|
||||
|
||||
# gofmt and goimports all go files.
|
||||
fmt:
|
||||
@@ -34,30 +37,30 @@ fmt:
|
||||
# run all linters.
|
||||
lint:
|
||||
make workspace
|
||||
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm $(DOCKER_REPOSITORY)/gotenberg:lint
|
||||
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm $(DOCKER_REGISTRY)/gotenberg:lint
|
||||
|
||||
# run all tests.
|
||||
tests:
|
||||
make workspace
|
||||
./scripts/tests.sh $(DOCKER_REPOSITORY) $(CODE_COVERAGE)
|
||||
./scripts/tests.sh $(DOCKER_REGISTRY) $(CODE_COVERAGE)
|
||||
|
||||
# generate documentation.
|
||||
doc:
|
||||
make workspace
|
||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REPOSITORY)/gotenberg:docs
|
||||
docker build -t $(DOCKER_REGISTRY)/gotenberg:docs -f build/docs/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REGISTRY)/gotenberg:docs
|
||||
|
||||
# build Gotenberg Docker image.
|
||||
image:
|
||||
make workspace
|
||||
docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
|
||||
# start the API using previously built Docker image.
|
||||
gotenberg:
|
||||
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e ROOT_PATH=$(ROOT_PATH) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||
|
||||
# publish Gotenberg images according to version.
|
||||
publish:
|
||||
make workspace
|
||||
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REPOSITORY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
||||
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
|
||||
<img src="https://user-images.githubusercontent.com/8983173/69229423-ac731300-0b85-11ea-8c2e-2cc00ecdb269.PNG" alt="Gotenberg logo" width="250" height="250" />
|
||||
</p>
|
||||
<h3 align="center">Gotenberg</h3>
|
||||
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
|
||||
@@ -39,4 +39,6 @@ to learn how to interact with it!
|
||||
|
||||
---
|
||||
|
||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
|
||||
</p>
|
||||
|
||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
BIN
assets/logo2.png
Normal file
BIN
assets/logo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -104,7 +104,10 @@ COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||
# | non-root user.
|
||||
# |
|
||||
|
||||
RUN groupadd --gid 1001 gotenberg \
|
||||
&& useradd --uid 1001 --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \
|
||||
ARG GOTENBERG_USER_GID=1001
|
||||
ARG GOTENBERG_USER_UID=1001
|
||||
|
||||
RUN groupadd --gid ${GOTENBERG_USER_GID} gotenberg \
|
||||
&& useradd --uid ${GOTENBERG_USER_UID} --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \
|
||||
&& mkdir /gotenberg \
|
||||
&& chown gotenberg: /gotenberg
|
||||
@@ -4,8 +4,6 @@ title: Install
|
||||
|
||||
Gotenberg is shipped within a Docker image.
|
||||
|
||||
> It uses a dedicated non-root user called `gotenberg` with uid and gid `1001`.
|
||||
|
||||
You may start it with:
|
||||
|
||||
```bash
|
||||
@@ -14,6 +12,23 @@ $ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
|
||||
|
||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
The image uses a dedicated non-root user called `gotenberg` with uid and gid `1001`.
|
||||
|
||||
If you wish to change those uid and gid, you will have to:
|
||||
|
||||
* clone the project
|
||||
* re-build the image
|
||||
* publish the image in your own Docker registry
|
||||
|
||||
For instance:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/thecodingmachine/gotenberg.git
|
||||
$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=6.1.0
|
||||
```
|
||||
|
||||
> `master` branch is always up-to-date with the latest version of the API.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
You may also add it in your Docker Compose stack:
|
||||
@@ -39,7 +54,7 @@ Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0
|
||||
|
||||
> The more resources are granted, the quicker will be the conversions.
|
||||
|
||||
In the deployment specification of the pod, also specify the uid `1001` of the user `gotenberg`:
|
||||
In the deployment specification of the pod, also specify the uid of the user `gotenberg`:
|
||||
|
||||
```
|
||||
securityContext:
|
||||
@@ -47,5 +62,9 @@ securityContext:
|
||||
runAsUser: 1001
|
||||
```
|
||||
|
||||
## Cloud Run (Google Cloud)
|
||||
|
||||
If you're looking for cost savings, you might be interested by [Cloud Run](https://cloud.google.com/run).
|
||||
|
||||
In the following examples, we will assume your
|
||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
@@ -7,9 +7,11 @@ 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/v6
|
||||
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7
|
||||
```
|
||||
|
||||
See also the example from the [README](https://github.com/thecodingmachine/gotenberg-go-client/blob/master/README.md).
|
||||
|
||||
## PHP client
|
||||
|
||||
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
||||
@@ -18,8 +20,15 @@ Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-a
|
||||
$ composer require php-http/guzzle6-adapter
|
||||
```
|
||||
|
||||
Then the PHP client:
|
||||
Then the [PHP client](https://github.com/thecodingmachine/gotenberg-php-client):
|
||||
|
||||
```bash
|
||||
$ composer require thecodingmachine/gotenberg-php-client
|
||||
```
|
||||
|
||||
See also the example from the [README](https://github.com/thecodingmachine/gotenberg-php-client/blob/master/README.md).
|
||||
|
||||
## Community clients
|
||||
|
||||
* [JavaScript/TypeScript client](https://github.com/yumauri/gotenberg-js-client) by [yumauri](https://github.com/yumauri)
|
||||
* [C# client](https://github.com/ChangemakerStudios/GotenbergSharpApiClient) by [ChangemakerStudios](https://github.com/ChangemakerStudios)
|
||||
|
||||
@@ -23,6 +23,18 @@ You may customize this value with the environment variable `DEFAULT_LISTEN_PORT`
|
||||
|
||||
This environment variable accepts any string that can be turned into a port number.
|
||||
|
||||
## Root path
|
||||
|
||||
By default, the API root path is `/`.
|
||||
|
||||
You may customize this value with the environment variable `ROOT_PATH`.
|
||||
|
||||
This environment variable accepts a string starting and ending with `/`.
|
||||
|
||||
For instance, `/gotenberg/` is a valid value while `gotenberg` is not.
|
||||
|
||||
> This is useful if you wish to do service discovery via URL paths.
|
||||
|
||||
## Disable Google Chrome
|
||||
|
||||
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`
|
||||
|
||||
@@ -39,14 +39,13 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -57,10 +56,10 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Header and footer
|
||||
@@ -103,6 +102,7 @@ There are some limitations:
|
||||
* `footer.html` CSS properties override the ones from `header.html`
|
||||
* only fonts installed in the Docker image are loaded (see the [fonts section](#fonts))
|
||||
* images only work using a `base64` encoded source (`<img src="data:image/png;base64, iVBORw0K... />`)
|
||||
* `background-color` and `color` CSS properties require an additional `-webkit-print-color-adjust: exact` CSS property in order to work
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -119,16 +119,17 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.Header("header.html")
|
||||
req.Footer("footer.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
header, _ := gotenberg.NewDocumentFromPath("header.html", "/path/to/file")
|
||||
footer, _ := gotenberg.NewDocumentFromPath("footer.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.Header(header)
|
||||
req.Footer(footer)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -139,14 +140,14 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$header = DocumentFactory::makeFromPath('header.html', 'header.html');
|
||||
$footer = DocumentFactory::makeFromPath('footer.html', 'footer.html');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$header = DocumentFactory::makeFromPath('header.html', '/path/to/file');
|
||||
$footer = DocumentFactory::makeFromPath('footer.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setHeader($header);
|
||||
$request->setFooter($footer);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Assets
|
||||
@@ -209,15 +210,17 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.Assets("font.woff", "img.gif", "style.css")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file")
|
||||
img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file")
|
||||
font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.Assets(style, img, font)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -228,15 +231,15 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$assets = [
|
||||
DocumentFactory::makeFromPath('style.css', 'style.css'),
|
||||
DocumentFactory::makeFromPath('img.png', 'img.png'),
|
||||
DocumentFactory::makeFromPath('font.woff', 'font.woff'),
|
||||
DocumentFactory::makeFromPath('style.css', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('img.png', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('font.woff', '/path/to/file'),
|
||||
];
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setAssets($assets);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -268,17 +271,16 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.PaperSize(gotenberg.A4)
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.PaperSize(gotenberg.A4)
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -290,12 +292,59 @@ 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');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$request->setLandscape(true);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Page ranges
|
||||
|
||||
You may specify the page ranges to convert.
|
||||
|
||||
The format is the same as the one from the print options
|
||||
of Google Chrome, e.g. `1-5,8,11-13`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form pageRanges='1-3,5' \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.PageRanges("1-3,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', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setPageRanges('1-3,5');
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -321,15 +370,14 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
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)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WaitDelay(5.5)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -341,10 +389,10 @@ 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');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitDelay(5.5);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -373,15 +421,14 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.GoogleChromeRpccBufferSize(1048576)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.GoogleChromeRpccBufferSize(1048576)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -393,9 +440,9 @@ 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');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setGoogleChromeRpccBufferSize(1048576);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -31,15 +31,13 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := gotenberg.NewURLRequest("https://google.com")
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := gotenberg.NewURLRequest("https://google.com")
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -51,6 +49,55 @@ use TheCodingMachine\Gotenberg\URLRequest;
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$request = new URLRequest('https://google.com');
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Custom HTTP headers
|
||||
|
||||
You may send your own HTTP headers to the `remoteURL`.
|
||||
|
||||
For instance, by adding the HTTP header `Gotenberg-Remoteurl-Your-Header` to your request,
|
||||
the API will send a request to the `remoteURL` with the HTTP header `Your-Header`.
|
||||
|
||||
> **Attention:** the API uses a canonical format for the HTTP headers:
|
||||
> it transforms the first
|
||||
> letter and any letter following a hyphen to upper case;
|
||||
> the rest are converted to lowercase. For example, the
|
||||
> canonical key for `accept-encoding` is `Accept-Encoding`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/url \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--header 'Gotenberg-Remoteurl-Your-Header: Foo' \
|
||||
--form remoteURL=https://google.com \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := gotenberg.NewURLRequest("https://google.com")
|
||||
req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\URLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$request = new URLRequest('https://google.com');
|
||||
$request->addRemoteURLHTTPHeader('Your-Header', 'Foo')
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -42,14 +42,14 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file")
|
||||
req := gotenberg.NewMarkdownRequest(index, markdown)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -60,11 +60,11 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\MarkdownRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$markdowns = [
|
||||
DocumentFactory::makeFromPath('file.md', 'file.md'),
|
||||
DocumentFactory::makeFromPath('file.md', '/path/to/file'),
|
||||
];
|
||||
$request = new MarkdownRequest($index, $markdowns);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -41,14 +41,14 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
|
||||
doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file")
|
||||
req := gotenberg.NewOfficeRequest(doc, doc2)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -60,11 +60,11 @@ use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
|
||||
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('document2.docx', '/path/to/file'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -88,15 +88,14 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest("document.docx")
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
|
||||
req := gotenberg.NewOfficeRequest(doc)
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -108,10 +107,61 @@ use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$request->setLandscape(true);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Page ranges
|
||||
|
||||
You may specify the page ranges to convert.
|
||||
|
||||
The format is the same as the one from the print options
|
||||
of LibreOffice, e.g. `1-1` or `1-4`.
|
||||
|
||||
> **Attention:** if more than one document, the page ranges will be
|
||||
> applied for each document.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form pageRanges='1-3' \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
|
||||
req := gotenberg.NewOfficeRequest(doc)
|
||||
req.PageRanges("1-3")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$request->setPageRanges('1-3');
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -27,14 +27,14 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
pdf, _ := gotenberg.NewDocumentFromPath("file.pdf", "/path/to/file")
|
||||
pdf2, _ := gotenberg.NewDocumentFromPath("file2.pdf", "/path/to/file")
|
||||
req := gotenberg.NewMergeRequest(pdf, pdf2)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -46,10 +46,10 @@ use TheCodingMachine\Gotenberg\MergeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('file.pdf', 'file.pdf'),
|
||||
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
|
||||
DocumentFactory::makeFromPath('file.pdf', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('file2.pdf', '/path/to/file'),
|
||||
];
|
||||
$request = new MergeRequest($files);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -26,14 +26,13 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WaitTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WaitTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -45,9 +44,9 @@ 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');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitTimeout(2.5);
|
||||
$dest = "result.pdf";
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
@@ -24,14 +24,13 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -42,7 +41,7 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$resp = $client->post($request);
|
||||
@@ -58,9 +57,7 @@ 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_webhook_url_timeout) section.
|
||||
|
||||
### Examples
|
||||
|
||||
#### cURL
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
@@ -71,21 +68,20 @@ $ curl --request POST \
|
||||
--form webhookURLTimeout=2.5
|
||||
```
|
||||
|
||||
#### Go
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
req.WebhookURLTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
req.WebhookURLTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
#### PHP
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
@@ -93,9 +89,61 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$request->setWebhookURLTimeout(2.5);
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
|
||||
## Custom HTTP headers
|
||||
|
||||
You may send your own HTTP headers to the `webhookURL`.
|
||||
|
||||
For instance, by adding the HTTP header `Gotenberg-Webhookurl-Your-Header` to your request,
|
||||
the API will send a request to the `webhookURL` with the HTTP header `Your-Header`.
|
||||
|
||||
> **Attention:** the API uses a canonical format for the HTTP headers:
|
||||
> it transforms the first
|
||||
> letter and any letter following a hyphen to upper case;
|
||||
> the rest are converted to lowercase. For example, the
|
||||
> canonical key for `accept-encoding` is `Accept-Encoding`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--header 'Gotenberg-Webhookurl-Your-Header: Foo' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$request->addWebhookURLHTTPHeader('Your-Header', 'Foo');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
@@ -24,14 +24,13 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.ResultFilename("foo.pdf")
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.ResultFilename("foo.pdf")
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -43,7 +42,7 @@ 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');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setResultFilename('foo.pdf');
|
||||
$resp = $client->post($request);
|
||||
|
||||
@@ -5,10 +5,10 @@ title: Scalability
|
||||
The API uses under the hood intricate programs.
|
||||
|
||||
Gotenberg tries to abstract as much complexity as possible but it can
|
||||
only do it to a certain extend.
|
||||
only do it to a certain extent.
|
||||
|
||||
For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk
|
||||
instances are there are requests. The limitation here is the available memory and CPU usage.
|
||||
instances as there are requests. The limitation here is the available memory and CPU usage.
|
||||
|
||||
On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel.
|
||||
Indeed, Google Chrome misbehaves if there are too many concurrent conversions.
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
title: Links
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
|
||||
</p>
|
||||
|
||||
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
|
||||
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
|
||||
* Thanks to [@mafredri](https://github.com/mafredri) for its help and its wonderful [cdp](https://github.com/mafredri/cdp) library
|
||||
|
||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
||||
|
||||
@@ -33,4 +33,4 @@ RUN go mod download &&\
|
||||
# Copy our code source.
|
||||
COPY --chown=gotenberg:gotenberg . .
|
||||
|
||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen" ]
|
||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit" ]
|
||||
@@ -2,7 +2,7 @@
|
||||
# | Binary
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Buils Gotenberg binary.
|
||||
# | Builds Gotenberg binary.
|
||||
# |
|
||||
|
||||
FROM thecodingmachine/gotenberg:workspace AS workspace
|
||||
@@ -42,7 +42,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
ARG TINI_VERSION
|
||||
|
||||
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
|
||||
ADD --chown=gotenberg https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static /tini
|
||||
RUN chmod +x /tini
|
||||
ENTRYPOINT [ "/tini", "--" ]
|
||||
|
||||
@@ -59,4 +59,4 @@ USER gotenberg
|
||||
WORKDIR /gotenberg
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "gotenberg" ]
|
||||
CMD [ "gotenberg" ]
|
||||
|
||||
648
docs/index.html
648
docs/index.html
File diff suppressed because it is too large
Load Diff
5
docs/theme/gotenberg/views/index.html
vendored
5
docs/theme/gotenberg/views/index.html
vendored
@@ -34,7 +34,7 @@
|
||||
<div class="Container">
|
||||
<div class="Header">
|
||||
<div class="Title center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
|
||||
<img src="https://user-images.githubusercontent.com/8983173/69229423-ac731300-0b85-11ea-8c2e-2cc00ecdb269.PNG" alt="Gotenberg logo" width="250" height="250" />
|
||||
<span class="text">{{.Title}}</span>
|
||||
<span class="subtext">{{.Subtitle}}</span>
|
||||
</div>
|
||||
@@ -58,7 +58,8 @@
|
||||
{{.Content}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="Footer"></div>
|
||||
<div class="Footer">
|
||||
</div>
|
||||
<script src="theme/gotenberg/js/index.js"></script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,31 +15,45 @@ import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
)
|
||||
|
||||
const (
|
||||
pingEndpoint string = "/ping"
|
||||
mergeEndpoint string = "/merge"
|
||||
convertGroupEndpoint string = "/convert"
|
||||
htmlEndpoint string = "/html"
|
||||
urlEndpoint string = "/url"
|
||||
markdownEndpoint string = "/markdown"
|
||||
officeEndpoint string = "/office"
|
||||
)
|
||||
func pingEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "ping")
|
||||
}
|
||||
|
||||
func mergeEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "merge")
|
||||
}
|
||||
|
||||
func htmlEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/html")
|
||||
}
|
||||
|
||||
func urlEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/url")
|
||||
}
|
||||
|
||||
func markdownEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/markdown")
|
||||
}
|
||||
|
||||
func officeEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/office")
|
||||
}
|
||||
|
||||
func isMultipartFormDataEndpoint(config conf.Config, path string) bool {
|
||||
var multipartFormDataEndpoints []string
|
||||
multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint)
|
||||
multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint(config))
|
||||
if !config.DisableGoogleChrome() {
|
||||
multipartFormDataEndpoints = append(
|
||||
multipartFormDataEndpoints,
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint),
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint),
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint),
|
||||
htmlEndpoint(config),
|
||||
urlEndpoint(config),
|
||||
markdownEndpoint(config),
|
||||
)
|
||||
}
|
||||
if !config.DisableUnoconv() {
|
||||
multipartFormDataEndpoints = append(
|
||||
multipartFormDataEndpoints,
|
||||
fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint),
|
||||
officeEndpoint(config),
|
||||
)
|
||||
}
|
||||
for _, endpoint := range multipartFormDataEndpoints {
|
||||
@@ -124,6 +138,7 @@ func urlHandler(c echo.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.CustomHTTPHeaders = resource.RemoteURLCustomHTTPHeaders(r)
|
||||
if !r.HasArg(resource.RemoteURLArgKey) {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
@@ -308,20 +323,50 @@ func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string
|
||||
defer f.Close() // nolint: errcheck
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"sending result file '%s' to '%s'",
|
||||
"preparing to send result file '%s' to '%s'...",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
httpClient := &http.Client{
|
||||
Timeout: xtime.Duration(webhookURLTimeout),
|
||||
}
|
||||
resp, err := httpClient.Post(webhookURL, "application/pdf", f) /* #nosec */
|
||||
req, err := http.NewRequest(http.MethodPost, webhookURL, f)
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
req.Header.Set(echo.HeaderContentType, "application/pdf")
|
||||
// set custom headers (if any).
|
||||
customHTTPHeaders := resource.WebhookURLCustomHTTPHeaders(r)
|
||||
if len(customHTTPHeaders) > 0 {
|
||||
for key, value := range customHTTPHeaders {
|
||||
req.Header.Set(key, value)
|
||||
logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key)
|
||||
}
|
||||
} else {
|
||||
logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...")
|
||||
}
|
||||
// send the result file.
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"sending result file '%s' to '%s'...",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
resp, err := httpClient.Do(req) /* #nosec */
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"result file '%s' sent to '%s'",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,49 +19,51 @@ func TestPingHandler(t *testing.T) {
|
||||
// should return 200.
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
endpoint := pingEndpoint(config)
|
||||
req := httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodPost, pingEndpoint, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
}
|
||||
|
||||
func TestMergeHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := mergeEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, mergeEndpoint, nil)
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
}
|
||||
@@ -69,7 +71,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
func TestHTMLHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint)
|
||||
endpoint := htmlEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.HTMLMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
@@ -224,7 +226,7 @@ func TestHTMLHandler(t *testing.T) {
|
||||
func TestURLHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint)
|
||||
endpoint := urlEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.URLMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
@@ -379,7 +381,7 @@ func TestURLHandler(t *testing.T) {
|
||||
func TestMarkdownHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint)
|
||||
endpoint := markdownEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.MarkdownMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
@@ -534,7 +536,7 @@ func TestMarkdownHandler(t *testing.T) {
|
||||
func TestOfficeHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint)
|
||||
endpoint := officeEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.OfficeMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
@@ -579,11 +581,18 @@ func TestOfficeHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
customHeaderRealKey := http.CanonicalHeaderKey("MyCustomHeader")
|
||||
customHeaderKey := fmt.Sprintf("%s%s", resource.WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderRealKey)
|
||||
customHeaderValue := "foo"
|
||||
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")
|
||||
if c.Request().Header.Get(echo.HeaderContentType) != "application/pdf" {
|
||||
status <- fmt.Errorf("wrong Content-type: got '%s' want '%s'", c.Request().Header.Get(echo.HeaderContentType), "application/pdf")
|
||||
return nil
|
||||
}
|
||||
if c.Request().Header.Get(customHeaderRealKey) != customHeaderValue {
|
||||
status <- fmt.Errorf("wrong '%s': got '%s' want '%s'", customHeaderRealKey, c.Request().Header.Get(customHeaderRealKey), customHeaderValue)
|
||||
return nil
|
||||
}
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
@@ -605,8 +614,9 @@ func TestWebhook(t *testing.T) {
|
||||
srv := New(config)
|
||||
// our custom server should receive the PDF.
|
||||
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"})
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
req.Header.Set(customHeaderKey, customHeaderValue)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
err := <-status
|
||||
assert.NoError(t, err)
|
||||
@@ -616,9 +626,9 @@ func TestResultFilename(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"})
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), 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"))
|
||||
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get(echo.HeaderContentDisposition))
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
|
||||
// there is no need to create a Resource.
|
||||
if !isMultipartFormDataEndpoint(config, ctx.Path()) {
|
||||
// validate method for healthcheck endpoint.
|
||||
if ctx.Path() == pingEndpoint && ctx.Request().Method != http.MethodGet {
|
||||
if ctx.Path() == pingEndpoint(config) && ctx.Request().Method != http.MethodGet {
|
||||
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
@@ -60,14 +60,14 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// loggerMiddleware logs the result of a request.
|
||||
func loggerMiddleware() echo.MiddlewareFunc {
|
||||
func loggerMiddleware(config conf.Config) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
err := next(ctx)
|
||||
// we do not want to log healthcheck requests if
|
||||
// log level is not set to DEBUG.
|
||||
isDebug := ctx.Path() == pingEndpoint
|
||||
isDebug := ctx.Path() == pingEndpoint(config)
|
||||
return ctx.LogRequestResult(err, isDebug)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,23 +48,29 @@ func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.Chro
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
pageRanges, err := r.StringArg(resource.PageRangesArgKey, "")
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
return printer.ChromePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
RpccBufferSize: googleChromeRpccBufferSize,
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
PageRanges: pageRanges,
|
||||
RpccBufferSize: googleChromeRpccBufferSize,
|
||||
CustomHTTPHeaders: make(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
@@ -85,9 +91,14 @@ func officePrinterOptions(r resource.Resource, config conf.Config) (printer.Offi
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
pageRanges, err := r.StringArg(resource.PageRangesArgKey, "")
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
return printer.OfficePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
Landscape: landscape,
|
||||
PageRanges: pageRanges,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
@@ -79,6 +78,10 @@ func (ctx *Context) WithResource(directoryName string) error {
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
// retrieve custom headers from request.
|
||||
for key, value := range ctx.Request().Header {
|
||||
r.WithCustomHTTPHeader(key, value[0])
|
||||
}
|
||||
// retrieve form values from request.
|
||||
for _, key := range resource.ArgKeys() {
|
||||
r.WithArg(key, ctx.FormValue(string(key)))
|
||||
@@ -103,11 +106,7 @@ func (ctx *Context) WithResource(directoryName string) error {
|
||||
return r, err
|
||||
}
|
||||
defer in.Close() // nolint: errcheck
|
||||
filename, err := normalize.String(fh.Filename)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if err := r.WithFile(filename, in); err != nil {
|
||||
if err := r.WithFile(fh.Filename, in); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ const (
|
||||
// LandscapeArgKey is the key
|
||||
// of the argument "landscape".
|
||||
LandscapeArgKey ArgKey = "landscape"
|
||||
// PageRangesArgKey is the key
|
||||
// of the argument "pageRanges".
|
||||
PageRangesArgKey ArgKey = "pageRanges"
|
||||
// GoogleChromeRpccBufferSizeArgKey is the key
|
||||
// of the argument "googleChromeRpccBufferSize".
|
||||
GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize"
|
||||
@@ -76,6 +79,7 @@ func ArgKeys() []ArgKey {
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
PageRangesArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestArgKeys(t *testing.T) {
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
PageRangesArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
}
|
||||
assert.Equal(t, expected, ArgKeys())
|
||||
|
||||
37
internal/app/xhttp/pkg/resource/header.go
Normal file
37
internal/app/xhttp/pkg/resource/header.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// RemoteURLCustomHTTPHeaderCanonicalBaseKey is the base key
|
||||
// of custom headers send to the remote URL.
|
||||
RemoteURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Remoteurl-"
|
||||
// WebhookURLCustomHTTPHeaderCanonicalBaseKey is the base key
|
||||
// of custom headers send to the webhook URL.
|
||||
WebhookURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Webhookurl-"
|
||||
)
|
||||
|
||||
func fetchCustomHTTPHeaders(r Resource, baseKey string) map[string]string {
|
||||
customHeaders := make(map[string]string)
|
||||
for key, value := range r.customHeaders {
|
||||
if strings.Contains(key, baseKey) {
|
||||
realKey := strings.Replace(key, baseKey, "", 1)
|
||||
customHeaders[realKey] = value
|
||||
}
|
||||
}
|
||||
return customHeaders
|
||||
}
|
||||
|
||||
// RemoteURLCustomHTTPHeaders is a helper for retrieving
|
||||
// the custom headers for the URL conversion.
|
||||
func RemoteURLCustomHTTPHeaders(r Resource) map[string]string {
|
||||
return fetchCustomHTTPHeaders(r, RemoteURLCustomHTTPHeaderCanonicalBaseKey)
|
||||
}
|
||||
|
||||
// WebhookURLCustomHTTPHeaders is a helper for retrieving
|
||||
// the custom headers for the webhook URL.
|
||||
func WebhookURLCustomHTTPHeaders(r Resource) map[string]string {
|
||||
return fetchCustomHTTPHeaders(r, WebhookURLCustomHTTPHeaderCanonicalBaseKey)
|
||||
}
|
||||
54
internal/app/xhttp/pkg/resource/header_test.go
Normal file
54
internal/app/xhttp/pkg/resource/header_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestRemoteURLCustomHeaders(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// should find the custom header.
|
||||
customHeaderValue := "bar"
|
||||
customHeaderCanonicalRealKey := "Foo"
|
||||
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", RemoteURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
|
||||
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
|
||||
r.WithCustomHTTPHeader("Bar", "Bar")
|
||||
expected := map[string]string{
|
||||
customHeaderCanonicalRealKey: customHeaderValue,
|
||||
}
|
||||
notExpected := map[string]string{
|
||||
customHeaderCanonicalKey: customHeaderValue,
|
||||
}
|
||||
v := RemoteURLCustomHTTPHeaders(r)
|
||||
assert.Equal(t, expected, v)
|
||||
assert.NotEqual(t, notExpected, v)
|
||||
}
|
||||
|
||||
func TestWebhookURLCustomHeaders(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// should find the custom header.
|
||||
customHeaderValue := "bar"
|
||||
customHeaderCanonicalRealKey := "Foo"
|
||||
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
|
||||
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
|
||||
r.WithCustomHTTPHeader("Bar", "Bar")
|
||||
expected := map[string]string{
|
||||
customHeaderCanonicalRealKey: customHeaderValue,
|
||||
}
|
||||
notExpected := map[string]string{
|
||||
customHeaderCanonicalKey: customHeaderValue,
|
||||
}
|
||||
v := WebhookURLCustomHTTPHeaders(r)
|
||||
assert.Equal(t, expected, v)
|
||||
assert.NotEqual(t, notExpected, v)
|
||||
}
|
||||
@@ -3,9 +3,12 @@ package resource
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
@@ -21,10 +24,11 @@ const TemporaryDirectory string = "tmp"
|
||||
// Resource helps managing
|
||||
// arguments and files for a conversion.
|
||||
type Resource struct {
|
||||
logger xlog.Logger
|
||||
dirPath string
|
||||
args map[ArgKey]string
|
||||
files map[string]file
|
||||
logger xlog.Logger
|
||||
dirPath string
|
||||
customHeaders map[string]string
|
||||
args map[ArgKey]string
|
||||
files map[string]file
|
||||
}
|
||||
|
||||
// New creates a Resource where its files will
|
||||
@@ -48,10 +52,11 @@ func New(logger xlog.Logger, directoryName string) (Resource, error) {
|
||||
}
|
||||
logger.DebugfOp(op, "resource directory '%s' created", directoryName)
|
||||
return Resource{
|
||||
logger: logger,
|
||||
dirPath: dirPath,
|
||||
args: make(map[ArgKey]string),
|
||||
files: make(map[string]file),
|
||||
logger: logger,
|
||||
dirPath: dirPath,
|
||||
customHeaders: make(map[string]string),
|
||||
args: make(map[ArgKey]string),
|
||||
files: make(map[string]file),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -70,6 +75,21 @@ func (r Resource) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithCustomHTTPHeader add a new custom header to the Resource.
|
||||
// Given key should be in canonical format.
|
||||
func (r *Resource) WithCustomHTTPHeader(key string, value string) {
|
||||
const op string = "resource.Resource.WithCustomHTTPHeader"
|
||||
// should already be in canonical format.
|
||||
canonicalKey := http.CanonicalHeaderKey(key)
|
||||
if strings.Contains(canonicalKey, RemoteURLCustomHTTPHeaderCanonicalBaseKey) ||
|
||||
strings.Contains(canonicalKey, WebhookURLCustomHTTPHeaderCanonicalBaseKey) {
|
||||
r.customHeaders[canonicalKey] = value
|
||||
r.logger.DebugfOp(op, "added '%s' with value '%s' to resource custom HTTP headers", canonicalKey, value)
|
||||
return
|
||||
}
|
||||
r.logger.DebugfOp(op, "skipping '%s' as it is not a custom HTTP header...", canonicalKey)
|
||||
}
|
||||
|
||||
// WithArg add a new argument to the Resource.
|
||||
func (r *Resource) WithArg(key ArgKey, value string) {
|
||||
const op string = "resource.Resource.WithArg"
|
||||
@@ -80,13 +100,24 @@ func (r *Resource) WithArg(key ArgKey, value string) {
|
||||
// WithFile add a new file to the Resource.
|
||||
func (r *Resource) WithFile(filename string, in io.Reader) error {
|
||||
const op string = "resource.Resource.WithFile"
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
file := file{fpath: fpath}
|
||||
if err := file.write(in); err != nil {
|
||||
resolver := func() error {
|
||||
// see https://github.com/thecodingmachine/gotenberg/issues/104.
|
||||
normalized, err := normalize.String(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, normalized)
|
||||
file := file{fpath: fpath}
|
||||
if err := file.write(in); err != nil {
|
||||
return err
|
||||
}
|
||||
r.files[filename] = file
|
||||
r.logger.DebugfOp(op, "resource file '%s' created", filename)
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
r.files[filename] = file
|
||||
r.logger.DebugfOp(op, "resource file '%s' created", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,22 +11,21 @@ func New(config conf.Config) *echo.Echo {
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
srv.Use(contextMiddleware(config))
|
||||
srv.Use(loggerMiddleware())
|
||||
srv.Use(loggerMiddleware(config))
|
||||
srv.Use(cleanupMiddleware())
|
||||
srv.Use(errorMiddleware())
|
||||
srv.GET(pingEndpoint, pingHandler)
|
||||
srv.POST(mergeEndpoint, mergeHandler)
|
||||
srv.GET(pingEndpoint(config), pingHandler)
|
||||
srv.POST(mergeEndpoint(config), mergeHandler)
|
||||
if config.DisableGoogleChrome() && config.DisableUnoconv() {
|
||||
return srv
|
||||
}
|
||||
g := srv.Group(convertGroupEndpoint)
|
||||
if !config.DisableGoogleChrome() {
|
||||
g.POST(htmlEndpoint, htmlHandler)
|
||||
g.POST(urlEndpoint, urlHandler)
|
||||
g.POST(markdownEndpoint, markdownHandler)
|
||||
srv.POST(htmlEndpoint(config), htmlHandler)
|
||||
srv.POST(urlEndpoint(config), urlHandler)
|
||||
srv.POST(markdownEndpoint(config), markdownHandler)
|
||||
}
|
||||
if !config.DisableUnoconv() {
|
||||
g.POST(officeEndpoint, officeHandler)
|
||||
srv.POST(officeEndpoint(config), officeHandler)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -28,31 +27,31 @@ func TestDisableChromeEndpoints(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 404.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL endpoint should return 404.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office endpoint should return 200.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// finally...
|
||||
@@ -65,31 +64,31 @@ func TestDisableUnoconvEndpoints(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 200.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// URL endpoint should return 200.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Markdown endpoint should return 200.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Office endpoint should return 404.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// finally...
|
||||
@@ -102,34 +101,71 @@ func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 404.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL endpoint should return 404.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office endpoint should return 404.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "0")
|
||||
}
|
||||
|
||||
func TestCustomRootPath(t *testing.T) {
|
||||
os.Setenv(conf.RootPathEnvVar, "/foo/")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 200.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// URL endpoint should return 200.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Markdown endpoint should return 200.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Office endpoint should return 200.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.RootPathEnvVar, "/")
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ const (
|
||||
// LogLevelEnvVar contains the name
|
||||
// of the environment variable "LOG_LEVEL".
|
||||
LogLevelEnvVar string = "LOG_LEVEL"
|
||||
// RootPathEnvVar contains the name
|
||||
// of the environment variable "ROOT_PATH".
|
||||
RootPathEnvVar string = "ROOT_PATH"
|
||||
// DefaultGoogleChromeRpccBufferSizeEnvVar contains the name
|
||||
// of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE".
|
||||
DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE"
|
||||
@@ -51,6 +54,7 @@ type Config struct {
|
||||
disableGoogleChrome bool
|
||||
disableUnoconv bool
|
||||
logLevel xlog.Level
|
||||
rootPath string
|
||||
maximumGoogleChromeRpccBufferSize int64
|
||||
defaultGoogleChromeRpccBufferSize int64
|
||||
}
|
||||
@@ -68,6 +72,7 @@ func DefaultConfig() Config {
|
||||
disableGoogleChrome: false,
|
||||
disableUnoconv: false,
|
||||
logLevel: xlog.InfoLevel,
|
||||
rootPath: "/",
|
||||
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
|
||||
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
|
||||
}
|
||||
@@ -163,6 +168,16 @@ func FromEnv() (Config, error) {
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
rootPath, err := xassert.StringFromEnv(
|
||||
RootPathEnvVar,
|
||||
c.rootPath,
|
||||
xassert.StringStartWith("/"),
|
||||
xassert.StringEndWith("/"),
|
||||
)
|
||||
c.rootPath = rootPath
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv(
|
||||
DefaultGoogleChromeRpccBufferSizeEnvVar,
|
||||
c.defaultGoogleChromeRpccBufferSize,
|
||||
@@ -242,6 +257,12 @@ func (c Config) LogLevel() xlog.Level {
|
||||
return c.logLevel
|
||||
}
|
||||
|
||||
// RootPath returns the rooth path from
|
||||
// the configuration.
|
||||
func (c Config) RootPath() string {
|
||||
return c.rootPath
|
||||
}
|
||||
|
||||
// MaximumGoogleChromeRpccBufferSize returns the maximum
|
||||
// Google Chrome rpcc buffer size from the configuration.
|
||||
func (c Config) MaximumGoogleChromeRpccBufferSize() int64 {
|
||||
|
||||
@@ -320,6 +320,29 @@ func TestLogLevelFromEnv(t *testing.T) {
|
||||
os.Unsetenv(LogLevelEnvVar)
|
||||
}
|
||||
|
||||
func TestRootPathFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// ROOT_PATH correctly set.
|
||||
os.Setenv(RootPathEnvVar, "/foo/")
|
||||
expected = DefaultConfig()
|
||||
expected.rootPath = "/foo/"
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(RootPathEnvVar)
|
||||
// ROOT_PATH wrongly set.
|
||||
os.Setenv(RootPathEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(RootPathEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
@@ -368,6 +391,7 @@ func TestGetters(t *testing.T) {
|
||||
assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome())
|
||||
assert.Equal(t, result.disableUnoconv, result.DisableUnoconv())
|
||||
assert.Equal(t, result.logLevel, result.LogLevel())
|
||||
assert.Equal(t, result.rootPath, result.RootPath())
|
||||
assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize())
|
||||
assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package printer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
@@ -30,18 +31,20 @@ type chromePrinter struct {
|
||||
// ChromePrinterOptions helps customizing the
|
||||
// Google Chrome Printer behaviour.
|
||||
type ChromePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
WaitDelay float64
|
||||
HeaderHTML string
|
||||
FooterHTML string
|
||||
PaperWidth float64
|
||||
PaperHeight float64
|
||||
MarginTop float64
|
||||
MarginBottom float64
|
||||
MarginLeft float64
|
||||
MarginRight float64
|
||||
Landscape bool
|
||||
RpccBufferSize int64
|
||||
WaitTimeout float64
|
||||
WaitDelay float64
|
||||
HeaderHTML string
|
||||
FooterHTML string
|
||||
PaperWidth float64
|
||||
PaperHeight float64
|
||||
MarginTop float64
|
||||
MarginBottom float64
|
||||
MarginLeft float64
|
||||
MarginRight float64
|
||||
Landscape bool
|
||||
PageRanges string
|
||||
RpccBufferSize int64
|
||||
CustomHTTPHeaders map[string]string
|
||||
}
|
||||
|
||||
// DefaultChromePrinterOptions returns the default
|
||||
@@ -49,18 +52,20 @@ type ChromePrinterOptions struct {
|
||||
func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions {
|
||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||
return ChromePrinterOptions{
|
||||
WaitTimeout: config.DefaultWaitTimeout(),
|
||||
WaitDelay: 0.0,
|
||||
HeaderHTML: defaultHeaderFooterHTML,
|
||||
FooterHTML: defaultHeaderFooterHTML,
|
||||
PaperWidth: 8.27,
|
||||
PaperHeight: 11.7,
|
||||
MarginTop: 1.0,
|
||||
MarginBottom: 1.0,
|
||||
MarginLeft: 1.0,
|
||||
MarginRight: 1.0,
|
||||
Landscape: false,
|
||||
RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(),
|
||||
WaitTimeout: config.DefaultWaitTimeout(),
|
||||
WaitDelay: 0.0,
|
||||
HeaderHTML: defaultHeaderFooterHTML,
|
||||
FooterHTML: defaultHeaderFooterHTML,
|
||||
PaperWidth: 8.27,
|
||||
PaperHeight: 11.7,
|
||||
MarginTop: 1.0,
|
||||
MarginBottom: 1.0,
|
||||
MarginLeft: 1.0,
|
||||
MarginRight: 1.0,
|
||||
Landscape: false,
|
||||
PageRanges: "",
|
||||
RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(),
|
||||
CustomHTTPHeaders: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +149,10 @@ func (p chromePrinter) Print(destination string) error {
|
||||
if err := p.enableEvents(ctx, targetClient); err != nil {
|
||||
return err
|
||||
}
|
||||
// add custom headers (if any).
|
||||
if err := p.setCustomHTTPHeaders(ctx, targetClient); err != nil {
|
||||
return err
|
||||
}
|
||||
// listen for all events.
|
||||
if err := p.listenEvents(ctx, targetClient); err != nil {
|
||||
return err
|
||||
@@ -156,23 +165,35 @@ func (p chromePrinter) Print(destination string) error {
|
||||
} else {
|
||||
p.logger.DebugOp(op, "no wait delay to apply, moving on...")
|
||||
}
|
||||
printToPdfArgs := 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 p.opts.PageRanges != "" {
|
||||
printToPdfArgs.SetPageRanges(p.opts.PageRanges)
|
||||
}
|
||||
// print the page to PDF.
|
||||
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),
|
||||
printToPdfArgs,
|
||||
)
|
||||
if err != nil {
|
||||
// find a way to check it in the handlers?
|
||||
if strings.Contains(err.Error(), "Page range syntax error") {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' is not a valid Google Chrome page ranges", p.opts.PageRanges),
|
||||
err,
|
||||
)
|
||||
}
|
||||
if strings.Contains(err.Error(), "rpcc: message too large") {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
@@ -247,6 +268,32 @@ func (p chromePrinter) enableEvents(ctx context.Context, client *cdp.Client) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p chromePrinter) setCustomHTTPHeaders(ctx context.Context, client *cdp.Client) error {
|
||||
const op string = "printer.chromePrinter.setCustomHTTPHeaders"
|
||||
resolver := func() error {
|
||||
if len(p.opts.CustomHTTPHeaders) == 0 {
|
||||
p.logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...")
|
||||
return nil
|
||||
}
|
||||
customHTTPHeaders := make(map[string]string)
|
||||
// useless but for the logs.
|
||||
for key, value := range p.opts.CustomHTTPHeaders {
|
||||
customHTTPHeaders[key] = value
|
||||
p.logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key)
|
||||
}
|
||||
b, err := json.Marshal(customHTTPHeaders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// should always be called after client.Network.Enable.
|
||||
return client.Network.SetExtraHTTPHeaders(ctx, network.NewSetExtraHTTPHeadersArgs(b))
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p chromePrinter) listenEvents(ctx context.Context, client *cdp.Client) error {
|
||||
const op string = "printer.chromePrinter.listenEvents"
|
||||
resolver := func() error {
|
||||
|
||||
@@ -38,6 +38,26 @@ func TestHTMLPrinter(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// options with a page ranges.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
opts.PageRanges = "1"
|
||||
p = NewHTMLPrinter(logger, fpath, opts)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as options have
|
||||
// a wrong page ranges.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
opts.PageRanges = "foo"
|
||||
p = NewHTMLPrinter(logger, fpath, opts)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, xerror.InvalidCode, xerror.Code(err))
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as context.Context
|
||||
// should timeout.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
|
||||
@@ -40,6 +40,28 @@ func TestMarkdownPrinter(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// options with a page ranges.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
opts.PageRanges = "1"
|
||||
p, err = NewMarkdownPrinter(logger, fpath, opts)
|
||||
assert.Nil(t, err)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as options have
|
||||
// a wrong page ranges.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
opts.PageRanges = "foo"
|
||||
p, err = NewMarkdownPrinter(logger, fpath, opts)
|
||||
assert.Nil(t, err)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, xerror.InvalidCode, xerror.Code(err))
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as context.Context
|
||||
// should timeout.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
|
||||
@@ -2,6 +2,7 @@ package printer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||
@@ -54,6 +55,8 @@ func (p mergePrinter) Print(destination string) error {
|
||||
defer cancel()
|
||||
p.ctx = ctx
|
||||
}
|
||||
// see https://github.com/thecodingmachine/gotenberg/issues/139.
|
||||
sort.Strings(p.fpaths)
|
||||
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
|
||||
resolver := func() error {
|
||||
var args []string
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/phayes/freeport"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
@@ -26,6 +28,7 @@ type officePrinter struct {
|
||||
type OfficePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
Landscape bool
|
||||
PageRanges string
|
||||
}
|
||||
|
||||
// DefaultOfficePrinterOptions returns the default
|
||||
@@ -34,6 +37,7 @@ func DefaultOfficePrinterOptions(config conf.Config) OfficePrinterOptions {
|
||||
return OfficePrinterOptions{
|
||||
WaitTimeout: config.DefaultWaitTimeout(),
|
||||
Landscape: false,
|
||||
PageRanges: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +57,15 @@ func (p officePrinter) Print(destination string) error {
|
||||
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||
defer cancel()
|
||||
resolver := func() error {
|
||||
// see https://github.com/thecodingmachine/gotenberg/issues/139.
|
||||
sort.Strings(p.fpaths)
|
||||
fpaths := make([]string, len(p.fpaths))
|
||||
dirPath := filepath.Dir(destination)
|
||||
for i, fpath := range p.fpaths {
|
||||
baseFilename := xrand.Get()
|
||||
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
||||
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
|
||||
if err := unoconv(ctx, p.logger, fpath, tmpDest, p.opts); err != nil {
|
||||
if err := p.unoconv(ctx, fpath, tmpDest); err != nil {
|
||||
return err
|
||||
}
|
||||
p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename)
|
||||
@@ -85,7 +91,7 @@ func (p officePrinter) Print(destination string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, opts OfficePrinterOptions) error {
|
||||
func (p officePrinter) unoconv(ctx context.Context, fpath, destination string) error {
|
||||
const op string = "printer.unoconv"
|
||||
resolver := func() error {
|
||||
port, err := freeport.GetFreePort()
|
||||
@@ -100,11 +106,25 @@ func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string,
|
||||
"--format",
|
||||
"pdf",
|
||||
}
|
||||
if opts.Landscape {
|
||||
if p.opts.Landscape {
|
||||
args = append(args, "--printer", "PaperOrientation=landscape")
|
||||
}
|
||||
if p.opts.PageRanges != "" {
|
||||
args = append(args, "--export", fmt.Sprintf("PageRange=%s", p.opts.PageRanges))
|
||||
}
|
||||
args = append(args, "--output", destination, fpath)
|
||||
return xexec.Run(ctx, logger, "unoconv", args...)
|
||||
if err := xexec.Run(ctx, p.logger, "unoconv", args...); err != nil {
|
||||
// find a way to check it in the handlers?
|
||||
if p.opts.PageRanges != "" && strings.Contains(err.Error(), "exit status 5") {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' is not a valid LibreOffice page ranges", p.opts.PageRanges),
|
||||
err,
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
|
||||
@@ -46,6 +46,26 @@ func TestOfficePrinter(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// options with page ranges.
|
||||
opts = DefaultOfficePrinterOptions(config)
|
||||
opts.PageRanges = "1-1"
|
||||
p = NewOfficePrinter(logger, []string{fpaths[0]}, opts)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as options have
|
||||
// a wrong page ranges.
|
||||
opts = DefaultOfficePrinterOptions(config)
|
||||
opts.PageRanges = "foo"
|
||||
p = NewOfficePrinter(logger, []string{fpaths[0]}, opts)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, xerror.InvalidCode, xerror.Code(err))
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as context.Context
|
||||
// should timeout.
|
||||
opts = DefaultOfficePrinterOptions(config)
|
||||
|
||||
@@ -38,6 +38,26 @@ func TestURLPrinter(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// options with a pages ranges.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
opts.PageRanges = "1"
|
||||
p = NewURLPrinter(logger, URL, opts)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
assert.Nil(t, err)
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as options have
|
||||
// a wrong page ranges.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
opts.PageRanges = "foo"
|
||||
p = NewURLPrinter(logger, URL, opts)
|
||||
dest = test.GenerateDestination()
|
||||
err = p.Print(dest)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, xerror.InvalidCode, xerror.Code(err))
|
||||
err = os.RemoveAll(dest)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as context.Context
|
||||
// should timeout.
|
||||
opts = DefaultChromePrinterOptions(config)
|
||||
|
||||
@@ -2,6 +2,7 @@ package xassert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
@@ -54,7 +55,67 @@ func StringOneOf(values []string) RuleString {
|
||||
}
|
||||
}
|
||||
|
||||
type ruleStringStartWith struct {
|
||||
*baseRuleString
|
||||
startWith string
|
||||
}
|
||||
|
||||
func (r ruleStringStartWith) validate() error {
|
||||
const op string = "xassert.ruleStringStartWith.validate"
|
||||
if strings.HasPrefix(r.value, r.startWith) {
|
||||
return nil
|
||||
}
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' should start with '%s', got '%s'", r.key, r.startWith, r.value),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
StringStartWith returns a RuleString for
|
||||
validating that a string starts with
|
||||
given string.
|
||||
*/
|
||||
func StringStartWith(startWith string) RuleString {
|
||||
return ruleStringStartWith{
|
||||
&baseRuleString{},
|
||||
startWith,
|
||||
}
|
||||
}
|
||||
|
||||
type ruleStringEndWith struct {
|
||||
*baseRuleString
|
||||
endWith string
|
||||
}
|
||||
|
||||
func (r ruleStringEndWith) validate() error {
|
||||
const op string = "xassert.ruleStringEndWith.validate"
|
||||
if strings.HasSuffix(r.value, r.endWith) {
|
||||
return nil
|
||||
}
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' should end with '%s', got '%s'", r.key, r.endWith, r.value),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
StringEndWith returns a RuleString for
|
||||
validating that a string ends with
|
||||
given string.
|
||||
*/
|
||||
func StringEndWith(endWith string) RuleString {
|
||||
return ruleStringEndWith{
|
||||
&baseRuleString{},
|
||||
endWith,
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = RuleString(new(ruleStringOneOf))
|
||||
_ = RuleString(new(ruleStringStartWith))
|
||||
_ = RuleString(new(ruleStringEndWith))
|
||||
)
|
||||
|
||||
@@ -18,3 +18,27 @@ func TestStringOfOne(t *testing.T) {
|
||||
err = rule.validate()
|
||||
test.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestStringStartWith(t *testing.T) {
|
||||
rule := StringStartWith("foo")
|
||||
// should be OK.
|
||||
rule.with("FOO", "foobarfoo")
|
||||
err := rule.validate()
|
||||
assert.Nil(t, err)
|
||||
// should not be OK.
|
||||
rule.with("FOO", "qux")
|
||||
err = rule.validate()
|
||||
test.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestStringEndWith(t *testing.T) {
|
||||
rule := StringEndWith("foo")
|
||||
// should be OK.
|
||||
rule.with("FOO", "foobarfoo")
|
||||
err := rule.validate()
|
||||
assert.Nil(t, err)
|
||||
// should not be OK.
|
||||
rule.with("FOO", "qux")
|
||||
err = rule.validate()
|
||||
test.AssertError(t, err)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ set -e
|
||||
|
||||
GOLANG_VERSION="$1"
|
||||
TINI_VERSION="$2"
|
||||
DOCKER_REPOSITORY="$3"
|
||||
DOCKER_REGISTRY="$3"
|
||||
VERSION="$4"
|
||||
DOCKER_USER="$5"
|
||||
DOCKER_PASSWORD="$6"
|
||||
@@ -23,13 +23,13 @@ fi
|
||||
docker build \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
--build-arg TINI_VERSION=${TINI_VERSION} \
|
||||
-t ${DOCKER_REPOSITORY}/gotenberg:latest \
|
||||
-t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]} \
|
||||
-t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]} \
|
||||
-t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \
|
||||
-t ${DOCKER_REGISTRY}/gotenberg:latest \
|
||||
-t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]} \
|
||||
-t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]} \
|
||||
-t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \
|
||||
-f build/package/Dockerfile .
|
||||
|
||||
docker push "${DOCKER_REPOSITORY}/gotenberg:latest"
|
||||
docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}"
|
||||
docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}"
|
||||
docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}"
|
||||
docker push "${DOCKER_REGISTRY}/gotenberg:latest"
|
||||
docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}"
|
||||
docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}"
|
||||
docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}"
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
set -e
|
||||
|
||||
DOCKER_REPOSITORY="$1"
|
||||
DOCKER_REGISTRY="$1"
|
||||
CODE_COVERAGE="$2"
|
||||
|
||||
touch "$PWD/coverage.txt"
|
||||
chmod 777 "$PWD/coverage.txt"
|
||||
docker build -t "$DOCKER_REPOSITORY/gotenberg:tests" -f build/tests/Dockerfile .
|
||||
docker build -t "$DOCKER_REGISTRY/gotenberg:tests" -f build/tests/Dockerfile .
|
||||
|
||||
if [ "$CODE_COVERAGE" = "1" ]; then
|
||||
docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" -v "$PWD/coverage.txt:/gotenberg/tests/coverage.txt" "$DOCKER_REPOSITORY/gotenberg:tests"
|
||||
docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" -v "$PWD/coverage.txt:/gotenberg/tests/coverage.txt" "$DOCKER_REGISTRY/gotenberg:tests"
|
||||
else
|
||||
docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" "$DOCKER_REPOSITORY/gotenberg:tests"
|
||||
docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" "$DOCKER_REGISTRY/gotenberg:tests"
|
||||
fi
|
||||
@@ -79,7 +79,7 @@ func multipartForm(
|
||||
require.Nil(t, err)
|
||||
}
|
||||
if kind == "url" {
|
||||
err := writer.WriteField("remoteURL", "http://google.com")
|
||||
err := writer.WriteField("remoteURL", "https://google.com")
|
||||
require.Nil(t, err)
|
||||
}
|
||||
for k, v := range formValues {
|
||||
|
||||
@@ -75,6 +75,7 @@ func OfficeFpaths(t *testing.T) []string {
|
||||
fpath(t, "office", "document.docx"),
|
||||
fpath(t, "office", "document.rtf"),
|
||||
fpath(t, "office", "document.txt"),
|
||||
fpath(t, "office", "document_with_special_éà.txt"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
test/testdata/office/document_with_special_éà.txt
vendored
Normal file
3
test/testdata/office/document_with_special_éà.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Gutenberg
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
Reference in New Issue
Block a user