Compare commits

...

111 Commits
6.0.1 ... 6.4.1

Author SHA1 Message Date
Julien Neuhart
1a9c9a7767 Merge pull request #273 from owsy/pdftk
Upgrade PDFtk
2021-01-07 18:05:36 +01:00
Rhodri Pugh
eb7ce7cef1 use pdftk 3.2.2 2021-01-07 16:17:24 +00:00
Julien Neuhart
68fd8dcd76 Merge pull request #274 from thecodingmachine/github-actions
GitHub Actions
2021-01-07 16:31:54 +01:00
Julien Neuhart
ce8bb2567c Fix typo in publish script 2021-01-07 16:19:21 +01:00
Julien Neuhart
ab3d9bc120 Remove TravisCI, add GitHub Actions 2021-01-07 16:18:48 +01:00
Julien Neuhart
432ec161c1 Add warning in documentation about public facing Gotenberg instances 2020-12-30 15:47:54 +01:00
Julien Neuhart
fcaf4a9b51 Merge pull request #260 from karakanb/add-openapi-docs
Add the first version of the OpenAPI documentation
2020-11-30 15:04:06 +01:00
Burak Karakan
cbb7e6599b Add OpenAPI docs to the links section 2020-11-22 19:59:41 +01:00
Burak Karakan
8f89a144ca Add the first version of the OpenAPI documentation 2020-11-22 19:58:34 +01:00
Julien Neuhart
50bd05b351 Update 15-links.md 2020-11-18 17:30:38 +01:00
Julien Neuhart
4e7f5401ec Merge pull request #241 from Tealsky/bugfix/issue-172
[bugfix/issue-172] fixing health check
2020-09-21 15:38:18 +02:00
Ophélie Volga
3fd56e8c36 [bugfix/issue-172] fixing health check 2020-09-21 14:57:16 +02:00
Julien Neuhart
daf028ab2b Merge pull request #208 from thecodingmachine/6.3.0-dev
6.3.0
2020-06-22 10:58:01 +02:00
Julien Neuhart
5467620b32 adding warning about URL conversion to a Docker Compose service named app 2020-06-09 16:38:28 +02:00
Julien Neuhart
a3cae0f094 Merge branch '6.3.0-dev' of https://github.com/thecodingmachine/gotenberg into 6.3.0-dev 2020-06-09 16:30:22 +02:00
Julien Neuhart
f74eea4314 Merge pull request #204 from WillyReyno/patch-1
Added warning for URLRequest and service named app
2020-06-09 16:30:05 +02:00
Julien Neuhart
04e930a766 fixing typos in documentation 2020-06-09 16:29:14 +02:00
Julien Neuhart
89ac13e805 Fixing linter issues 2020-06-08 10:44:46 +02:00
Julien Neuhart
0b86d38517 go fmt and regenerating documentation 2020-06-08 09:44:37 +02:00
Julien Neuhart
7e76809860 Merge pull request #206 from tomjvdberg/URL-allow-insecure-connection
Url allow insecure connection
2020-06-08 09:28:38 +02:00
tomjvdberg
4d46b98e7f Apply suggestions from code review
Co-authored-by: Julien Neuhart <j.neuhart@thecodingmachine.com>
2020-06-08 08:31:57 +02:00
Julien Neuhart
6c1a46aee5 minor update of the documentation 2020-06-05 15:56:07 +02:00
Julien Neuhart
a7cef5a1c6 got fmt 2020-06-05 15:05:51 +02:00
Julien Neuhart
8a218a6626 using a (more) random directory name for user profile tmp dir + improving performance with a go routine 2020-06-05 15:05:27 +02:00
Julien Neuhart
542fe18dcd avoir directory traversal when uploading a file 2020-06-05 12:06:01 +02:00
Julien Neuhart
25f18ac951 fixing dependencies with master 2020-06-05 12:03:16 +02:00
Julien Neuhart
5540438595 adding missing comment from previous commit 2020-06-05 12:00:57 +02:00
Julien Neuhart
b25456835a sanitizing filename parameter of toHTML function 2020-06-05 11:52:17 +02:00
Julien Neuhart
f86091382f updating dependencies 2020-06-05 09:19:31 +02:00
Julien Neuhart
dbdf8871cc tini no more writeable by gotenberg user 2020-06-05 09:19:12 +02:00
Tom van den Berg
1e6926ee2d Add ignoreCertificateErrors tests + docs 2020-06-02 16:06:17 +02:00
Tom van den Berg
24cbe98ac2 Add feature to have chrome ignore certificate errors when printing using an URL. 2020-06-01 15:31:36 +02:00
Willy Reyno
c590b026d6 Added warning for URLRequest and service named app
A docker-compose service named "app" will systematically return a blank PDF when using `URLRequest()`.
Added a warning to recommend people to rename their service if it's named "app".
2020-06-01 11:15:55 +02:00
Julien Neuhart
d28221aec9 Merge pull request #196 from thecodingmachine/dependabot/go_modules/github.com/sirupsen/logrus-1.6.0
Bump github.com/sirupsen/logrus from 1.5.0 to 1.6.0
2020-05-04 08:32:27 +02:00
dependabot-preview[bot]
df0c6c8c2d Bump github.com/sirupsen/logrus from 1.5.0 to 1.6.0
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.5.0...v1.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 05:06:23 +00:00
Julien Neuhart
876f0f9a14 Improving error message when the removing of a user profile directory fails 2020-05-01 15:14:42 +02:00
Julien Neuhart
f9f0fc1d67 Removing user profile directory when an Office conversion is over 2020-05-01 15:12:51 +02:00
Julien Neuhart
42e357341a Updating dependencies and fixing lint issues 2020-05-01 14:22:42 +02:00
Julien Neuhart
edc5402aad Adding new badge for Docker pulls in README 2020-05-01 14:22:10 +02:00
Julien Neuhart
3f61ffa6fb Updating CONTRIBUTING.md (Go version) + Rebuilding documentation 2020-05-01 13:08:53 +02:00
Julien Neuhart
1c22b889fe Merge branch 'master' of https://github.com/thecodingmachine/gotenberg 2020-05-01 12:58:15 +02:00
Julien Neuhart
e80f5939b2 Updating markdown files according to some users PRs 2020-05-01 12:58:11 +02:00
Julien Neuhart
c068088cc1 Merge pull request #182 from sdawka/patch-1
Update index.html
2020-05-01 12:53:07 +02:00
Julien Neuhart
1767f371f7 Merge branch 'master' of https://github.com/thecodingmachine/gotenberg 2020-05-01 12:52:14 +02:00
Julien Neuhart
5d5caf64fa Merge pull request #148 from thecodingmachine/bugfix/typo-in-readme
Fixing documentation typo
2020-05-01 12:52:06 +02:00
Julien Neuhart
e095b9419a Updating go version 2020-05-01 12:48:48 +02:00
Sahil Dawka
a5ef072f56 Update index.html 2020-03-18 17:26:22 -04:00
Julien Neuhart
5f6a2456d4 Merge pull request #177 from thecodingmachine/dependabot/go_modules/github.com/stretchr/testify-1.5.1
Bump github.com/stretchr/testify from 1.4.0 to 1.5.1
2020-03-06 14:01:58 +01:00
dependabot-preview[bot]
b0f24889f4 Bump github.com/stretchr/testify from 1.4.0 to 1.5.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.4.0 to 1.5.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.4.0...v1.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-06 08:44:44 +00:00
Julien Neuhart
0a69f0fd51 Merge pull request #176 from pgrimaud/master
Fix typo in README.md
2020-03-05 14:47:53 +01:00
Pierre Grimaud
43c0142682 Fix typo in README.md 2020-03-05 14:43:34 +01:00
Julien Neuhart
1ff16cc54b minor refactoring: using default scale from default options 2020-02-10 10:10:49 +01:00
Julien Neuhart
3609cf54c0 Merge pull request #168 from mdjward/feature/chrome-scaling-support
Add support for Chrome scaling.
2020-02-10 10:03:24 +01:00
Matt Ward
198038f5ea Add support for Chrome scaling. 2020-02-01 19:10:40 +00:00
Julien Neuhart
b5a3df85a8 Merge pull request #163 from bgentry/fix-kerning-with-font-hinting-none
use --font-render-hinting=none to fix kerning
2020-01-06 09:32:48 +01:00
Blake Gentry
6e56c6e0d4 use --font-render-hinting=none to fix kerning
When using font render hinting on Chrome headless on Linux, the kerning
is inconsistent. Several issues cover this, including:

* https://github.com/puppeteer/puppeteer/issues/661
* https://github.com/nzzdev/Q-server/pull/193

Disabling that render hinting entirely results in correct kerning.
2019-12-30 20:56:57 -08:00
Julien Neuhart
568f3d6007 Merge pull request #158 from thecodingmachine/6.1.1
6.1.1
2019-12-12 11:35:13 +01:00
Julien Neuhart
6b505a1d06 updating dependencies 2019-12-12 11:22:04 +01:00
Julien Neuhart
acc7e91c55 fixing #157 2019-12-12 11:14:15 +01:00
Julien Neuhart
8e43df8d95 fixing missing renaming: GOTENBERG_REPOSITORY => GOTENBERG_REGISTRY 2019-12-12 11:14:03 +01:00
Julien Neuhart
ca81b382d3 Merge pull request #155 from thecodingmachine/6.1.0
6.1.0
2019-12-10 15:58:39 +01:00
Julien Neuhart
a60d7976f5 fixing typo in go client version 2019-12-10 15:58:21 +01:00
Julien Neuhart
46cf63da37 Merge pull request #122 from thecodingmachine/page_ranges
pageRanges formField
2019-12-10 15:38:01 +01:00
Julien Neuhart
099079b962 fixing typo 2019-12-10 15:25:56 +01:00
Julien Neuhart
5b8121516f updating with base branch 2019-12-10 15:17:25 +01:00
Julien Neuhart
23eacaa78a Merge branch '6.1.0' of https://github.com/thecodingmachine/gotenberg into 6.1.0 2019-12-10 14:40:12 +01:00
Julien Neuhart
659292a3f1 updating documentation for go client v7 2019-12-10 14:39:15 +01:00
Julien Neuhart
f88756d3a8 Merge pull request #153 from thecodingmachine/custom_headers
Custom headers
2019-12-09 17:55:14 +01:00
Julien Neuhart
81e7bab22a updating documentation with Golang examples for custom HTTP headers 2019-12-09 17:26:00 +01:00
Julien Neuhart
1eeb5fc2b0 typo in variable 2019-12-09 17:20:56 +01:00
Julien Neuhart
126cdd73e4 adding PHP example for custom HTTP headers in documentation + improving logs for custom HTTP headers 2019-12-09 16:50:08 +01:00
Julien Neuhart
2251267ae4 Merge branch '6.1.0' of https://github.com/thecodingmachine/gotenberg into custom_headers 2019-12-09 15:15:18 +01:00
Julien Neuhart
f3e4b6d9a2 Merge pull request #156 from thecodingmachine/custom_uid
Custom uid/gid
2019-12-09 15:15:11 +01:00
Julien Neuhart
b22379537b Merge branch '6.1.0' of https://github.com/thecodingmachine/gotenberg into custom_headers 2019-12-09 15:14:15 +01:00
Julien Neuhart
feb5bb0653 updating with current version of 6.1.0 branch 2019-12-09 14:38:31 +01:00
Julien Neuhart
ebfe99be79 adding documentation on how to change gid/uid + renamming DOCKER_REPOSITORY to DOCKER_REGISTRY 2019-12-09 14:36:01 +01:00
Julien Neuhart
49c1b939d5 Merge pull request #154 from thecodingmachine/http_namespacing
ROOT_PATH for customizing the root path
2019-12-06 17:39:18 +01:00
Julien Neuhart
84a37f124b adding documentation for ROOT_PATH 2019-12-06 17:30:36 +01:00
Julien Neuhart
f0f48f4ddf adding ROOT_PATH + tests 2019-12-06 17:23:17 +01:00
Julien Neuhart
cf15e3a9a1 updating documentation (without go & php examples) 2019-12-06 16:18:14 +01:00
Julien Neuhart
927f98c66b adding custom headers for remoteURL 2019-12-06 11:22:11 +01:00
Julien Neuhart
782f6ac27e rolling back to golangci-lint 1.20.1 + moving normize filename to resource & tests with a file with special chars in its name + webhookurl custom headers done + preparing remote url custom headers 2019-12-05 18:16:02 +01:00
Julien Neuhart
5140e4ec9a updating golangci-lint 2019-12-05 15:43:24 +01:00
Julien Neuhart
e255f9bfc5 adding c# community client 2019-12-03 15:21:14 +01:00
David Négrier
9e9b369dc2 Fixing documentation typo
s/others/other/g
2019-11-20 19:03:17 +01:00
Julien Neuhart
765dc734a4 adding new logo 2019-11-20 11:44:18 +01:00
Julien Neuhart
3abe8a1103 improving documentation about documentation generation 2019-11-20 11:43:51 +01:00
Julien Neuhart
73333074fd Merge pull request #146 from alanpoulain/doc-fix-php-param-examples
[Doc] Fix dest param in PHP examples
2019-11-15 15:47:26 +01:00
Alan Poulain
a415786e1c Modify MD files 2019-11-15 15:41:51 +01:00
Alan Poulain
a3a1077dea Return of store method is void 2019-11-15 15:22:40 +01:00
Alan Poulain
58a812c2c9 Fix dest param in PHP examples 2019-11-15 15:17:27 +01:00
Julien Neuhart
7e69ec4367 Merge pull request #141 from thecodingmachine/merge_order
fixing merge order
2019-11-04 17:14:45 +01:00
Julien Neuhart
4623c01896 fixing merging order 2019-11-04 17:01:27 +01:00
Julien Neuhart
e85322de62 Merge pull request #137 from thecodingmachine/6.0.3
6.0.3
2019-10-31 17:53:48 +01:00
Julien Neuhart
090ff3da13 fixing typos in markdown files too 2019-10-31 17:44:11 +01:00
Julien Neuhart
fe068b1bfb Merge pull request #133 from dmfaux/patch-1
Corrected typos
2019-10-31 17:42:40 +01:00
Julien Neuhart
d3b0a685b9 adding a (very) small section for Cloud Run 2019-10-31 17:37:06 +01:00
Julien Neuhart
245c04fc5a Merge pull request #138 from odarriba/use-tiny-static
Use statically linked version of tini
2019-10-31 17:14:01 +01:00
Julien Neuhart
94acb0c6ae updating documentation with details on how to make background-color and color CSS properties work in header/footer 2019-10-31 17:12:34 +01:00
Julien Neuhart
ad4a16cc4e adding community clients section 2019-10-31 17:03:51 +01:00
Óscar de Arriba
dae39113f6 use statically linked version of tini 2019-10-31 17:02:29 +01:00
taylor
1e1cb8fe57 Make the gotenberg user owner of /tini binary to fix running image in cloud environments 2019-10-31 16:31:58 +01:00
David Faux
606254d941 Corrected typos
Correct `extend` to `extent`
Correct `are` to `as`
2019-10-28 07:28:40 +02:00
Julien Neuhart
8fd51b9e9e fixing typo in package/Dockerfile 2019-10-14 11:32:37 +02:00
Julien Neuhart
f90442cfb1 Merge pull request #124 from thecodingmachine/wrong_http_error_codes
improving HTTP error code and fixing #67
2019-10-08 11:05:29 +02:00
Julien Neuhart
2a44c0f65a improving HTTP error code and fixing #67 2019-10-08 10:50:15 +02:00
Julien Neuhart
3255f8af0c updating documentation: if office and more than one document, page ranges will be applied for each of them 2019-10-05 12:13:06 +02:00
Julien Neuhart
ca0784fa59 adding tests for page ranges 2019-10-05 12:09:39 +02:00
Julien Neuhart
827060b6ce emphazing that pageRanges does not work if more than one office document 2019-10-04 18:22:46 +02:00
Julien Neuhart
60c069cb1f updating documention with pageRanges 2019-10-04 18:19:13 +02:00
Julien Neuhart
70dfea5876 adding pageRanges formField 2019-10-04 17:49:26 +02:00
68 changed files with 2763 additions and 889 deletions

View File

@@ -8,7 +8,7 @@ find below useful information about how to contribute to the Gotenberg project.
### Install from sources
1. Install and run the latest version of Docker
2. Verify your Go version (>= 1.13)
2. Verify your Go version (>= 1.14)
3. Fork this repository
4. Clone it outside of your `GOPATH` (we're using Go modules)

View File

@@ -40,6 +40,6 @@ Fixes #
- [ ] Have you lint your code locally prior to submission (`make lint`)?
- [ ] Have you written new tests for your core changes, as applicable?
- [ ] Have you successfully ran tests with your changes locally (`make tests`)?
- [ ] Have you updated the documentation (`make doc`)?
- [ ] Have you updated the documentation (Markdown files under `build > docs > content` and then `make doc`)?
- [ ] I have squashed any insignificant commits
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code

View File

@@ -1,14 +0,0 @@
name: Publish
on:
push:
branches:
- releases/*
jobs:
publish:
name: Publish to Docker Hub
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- run: echo $GITHUB_REF
#- run: make publish VERSION=$GITHUB_REF DOCKER_USER=${{ secrets.DOCKER_USER }} DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,12 +0,0 @@
name: Push
on: push
jobs:
tests:
name: Lint, tests and code coverage
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
#- run: make lint
#- run: make tests CODE_COVERAGE=1
#- run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}

View File

@@ -0,0 +1,26 @@
name: tests_maybe_release
on:
push:
pull_request:
release:
types: [published]
jobs:
tests_maybe_release:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v1
- name: Lint code
run: make lint
- name: Run tests
run: |
make tests CODE_COVERAGE=1
bash <(curl -s https://codecov.io/bash)
- name: Release
if: ${{ github.event_name == 'release' }}
run: make publish VERSION=${{ github.event.release.tag_name }} DOCKER_USER=${{ secrets.DOCKER_USER }} DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,26 +0,0 @@
sudo: required
language: generic
services:
- docker
env:
global:
- secure: d6pliM7iR7KJ3TQkr1C0tHi0HgU6Vdmc/ycw11XrXs794/dgj6g9UDrhgcvT2mTzOVIUNouHx7gyEWAlwJv7p8PPyguHJfIsyqMWfaXNPoytTx1ETUiygl1G/UsvXVdry6I5tZF/U+/PJ3hBXoAvo8EOFe0/MxxvP3ghCeeCpeEwEdoYP2jbADGffywVUNI7OEdmS29A28GusXXp2cWCIZYSq5Gn5iu1ZMPaJA6SkRTXpS+iVVLUvlbz5HW09ETFfurTvsdz5Ze4JICiuQt///CR1uwdxD8K7Ils/mkenaMtja9NHA9RdQBa51Azn8iCLsObCTYc52Zb6atbCi/X4ae9M8GA8qaaLigNz5G4v1BHneJylYfWKj5GwqUA3VmRu1mlEw7d2sN2uFRmgIl5kgsYYyOVOLBOKaJWhyBhNnR4U8KeqwUkOnlSdmNZnjfexXXBEcKHen1SozpXwMnzZh8LZuN6BNYd6TVpKmjulGI3vqNMM0c4JJZ9pSm4q5FjbNGQArwCsujDDwY4bCGZSJJTPMXjYOrx4yJ1RY+0TpyV9oDU3ogEqaWnhrkgrphfiYBlDY2bEvrxMTMukLCxJb0Nru6LGCpg4X1P9hu9r0gceqUSnBgdJI4rYTNddenxlEd4j/Thqv835stJWz6YoV+rfvERuSUpntl8xvrk3U8=
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
stages:
- tests
- publish
jobs:
include:
- stage: tests
script:
- make lint
- make tests CODE_COVERAGE=1
- bash <(curl -s https://codecov.io/bash)
- stage: publish
if: tag IS present
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS

View File

@@ -1,11 +1,13 @@
GOLANG_VERSION=1.13
GOLANG_VERSION=1.14
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.27.0
CODE_COVERAGE=0
TINI_VERSION=0.18.0
TINI_VERSION=0.19.0
MAXIMUM_WAIT_TIMEOUT=30.0
MAXIMUM_WAIT_DELAY=10.0
MAXIMUM_WEBHOOK_URL_TIMEOUT=30.0
@@ -15,16 +17,18 @@ DEFAULT_LISTEN_PORT=3000
DISABLE_GOOGLE_CHROME=0
DISABLE_UNOCONV=0
LOG_LEVEL=INFO
ROOT_PATH=/
DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576
GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=0
# 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:
@@ -33,31 +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) -e GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=$(GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REGISTRY)/gotenberg:$(VERSION)
# publish Gotenberg images according to version.
publish:
make workspace
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REPOSITORY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)

View File

@@ -1,5 +1,5 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
<img src="https://user-images.githubusercontent.com/8983173/69229423-ac731300-0b85-11ea-8c2e-2cc00ecdb269.PNG" alt="Gotenberg logo" width="250" height="250" />
</p>
<h3 align="center">Gotenberg</h3>
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
@@ -13,7 +13,7 @@ At TheCodingMachine, we build a lot of web applications (intranets, extranets an
* HTML and Markdown conversions using Google Chrome headless
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beautiful PDFs!
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
## Quick start
@@ -31,12 +31,15 @@ to learn how to interact with it!
## Badges
[![Docker pulls](https://img.shields.io/docker/pulls/thecodingmachine/gotenberg)](https://hub.docker.com/r/thecodingmachine/gotenberg)
[![Docker image layers](https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:6.svg)](https://microbadger.com/images/thecodingmachine/gotenberg:6)
[![Travis CI](https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master)](https://travis-ci.org/thecodingmachine/gotenberg)
[![GitHub Actions](https://github.com/thecodingmachine/gotenberg/workflows/tests_maybe_release/badge.svg)](https://github.com/thecodingmachine/gotenberg/workflows/tests_maybe_release)
[![GoDoc](https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg)](https://godoc.org/github.com/thecodingmachine/gotenberg)
[![Codecov](https://codecov.io/gh/thecodingmachine/gotenberg/branch/master/graph/badge.svg)](https://codecov.io/gh/thecodingmachine/gotenberg)
[![Go Report Card](https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg)](https://goreportcard.com/report/thecodingmachine/gotenberg)
---
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
</p>

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
assets/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -58,7 +58,10 @@ RUN curl -Ls $UNO_URL -o /usr/bin/unoconv &&\
# | https://github.com/thecodingmachine/gotenberg/issues/29
# |
RUN apt-get -y install pdftk
ARG PDFTK_VERSION=924565150
RUN wget -O /usr/bin/pdftk "https://gitlab.com/pdftk-java/pdftk/-/jobs/${PDFTK_VERSION}/artifacts/raw/build/native-image/pdftk" \
&& chmod a+x /usr/bin/pdftk
# |--------------------------------------------------------------------------
# | Fonts
@@ -104,7 +107,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

View File

@@ -4,7 +4,7 @@ title: Install
Gotenberg is shipped within a Docker image.
> It uses a dedicated non-root user called `gotenberg` with uid and gid `1001`.
**[Gotenberg should ONLY be used in a trusted network by trusted applications. Do NOT expose Gotenberg to the external world.](https://blazej-adamczyk.medium.com/0-day-bug-breaks-multi-million-dollar-system-38c9e31b27e9)**
You may start it with:
@@ -14,6 +14,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=version
```
> `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:
@@ -23,7 +40,7 @@ version: '3'
services:
# your others services
# your other services
gotenberg:
image: thecodingmachine/gotenberg:6
@@ -39,7 +56,7 @@ Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0
> The more resources are granted, the quicker will be the conversions.
In the deployment specification of the pod, also specify the uid `1001` of the user `gotenberg`:
In the deployment specification of the pod, also specify the uid of the user `gotenberg`:
```
securityContext:
@@ -47,5 +64,10 @@ securityContext:
runAsUser: 1001
```
## Cloud Run (Google Cloud)
If you're looking for cost savings, you might be interested by [Cloud Run](https://cloud.google.com/run).
However, according to some users, doing asynchronous conversion (with a webhook) might not working.
In the following examples, we will assume your
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).

View File

@@ -7,9 +7,11 @@ We provide clients in various languages for easing the interactions with the API
## Go client
```bash
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v6
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7
```
See also the example from the [README](https://github.com/thecodingmachine/gotenberg-go-client/blob/master/README.md).
## PHP client
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
@@ -18,8 +20,15 @@ Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-a
$ composer require php-http/guzzle6-adapter
```
Then the PHP client:
Then the [PHP client](https://github.com/thecodingmachine/gotenberg-php-client):
```bash
$ composer require thecodingmachine/gotenberg-php-client
```
See also the example from the [README](https://github.com/thecodingmachine/gotenberg-php-client/blob/master/README.md).
## Community clients
* [JavaScript/TypeScript client](https://github.com/yumauri/gotenberg-js-client) by [yumauri](https://github.com/yumauri)
* [C# client](https://github.com/ChangemakerStudios/GotenbergSharpApiClient) by [ChangemakerStudios](https://github.com/ChangemakerStudios)

View File

@@ -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`
@@ -46,6 +58,14 @@ The hard limit is 100 MB and is defined by Google Chrome itself.
> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`.
> See the [rpcc buffer size section](#html.rpcc_buffer_size).
## Google Chrome ignore certificate errors
When performing a [URL](#url) conversion, Google Chrome will not accept certificate errors.
You may allow insecure connections by setting the `GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS` environment variable to `"1"`.
**You should be careful with this feature and only enable it in your development environment.**
## Disable LibreOffice (unoconv)
You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`.

View File

@@ -39,14 +39,13 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v6"
import "github.com/thecodingmachine/gotenberg-go-client/v7"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
dest := "result.pdf"
c.Store(req, dest)
}
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
@@ -57,10 +56,10 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Header and footer
@@ -103,6 +102,7 @@ There are some limitations:
* `footer.html` CSS properties override the ones from `header.html`
* only fonts installed in the Docker image are loaded (see the [fonts section](#fonts))
* images only work using a `base64` encoded source (`<img src="data:image/png;base64, iVBORw0K... />`)
* `background-color` and `color` CSS properties require an additional `-webkit-print-color-adjust: exact` CSS property in order to work
### cURL
@@ -119,16 +119,17 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v6"
import "github.com/thecodingmachine/gotenberg-go-client/v7"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.Header("header.html")
req.Footer("footer.html")
dest := "result.pdf"
c.Store(req, dest)
}
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
header, _ := gotenberg.NewDocumentFromPath("header.html", "/path/to/file")
footer, _ := gotenberg.NewDocumentFromPath("footer.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.Header(header)
req.Footer(footer)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
@@ -139,14 +140,14 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$header = DocumentFactory::makeFromPath('header.html', 'header.html');
$footer = DocumentFactory::makeFromPath('footer.html', 'footer.html');
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$header = DocumentFactory::makeFromPath('header.html', '/path/to/file');
$footer = DocumentFactory::makeFromPath('footer.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setHeader($header);
$request->setFooter($footer);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Assets
@@ -156,7 +157,7 @@ You may also send additional files. For instance: images, fonts, stylesheets and
The only requirement is to make sure that their paths
are on the same level as the `index.html` file.
In others words, this will work:
In other words, this will work:
```html
<!doctype html>
@@ -209,15 +210,17 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v6"
import "github.com/thecodingmachine/gotenberg-go-client/v7"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.Assets("font.woff", "img.gif", "style.css")
dest := "result.pdf"
c.Store(req, dest)
}
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file")
img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file")
font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.Assets(style, img, font)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
@@ -228,23 +231,23 @@ 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);
```
## Paper size, margins, orientation
## Paper size, margins, orientation, scaling
You may also customize the resulting PDF format.
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation and 100% (`1.0`) page scale.
> Paper size and margins have to be provided in `inches`. Same for margins.
@@ -262,23 +265,24 @@ $ curl --request POST \
--form marginLeft=0 \
--form marginRight=0 \
--form landscape=true \
--form scale=0.75 \
-o result.pdf
```
### 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)
req.Scale(0.75)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
@@ -290,12 +294,60 @@ 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";
$request->setScale(0.75);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Page ranges
You may specify the page ranges to convert.
The format is the same as the one from the print options
of Google Chrome, e.g. `1-5,8,11-13`.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form pageRanges='1-3,5' \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.PageRanges("1-3,5")
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setPageRanges('1-3,5');
$dest = 'result.pdf';
$client->store($request, $dest);
```
@@ -321,15 +373,14 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v6"
import "github.com/thecodingmachine/gotenberg-go-client/v7"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.WaitDelay(5.5)
dest := "result.pdf"
c.Store(req, dest)
}
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.WaitDelay(5.5)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
@@ -341,10 +392,10 @@ use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setWaitDelay(5.5);
$dest = "result.pdf";
$dest = 'result.pdf';
$client->store($request, $dest);
```
@@ -373,15 +424,14 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v6"
import "github.com/thecodingmachine/gotenberg-go-client/v7"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.GoogleChromeRpccBufferSize(1048576)
dest := "result.pdf"
c.Store(req, dest)
}
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.GoogleChromeRpccBufferSize(1048576)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
@@ -393,9 +443,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);
```

View File

@@ -14,6 +14,9 @@ named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conv
> **Attention:** when converting a website to PDF, you should remove all margins.
> If not, some of the content of the page might be hidden.
> **Attention:** if you try to convert a URL from a Docker Compose service named `app` (i.e. `removeURL` = `http://app/an/entrypoint`),
> the resulting PDF will be blank. Make sure to rename your service to avoid this issue.
### cURL
```bash
@@ -31,15 +34,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 +52,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);
```

View File

@@ -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);
```

View File

@@ -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);
```

View File

@@ -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);
```

View File

@@ -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);
```

View File

@@ -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);
```

View File

@@ -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);

View File

@@ -5,10 +5,10 @@ title: Scalability
The API uses under the hood intricate programs.
Gotenberg tries to abstract as much complexity as possible but it can
only do it to a certain extend.
only do it to a certain extent.
For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk
instances are there are requests. The limitation here is the available memory and CPU usage.
instances as there are requests. The limitation here is the available memory and CPU usage.
On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel.
Indeed, Google Chrome misbehaves if there are too many concurrent conversions.
@@ -37,7 +37,7 @@ version: '3'
services:
# your others services
# your other services
gotenberg:
image: thecodingmachine/gotenberg:6

View File

@@ -2,8 +2,11 @@
title: Links
---
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
</p>
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
* Thanks to [@mafredri](https://github.com/mafredri) for its help and its wonderful [cdp](https://github.com/mafredri/cdp) library
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
* Thanks to [@mafredri](https://github.com/mafredri) for his help and his wonderful [cdp](https://github.com/mafredri/cdp) library
* OpenAPI documentation for the API can be found [on GitHub](https://github.com/thecodingmachine/gotenberg/blob/master/docs/openapi.yaml)

View File

@@ -1,17 +1,6 @@
FROM thecodingmachine/gotenberg:workspace
# |--------------------------------------------------------------------------
# | GolangCI-Lint
# |--------------------------------------------------------------------------
# |
# | Installs GolangCI-Lint, a linters Runner for Go. 5x faster
# | than gometalinter.
# |
ARG GOLANGCI_LINT_VERSION
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
golangci-lint --version
FROM golangci/golangci-lint:v$GOLANGCI_LINT_VERSION
# |--------------------------------------------------------------------------
# | Final touch
@@ -21,16 +10,13 @@ RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.s
# |
# Define our working directory outside of $GOPATH (we're using go modules).
USER gotenberg
WORKDIR /gotenberg/lint
# Install module dependencies.
COPY --chown=gotenberg:gotenberg go.mod go.sum ./
# Copy our code source.
COPY . .
# Install module dependencies.
RUN go mod download &&\
go mod verify
# Copy our code source.
COPY --chown=gotenberg:gotenberg . .
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen" ]
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit", "--disable=gomnd", "--disable=goimports" ]

View File

@@ -2,7 +2,7 @@
# | Binary
# |--------------------------------------------------------------------------
# |
# | Buils Gotenberg binary.
# | Builds Gotenberg binary.
# |
FROM thecodingmachine/gotenberg:workspace AS workspace
@@ -10,7 +10,7 @@ FROM thecodingmachine/gotenberg:workspace AS workspace
ARG VERSION
ENV GOOS=linux \
GOARCH=amd64 \
GOARCH=amd64 \
CGO_ENABLED=0
# Define our workding outside of $GOPATH (we're using go modules).
@@ -42,7 +42,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
ARG TINI_VERSION
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static /tini
RUN chmod +x /tini
ENTRYPOINT [ "/tini", "--" ]
@@ -59,4 +59,4 @@ USER gotenberg
WORKDIR /gotenberg
EXPOSE 3000
CMD [ "gotenberg" ]
CMD [ "gotenberg" ]

View File

@@ -1,6 +1,7 @@
package main
import (
"errors"
"fmt"
"net/http"
"os"
@@ -24,11 +25,11 @@ func main() {
if err != nil {
systemLogger.FatalOp(op, err)
}
systemLogger.InfofOp(op, "Gotenberg %s", version)
systemLogger.DebugfOp(op, "configuration: %+v", config)
systemLogger.InfoOpf(op, "Gotenberg %s", version)
systemLogger.DebugOpf(op, "configuration: %+v", config)
if !config.DisableGoogleChrome() {
// start Google Chrome headless.
if err := chrome.Start(systemLogger); err != nil {
if err := chrome.Start(systemLogger, config.GoogleChromeIgnoreCertificateErrors()); err != nil {
systemLogger.FatalOp(op, err)
}
}
@@ -36,9 +37,9 @@ func main() {
srv := xhttp.New(config)
// run our API in a goroutine so that it doesn't block.
go func() {
systemLogger.InfofOp(op, "http server started on port '%d'", config.DefaultListenPort())
systemLogger.InfoOpf(op, "http server started on port '%d'", config.DefaultListenPort())
if err := srv.Start(fmt.Sprintf(":%d", config.DefaultListenPort())); err != nil {
if err != http.ErrServerClosed {
if errors.Is(err, http.ErrServerClosed) {
systemLogger.FatalOp(op, err)
}
}

File diff suppressed because it is too large Load Diff

558
docs/openapi.yaml Normal file
View File

@@ -0,0 +1,558 @@
openapi: 3.0.0
info:
title: Gotenberg
version: 6.3.1
license:
name: MIT
url: 'https://github.com/thecodingmachine/gotenberg/blob/master/LICENSE'
contact:
url: 'https://github.com/thecodingmachine/gotenberg'
description: >-
A Docker-powered stateless API for converting HTML, Markdown and Office
documents to PDF.
servers:
- url: 'http://localhost:3000'
description: Local server with the default Docker image and port
paths:
/convert/html:
post:
summary: Convert a given HTML file to PDF
description: >-
Send an HTML file called `index.html` as a multipart form request, and
get the resulting PDF file. You can optionally include `header.html` and
`footer.html` files as part of the request as well.
An example `index.html` file can be as follows:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
```
You may also add a header and/or a footer in the resulting PDF by
sending a file named `header.html` and `footer.html` respectively. Both
the header and the footer files has to be a complete HTML document. An
example `footer.html` can be as follows:
```html
<html>
<head>
<style>
body {
font-size: 8rem;
margin: 4rem auto;
}
</style>
</head>
<body>
<p>
<span class="pageNumber"></span> of <span class="totalPages"></span>
</p>
</body>
</html>
```
The following classes will allow you to inject printing values in your
document:
- `date`: Formatted print date.
- `title`: Document title.
- `pageNumber`: Current page number.
- `totalPage`: Total pages in the document.
There are some limitations with header and footer files:
- JavaScript is not executed.
- External resources are not loaded.
- The CSS properties are independant of the ones used in the
`index.html` file.
- `footer.html` CSS properties override the ones from `header.html`.
- Only fonts installed in the Docker image are loaded (see the fonts
section)
- Images only work using a `base64` encoded source, e.g. `<img
src="data:image/png;base64, iVBORw0K... />`
- `background-color` and `color` CSS properties require an additional
`-webkit-print-color-adjust: exact` CSS property in order to work.
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/HTMLConvertRequestBody'
description: >-
The request body must have an `index.html` file in the `files` array,
as well as all the referred resources on the same level as the
`index.html` file. The request can also include `header.html` and
`footer.html`, given the limitations in the API description above.
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
/convert/url:
post:
summary: Convert the contents of a given URL to PDF
description: >-
Send a remote URL in your API request via the `remoteURL` parameter, and
get the resulting PDF file. The API will fetch the given URL and render
the page to PDF using the underlying headless Chrome instance.
requestBody:
required: true
description: >-
The URL conversion request has to be a `multipart/form-data` request
that includes a `remoteURL`. The API uses a headless Chrome instance
to do the conversion, therefore print parameter such as margins and
paper size are also accepted as optional parameters.
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/URLConvertRequestBody'
examples: {}
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
/convert/markdown:
post:
summary: Convert a Markdown file to PDF
description: >-
Send an HTML file called `index.html` as a multipart form request and
embed a markdown file into the HTML file using the Golang template
function `toHTML`, the API will convert the markdown and embed it into
the HTML this way and render the resulting page. Markdown conversion
works almost exactly the same way as HTML conversion, therefore refer to
the HTML conversion page for all the options you can use when converting
Markdown documents as well. You can optionally include `header.html` and
`footer.html` files as part of the request as well.
An example `index.html` file can be as follows:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
{{ toHTML .DirPath "file.md" }}
</body>
</html>
```
Whereas your `file.md` file in the same level would be like:
```md
# Title
Content
```
The API will convert the markdown to HTML and embed it into your
`index.html` file, then render the resulting page. You can include your
own styling and more in your HTML file.
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/MarkdownConvertRequestBody'
description: >-
The request body must have an `index.html` file in the `files` array,
as well as all the referred markdown resources on the same level as
the `index.html` file.
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
/convert/office:
post:
summary: Convert an Office document to PDF
description: >-
Send one or more Office documents and get the resulting PDF file by
merging all the files. The following file extensions are accepted:
- `.txt`
- `.rtf`
- `.fodt`
- `.doc`
- `.docx`
- `.odt`
- `.xls`
- `.xlsx`
- `.ods`
- `.ppt`
- `.pptx`
- `.odp`
All files will be merged into a single PDF.
> **Attention:** The files will be merged alphabetically for the
resulting PDF.
You may also specify the page ranges to convert from the incoming Office
documents. The expected 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.
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/OfficeConvertRequestBody'
description: >-
The request body must have an `index.html` file in the `files` array,
as well as all the referred markdown resources on the same level as
the `index.html` file.
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
content:
application/json:
schema:
type: object
properties: {}
/convert/merge:
post:
summary: Merge multiple PDFs into a single PDF
description: >-
You can send multiple PDF files to this endpoint, the API will merge
them into a single PDF and return the resulting PDF file.
> **Attention:** The PDF files will be merged alphabetically.
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
files:
type: array
description: >-
List of HTML files to be converted to PDF. An `index.html`
file is required, and any other resources that are
referenced through the HTML file must be included as well.
All the referenced files must be on the same level as the
`index.html` file.
items:
type: string
format: binary
resultFilename:
type: string
example: output.pdf
description: >-
If provided, the API will return the resulting PDF file with
the given filename. Otherwise a random filename is used.
required:
- files
description: >-
The request body must have an `index.html` file in the `files` array,
as well as all the referred markdown resources on the same level as
the `index.html` file.
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
/ping:
get:
summary: Your GET endpoint
tags: []
responses:
'200':
description: The API is working fine.
operationId: ''
description: >-
A simple endpoint to use as a healthcheck for the API availability with
a simple request.
components:
schemas:
HTMLConvertRequestBody:
title: HTML Conversion Request Body
type: object
properties:
files:
type: array
description: >-
List of HTML files to be converted to PDF. An `index.html` file is
required, and any other resources that are referenced through the
HTML file must be included as well. All the referenced files must be
on the same level as the `index.html` file.
items:
type: string
format: binary
marginTop:
type: number
example: 0
default: 1
description: Top margin for the page in inches.
marginBottom:
type: number
example: 0
default: 1
description: Bottom margin for the page in inches.
marginLeft:
type: number
example: 0
default: 1
description: Left margin for the page in inches.
marginRight:
type: number
example: 0
default: 1
description: Right margin for the page in inches.
paperWidth:
type: number
example: 8.27
description: >-
Paper width to be used while rendering the PDF. The default page
size is A4.
paperHeight:
type: number
example: 11.69
description: >-
Paper height to be used while rendering the PDF. The default page
size is A4.
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
resultFilename:
type: string
example: output.pdf
description: >-
If provided, the API will return the resulting PDF file with the
given filename. Otherwise a random filename is used.
required:
- files
MarkdownConvertRequestBody:
title: Markdown Conversion Request Body
type: object
properties:
files:
type: array
description: >-
List of HTML files to be converted to PDF. An `index.html` file is
required, and any other resources that are referenced through the
HTML file must be included as well. All the referenced files must be
on the same level as the `index.html` file.
items:
type: string
format: binary
marginTop:
type: number
example: 0
default: 1
description: Top margin for the page in inches.
marginBottom:
type: number
example: 0
default: 1
description: Bottom margin for the page in inches.
marginLeft:
type: number
example: 0
default: 1
description: Left margin for the page in inches.
marginRight:
type: number
example: 0
default: 1
description: Right margin for the page in inches.
paperWidth:
type: number
example: 8.27
description: >-
Paper width to be used while rendering the PDF. The default page
size is A4.
paperHeight:
type: number
example: 11.69
description: >-
Paper height to be used while rendering the PDF. The default page
size is A4.
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
resultFilename:
type: string
example: output.pdf
description: >-
If provided, the API will return the resulting PDF file with the
given filename. Otherwise a random filename is used.
required:
- files
URLConvertRequestBody:
title: URL Conversion Request Body
type: object
properties:
remoteURL:
type: string
example: 'https://google.com'
marginTop:
type: number
example: 0
default: 1
description: Top margin for the page in inches.
marginBottom:
type: number
example: 0
default: 1
description: Bottom margin for the page in inches.
marginLeft:
type: number
example: 0
default: 1
description: Left margin for the page in inches.
marginRight:
type: number
example: 0
default: 1
description: Right margin for the page in inches.
paperWidth:
type: number
example: 8.27
description: >-
Paper width to be used while rendering the PDF. The default page
size is A4.
paperHeight:
type: number
example: 11.69
description: >-
Paper height to be used while rendering the PDF. The default page
size is A4.
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
resultFilename:
type: string
example: output.pdf
description: >-
If provided, the API will return the resulting PDF file with the
given filename. Otherwise a random filename is used.
required:
- remoteURL
OfficeConvertRequestBody:
title: Office Conversion Request Body
type: object
properties:
files:
type: array
description: >-
List of HTML files to be converted to PDF. An `index.html` file is
required, and any other resources that are referenced through the
HTML file must be included as well. All the referenced files must be
on the same level as the `index.html` file.
items:
type: string
format: binary
pageRanges:
type: string
example: 1-3
description: >-
The page ranges to be converted to PDF for the incoming Office
documents. **If there are multiple files sent to the API, this page
range will apply to all of the documents**.
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
resultFilename:
type: string
example: output.pdf
description: >-
If provided, the API will return the resulting PDF file with the
given filename. Otherwise a random filename is used.
required:
- files
MergeFilesRequestBody:
title: Merge Files Request Body
type: object
properties:
files:
type: array
description: >-
List of HTML files to be converted to PDF. An `index.html` file is
required, and any other resources that are referenced through the
HTML file must be included as well. All the referenced files must be
on the same level as the `index.html` file.
items:
type: string
format: binary
resultFilename:
type: string
example: output.pdf
description: >-
If provided, the API will return the resulting PDF file with the
given filename. Otherwise a random filename is used.
required:
- files
securitySchemes: {}
responses:
SuccessfulPDF:
description: Resulting PDF file from the conversion.
content:
application/pdf:
schema:
type: string
format: binary

View File

@@ -34,7 +34,7 @@
<div class="Container">
<div class="Header">
<div class="Title center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
<img src="https://user-images.githubusercontent.com/8983173/69229423-ac731300-0b85-11ea-8c2e-2cc00ecdb269.PNG" alt="Gotenberg logo" width="250" height="250" />
<span class="text">{{.Title}}</span>
<span class="subtext">{{.Subtitle}}</span>
</div>
@@ -58,7 +58,8 @@
{{.Content}}
</div>
{{end}}
<div class="Footer"></div>
<div class="Footer">
</div>
<script src="theme/gotenberg/js/index.js"></script>
</div>
</div>

29
go.mod
View File

@@ -1,27 +1,26 @@
module github.com/thecodingmachine/gotenberg
go 1.13
go 1.14
require (
github.com/dustin/go-humanize v1.0.0
github.com/google/go-cmp v0.3.1 // indirect
github.com/gorilla/websocket v1.4.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo/v4 v4.1.10
github.com/kr/text v0.2.0 // indirect
github.com/labstack/echo/v4 v4.1.16
github.com/labstack/gommon v0.3.0
github.com/mafredri/cdp v0.24.2
github.com/mattn/go-isatty v0.0.9
github.com/mafredri/cdp v0.28.0
github.com/mattn/go-isatty v0.0.12
github.com/microcosm-cc/bluemonday v1.0.2
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad // indirect
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
)

95
go.sum
View File

@@ -1,37 +1,39 @@
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.1.10 h1:/yhIpO50CBInUbE/nHJtGIyhBv0dJe2cDAYxc3V3uMo=
github.com/labstack/echo/v4 v4.1.10/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/mafredri/cdp v0.24.2 h1:Rzhj/EQw9opbiwUpNML7P+4Hvf0/nSYPaDbiCEpILOM=
github.com/mafredri/cdp v0.24.2/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mafredri/cdp v0.28.0 h1:v/LWC3GpactA1EaS737bl+eNUhvOaDh+FzvyoDxNM5o=
github.com/mafredri/cdp v0.28.0/go.mod h1:11586MgpyJuQR4qiXQ/7HBame2FtGIBzymaUHZcQ0PY=
github.com/mafredri/go-lint v0.0.0-20180911205320-920981dfc79e/go.mod h1:k/zdyxI3q6dup24o8xpYjJKTCf2F7rfxLp6w/efTiWs=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -40,42 +42,61 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200330183114-f8bfb4ee3038 h1:FUufmZ17Dp41BwLDPii6R8vp0QtIquTiDIxcbFlY1ek=
golang.org/x/tools v0.0.0-20200330183114-f8bfb4ee3038/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -8,28 +8,79 @@ import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/chrome"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
"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(config))
if !config.DisableGoogleChrome() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
htmlEndpoint(config),
urlEndpoint(config),
markdownEndpoint(config),
)
}
if !config.DisableUnoconv() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
officeEndpoint(config),
)
}
for _, endpoint := range multipartFormDataEndpoints {
if endpoint == path {
return true
}
}
return false
}
// pingHandler is the handler for healthcheck.
func pingHandler(c echo.Context) error {
const op string = "xhttp.pingHandler"
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
config := ctx.Config()
logger.DebugOp(op, "handling ping request...")
if !config.DisableGoogleChrome() {
_, err := chrome.IsViable(logger)
if err != nil {
return xerror.New(op, err)
}
}
return nil
}
@@ -98,6 +149,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,
@@ -198,13 +250,13 @@ func convert(ctx context.Context, p printer.Printer) error {
// and directly return the resulting PDF file
// or an error.
if !r.HasArg(resource.WebhookURLArgKey) {
logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
logger.DebugOpf(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
return convertSync(ctx, p, filename, fpath)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
logger.DebugOpf(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
return convertAsync(ctx, p, filename, fpath)
}
if err := resolver(); err != nil {
@@ -223,7 +275,7 @@ func convertSync(ctx context.Context, p printer.Printer, filename, fpath string)
return err
}
if !r.HasArg(resource.ResultFilenameArgKey) {
logger.DebugfOp(
logger.DebugOpf(
op,
"no '%s' found, using generated filename '%s'",
resource.RemoteURLArgKey,
@@ -234,7 +286,7 @@ func convertSync(ctx context.Context, p printer.Printer, filename, fpath string)
}
return nil
}
logger.DebugfOp(
logger.DebugOpf(
op,
"'%s' found, so not using generated filename",
resource.ResultFilenameArgKey,
@@ -267,7 +319,7 @@ func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string
return xerror.New(op, err)
}
go func() {
defer r.Close() // nolint: errcheck
defer r.Close()
if err := p.Print(fpath); err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
@@ -279,23 +331,53 @@ func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
defer f.Close() // nolint: errcheck
logger.DebugfOp(
defer f.Close()
logger.DebugOpf(
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
}
defer resp.Body.Close() // nolint: errcheck
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.DebugOpf(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.DebugOpf(
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()
logger.DebugOpf(
op,
"result file '%s' sent to '%s'",
filename,
webhookURL,
)
}()
return nil
}

View File

@@ -19,39 +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, 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, 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, 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)
}
@@ -59,12 +71,19 @@ 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)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -207,12 +226,19 @@ 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)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -355,12 +381,19 @@ 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)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -503,12 +536,20 @@ 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)
// note: increased the wait timeout as it happens to timeout from time to time.
body, contentType := test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "30"})
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
@@ -541,11 +582,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)
@@ -567,8 +615,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)
@@ -578,9 +627,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))
}

View File

@@ -2,6 +2,7 @@ package xhttp
import (
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
@@ -25,12 +26,28 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
// extend the current echo context with our custom
// context.
ctx := context.New(c, logger, config)
// if its an healthcheck request, there
// is no need to create a Resource.
if ctx.Path() == pingEndpoint {
// if it's not a multipart/form-data request,
// there is no need to create a Resource.
if !isMultipartFormDataEndpoint(config, ctx.Path()) {
// validate method for healthcheck endpoint.
if ctx.Path() == pingEndpoint(config) && ctx.Request().Method != http.MethodGet {
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
return ctx.LogRequestResult(err, false)
}
return next(ctx)
}
// if the endpoint is not for healthcheck, create a
// validate method.
if ctx.Request().Method != http.MethodPost {
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
return ctx.LogRequestResult(err, false)
}
// validate Content-Type.
contentType := ctx.Request().Header.Get("Content-Type")
if !strings.Contains(contentType, "multipart/form-data") {
err := doErr(ctx, echo.NewHTTPError(http.StatusUnsupportedMediaType))
return ctx.LogRequestResult(err, false)
}
// it's a multipart/form-data request, create a
// Resource.
if err := ctx.WithResource(trace); err != nil {
err = doCleanup(ctx, err)
@@ -43,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)
}
}

View File

@@ -48,23 +48,34 @@ 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
}
scale, err := resource.ScaleArg(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),
Scale: scale,
}, nil
}
opts, err := resolver()
@@ -85,9 +96,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()

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"net/http"
"path/filepath"
"reflect"
"strconv"
"strings"
@@ -12,7 +13,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 +79,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)))
@@ -102,11 +106,9 @@ func (ctx *Context) WithResource(directoryName string) error {
if err != nil {
return r, err
}
defer in.Close() // nolint: errcheck
filename, err := normalize.String(fh.Filename)
if err != nil {
return r, err
}
defer in.Close()
// avoid directory traversal.
filename := filepath.Base(fh.Filename)
if err := r.WithFile(filename, in); err != nil {
return r, err
}
@@ -168,14 +170,14 @@ func (ctx Context) LogRequestResult(err error, isDebug bool) error {
"bytes_out": bytesOut(resp),
}
if err != nil {
ctx.logger.WithFields(fields).ErrorfOp(op, "request failed")
ctx.logger.WithFields(fields).ErrorOpf(op, "request failed")
return err
}
if isDebug {
ctx.logger.WithFields(fields).DebugfOp(op, "request handled")
ctx.logger.WithFields(fields).DebugOpf(op, "request handled")
return nil
}
ctx.logger.WithFields(fields).InfofOp(op, "request handled")
ctx.logger.WithFields(fields).InfoOpf(op, "request handled")
return nil
}

View File

@@ -51,9 +51,15 @@ 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"
// ScaleArgKey is the key
// of the argument "scale".
ScaleArgKey ArgKey = "scale"
)
/*
@@ -76,7 +82,9 @@ func ArgKeys() []ArgKey {
MarginLeftArgKey,
MarginRightArgKey,
LandscapeArgKey,
PageRangesArgKey,
GoogleChromeRpccBufferSizeArgKey,
ScaleArgKey,
}
}
@@ -290,3 +298,22 @@ func GoogleChromeRpccBufferSizeArg(r Resource, config conf.Config) (int64, error
}
return result, nil
}
/*
ScaleArg is a helper for retrieving
the "scale" argument as float64.
*/
func ScaleArg(r Resource, config conf.Config) (float64, error) {
const op string = "resource.ScaleArg"
opts := printer.DefaultChromePrinterOptions(config)
result, err := r.Float64Arg(
ScaleArgKey,
opts.Scale,
xassert.Float64NotInferiorTo(0.1),
xassert.Float64NotSuperiorTo(2.0),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}

View File

@@ -24,7 +24,9 @@ func TestArgKeys(t *testing.T) {
MarginLeftArgKey,
MarginRightArgKey,
LandscapeArgKey,
PageRangesArgKey,
GoogleChromeRpccBufferSizeArgKey,
ScaleArgKey,
}
assert.Equal(t, expected, ArgKeys())
}
@@ -345,3 +347,50 @@ func TestGoogleChromeRpccBufferSizeArg(t *testing.T) {
err = r.Close()
assert.Nil(t, err)
}
func TestScaleArg(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue float64 = 1.0
)
var expected float64
logger := test.DebugLogger()
config := conf.DefaultConfig()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// argument does not exist.
expected = defaultValue
v, err := ScaleArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// argument exist.
expected = 0.5
r.WithArg(ScaleArgKey, "0.5")
v, err = ScaleArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is < 0.1.
expected = defaultValue
r.WithArg(ScaleArgKey, "-1.0")
v, err = ScaleArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is > 2.0.
expected = defaultValue
r.WithArg(ScaleArgKey, "2.1")
v, err = ScaleArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as
// argument value is invalid.
expected = defaultValue
r.WithArg(ScaleArgKey, "foo")
v, err = ScaleArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}

View File

@@ -24,8 +24,8 @@ func (f file) write(in io.Reader) error {
if err != nil {
return err
}
defer out.Close() // nolint: errcheck
if err := out.Chmod(0644); err != nil {
defer out.Close()
if err := out.Chmod(0600); err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {

View File

@@ -0,0 +1,37 @@
package resource
import (
"strings"
)
const (
// RemoteURLCustomHTTPHeaderCanonicalBaseKey is the base key
// of custom headers send to the remote URL.
RemoteURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Remoteurl-"
// WebhookURLCustomHTTPHeaderCanonicalBaseKey is the base key
// of custom headers send to the webhook URL.
WebhookURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Webhookurl-"
)
func fetchCustomHTTPHeaders(r Resource, baseKey string) map[string]string {
customHeaders := make(map[string]string)
for key, value := range r.customHeaders {
if strings.Contains(key, baseKey) {
realKey := strings.Replace(key, baseKey, "", 1)
customHeaders[realKey] = value
}
}
return customHeaders
}
// RemoteURLCustomHTTPHeaders is a helper for retrieving
// the custom headers for the URL conversion.
func RemoteURLCustomHTTPHeaders(r Resource) map[string]string {
return fetchCustomHTTPHeaders(r, RemoteURLCustomHTTPHeaderCanonicalBaseKey)
}
// WebhookURLCustomHTTPHeaders is a helper for retrieving
// the custom headers for the webhook URL.
func WebhookURLCustomHTTPHeaders(r Resource) map[string]string {
return fetchCustomHTTPHeaders(r, WebhookURLCustomHTTPHeaderCanonicalBaseKey)
}

View File

@@ -0,0 +1,54 @@
package resource
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestRemoteURLCustomHeaders(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// should find the custom header.
customHeaderValue := "bar"
customHeaderCanonicalRealKey := "Foo"
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", RemoteURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
r.WithCustomHTTPHeader("Bar", "Bar")
expected := map[string]string{
customHeaderCanonicalRealKey: customHeaderValue,
}
notExpected := map[string]string{
customHeaderCanonicalKey: customHeaderValue,
}
v := RemoteURLCustomHTTPHeaders(r)
assert.Equal(t, expected, v)
assert.NotEqual(t, notExpected, v)
}
func TestWebhookURLCustomHeaders(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// should find the custom header.
customHeaderValue := "bar"
customHeaderCanonicalRealKey := "Foo"
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
r.WithCustomHTTPHeader("Bar", "Bar")
expected := map[string]string{
customHeaderCanonicalRealKey: customHeaderValue,
}
notExpected := map[string]string{
customHeaderCanonicalKey: customHeaderValue,
}
v := WebhookURLCustomHTTPHeaders(r)
assert.Equal(t, expected, v)
assert.NotEqual(t, notExpected, v)
}

View File

@@ -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
@@ -46,12 +50,13 @@ func New(logger xlog.Logger, directoryName string) (Resource, error) {
if err != nil {
return Resource{}, xerror.New(op, err)
}
logger.DebugfOp(op, "resource directory '%s' created", directoryName)
logger.DebugOpf(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
}
@@ -60,33 +65,59 @@ func New(logger xlog.Logger, directoryName string) (Resource, error) {
func (r Resource) Close() error {
const op string = "resource.Resource.Close"
if _, err := os.Stat(r.dirPath); os.IsNotExist(err) {
r.logger.DebugfOp(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
r.logger.DebugOpf(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
return nil
}
if err := os.RemoveAll(r.dirPath); err != nil {
return xerror.New(op, err)
}
r.logger.DebugfOp(op, "resource directory '%s' removed", r.dirPath)
r.logger.DebugOpf(op, "resource directory '%s' removed", r.dirPath)
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.DebugOpf(op, "added '%s' with value '%s' to resource custom HTTP headers", canonicalKey, value)
return
}
r.logger.DebugOpf(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"
r.args[key] = value
r.logger.DebugfOp(op, "added '%s' with value '%s' to resource args", key, value)
r.logger.DebugOpf(op, "added '%s' with value '%s' to resource args", key, value)
}
// 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.DebugOpf(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
}

View File

@@ -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
}

View File

@@ -1,7 +1,6 @@
package xhttp
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
@@ -13,37 +12,46 @@ import (
"github.com/thecodingmachine/gotenberg/test"
)
func TestNonExistingEndpoint(t *testing.T) {
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// "/" endpoint should return 404.
req := httptest.NewRequest(http.MethodGet, "/", nil)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
}
func TestDisableChromeEndpoints(t *testing.T) {
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
config, err := conf.FromEnv()
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...
@@ -56,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 404.
// Markdown endpoint should return 200.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
req = 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...
@@ -93,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, "/")
}

View File

@@ -16,11 +16,11 @@ import (
)
// Start starts Google Chrome headless in background.
func Start(logger xlog.Logger) error {
func Start(logger xlog.Logger, ignoreCertificateErrors bool) error {
const op string = "chrome.Start"
logger.DebugOp(op, "starting new Google Chrome headless process on port 9222...")
resolver := func() error {
cmd, err := cmd(logger)
cmd, err := cmd(logger, ignoreCertificateErrors)
if err != nil {
return err
}
@@ -31,8 +31,9 @@ func Start(logger xlog.Logger) error {
}
// if the process failed to start correctly,
// we have to restart it.
if !isViable(logger) {
return restart(logger, cmd.Process)
isViable, _ := IsViable(logger)
if !isViable {
return restart(logger, cmd.Process, ignoreCertificateErrors)
}
return nil
}
@@ -42,14 +43,17 @@ func Start(logger xlog.Logger) error {
return nil
}
func cmd(logger xlog.Logger) (*exec.Cmd, error) {
func cmd(logger xlog.Logger, ignoreCertificateErrors bool) (*exec.Cmd, error) {
const op string = "chrome.cmd"
binary := "google-chrome-stable"
args := []string{
"--no-sandbox",
"--headless",
// see https://github.com/GoogleChrome/puppeteer/issues/2410.
"--font-render-hinting=medium",
// see https://github.com/thecodingmachine/gotenberg/issues/157.
"--disable-dev-shm-usage",
// See https://github.com/puppeteer/puppeteer/issues/661
// and https://github.com/puppeteer/puppeteer/issues/2410.
"--font-render-hinting=none",
"--remote-debugging-port=9222",
"--disable-gpu",
"--disable-translate",
@@ -63,6 +67,11 @@ func cmd(logger xlog.Logger) (*exec.Cmd, error) {
"--mute-audio",
"--no-first-run",
}
if ignoreCertificateErrors {
args = append(args, "--ignore-certificate-errors")
}
cmd, err := xexec.Command(logger, binary, args...)
if err != nil {
return nil, xerror.New(op, err)
@@ -90,7 +99,7 @@ func kill(logger xlog.Logger, proc *os.Process) error {
return nil
}
func restart(logger xlog.Logger, proc *os.Process) error {
func restart(logger xlog.Logger, proc *os.Process, ignoreCertificateErrors bool) error {
const op string = "chrome.restart"
logger.DebugOp(op, "restarting Google Chrome headless process using port 9222...")
resolver := func() error {
@@ -98,7 +107,7 @@ func restart(logger xlog.Logger, proc *os.Process) error {
if err := kill(logger, proc); err != nil {
return err
}
cmd, err := cmd(logger)
cmd, err := cmd(logger, ignoreCertificateErrors)
if err != nil {
return err
}
@@ -109,8 +118,9 @@ func restart(logger xlog.Logger, proc *os.Process) error {
}
// if the process failed to restart correctly,
// we have to restart it again.
if !isViable(logger) {
return restart(logger, cmd.Process)
isViable, _ := IsViable(logger)
if !isViable {
return restart(logger, cmd.Process, ignoreCertificateErrors)
}
return nil
}
@@ -120,48 +130,54 @@ func restart(logger xlog.Logger, proc *os.Process) error {
return nil
}
func isViable(logger xlog.Logger) bool {
// IsViable checks if Google Chrome is healthy.
func IsViable(logger xlog.Logger) (bool, error) {
const (
op string = "chrome.isViable"
op string = "chrome.IsViable"
maxViabilityTests int = 20
)
viable := func() bool {
viable := func() (bool, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
endpoint := "http://localhost:9222"
logger.DebugfOp(
logger.DebugOpf(
op,
"checking Google Chrome headless process viability via endpoint '%s/json/version'",
endpoint,
)
v, err := devtool.New(endpoint).Version(ctx)
if err != nil {
logger.DebugfOp(
logger.DebugOpf(
op,
"Google Chrome headless is not viable as endpoint returned '%v'",
err.Error(),
)
return false
return false, err
}
logger.DebugfOp(
logger.DebugOpf(
op,
"Google Chrome headless is viable as endpoint returned '%v'",
v,
)
return true
return true, nil
}
result := false
var err error
for i := 0; i < maxViabilityTests && !result; i++ {
warmup(logger)
result = viable()
result, err = viable()
}
return result
return result, err
}
func warmup(logger xlog.Logger) {
const op string = "chrome.warmup"
warmupTime := xtime.Duration(0.5)
logger.DebugfOp(
const (
op string = "chrome.warmup"
seconds float64 = 0.5
)
warmupTime := xtime.Duration(seconds)
logger.DebugOpf(
op,
"waiting '%v' for allowing Google Chrome to warmup",
warmupTime,

View File

@@ -34,42 +34,52 @@ 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"
// GoogleChromeIgnoreCertificateErrorsEnvVar contains the name
// of the environment variable "GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS".
GoogleChromeIgnoreCertificateErrorsEnvVar string = "GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS"
)
// Config contains the application
// configuration.
type Config struct {
maximumWaitTimeout float64
maximumWaitDelay float64
maximumWebhookURLTimeout float64
defaultWaitTimeout float64
defaultWebhookURLTimeout float64
defaultListenPort int64
disableGoogleChrome bool
disableUnoconv bool
logLevel xlog.Level
maximumGoogleChromeRpccBufferSize int64
defaultGoogleChromeRpccBufferSize int64
maximumWaitTimeout float64
maximumWaitDelay float64
maximumWebhookURLTimeout float64
defaultWaitTimeout float64
defaultWebhookURLTimeout float64
defaultListenPort int64
disableGoogleChrome bool
disableUnoconv bool
googleChromeIgnoreCertificateErrors bool
logLevel xlog.Level
rootPath string
maximumGoogleChromeRpccBufferSize int64
defaultGoogleChromeRpccBufferSize int64
}
// DefaultConfig returns the default
// configuration.
func DefaultConfig() Config {
return Config{
maximumWaitTimeout: 30.0,
maximumWaitDelay: 10.0,
maximumWebhookURLTimeout: 30.0,
defaultWaitTimeout: 10.0,
defaultWebhookURLTimeout: 10.0,
defaultListenPort: 3000,
disableGoogleChrome: false,
disableUnoconv: false,
logLevel: xlog.InfoLevel,
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
maximumWaitTimeout: 30.0,
maximumWaitDelay: 10.0,
maximumWebhookURLTimeout: 30.0,
defaultWaitTimeout: 10.0,
defaultWebhookURLTimeout: 10.0,
defaultListenPort: 3000,
disableGoogleChrome: false,
disableUnoconv: false,
logLevel: xlog.InfoLevel,
rootPath: "/",
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
googleChromeIgnoreCertificateErrors: false,
}
}
@@ -163,6 +173,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,
@@ -173,6 +193,14 @@ func FromEnv() (Config, error) {
if err != nil {
return c, err
}
googleChromeIgnoreCertificateErrors, err := xassert.BoolFromEnv(
GoogleChromeIgnoreCertificateErrorsEnvVar,
c.googleChromeIgnoreCertificateErrors,
)
c.googleChromeIgnoreCertificateErrors = googleChromeIgnoreCertificateErrors
if err != nil {
return c, err
}
return c, nil
}
result, err := resolver()
@@ -242,6 +270,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 {
@@ -253,3 +287,7 @@ func (c Config) MaximumGoogleChromeRpccBufferSize() int64 {
func (c Config) DefaultGoogleChromeRpccBufferSize() int64 {
return c.defaultGoogleChromeRpccBufferSize
}
func (c Config) GoogleChromeIgnoreCertificateErrors() bool {
return c.googleChromeIgnoreCertificateErrors
}

View File

@@ -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
@@ -357,6 +380,43 @@ func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) {
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
}
func TestGoogleChromeIgnoreCertificateErrorsFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS correctly set to true.
os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "1")
expected = DefaultConfig()
expected.googleChromeIgnoreCertificateErrors = true
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS correctly set to false.
os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "0")
expected = DefaultConfig()
expected.googleChromeIgnoreCertificateErrors = false
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS wrongly set.
os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS not set at all.
expected = DefaultConfig()
expected.googleChromeIgnoreCertificateErrors = false
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
}
func TestGetters(t *testing.T) {
result := DefaultConfig()
assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout())
@@ -368,6 +428,8 @@ 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())
assert.Equal(t, result.googleChromeIgnoreCertificateErrors, result.GoogleChromeIgnoreCertificateErrors())
}

View File

@@ -2,6 +2,7 @@ package printer
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"strings"
@@ -30,18 +31,21 @@ 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
Scale float64
}
// DefaultChromePrinterOptions returns the default
@@ -49,18 +53,21 @@ type ChromePrinterOptions struct {
func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions {
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
return ChromePrinterOptions{
WaitTimeout: config.DefaultWaitTimeout(),
WaitDelay: 0.0,
HeaderHTML: defaultHeaderFooterHTML,
FooterHTML: defaultHeaderFooterHTML,
PaperWidth: 8.27,
PaperHeight: 11.7,
MarginTop: 1.0,
MarginBottom: 1.0,
MarginLeft: 1.0,
MarginRight: 1.0,
Landscape: false,
RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(),
WaitTimeout: config.DefaultWaitTimeout(),
WaitDelay: 0.0,
HeaderHTML: defaultHeaderFooterHTML,
FooterHTML: defaultHeaderFooterHTML,
PaperWidth: 8.27,
PaperHeight: 11.7,
MarginTop: 1.0,
MarginBottom: 1.0,
MarginLeft: 1.0,
MarginRight: 1.0,
Landscape: false,
PageRanges: "",
RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(),
CustomHTTPHeaders: make(map[string]string),
Scale: 1.0,
}
}
@@ -87,10 +94,11 @@ func (p chromePrinter) Print(destination string) error {
if err != nil {
return err
}
defer devtConn.Close() // nolint: errcheck
defer devtConn.Close()
// create a new CDP Client that uses conn.
devtClient := cdp.NewClient(devtConn)
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
createBrowserContextArgs := target.NewCreateBrowserContextArgs()
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx, createBrowserContextArgs)
if err != nil {
return err
}
@@ -128,7 +136,7 @@ func (p chromePrinter) Print(destination string) error {
if err != nil {
return err
}
defer newContextConn.Close() // nolint: errcheck
defer newContextConn.Close()
// create a new CDP Client that uses newContextConn.
targetClient := cdp.NewClient(newContextConn)
/*
@@ -144,6 +152,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
@@ -151,28 +163,41 @@ func (p chromePrinter) Print(destination string) error {
// apply a wait delay (if any).
if p.opts.WaitDelay > 0.0 {
// wait for a given amount of time (useful for javascript delay).
p.logger.DebugfOp(op, "applying a wait delay of '%.2fs'...", p.opts.WaitDelay)
p.logger.DebugOpf(op, "applying a wait delay of '%.2fs'...", p.opts.WaitDelay)
time.Sleep(xtime.Duration(p.opts.WaitDelay))
} else {
p.logger.DebugOp(op, "no wait delay to apply, moving on...")
}
// print the page to PDF.
print, err := targetClient.Page.PrintToPDF(
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).
SetScale(p.opts.Scale)
if p.opts.PageRanges != "" {
printToPdfArgs.SetPageRanges(p.opts.PageRanges)
}
// printToPDF the page to PDF.
printToPDF, 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,
@@ -185,7 +210,7 @@ func (p chromePrinter) Print(destination string) error {
}
return err
}
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
if err := ioutil.WriteFile(destination, printToPDF.Data, 0600); err != nil {
return err
}
return nil
@@ -247,6 +272,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.DebugOpf(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 {
@@ -263,22 +314,22 @@ func (p chromePrinter) listenEvents(ctx context.Context, client *cdp.Client) err
if err != nil {
return err
}
defer domContentEventFired.Close() // nolint: errcheck
defer domContentEventFired.Close()
loadEventFired, err := client.Page.LoadEventFired(ctx)
if err != nil {
return err
}
defer loadEventFired.Close() // nolint: errcheck
defer loadEventFired.Close()
lifecycleEvent, err := client.Page.LifecycleEvent(ctx)
if err != nil {
return err
}
defer lifecycleEvent.Close() // nolint: errcheck
defer lifecycleEvent.Close()
loadingFinished, err := client.Network.LoadingFinished(ctx)
if err != nil {
return err
}
defer loadingFinished.Close() // nolint: errcheck
defer loadingFinished.Close()
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
return err
}
@@ -307,7 +358,7 @@ func (p chromePrinter) listenEvents(ctx context.Context, client *cdp.Client) err
if err != nil {
return err
}
p.logger.DebugfOp(op, "event '%s' received", ev.Name)
p.logger.DebugOpf(op, "event '%s' received", ev.Name)
if ev.Name == networkIdleEventName {
break
}

View File

@@ -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)

View File

@@ -3,15 +3,14 @@ package printer
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"path/filepath"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
"html/template"
"io/ioutil"
"path/filepath"
)
// NewMarkdownPrinter returns a Printer which
@@ -36,7 +35,7 @@ func NewMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOpti
baseFilename := xrand.Get()
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
logger.DebugOp(op, "writing the HTML from previous conversion(s) into new file...")
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0600); err != nil {
return "", err
}
return fmt.Sprintf("file://%s", dst), nil
@@ -58,6 +57,8 @@ type templateData struct {
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
const op string = "printer.markdownToHTML"
// avoid directory traversal.
filename = filepath.Base(filename)
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
b, err := ioutil.ReadFile(fpath)
if err != nil {

View File

@@ -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)

View File

@@ -2,6 +2,7 @@ package printer
import (
"context"
"sort"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
@@ -54,7 +55,9 @@ func (p mergePrinter) Print(destination string) error {
defer cancel()
p.ctx = ctx
}
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
// see https://github.com/thecodingmachine/gotenberg/issues/139.
sort.Strings(p.fpaths)
p.logger.DebugOpf(op, "merging '%v'...", p.fpaths)
resolver := func() error {
var args []string
args = append(args, p.fpaths...)

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/phayes/freeport"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
@@ -26,6 +28,7 @@ type officePrinter struct {
type OfficePrinterOptions struct {
WaitTimeout float64
Landscape bool
PageRanges string
}
// DefaultOfficePrinterOptions returns the default
@@ -34,6 +37,7 @@ func DefaultOfficePrinterOptions(config conf.Config) OfficePrinterOptions {
return OfficePrinterOptions{
WaitTimeout: config.DefaultWaitTimeout(),
Landscape: false,
PageRanges: "",
}
}
@@ -53,16 +57,18 @@ func (p officePrinter) Print(destination string) error {
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
defer cancel()
resolver := func() error {
// see https://github.com/thecodingmachine/gotenberg/issues/139.
sort.Strings(p.fpaths)
fpaths := make([]string, len(p.fpaths))
dirPath := filepath.Dir(destination)
for i, fpath := range p.fpaths {
baseFilename := xrand.Get()
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
if err := unoconv(ctx, p.logger, fpath, tmpDest, p.opts); err != nil {
p.logger.DebugOpf(op, "converting '%s' to PDF...", fpath)
if err := p.unoconv(ctx, fpath, tmpDest); err != nil {
return err
}
p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename)
p.logger.DebugOpf(op, "'%s.pdf' created", baseFilename)
fpaths[i] = tmpDest
}
if len(fpaths) == 1 {
@@ -85,26 +91,45 @@ 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 {
dirName := xrand.Get()
port, err := freeport.GetFreePort()
if err != nil {
return err
}
args := []string{
"--user-profile",
fmt.Sprintf("///tmp/%d", port),
fmt.Sprintf("///tmp/%s", dirName),
"--port",
fmt.Sprintf("%d", port),
"--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...)
err = xexec.Run(ctx, p.logger, "unoconv", args...)
// always remove user profile folders created by LibreOffice.
// see https://github.com/thecodingmachine/gotenberg/issues/192.
go cleanupUserProfile(p.logger, dirName)
if 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)
@@ -112,6 +137,15 @@ func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string,
return nil
}
func cleanupUserProfile(logger xlog.Logger, dirName string) {
const op = "printer.cleanupUserProfile"
path := fmt.Sprintf("/tmp/%s", dirName)
if err := os.RemoveAll(path); err != nil {
// find a way to bubble up this error?
logger.ErrorOpf(op, "failed to remove user profile directory '%s': %s", path, err.Error())
}
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Printer(new(officePrinter))

View File

@@ -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)

View File

@@ -12,5 +12,5 @@ type Printer interface {
func logOptions(logger xlog.Logger, opts interface{}) {
const op string = "printer.logOptions"
logger.DebugfOp(op, "options: %+v", opts)
logger.DebugOpf(op, "options: %+v", opts)
}

View File

@@ -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)

View File

@@ -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))
)

View File

@@ -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)
}

View File

@@ -14,7 +14,7 @@ import (
// times out after given seconds.
func WithTimeout(logger xlog.Logger, seconds float64) (context.Context, context.CancelFunc) {
const op string = "xcontext.WithTimeout"
logger.DebugfOp(op, "creating context with '%.2fs' of timeout...", seconds)
logger.DebugOpf(op, "creating context with '%.2fs' of timeout...", seconds)
return context.WithTimeout(context.Background(), xtime.Duration(seconds))
}

View File

@@ -84,11 +84,11 @@ func Run(ctx context.Context, logger xlog.Logger, binary string, args ...string)
}()
select {
case err := <-result:
logger.DebugfOp(op, "command '%s' finished", strings.Join(cmd.Args, " "))
logger.DebugOpf(op, "command '%s' finished", strings.Join(cmd.Args, " "))
kill()
return err
case <-ctx.Done():
logger.DebugfOp(op, "command '%s' failed to finish before context.Context deadline", strings.Join(cmd.Args, " "))
logger.DebugOpf(op, "command '%s' failed to finish before context.Context deadline", strings.Join(cmd.Args, " "))
kill()
return ctx.Err()
}
@@ -102,7 +102,7 @@ func Run(ctx context.Context, logger xlog.Logger, binary string, args ...string)
// LogBeforeExecute logs a command before its execution.
func LogBeforeExecute(logger xlog.Logger, cmd *exec.Cmd) {
const op string = "xexec.LogBeforeExecute"
logger.DebugfOp(op, "executing command: %s", strings.Join(cmd.Args, " "))
logger.DebugOpf(op, "executing command: %s", strings.Join(cmd.Args, " "))
}
func pipe(logger xlog.Logger, cmd *exec.Cmd) error {
@@ -139,7 +139,7 @@ func logCommandOutput(logger xlog.Logger, reader io.ReadCloser, outputType strin
}
op := buf.String()
r := bufio.NewReader(reader)
defer reader.Close() // nolint: errcheck
defer reader.Close()
for {
line, _, err := r.ReadLine()
if err != nil {

View File

@@ -104,9 +104,9 @@ func (l Logger) DebugOp(op, message string) {
l.entry.WithField("op", op).Debug(message)
}
// DebugfOp logs a debug message for given
// DebugOpf logs a debug message for given
// logical operation and format.
func (l Logger) DebugfOp(op, format string, args ...interface{}) {
func (l Logger) DebugOpf(op, format string, args ...interface{}) {
l.entry.WithField("op", op).Debugf(format, args...)
}
@@ -116,9 +116,9 @@ func (l Logger) InfoOp(op, message string) {
l.entry.WithField("op", op).Info(message)
}
// InfofOp logs an info message for given
// InfoOpf logs an info message for given
// logical operation and format.
func (l Logger) InfofOp(op, format string, args ...interface{}) {
func (l Logger) InfoOpf(op, format string, args ...interface{}) {
l.entry.WithField("op", op).Infof(format, args...)
}
@@ -128,9 +128,9 @@ func (l Logger) ErrorOp(op string, err error) {
l.entry.WithField("op", op).Error(err.Error())
}
// ErrorfOp logs an error message for given
// ErrorOpf logs an error message for given
// logical operation and format.
func (l Logger) ErrorfOp(op, format string, args ...interface{}) {
func (l Logger) ErrorOpf(op, format string, args ...interface{}) {
l.entry.WithField("op", op).Errorf(format, args...)
}

View File

@@ -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"
@@ -21,15 +21,15 @@ if [ $VERSION_LENGTH -ne 3 ]; then
fi
docker build \
--build-arg VERSION=${VERSION} \
--build-arg VERSION=${VERSION} \
--build-arg TINI_VERSION=${TINI_VERSION} \
-t ${DOCKER_REPOSITORY}/gotenberg:latest \
-t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]} \
-t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]} \
-t ${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \
-t ${DOCKER_REGISTRY}/gotenberg:latest \
-t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]} \
-t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]} \
-t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \
-f build/package/Dockerfile .
docker push "${DOCKER_REPOSITORY}/gotenberg:latest"
docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}"
docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}"
docker push "${DOCKER_REPOSITORY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}"
docker push "${DOCKER_REGISTRY}/gotenberg:latest"
docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}"
docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}"
docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}"

View File

@@ -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

View File

@@ -14,7 +14,7 @@ func main() {
systemLogger.FatalOp(op, err)
}
// start Google Chrome headless.
if err := chrome.Start(systemLogger); err != nil {
if err := chrome.Start(systemLogger, config.GoogleChromeIgnoreCertificateErrors()); err != nil {
systemLogger.FatalOp(op, err)
}
}

View File

@@ -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 {

View File

@@ -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"),
}
}

View File

@@ -0,0 +1,3 @@
Gutenberg
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.