diff --git a/Makefile b/Makefile index ece45530..5a0d93fb 100644 --- a/Makefile +++ b/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) \ No newline at end of file + ./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD) \ No newline at end of file diff --git a/build/base/Dockerfile b/build/base/Dockerfile index c40df230..31f78d84 100644 --- a/build/base/Dockerfile +++ b/build/base/Dockerfile @@ -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 \ No newline at end of file diff --git a/build/docs/content/01-install.md b/build/docs/content/01-install.md index ec4cdca5..7e2c4d0e 100644 --- a/build/docs/content/01-install.md +++ b/build/docs/content/01-install.md @@ -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: diff --git a/build/docs/content/02-clients.md b/build/docs/content/02-clients.md index 560601e6..0f4ce7d2 100644 --- a/build/docs/content/02-clients.md +++ b/build/docs/content/02-clients.md @@ -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,12 +20,14 @@ 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) diff --git a/build/docs/content/03-environment-variables.md b/build/docs/content/03-environment-variables.md index e1f18731..b1522740 100644 --- a/build/docs/content/03-environment-variables.md +++ b/build/docs/content/03-environment-variables.md @@ -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` diff --git a/build/docs/content/04-html.md b/build/docs/content/04-html.md index 060da6ef..85c426d5 100644 --- a/build/docs/content/04-html.md +++ b/build/docs/content/04-html.md @@ -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,9 +56,9 @@ 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); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -120,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 @@ -140,13 +140,13 @@ 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); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); ``` @@ -210,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 @@ -229,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); ``` @@ -269,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 @@ -291,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); ``` @@ -322,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 @@ -342,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); ``` @@ -374,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 @@ -394,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); ``` diff --git a/build/docs/content/05-url.md b/build/docs/content/05-url.md index 6517843a..fc307445 100644 --- a/build/docs/content/05-url.md +++ b/build/docs/content/05-url.md @@ -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); ``` diff --git a/build/docs/content/06-markdown.md b/build/docs/content/06-markdown.md index 66c50ee4..5838ea7b 100644 --- a/build/docs/content/06-markdown.md +++ b/build/docs/content/06-markdown.md @@ -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); ``` diff --git a/build/docs/content/07-office.md b/build/docs/content/07-office.md index 39e91e37..1dbae793 100644 --- a/build/docs/content/07-office.md +++ b/build/docs/content/07-office.md @@ -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); ``` diff --git a/build/docs/content/08-merge.md b/build/docs/content/08-merge.md index 49ac0b2b..8e693743 100644 --- a/build/docs/content/08-merge.md +++ b/build/docs/content/08-merge.md @@ -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); ``` diff --git a/build/docs/content/09-timeout.md b/build/docs/content/09-timeout.md index 9392b84f..c7cbbd70 100644 --- a/build/docs/content/09-timeout.md +++ b/build/docs/content/09-timeout.md @@ -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); ``` diff --git a/build/docs/content/10-webhook.md b/build/docs/content/10-webhook.md index fff2456d..3d5e69e1 100644 --- a/build/docs/content/10-webhook.md +++ b/build/docs/content/10-webhook.md @@ -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); +``` \ No newline at end of file diff --git a/build/docs/content/11-result-filename.md b/build/docs/content/11-result-filename.md index aabb4177..94bc230f 100644 --- a/build/docs/content/11-result-filename.md +++ b/build/docs/content/11-result-filename.md @@ -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); diff --git a/build/lint/Dockerfile b/build/lint/Dockerfile index fd08c9c5..42a8c054 100644 --- a/build/lint/Dockerfile +++ b/build/lint/Dockerfile @@ -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" ] \ No newline at end of file +CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit" ] \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 85c3b75a..4853baa6 100755 --- a/docs/index.html +++ b/docs/index.html @@ -132,33 +132,49 @@ Install
Gotenberg is shipped within a Docker image.
---It uses a dedicated non-root user called
-gotenbergwith uid and gid1001.
You may start it with:
-$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6 +$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6+The API will be available at http://localhost:3000.
The image uses a dedicated non-root user called
+ +gotenbergwith uid and gid1001.If you wish to change those uid and gid, you will have to:
+ +
For instance:
+ +$ 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 ++ +
+++
masterbranch is always up-to-date with the latest version of the API.
You may also add it in your Docker Compose stack:
-version: '3' +@@ -731,15 +820,14 @@ a lot on JavaScript for rendering. Go -version: '3' -services: +services: # your others services - gotenberg: - image: thecodingmachine/gotenberg:6 + gotenberg: + image: thecodingmachine/gotenberg:6@@ -177,7 +193,7 @@-The more resources are granted, the quicker will be the conversions.
In the deployment specification of the pod, also specify the uid
+1001of the usergotenberg:In the deployment specification of the pod, also specify the uid of the user
gotenberg:securityContext: privileged: false @@ -205,9 +221,11 @@ Gotenberg API is available at http://localhost:3 Go client ---url http://localhost:3000/convert/html \ --header 'Content-Type: multipart/form-data' \ --form files=@index.html \ - --form paperWidth=8.27 \ - --form paperHeight=11.69 \ + --form paperWidth=8.27 \ + --form paperHeight=11.69 \ --form marginTop=0 \ --form marginBottom=0 \ --form marginLeft=0 \ @@ -671,17 +709,16 @@ $client->store($request, $dest); Go -$ 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.
+PHP client
@@ -217,11 +235,13 @@ Gotenberg API is available at http://localhost:3$ composer require php-http/guzzle6-adapter-Then the PHP client:
+Then the PHP client:
$ composer require thecodingmachine/gotenberg-php-client+See also the example from the README.
+Community clients
@@ -264,6 +284,22 @@ about what’s going on.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 whilegotenbergis not.++This is useful if you wish to do service discovery via URL paths.
+Disable Google Chrome
@@ -421,14 +457,13 @@ which will be converted to PDF. Go -import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -510,16 +545,17 @@ Respectively, a file namedimport "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)@@ -440,9 +475,9 @@ 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); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest);
header.htmlandfooter.htmlGo -import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -605,15 +641,17 @@ see to the fonts section. Go -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)@@ -531,13 +567,13 @@ 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); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest);
import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -657,8 +695,8 @@ $client->store($request, $dest);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)@@ -625,15 +663,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);
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)@@ -694,12 +731,64 @@ 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
+ +$ 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
+ +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
+ +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);@@ -723,7 +812,7 @@ a lot on JavaScript for rendering. --url http://localhost:3000/convert/html \ --header 'Content-Type: multipart/form-data' \ --form files=@index.html \ - --form waitDelay=5.5 \ + --form waitDelay=5.5 \ -o result.pdf
import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -790,15 +878,14 @@ The hard limit is 100 MB and is defined by Google Chrome itself. Go -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)@@ -752,10 +840,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);
import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -859,15 +946,13 @@ If not, some of the content of the page might be hidden. Go -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)@@ -811,10 +898,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->setGoogleChromeRpccBufferSize(1048576); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest);
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)@@ -880,7 +965,63 @@ 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); +
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-encodingisAccept-Encoding.
$ 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 ++ +
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) ++ +
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);
@@ -933,14 +1074,14 @@ in the file index.html. This function will convert a given markdown
Go
-import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -1014,14 +1155,14 @@ $client->store($request, $dest); Go -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)@@ -952,12 +1093,12 @@ 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);
import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -1066,15 +1207,14 @@ $client->store($request, $dest); Go -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)@@ -1034,11 +1175,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);
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)@@ -1087,11 +1227,69 @@ 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); +
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 --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 ++ +
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) ++ +
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);
@@ -1132,14 +1330,14 @@ will merge them and return the resulting PDF file.
Go
-import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -1189,21 +1387,20 @@ If unsucessful, it returns aimport "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)@@ -1152,11 +1350,11 @@ 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);
504 HTTP code.
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
- --form waitTimeout=2.5
+ --form waitTimeout=2.5
import "github.com/thecodingmachine/gotenberg-go-client/v6" +@@ -1255,14 +1452,13 @@ to given URL. Go -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)@@ -1216,10 +1413,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->setWaitTimeout(2.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest);
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)@@ -1274,7 +1470,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); @@ -1294,51 +1490,105 @@ $resp = $client->post($request);
-You may also define this value globally: see the environment variables section.
-+
-Examples
- -- -cURL
+cURL$ curl --request POST \ --url http://localhost:3000/convert/html \ --header 'Content-Type: multipart/form-data' \ --form files=@index.html \ --form webhookURL='http://myapp.com/webhook/' \ - --form webhookURLTimeout=2.5 + --form webhookURLTimeout=2.5-+
+Go-Go
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
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', '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-Headerto your request, +the API will send a request to thewebhookURLwith the HTTP headerYour-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-encodingisAccept-Encoding.+ +cURL
+ +$ 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
+ +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
+ +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);@@ -1375,14 +1625,13 @@ Otherwise a random filename is used. Go -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)@@ -1395,7 +1644,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); @@ -1447,14 +1696,14 @@ if the API is under heavy load.
For instance, using the following Docker Compose file:
-version: '3' +version: '3' -services: +services: # your others services - gotenberg: - image: thecodingmachine/gotenberg:6 + gotenberg: + image: thecodingmachine/gotenberg:6You may now launch your services using:
diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go index 7c819bd2..92cf22ea 100644 --- a/internal/app/xhttp/handler.go +++ b/internal/app/xhttp/handler.go @@ -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 } diff --git a/internal/app/xhttp/handler_test.go b/internal/app/xhttp/handler_test.go index 5602d90b..7e141d53 100644 --- a/internal/app/xhttp/handler_test.go +++ b/internal/app/xhttp/handler_test.go @@ -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)) } diff --git a/internal/app/xhttp/middleware.go b/internal/app/xhttp/middleware.go index ff722c6a..4049b9ac 100644 --- a/internal/app/xhttp/middleware.go +++ b/internal/app/xhttp/middleware.go @@ -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) } } diff --git a/internal/app/xhttp/option.go b/internal/app/xhttp/option.go index 2afb6984..5a9a3759 100644 --- a/internal/app/xhttp/option.go +++ b/internal/app/xhttp/option.go @@ -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() diff --git a/internal/app/xhttp/pkg/context/context.go b/internal/app/xhttp/pkg/context/context.go index d1065b58..b8e0b892 100644 --- a/internal/app/xhttp/pkg/context/context.go +++ b/internal/app/xhttp/pkg/context/context.go @@ -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 } } diff --git a/internal/app/xhttp/pkg/resource/arg.go b/internal/app/xhttp/pkg/resource/arg.go index 706e8952..80063925 100644 --- a/internal/app/xhttp/pkg/resource/arg.go +++ b/internal/app/xhttp/pkg/resource/arg.go @@ -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, } } diff --git a/internal/app/xhttp/pkg/resource/arg_test.go b/internal/app/xhttp/pkg/resource/arg_test.go index 1485c5d5..230add46 100644 --- a/internal/app/xhttp/pkg/resource/arg_test.go +++ b/internal/app/xhttp/pkg/resource/arg_test.go @@ -24,6 +24,7 @@ func TestArgKeys(t *testing.T) { MarginLeftArgKey, MarginRightArgKey, LandscapeArgKey, + PageRangesArgKey, GoogleChromeRpccBufferSizeArgKey, } assert.Equal(t, expected, ArgKeys()) diff --git a/internal/app/xhttp/pkg/resource/header.go b/internal/app/xhttp/pkg/resource/header.go new file mode 100644 index 00000000..fa4c8814 --- /dev/null +++ b/internal/app/xhttp/pkg/resource/header.go @@ -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) +} diff --git a/internal/app/xhttp/pkg/resource/header_test.go b/internal/app/xhttp/pkg/resource/header_test.go new file mode 100644 index 00000000..35a944f8 --- /dev/null +++ b/internal/app/xhttp/pkg/resource/header_test.go @@ -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) +} diff --git a/internal/app/xhttp/pkg/resource/resource.go b/internal/app/xhttp/pkg/resource/resource.go index 81cf001a..3a983f18 100644 --- a/internal/app/xhttp/pkg/resource/resource.go +++ b/internal/app/xhttp/pkg/resource/resource.go @@ -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 } diff --git a/internal/app/xhttp/xhttp.go b/internal/app/xhttp/xhttp.go index 7324997e..02db10e1 100644 --- a/internal/app/xhttp/xhttp.go +++ b/internal/app/xhttp/xhttp.go @@ -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 } diff --git a/internal/app/xhttp/xhttp_test.go b/internal/app/xhttp/xhttp_test.go index 801407ec..f4fe3df0 100644 --- a/internal/app/xhttp/xhttp_test.go +++ b/internal/app/xhttp/xhttp_test.go @@ -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, "/") +} diff --git a/internal/pkg/conf/conf.go b/internal/pkg/conf/conf.go index 89100c73..45d1bce1 100644 --- a/internal/pkg/conf/conf.go +++ b/internal/pkg/conf/conf.go @@ -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 { diff --git a/internal/pkg/conf/conf_test.go b/internal/pkg/conf/conf_test.go index 45ed01da..678ac2aa 100644 --- a/internal/pkg/conf/conf_test.go +++ b/internal/pkg/conf/conf_test.go @@ -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()) } diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index 8f1cc473..4fe79d33 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -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 = "" 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 { diff --git a/internal/pkg/printer/html_test.go b/internal/pkg/printer/html_test.go index 4a233163..e69539fa 100644 --- a/internal/pkg/printer/html_test.go +++ b/internal/pkg/printer/html_test.go @@ -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) diff --git a/internal/pkg/printer/markdown_test.go b/internal/pkg/printer/markdown_test.go index c5115a3a..9627cdea 100644 --- a/internal/pkg/printer/markdown_test.go +++ b/internal/pkg/printer/markdown_test.go @@ -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) diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go index 1b31d461..020e5688 100644 --- a/internal/pkg/printer/office.go +++ b/internal/pkg/printer/office.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "sort" + "strings" "github.com/phayes/freeport" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" @@ -27,6 +28,7 @@ type officePrinter struct { type OfficePrinterOptions struct { WaitTimeout float64 Landscape bool + PageRanges string } // DefaultOfficePrinterOptions returns the default @@ -35,6 +37,7 @@ func DefaultOfficePrinterOptions(config conf.Config) OfficePrinterOptions { return OfficePrinterOptions{ WaitTimeout: config.DefaultWaitTimeout(), Landscape: false, + PageRanges: "", } } @@ -62,7 +65,7 @@ func (p officePrinter) Print(destination string) error { 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) @@ -88,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() @@ -103,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) diff --git a/internal/pkg/printer/office_test.go b/internal/pkg/printer/office_test.go index b6a0235b..5eb8992e 100644 --- a/internal/pkg/printer/office_test.go +++ b/internal/pkg/printer/office_test.go @@ -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) diff --git a/internal/pkg/printer/url_test.go b/internal/pkg/printer/url_test.go index 2c0b99f7..ef477e75 100644 --- a/internal/pkg/printer/url_test.go +++ b/internal/pkg/printer/url_test.go @@ -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) diff --git a/internal/pkg/xassert/string.go b/internal/pkg/xassert/string.go index 540eee9b..d59bea8a 100644 --- a/internal/pkg/xassert/string.go +++ b/internal/pkg/xassert/string.go @@ -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)) ) diff --git a/internal/pkg/xassert/string_test.go b/internal/pkg/xassert/string_test.go index 87ad8065..fb1c0d4e 100644 --- a/internal/pkg/xassert/string_test.go +++ b/internal/pkg/xassert/string_test.go @@ -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) +} diff --git a/scripts/publish.sh b/scripts/publish.sh index 009fce2d..74e0073d 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -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]}" \ No newline at end of file +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]}" \ No newline at end of file diff --git a/scripts/tests.sh b/scripts/tests.sh index 3824cc6d..814d50f0 100755 --- a/scripts/tests.sh +++ b/scripts/tests.sh @@ -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 \ No newline at end of file diff --git a/test/multipartform.go b/test/multipartform.go index 0bc73508..00e8e552 100644 --- a/test/multipartform.go +++ b/test/multipartform.go @@ -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 { diff --git a/test/testdata.go b/test/testdata.go index e40ab818..fc72472b 100644 --- a/test/testdata.go +++ b/test/testdata.go @@ -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"), } } diff --git a/test/testdata/office/document_with_special_éà.txt b/test/testdata/office/document_with_special_éà.txt new file mode 100644 index 00000000..16eab9d7 --- /dev/null +++ b/test/testdata/office/document_with_special_éà.txt @@ -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. \ No newline at end of file