Compare commits

..

306 Commits
1.0.0 ... 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
cd8d2b5e95 Merge pull request #121 from thecodingmachine/rpcc_message_too_large
PrintToPDF: rpcc: message too large
2019-10-07 18:07:09 +02:00
Julien Neuhart
8d89b2e80a rpcc buffer size may now be set from env var or form field 2019-10-07 15:31:05 +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
Julien Neuhart
ab475d65ee fixing PrintToPDF: rpcc: message too large 2019-10-04 16:19:53 +02:00
Julien Neuhart
fa9963a737 fixing load testing link in documentation 2019-10-04 14:11:32 +02:00
Julien Neuhart
d65d9c9a4c fixing ineffassign issues 2019-10-03 11:59:48 +02:00
Julien Neuhart
6f8c7a613f improving documentation about header/footer, fonts and fixing a typo about paper size (fixes #118, closes #119) 2019-10-03 11:28:04 +02:00
Julien Neuhart
1b45c927b2 fixing publish command (for good this time) 2019-10-02 14:49:47 +02:00
Julien Neuhart
ab1cc896de fixing publish command 2019-10-02 14:28:50 +02:00
Julien Neuhart
4b91600e35 Merge pull request #106 from thecodingmachine/6.0.0
6.0.0
2019-10-02 14:11:11 +02:00
Julien Neuhart
69e43e40eb improving documentation 2019-10-02 11:41:35 +02:00
Julien Neuhart
930622ea3c adding load testing results 2019-10-02 11:09:21 +02:00
Julien Neuhart
7ac104c3c6 refactoring process management: no more orphan processes and quicker chrome startup 2019-10-02 10:38:47 +02:00
Julien Neuhart
c47dc5ecdb office: no more orphan processes (hopefully) 2019-10-01 17:33:00 +02:00
Julien Neuhart
a3f05c1146 minor refactoring of building process 2019-10-01 17:32:15 +02:00
Julien Neuhart
04b1fa378d fixing typo in documentation 2019-10-01 09:33:31 +02:00
Julien Neuhart
cbc9d9ab8c using tini to reap zombie processes 2019-09-30 20:17:39 +02:00
Julien Neuhart
7f61c63425 adding information about how to use images in header/footer. Thanks @LoicHa 2019-09-30 17:27:52 +02:00
Julien Neuhart
3d379dec58 adding reference to the load testing use case in the documentation 2019-09-30 17:19:14 +02:00
Julien Neuhart
9968a816db improving document 2019-09-30 17:16:40 +02:00
Julien Neuhart
8f721cd12a fixing tests 2019-09-30 16:38:52 +02:00
Julien Neuhart
d45f2e1ec7 updating go modules to 1.13 2019-09-30 16:32:06 +02:00
Julien Neuhart
7e0f1794a5 updating dependencies 2019-09-30 16:24:39 +02:00
Julien Neuhart
20959fc68f updating Golang and Golang CI Lint versions 2019-09-30 15:55:01 +02:00
Julien Neuhart
a942224f46 adding back context to unoconv cmd 2019-09-30 15:54:17 +02:00
Julien Neuhart
fef16f6b28 removing useless whitespace 2019-09-30 15:53:48 +02:00
Julien Neuhart
1abc4d0365 lint: disabling funlen 2019-09-30 15:53:15 +02:00
Julien Neuhart
862761d46f removing PM2 references in README 2019-09-30 14:52:37 +02:00
Julien Neuhart
94743a16be adding TODO comment 2019-09-30 14:52:16 +02:00
Julien Neuhart
f6729834b6 removing comment 2019-09-30 14:51:49 +02:00
Julien Neuhart
1b4c435900 rolling back to an easier solution for executing unoconv as previous one did not remove zombies 2019-09-29 15:13:19 +02:00
Julien Neuhart
24e6a86bf5 rolling back from previous implementation, now using master version of unoconv 2019-09-26 18:07:43 +02:00
Julien Neuhart
88f74d5e9c process balancing: better implementation (wip) 2019-09-23 18:29:24 +02:00
Julien Neuhart
171f93662f process load balancing: broken but in progress 2019-09-19 17:28:29 +02:00
Julien Neuhart
c5429f7efa test: removing unoconv listener as soffice in server mode has a memory leak 2019-08-27 16:51:03 +02:00
Julien Neuhart
36847e6a06 fixing wrong status code if alreay echo http error 2019-08-27 16:50:27 +02:00
Julien Neuhart
438fd7d825 fixing CI permissions with coverage file 2019-08-27 11:20:25 +02:00
Julien Neuhart
767b61a3bb typos in documentation 2019-08-27 11:11:40 +02:00
Julien Neuhart
dd2542dcd6 updating Travis and GitHub Actions workflows 2019-08-27 11:07:05 +02:00
Julien Neuhart
f8b63d487a adding back travis CI badge 2019-08-27 11:03:26 +02:00
Julien Neuhart
fcd0feb7f6 adding back Travis as GitHub actions are not yet ready 2019-08-27 11:02:03 +02:00
Julien Neuhart
8287cf386b improving documentation on timeout issues 2019-08-27 11:00:26 +02:00
Julien Neuhart
8415bee5da reducing the number of concurrent dev tools connections to Chrome 2019-08-27 09:08:00 +02:00
Julien Neuhart
30eb48321c reordering code 2019-08-26 11:20:16 +02:00
Julien Neuhart
7cff2e01a4 reducing memory footprint 2019-08-26 10:22:39 +02:00
Julien Neuhart
fa94341b60 updating to Libreoffice 6.3 @tiago-soczek 2019-08-22 09:45:12 +02:00
Julien Neuhart
a1b34d5e7b adding 'restart_time' information about a process in ping endpoint if LOG_LEVEL=DEBUG 2019-08-21 15:28:34 +02:00
Julien Neuhart
14ef3cf07c fixing typo in main.go 2019-08-21 11:38:50 +02:00
Julien Neuhart
db6cddefd4 updating documentation according to changes 2019-08-20 16:20:42 +02:00
Julien Neuhart
95c6ecb724 adding PM2 processes details as response of ping endpoint if LOG_LEVEL=DEBUG 2019-08-20 15:23:02 +02:00
Julien Neuhart
fc27f6b3f0 improving speed of the command make image 2019-08-20 15:06:17 +02:00
Julien Neuhart
2ce0bb2caa updating ISSUE_TEMPLATE.md 2019-08-20 13:04:40 +02:00
Julien Neuhart
94e1c006ab commenting for now the main step for publish action: need to check what contains 2019-08-20 12:58:02 +02:00
Julien Neuhart
fc2e3586a2 renaming workflows (easier for badge) and removing travis 2019-08-20 12:56:53 +02:00
Julien Neuhart
bca8b6b606 adding codecov token + improving GitHub jobs names 2019-08-20 12:23:25 +02:00
Julien Neuhart
bb6a3471d5 updating CI process + tests for code coverage 2019-08-20 12:06:08 +02:00
Julien Neuhart
09d07e4890 removing TTY flag from lint and tests commands 2019-08-20 11:34:26 +02:00
Julien Neuhart
8826ad8990 fixing issue in cd workflow 2019-08-20 11:27:26 +02:00
Julien Neuhart
b350edc250 fixing issue in cd workflow 2019-08-20 11:21:55 +02:00
Julien Neuhart
af326c0b1e adding GitHub workflows 2019-08-20 11:20:21 +02:00
Julien Neuhart
edc346a6b3 removing testfunc as it no more needed 2019-08-20 10:51:32 +02:00
Julien Neuhart
1e3fc97f2b finalizing xhttp package tests 2019-08-20 10:46:21 +02:00
Julien Neuhart
51b78ef5f3 fixing typos 2019-08-20 10:46:05 +02:00
Julien Neuhart
4e4036382b improving load time for lint and tests commands 2019-08-20 10:45:56 +02:00
Julien Neuhart
daf7cb9d6b adding xhttp package tests for checking that endpoints return a correct status code according if google chrome and/or unoconv are disabled in the configuration 2019-08-19 17:21:30 +02:00
Julien Neuhart
08c97f280f typo in http client variable name 2019-08-19 16:50:32 +02:00
Julien Neuhart
9c836a34ee handling webhookURLTimeout 2019-08-19 16:49:59 +02:00
Julien Neuhart
e839430270 adding WebhookURLTimeoutArg for retrieving and validating webhookURLTimeout form field 2019-08-19 16:44:13 +02:00
Julien Neuhart
cf5e530ed9 special case handling: if one file has been sent and it is empty, return a correct invalid HTTP code AND remove the resource folder 2019-08-19 16:29:58 +02:00
Julien Neuhart
49ac5e647f fixes #87 (dummy commit) 2019-08-19 09:51:15 +02:00
Julien Neuhart
5a7b3ded20 fixes #92 (dummy commit) 2019-08-19 09:51:05 +02:00
Julien Neuhart
bcf06f0d69 fixes #104 2019-08-18 20:03:47 +02:00
Julien Neuhart
4f3fe28905 adding missingt test in context package 2019-08-18 19:24:08 +02:00
Julien Neuhart
f695861731 adding resource package tests 2019-08-18 18:58:54 +02:00
Julien Neuhart
2fee966fc8 adding test for printer package 2019-08-18 17:31:07 +02:00
Julien Neuhart
6964124c1b typo fix 2019-07-28 18:15:47 +02:00
Julien Neuhart
f9e2d91e48 updating where 'custom' fonts are installed 2019-07-28 18:14:55 +02:00
Julien Neuhart
2c483e7f7c Merge pull request #100 from vbaidak/noto-emoji-font
Noto emoji font
2019-07-28 18:04:49 +02:00
Julien Neuhart
3d078108a3 Merge pull request #99 from vbaidak/inconsistent-font-rendering
Inconsistent text rendering in headless mode
2019-07-28 18:04:15 +02:00
Julien Neuhart
794967bd9f fixing issue with Google fonts: now waiting for network idle event 2019-07-28 18:02:11 +02:00
Julien Neuhart
554474776c refactoring tests util methods + adding context tests 2019-07-24 14:56:12 +02:00
Vladyslav Baidak
81c5f9cd4e Merge branch '6.0.0' into noto-emoji-font 2019-07-24 13:09:13 +03:00
Vladyslav Baidak
e4735401a7 Added Noto Color Emoji Font 2019-07-24 12:57:59 +03:00
Vladyslav Baidak
758c3be3c5 Added Noto Emoji Font 2019-07-24 12:52:04 +03:00
Julien Neuhart
2be9ae7868 updating dependencies 2019-07-24 10:58:51 +02:00
Julien Neuhart
3ee4540f7d fixing method HasResource 2019-07-24 10:20:36 +02:00
Julien Neuhart
84c1175379 better debug logs for Markdown printer 2019-07-23 17:06:30 +02:00
Julien Neuhart
ad4d87a138 improving context timeout tests for office and merge printers + test with only one file to convert for the office printer 2019-07-23 16:59:32 +02:00
Julien Neuhart
5feff0d0ec adding merge printer tests 2019-07-23 16:50:24 +02:00
Julien Neuhart
08e7a2c065 fixing lint issue in xexec package 2019-07-23 16:40:54 +02:00
Julien Neuhart
a524521fdb improving lock mechanism for office conversion: now check if context has timeout when trying to acquire the lock + office printer test 2019-07-23 16:40:39 +02:00
Julien Neuhart
0b2312a407 better logical operation for xexec command output 2019-07-23 16:25:55 +02:00
Julien Neuhart
c7d9e24c00 now using Gateway timeout status code in case of timeout 2019-07-23 15:11:37 +02:00
Julien Neuhart
0d827e490e using (again) Golang binaries from official Docker image instead of standard install without checkum + added a TODO for healtheck 2019-07-23 14:25:31 +02:00
Vladyslav Baidak
6402125233 Inconsistent text rendering in headless mode 2019-07-23 15:06:33 +03:00
Julien Neuhart
7bfbda4490 huge refactoring 2019-07-23 13:57:18 +02:00
Julien Neuhart
f6b357691c minor refactoring of const + better timeout handling in pinter package + api package tests 2019-07-10 11:46:58 +02:00
Julien Neuhart
0c1e4e6888 WIP: tests of api pkg 2019-07-09 22:22:22 +02:00
Julien Neuhart
c7ecdcf625 handling context timeout/cancelled in printer package 2019-07-09 19:39:14 +02:00
Julien Neuhart
957b9b1cf4 adding config to test suite in the Dockerfile entrypoint 2019-07-08 18:28:13 +02:00
Julien Neuhart
1b4eed1a6e fixing pm2 tests + adding pkg config tests 2019-07-08 18:26:17 +02:00
Julien Neuhart
acd47b870f fixing typo in godoc in pkg random 2019-07-08 18:07:44 +02:00
Julien Neuhart
9ab8d03697 adding tests for random pkg 2019-07-08 18:06:43 +02:00
Julien Neuhart
9c6f23d7cc typo in timeout tests + adding tests for standarderror 2019-07-08 17:50:31 +02:00
Julien Neuhart
9af00d6d58 fixes waitTimeout, waitDelay not working with float 2019-07-08 17:24:17 +02:00
Julien Neuhart
186c09f29b adding op and standarderror in printer package 2019-07-08 11:16:47 +02:00
Julien Neuhart
70b185a37a improving logging 2019-07-08 11:06:26 +02:00
Julien Neuhart
5af01505c2 better op names + fixing issue with op chaining in errors 2019-07-08 10:28:43 +02:00
Julien Neuhart
5f6ab2fc08 all logs now have an op field 2019-07-07 19:54:44 +02:00
Julien Neuhart
c8f7ea934c wip refactoring: better logging and error systems 2019-07-07 17:45:07 +02:00
Julien Neuhart
a4aa8dafac WIP: refactoring logging system 2019-07-01 17:11:23 +02:00
Julien Neuhart
8ca9866440 minor refactoring of @tbflw work 2019-06-21 13:32:46 +02:00
Julien Neuhart
97eff7f21d Merge pull request #88 from tbflw/feature/run_non_root_and_log_startup
Feature/run non root and log startup
2019-06-21 11:18:26 +02:00
Thomas Bøgh Fangel
e30fd8eb9b fixed compile errors after accepting review suggestions 2019-06-17 11:55:36 +02:00
Thomas Bøgh Fangel
6fa48c9222 Apply suggestions from code review
Co-Authored-By: Julien Neuhart <j.neuhart@thecodingmachine.com>
2019-06-17 11:29:52 +02:00
Thomas Bøgh Fangel
c36c7144cb updates documentation with info about new env var and dedicated user gotenberg 2019-06-13 16:08:04 +02:00
Thomas Bøgh Fangel
bbd0a893b1 adds configurable debugging of startup of processes and print out some info from the chrome viable() function for debugging purposes 2019-06-13 16:08:04 +02:00
Thomas Bøgh Fangel
cfea2b8d9d updates Dockerfile for creating image to use the new dedicated user and sets PM2_HOME inside the /gotenberg dir to avoid permission problems 2019-06-13 15:55:52 +02:00
Thomas Bøgh Fangel
0fcbe821cd updates .gitignore and adds .dockerignore with irrelevant dirs for building images 2019-06-13 15:55:52 +02:00
Thomas Bøgh Fangel
ca41164980 add dedicated gotenberg user to the base image, create the /gotenberg directory with gotenberg user as owner, updates base debian image to newest 9.* 2019-06-13 15:55:26 +02:00
Thomas Bøgh Fangel
cf20e2a510 adds possibility of specifying docker to repo image target 2019-06-13 15:55:19 +02:00
Julien Neuhart
02f1231e7d New environment variables : DISABLE_HEALTHCHECK_LOGGING and DEFAULT_LISTEN_PORT (#78)
* skip logging when healthcheck url is called; closes #74 (#75)

Signed-off-by: Casey Kuhlman <casey@monax.io>

* Configure listen port via environment variable (#77)

* skip logging when healthcheck url is called; closes #74

Signed-off-by: Casey Kuhlman <casey@monax.io>

* skip logging when healthcheck url is called; closes #74

Signed-off-by: Casey Kuhlman <casey@monax.io>

* adds the ability to establish the listen port via env var

Signed-off-by: Casey Kuhlman <casey@monax.io>

* minor refactoring of @compleatang work

* fixing typo
2019-06-03 15:05:09 +02:00
Nicholas Jones
6846e7941f Install pm2 via npm (#72)
As noted in the Dockerfile, the previous route to install pm2 was broken at
some point. As a result, pm2 related files are now being copied over from an
image created prior to the breakage.

This change now installs pm2 via npm, which is the recommended route for this.
A small adjustment had to be made in the processManager code; an
argument was being passed with a space in it. For reasons that I've not been
able to fully trace, this no longer works. It should be noted that passing
arguments in this way can result in undesirable behaviour - in this case I
believe "--interpreter none" was being passed as a single argument, rather than
"--interpreter", "none". I've adjusted this to use an equals - splitting into 2
separate strings works well too.
2019-05-09 11:15:37 +02:00
Hong Shick Pak
719287c3a6 Fix google chrome env var error message (#68) 2019-04-25 09:13:10 +02:00
Julien Neuhart
194670c3bf 5.0.0 (#66) 2019-04-14 17:07:45 +02:00
Jonathan
c4222e0981 Fix documentation url example (#59)
* Fix documentation url example

One \ was forgotten

* fix: doc
2019-03-31 22:16:35 +02:00
Julien Neuhart
61dc4b8bb8 improving documentation about Kubernetes (fixes #40 #53) 2019-03-27 11:44:42 +01:00
Julien Neuhart
df357c1008 fixing typo in documentation 2019-03-26 17:37:25 +01:00
Julien Neuhart
871b52a288 adding filename form field + updating echo package to v4 (#57) 2019-03-26 17:35:38 +01:00
Julien Neuhart
8a4a043e9f fixing documentation about go library 2019-03-15 17:15:47 +01:00
Julien Neuhart
c0297aa061 removing go client library => now available at github.com/thecodingmachine/gotenberg-go-client 2019-03-15 17:07:51 +01:00
Julien Neuhart
2aaa2eeaff Merge branch 'master' of https://github.com/thecodingmachine/gotenberg 2019-03-14 16:39:18 +01:00
Julien Neuhart
5f62e5bacf removing 'v' from tag 2019-03-14 16:39:08 +01:00
Alexander Joseph Trelore
ab78559710 remove unused imports (#55)
Thanks!
2019-03-13 10:53:20 +01:00
Julien Neuhart
0d75bb1d3e improving documentation on merge 2019-03-05 11:27:25 +01:00
Julien Neuhart
6441062e7f adding Google analytics 2019-02-19 15:27:20 +01:00
Julien Neuhart
4827a45f22 adding flat open document support (fixes #47) 2019-02-18 13:46:58 +01:00
Julien Neuhart
40eef457ab adding latest tag (fixes #48) 2019-02-18 13:44:30 +01:00
Julien Neuhart
c22a4d109b minor fix in doc for web fonts 2019-02-01 11:22:06 +01:00
Julien Neuhart
571c3e04c4 adding form field web fonts timeout 2019-02-01 09:40:16 +01:00
Sergei Opletaev
f9fb97a572 Fix for #44 promise wait for fonts (#45)
`runtime.ExceptionDetails: Uncaught exception at 4:47: SyntaxError: Unexpected token %`
2019-02-01 09:21:02 +01:00
Julien Neuhart
e6484bb2d5 Trigger GitHub documentation 2019-01-28 15:56:13 +01:00
Julien Neuhart
19f51fdc1f fixing PHP documentation 2019-01-28 15:36:59 +01:00
Julien Neuhart
25f7ba3ee6 4.0.0 (#42)
* adding URL conversions

* updating gotenberg version in documentation

* pkg: input as variadic string (#39)

* pkg: input as variadic string

* pkg: fix link to github doc

* update doc about v4 client

* make doc

* fixing missing variadic inputs in doc

* adding more fonts

* updating PHP documentation
2019-01-28 15:18:12 +01:00
Julien Neuhart
a3cd1ff4e1 Merge pull request #41 from thecodingmachine/office-paperSize
removing paper size for Office conversion
2019-01-25 15:57:25 +01:00
Julien Neuhart
f62c717f79 fixing typo in docs 2019-01-25 15:50:15 +01:00
Julien Neuhart
3ed99133d4 removing paper size for Office conversion 2019-01-25 15:48:40 +01:00
Julien Neuhart
ab25cc331d fixes #35 2019-01-25 15:02:18 +01:00
Julien Neuhart
2648992a9b adding moar favicons 2019-01-03 16:02:02 +01:00
Julien Neuhart
7bcc3d081e adding liveness section in documentation 2019-01-03 15:56:53 +01:00
Julien Neuhart
affd1c45ca Merge pull request #32 from Peltoche/master
Add a /ping endpoint
2019-01-03 15:37:18 +01:00
Peltoche
8c7458812d Add a /ping endpoint
This endpoint can be used to check the service liveness. It can be used
for the kubernetes liveness and readiness probs.
2019-01-03 15:21:57 +01:00
Julien Neuhart
053908c772 Merge pull request #31 from thecodingmachine/merge-alt
adding PDFtk for merging PDF files < 1.7
2019-01-03 11:10:07 +01:00
Julien Neuhart
d623736f9f rollbacking to debian 9.5 as the 9.6 image has issue with git installation 2019-01-03 11:01:02 +01:00
Julien Neuhart
a3e416ff68 adding PDFtk for merging PDF files < 1.7 2019-01-03 10:06:56 +01:00
Julien Neuhart
8bf207b9f7 improving contributing doc 2018-12-14 16:13:52 +01:00
Julien Neuhart
e1520d5e7e Merge branch 'master' of https://github.com/thecodingmachine/gotenberg 2018-12-14 15:59:59 +01:00
Julien Neuhart
4f89c9f7c6 updating logo and documentation 2018-12-14 15:59:45 +01:00
Ben Davies
d34fb92211 docs: cURL write to a file instead of stdout (#25) 2018-12-14 09:28:18 +01:00
Julien Neuhart
3f7b006d16 fixing docs generation: was adding Dockerfile too.. 2018-12-14 09:15:51 +01:00
Julien Neuhart
a15cf33691 Dockerfiles: removing useless labels. Docs: now generating with Docker. Travis: publishing done with make 2018-12-14 09:13:04 +01:00
Julien Neuhart
4c1699df6c webhook: resulting file was deleted because of defer outside of goroutine 2018-12-13 18:15:28 +01:00
Julien Neuhart
404535c512 fixing/simplifying godoc 2018-12-13 17:06:29 +01:00
Julien Neuhart
2cedcecc38 adding concurrent testing 2018-12-13 14:46:59 +01:00
Julien Neuhart
ba7b02d8d4 fixing various typos in documentation 2018-12-12 17:44:27 +01:00
Julien Neuhart
6aa3c16646 fixing golang client README 2018-12-12 16:44:32 +01:00
Julien Neuhart
5a03e17e00 fixing introduction in documentation 2018-12-12 16:37:36 +01:00
Julien Neuhart
417d145b43 fixing documentation typos 2018-12-12 16:36:21 +01:00
Julien Neuhart
e5636a2afa adding .txt and .rtf files support (#20) 2018-12-12 16:34:34 +01:00
Julien Neuhart
5291d2d129 disabling pdfcpu logs (#22) 2018-12-12 16:24:54 +01:00
Julien Neuhart
c6f6a87499 now using latest version of unoconv, allowing to specify paper size and orientation + updating go client (#24) 2018-12-12 16:16:35 +01:00
Julien Neuhart
1ef21c6ed6 RPC error (#23) 2018-12-12 12:20:07 +01:00
Julien Neuhart
6ee1932cbf improving API tests: now verifying response content type and status code 2018-12-11 14:16:43 +01:00
Julien Neuhart
83cf43a314 fixing cURL commands in documentation 2018-12-11 14:09:04 +01:00
Julien Neuhart
52c14a79a1 fixing GoDoc + some test function names 2018-12-10 20:21:31 +01:00
Julien Neuhart
152db0551d fixing GoDoc links in badges 2018-12-10 20:11:20 +01:00
Julien Neuhart
3a2467ccfe improving documentation 2018-12-10 20:08:45 +01:00
Julien Neuhart
5a28f3597c fixing typos in documentation 2018-12-10 19:59:45 +01:00
Julien Neuhart
9ce179f91b adding PHP examples in documentation 2018-12-10 19:12:54 +01:00
Julien Neuhart
7aeb072cc3 fixing issue with merge endpoint: was not returning file 2018-12-10 18:42:23 +01:00
Julien Neuhart
8a652761a3 fixing Go Report Card issues (rollback gocycly) 2018-12-10 17:03:23 +01:00
Julien Neuhart
be667575d5 fixing Go Report Card issues #2 2018-12-10 17:02:48 +01:00
Julien Neuhart
59ca8a44a0 fixing Go Report Card issues 2018-12-10 16:57:08 +01:00
Julien Neuhart
c9f0d3bfd8 fixes publishing Docker images in CI 2018-12-10 16:36:34 +01:00
Julien Neuhart
06e7a1fda1 fixing website 2018-12-10 16:13:27 +01:00
Julien Neuhart
495203c112 v3.0.0 (#18) 2018-12-10 16:09:19 +01:00
Julien Neuhart
c1f6100382 updating documentation for 2.0.0 (#9)
* updating documentation for 2.0.0

* improving documentation
2018-05-16 14:21:32 +02:00
Julien Neuhart
223c0de35a adding goreleaser 2018-05-16 13:51:51 +02:00
Julien Neuhart
e3366d0b62 adding new attribute lock (#8) 2018-05-16 12:06:30 +02:00
Julien Neuhart
5100693789 new attribute interpreter for commands (#7) 2018-05-16 10:55:08 +02:00
Julien Neuhart
faec55dd01 slighty improvement of logging (#6) 2018-05-16 10:02:07 +02:00
Julien Neuhart
2871fb1ffe improving logging (#5)
* small refactoring of error messages

* (very) small refactoring of structs instantiation

* improving logging with debug messages

* using INFO level as default + changing previous debug message to info message
2018-05-04 17:43:19 +02:00
Julien Neuhart
810d35ffbe fixing typo in image name (again) 2018-05-03 17:26:47 +02:00
Julien Neuhart
4997b73bac new feature: users are now able to provide more file types to convert by adding entries in their configuration file (#3)
* new feature: users are now able to provide more file types to convert by adding entries in their configuration file

* removing README blueprint: was not working correctly

* improving code coverage of config package
2018-05-03 17:18:55 +02:00
Julien Neuhart
ef9801b1fd typo in image name... 2018-04-30 10:53:22 +02:00
Julien Neuhart
1f2317dc74 master branch for contributions 2018-04-23 13:20:54 +02:00
533 changed files with 15036 additions and 172548 deletions

View File

@@ -1,64 +0,0 @@
FROM {{ if eq "CI" .Orbit.Image }}golang:1.10-stretch{{ else }}debian:stretch-slim{{ end }}
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
# |--------------------------------------------------------------------------
# | Libraries
# |--------------------------------------------------------------------------
# |
# | Installs all required libraries.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
RUN apt-get install -y curl gnupg &&\
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
apt-get update &&\
apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
apt-get update &&\
apt-get install -y yarn bzip2 &&\
yarn global add markdown-pdf --prefix /usr/local
# |--------------------------------------------------------------------------
# | Gotenberg
# |--------------------------------------------------------------------------
# |
# | All Gotenberg related stuff.
# |
{{- if eq "CI" .Orbit.Image }}
WORKDIR /go/src/github.com/thecodingmachine/gotenberg
# Installs lint dependencies.
RUN go get -u gopkg.in/alecthomas/gometalinter.v2 &&\
gometalinter.v2 --install
# Copies our Go source.
COPY . .
# Installs project dependencies.
RUN go get -d -v ./...
ENTRYPOINT [".ci/docker-entrypoint.sh"]
{{- else }}
COPY .ci/gotenberg /usr/bin/gotenberg
RUN ln -s /usr/bin/gotenberg /usr/local/bin/gotenberg
COPY .ci/gotenberg.yml /gotenberg/gotenberg.yml
WORKDIR /gotenberg
EXPOSE 3000
CMD ["gotenberg"]
{{- end }}

View File

@@ -1,136 +0,0 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/38133342-11df3bd8-340f-11e8-9fe4-50baecdceeca.png" alt="Gotenberg's logo" width="250" height="250" />
</p>
<h3 align="center">Gotenberg</h3>
<p align="center">A stateless API for converting Markdown files, HTML files and Office documents to PDF</p>
<p align="center">
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:{{ .Orbit.Latest }}">
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:{{ .Orbit.Latest }}.svg" alt="MicroBadger layers">
</a>
<a href="https://travis-ci.org/thecodingmachine/gotenberg">
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch={{ .Orbit.Branch }}" alt="Travis CI">
</a>
<a href="https://godoc.org/github.com/thecodingmachine/gotenberg">
<img src="https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg" alt="GoDoc">
</a>
<a href="https://goreportcard.com/report/thecodingmachine/gotenberg">
<img src="https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg" alt="Go Report Card">
</a>
<a href="https://codecov.io/gh/thecodingmachine/gotenberg/branch/{{ .Orbit.Branch }}">
<img src="https://codecov.io/gh/thecodingmachine/gotenberg/branch/{{ .Orbit.Branch }}/graph/badge.svg" alt="Codecov">
</a>
</p>
---
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from
various sources. Each time, we ended up using some well known libraries like **wkhtmltopdf** or **unoconv** and kind of lost time by
reimplementing a solution from a project to another project. Meh.
# Menu
* [Usage](#usage)
* [Security](#security)
* [Scalability](#scalability)
* [Custom implementation](#custom-implementation)
* [Clients](#clients)
## Usage
Let's say you're starting the API using this simple command:
```sh
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:{{ .Orbit.Latest }}
```
The API is now available on your host under `http://127.0.0.1:3000`.
It accepts `POST` requests with a `multipart/form-data` Content-Type. Your form data should provide one or more files to convert.
It currently accepts the following:
* Markdown files
* HTML files
* Office documents (.docx, .doc, .odt, .pptx, .ppt, .odp and so on)
* PDF files (if more than one file to convert)
**Heads up:** the API relies on the file extension to determine which library to use for conversion.
There are two use cases:
* If you send one file, it will convert it and return the resulting PDF
* If many files, it will convert them to PDF, merge the resulting PDFs into a single PDF and return it
### Examples:
* One file
```sh
$ curl --request POST \
--url http://127.0.0.1:3000 \
--header 'Content-Type: multipart/form-data' \
--form files=@file.docx \
> result.pdf
```
* Many files
```sh
$ curl --request POST \
--url http://127.0.0.1:3000 \
--header 'Content-Type: multipart/form-data' \
--form files=@file.md \
--form files=@file.html \
--form files=@file.pdf \
--form files=@file.docx \
> result.pdf
```
## Security
The API does not provide any authentication mechanisms. Make sure to not put it on a public facing port and your client(s) should always
controls what is sent to the API.
## Scalability
Some libraries like **unoconv** cannot perform concurrent conversions. That's why the API does only one conversion at a time.
If your API is under heavy load, a request will take time to be processed.
Fortunately, you may pass through this limitation by scaling the API.
In the following example, I'll demonstrate how to do some vertical scaling (= on the same machine) with Docker Compose, but of course horizontal scaling works too!
```yaml
version: '3'
services:
# your others services
gotenberg:
image: gotenberg:1.0.0
```
You may now launch your services using:
```bash
docker-compose up --scale gotenberg=your_number_of_instances
```
When requesting the Gotenberg service with your client(s), Docker will automatically redirect a request to a Gotenberg container
according to the round-robin strategy.
## Custom implementation
The API relies on a simple YAML configuration file called `gotenberg.yml`. It allows you to tweak some values and even provides you
a way to change the commands called for each kind of conversion. The configuration file should be located under `/gotenberg` in your container.
The default configuration is located here: [.ci/gotenberg.yml](.ci/gotenberg.yml)
## Clients
* https://github.com/thecodingmachine/gotenberg-php-client (PHP client)
* Add your own client by submitting a [pull request](../../pulls)!
---
Would you like to update this documentation ? Feel free to open an [issue](../../issues).

View File

@@ -1,33 +0,0 @@
#!/bin/bash
set -xe
# Statically checking Go source for errors and warnings.
gometalinter.v2 --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode -E gocyclo --vendor ./...;
# Running tests according to current Gotenberg version.
if [[ "$VERSION" == "snapshot" ]]; then
for d in $(go list ./... | grep -v vendor); do
go test -race -cover -covermode=atomic $d;
done
else
echo "" > .ci/coverage.txt;
for d in $(go list ./... | grep -v vendor); do
go test -race -coverprofile=profile.out -covermode=atomic $d;
if [ -f profile.out ]; then
cat profile.out >> .ci/coverage.txt;
rm profile.out;
fi
done
fi
# Builds the Linux binary.
if [ -f .ci/gotenberg ]; then
rm .ci/gotenberg
fi
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" && mv gotenberg .ci/;
# Bye!
exit 0;

View File

@@ -1,33 +0,0 @@
# The port the application will listen to.
port: 3000
logs:
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
# Messages at and above the selected level will be logged.
level: "INFO"
# Accepted values: text, json.
# When a TTY is not attached, the output will be in the defined format.
format: "text"
# You don't like a library which is used for a conversion? You may provide here your own implementation.
commands:
markdown:
# Duration in seconds after which the command will be killed if it has not finished.
timeout: 30
# The command template: you have access to FilePath and ResultFilePath variables.
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -43,4 +43,4 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,15 +1,16 @@
# Contributing
Hi! Thank your for considering contributing to Gotenberg. You'll
Hi! Thank you for considering contributing to Gotenberg. You'll
find below useful information about how to contribute to the Gotenberg project.
## Contributing code
### Install from sources
1. Fork this repository
2. Clone it to your local Go environment (requires *Go* >= 1.10)
3. Install the latest [Orbit](https://github.com/gulien/orbit) release
1. Install and run the latest version of Docker
2. Verify your Go version (>= 1.14)
3. Fork this repository
4. Clone it outside of your `GOPATH` (we're using Go modules)
### Working with git
@@ -18,6 +19,11 @@ find below useful information about how to contribute to the Gotenberg project.
3. Push to the branch (`git push origin my-new-feature`)
4. Create a new pull request
### Testing
1. Run all linters (`make lint`)
2. Run all tests (`make tests`)
## Reporting bugs and feature request
Your issue or feature request may already be reported!
@@ -30,4 +36,4 @@ add a new one!
* [Code of conduct](CODE_OF_CONDUCT.md)
* [Issue template](ISSUE_TEMPLATE.md)
* [Pull request template](PULL_REQUEST_TEMPLATE.md)
* [Pull request template](PULL_REQUEST_TEMPLATE.md)

View File

@@ -21,6 +21,9 @@ Please search on the [issue tracker](../../../issues) before creating one.
3.
4.
## Logs (LOG_LEVEL="DEBUG")
<!--- Include here a relevant snippet of the log entries of your Gotenberg instance -->
## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
@@ -29,4 +32,4 @@ Please search on the [issue tracker](../../../issues) before creating one.
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
* Operating System and version:
* Link to your project:
* Link to your project:

View File

@@ -27,7 +27,7 @@ Explain the **motivation** for making this change. What existing problem does th
Demonstrate the code is solid. Example: The exact commands you ran and their output.
<!-- Make sure tests pass on both Travis and AppVeyor. -->
<!-- Make sure tests pass on Travis. -->
**Closing issues**
@@ -37,8 +37,9 @@ Fixes #
**Checklist**
- [ ] Have you followed the guidelines in our [CONTRIBUTING](CONTRIBUTING.md) guide?
- [ ] Have you lint your code locally prior to submission (`orbit run fmt`)?
- [ ] 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 (`orbit run ci`)?
- [ ] Have you successfully ran tests with your changes locally (`make tests`)?
- [ ] 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
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code

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

4
.gitignore vendored
View File

@@ -1,2 +1,2 @@
.ci/coverage.txt
.ci/gotenberg
.idea
coverage.txt

View File

@@ -1,23 +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=
before_install:
- wget -qO- https://github.com/gulien/orbit/releases/download/v3.0.0/orbit_Linux_x86_64.tar.gz | tar xvz -C .
- sudo mv ./orbit /usr/local/bin && chmod +x /usr/local/bin/orbit
script:
- orbit run ci -p "Version,${TRAVIS_BRANCH}"
- orbit run build -p "Version,${TRAVIS_BRANCH}"
# Uploads coverage.
- bash <(curl -s https://codecov.io/bash)
# Let's push to Docker hub only if not in a pull request and the branch name matches the semver format.
- if [[ "$TRAVIS_PULL_REQUEST" = false && "$TRAVIS_BRANCH" =~ ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$ ]]; then docker login -u $DOCKER_USER -p $DOCKER_PASS && docker push thecodingmachine/gotenberg:${TRAVIS_BRANCH}; fi

View File

@@ -1,46 +0,0 @@
FROM debian:stretch-slim
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
# |--------------------------------------------------------------------------
# | Libraries
# |--------------------------------------------------------------------------
# |
# | Installs all required libraries.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
RUN apt-get install -y curl gnupg &&\
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
apt-get update &&\
apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
apt-get update &&\
apt-get install -y yarn bzip2 &&\
yarn global add markdown-pdf --prefix /usr/local
# |--------------------------------------------------------------------------
# | Gotenberg
# |--------------------------------------------------------------------------
# |
# | All Gotenberg related stuff.
# |
COPY .ci/gotenberg /usr/bin/gotenberg
RUN ln -s /usr/bin/gotenberg /usr/local/bin/gotenberg
COPY .ci/gotenberg.yml /gotenberg/gotenberg.yml
WORKDIR /gotenberg
EXPOSE 3000
CMD ["gotenberg"]

View File

@@ -1,50 +0,0 @@
FROM golang:1.10-stretch
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
# |--------------------------------------------------------------------------
# | Libraries
# |--------------------------------------------------------------------------
# |
# | Installs all required libraries.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
RUN apt-get install -y curl gnupg &&\
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
apt-get update &&\
apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
apt-get update &&\
apt-get install -y yarn bzip2 &&\
yarn global add markdown-pdf --prefix /usr/local
# |--------------------------------------------------------------------------
# | Gotenberg
# |--------------------------------------------------------------------------
# |
# | All Gotenberg related stuff.
# |
WORKDIR /go/src/github.com/thecodingmachine/gotenberg
# Installs lint dependencies.
RUN go get -u gopkg.in/alecthomas/gometalinter.v2 &&\
gometalinter.v2 --install
# Copies our Go source.
COPY . .
# Installs project dependencies.
RUN go get -d -v ./...
ENTRYPOINT [".ci/docker-entrypoint.sh"]

60
Gopkg.lock generated
View File

@@ -1,60 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "53c1911da2b537f792e7cafcb446b05ffe33b996"
version = "v1.6.1"
[[projects]]
branch = "master"
name = "github.com/justinas/alice"
packages = ["."]
revision = "03f45bd4b7dad4734bc4620e46a35789349abb20"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "88942b9c40a4c9d203b82b3731787b672d6e809b"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "86f5ed62f8a0ee96bd888d2efdfd6d4fb100a4eb"
version = "v2.2.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "9d7a607d9b015e46c535126cbc62648ce78459507cd523d6ff0e048051cf3228"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,50 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/satori/go.uuid"
version = "1.2.0"
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.1"
[[constraint]]
branch = "master"
name = "github.com/justinas/alice"
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "2.2.0"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.0.5"

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 TheCodingMachine
Copyright (c) 2019 TheCodingMachine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

66
Makefile Normal file
View File

@@ -0,0 +1,66 @@
GOLANG_VERSION=1.14
VERSION=snapshot
DOCKER_USER=
DOCKER_PASSWORD=
DOCKER_REGISTRY=thecodingmachine
GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001
GOLANGCI_LINT_VERSION=1.27.0
CODE_COVERAGE=0
TINI_VERSION=0.19.0
MAXIMUM_WAIT_TIMEOUT=30.0
MAXIMUM_WAIT_DELAY=10.0
MAXIMUM_WEBHOOK_URL_TIMEOUT=30.0
DEFAULT_WAIT_TIMEOUT=10.0
DEFAULT_WEBHOOK_URL_TIMEOUT=10.0
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 --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_REGISTRY)/gotenberg:workspace -f build/workspace/Dockerfile .
# gofmt and goimports all go files.
fmt:
go fmt ./...
go mod tidy
# run all linters.
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_REGISTRY) $(CODE_COVERAGE)
# generate documentation.
doc:
make workspace
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_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 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_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)

141
README.md
View File

@@ -1,136 +1,45 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/38133342-11df3bd8-340f-11e8-9fe4-50baecdceeca.png" alt="Gotenberg's 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 stateless API for converting Markdown files, HTML files and Office documents to PDF</p>
<p align="center">
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:1.0.0">
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:1.0.0.svg" alt="MicroBadger layers">
</a>
<a href="https://travis-ci.org/thecodingmachine/gotenberg">
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=1.0.0" alt="Travis CI">
</a>
<a href="https://godoc.org/github.com/thecodingmachine/gotenberg">
<img src="https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg" alt="GoDoc">
</a>
<a href="https://goreportcard.com/report/thecodingmachine/gotenberg">
<img src="https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg" alt="Go Report Card">
</a>
<a href="https://codecov.io/gh/thecodingmachine/gotenberg/branch/1.0.0">
<img src="https://codecov.io/gh/thecodingmachine/gotenberg/branch/1.0.0/graph/badge.svg" alt="Codecov">
</a>
</p>
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
<p align="center"><a href="https://thecodingmachine.github.io/gotenberg">Documentation</a> &#183; <a href="/.github/CONTRIBUTING.md">Contributing</a></p>
---
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from
various sources. Each time, we ended up using some well known libraries like **wkhtmltopdf** or **unoconv** and kind of lost time by
reimplementing a solution from a project to another project. Meh.
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from various sources. Each time, we ended up using some well known libraries and kind of lost time by reimplementing a solution from a project to another project. Meh.
# Menu
## Features
* [Usage](#usage)
* [Security](#security)
* [Scalability](#scalability)
* [Custom implementation](#custom-implementation)
* [Clients](#clients)
* 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 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
## Usage
## Quick start
Let's say you're starting the API using this simple command:
```sh
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:1.0.0
```
The API is now available on your host under `http://127.0.0.1:3000`.
It accepts `POST` requests with a `multipart/form-data` Content-Type. Your form data should provide one or more files to convert.
It currently accepts the following:
* Markdown files
* HTML files
* Office documents (.docx, .doc, .odt, .pptx, .ppt, .odp and so on)
* PDF files (if more than one file to convert)
**Heads up:** the API relies on the file extension to determine which library to use for conversion.
There are two use cases:
* If you send one file, it will convert it and return the resulting PDF
* If many files, it will convert them to PDF, merge the resulting PDFs into a single PDF and return it
### Examples:
* One file
```sh
$ curl --request POST \
--url http://127.0.0.1:3000 \
--header 'Content-Type: multipart/form-data' \
--form files=@file.docx \
> result.pdf
```
* Many files
```sh
$ curl --request POST \
--url http://127.0.0.1:3000 \
--header 'Content-Type: multipart/form-data' \
--form files=@file.md \
--form files=@file.html \
--form files=@file.pdf \
--form files=@file.docx \
> result.pdf
```
## Security
The API does not provide any authentication mechanisms. Make sure to not put it on a public facing port and your client(s) should always
controls what is sent to the API.
## Scalability
Some libraries like **unoconv** cannot perform concurrent conversions. That's why the API does only one conversion at a time.
If your API is under heavy load, a request will take time to be processed.
Fortunately, you may pass through this limitation by scaling the API.
In the following example, I'll demonstrate how to do some vertical scaling (= on the same machine) with Docker Compose, but of course horizontal scaling works too!
```yaml
version: '3'
services:
# your others services
gotenberg:
image: gotenberg:1.0.0
```
You may now launch your services using:
Open a terminal and run the following command:
```bash
docker-compose up --scale gotenberg=your_number_of_instances
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
```
When requesting the Gotenberg service with your client(s), Docker will automatically redirect a request to a Gotenberg container
according to the round-robin strategy.
The API is now available on your host at `http://localhost:3000`.
## Custom implementation
Head to the [documentation](https://thecodingmachine.github.io/gotenberg)
to learn how to interact with it!
The API relies on a simple YAML configuration file called `gotenberg.yml`. It allows you to tweak some values and even provides you
a way to change the commands called for each kind of conversion. The configuration file should be located under `/gotenberg` in your container.
## Badges
The default configuration is located here: [.ci/gotenberg.yml](.ci/gotenberg.yml)
## Clients
* https://github.com/thecodingmachine/gotenberg-php-client (PHP client)
* Add your own client by submitting a [pull request](../../pulls)!
[![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)
[![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)
---
Would you like to update this documentation ? Feel free to open an [issue](../../issues).
<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>

View File

@@ -1,20 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: {
value: [
...
}
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "text"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "text"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 0
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "text"
commands:
markdown:
timeout: 0
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 0
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 0
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 0
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "text"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "DEBUG"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "text"
format: "text"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "text"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "text"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output {{ .ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} cat output {{ .ResultFilePath }}"

View File

@@ -1,17 +0,0 @@
port: 3000
logs:
level: "DEBUG"
format: "text"
commands:
markdown:
timeout: 30
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
html:
timeout: 30
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
office:
timeout: 30
template: "unoconv --format pdf --output \"{{ ResultFilePath }}\" \"{{ .FilePath }}\""
merge:
timeout: 30
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"

Binary file not shown.

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Some title</title>
<meta name="author" content="Julien Neuhart">
<meta name="description" content="Simple HTML page">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>Hi! I have been converted from an HTML page!</p>
</body>
</html>

View File

@@ -1 +0,0 @@
# Hi, I've been converted from a markdown file!

Binary file not shown.

View File

@@ -1,254 +0,0 @@
/*
Package config contains all the logic allowing us to instantiate the application's configuration.
The application's configuration is loaded from a YAML file named gotenberg.yml.
It should be located where the user starts the application from the CLI.
*/
package config
import (
"io/ioutil"
"text/template"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
type (
// AppConfig gathers all data required to instantiate the application.
AppConfig struct {
// Port is the port which the application will listen to.
Port string
// Logs contains the logging configuration.
Logs struct {
// Level is the level of messages which will be logged.
Level logrus.Level
// Formatter defines the logging format when a TTY is not attached.
Formatter logrus.Formatter
}
// CommandsConfig is... an instance of CommandsConfig.
CommandsConfig *CommandsConfig
}
// CommandsConfig gathers all commands' configurations as defined
// by the user in the gotenberg.yml file.
CommandsConfig struct {
// Markdown is the command's configuration for converting
// an Markdown file to PDF.
Markdown *CommandConfig
// HTML is the command's configuration for converting
// an HTML file to PDF.
HTML *CommandConfig
// Office is the command's configuration for converting
// an Office document to PDF.
Office *CommandConfig
// Merge is the command's configuration for merging
// multiple PDF files into one PDF file.
Merge *CommandConfig
}
// CommandConfig is a command's configuration.
CommandConfig struct {
// Timeout is the duration in seconds after which the command's process will be killed
// if it does not finish before.
Timeout int
// Template is the data-driven template of the command.
Template *template.Template
}
)
// NewAppConfig instantiates the application's configuration.
// If something bad happens here, the application should not start.
func NewAppConfig(configurationFilePath string) (*AppConfig, error) {
fileConfig, err := loadFileConfig(configurationFilePath)
if err != nil {
return nil, err
}
c := &AppConfig{}
c.Port = fileConfig.Port
if err := makeLogs(c, fileConfig); err != nil {
return nil, err
}
if err := makeCommandsConfig(c, fileConfig); err != nil {
return nil, err
}
return c, nil
}
// fileConfig gathers all data coming from the configuration file gotenberg.yml.
type fileConfig struct {
Port string `yaml:"port"`
Logs struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
} `yaml:"logs"`
Commands struct {
Markdown struct {
Timeout int `yaml:"timeout"`
Template string `yaml:"template"`
} `yaml:"markdown"`
HTML struct {
Timeout int
Template string
} `yaml:"html"`
Office struct {
Timeout int `yaml:"timeout"`
Template string `yaml:"template"`
} `yaml:"office"`
Merge struct {
Timeout int `yaml:"timeout"`
Template string `yaml:"template"`
} `yaml:"merge"`
} `yaml:"commands"`
}
// loadFileConfig instantiates a fileConfig instance by loading
// the configuration file gotenberg.yml.
func loadFileConfig(configurationFilePath string) (*fileConfig, error) {
c := &fileConfig{}
data, err := ioutil.ReadFile(configurationFilePath)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(data, &c); err != nil {
return nil, err
}
return c, nil
}
// makeLogs is a simple wrapper which populates all data related
// to application's logging.
func makeLogs(appConfig *AppConfig, fileConfig *fileConfig) error {
lvl, err := getLoggingLevelFromFileConfig(fileConfig)
if err != nil {
return err
}
formatter, err := getLoggingFormatterFromFileConfig(fileConfig)
if err != nil {
return err
}
appConfig.Logs.Level = lvl
appConfig.Logs.Formatter = formatter
return nil
}
// levels associates logging levels as defined in the configuration file gotenberg.yml
// with its counterpart from the logrus library.
var levels = map[string]logrus.Level{
"DEBUG": logrus.DebugLevel,
"INFO": logrus.InfoLevel,
"WARN": logrus.WarnLevel,
"ERROR": logrus.ErrorLevel,
"FATAL": logrus.FatalLevel,
"PANIC": logrus.PanicLevel,
}
type wrongLoggingLevelError struct{}
const wrongLoggingLevelErrorMessage = "Accepted values for logging level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC"
func (e *wrongLoggingLevelError) Error() string {
return wrongLoggingLevelErrorMessage
}
// getLoggingLevelFromFileConfig returns a logrus level if a matching was found
// with the one defined by the user.
// If no match, throws an error.
func getLoggingLevelFromFileConfig(c *fileConfig) (logrus.Level, error) {
l, ok := levels[c.Logs.Level]
if !ok {
return 999, &wrongLoggingLevelError{}
}
return l, nil
}
// levels associates logging formats as defined in the configuration file gotenberg.yml
// with its counterpart from the logrus library.
var formatters = map[string]logrus.Formatter{
"text": &logrus.TextFormatter{},
"json": &logrus.JSONFormatter{},
}
type wrongLoggingFormatError struct{}
const wrongLoggingFormatErrorMessage = "Accepted value for logging format: text, json"
func (e *wrongLoggingFormatError) Error() string {
return wrongLoggingFormatErrorMessage
}
// getLoggingLevelFromFileConfig returns a logrus Formatter if a matching was found
// with the format defined by the user.
// If no match, throws an error.
func getLoggingFormatterFromFileConfig(c *fileConfig) (logrus.Formatter, error) {
f, ok := formatters[c.Logs.Format]
if !ok {
return nil, &wrongLoggingFormatError{}
}
return f, nil
}
// makeCommandsConfigs is a simple wrapper which populates all data related
// to commands' configurations.
func makeCommandsConfig(appConfig *AppConfig, fileConfig *fileConfig) error {
appConfig.CommandsConfig = &CommandsConfig{}
appConfig.CommandsConfig.Markdown = &CommandConfig{}
appConfig.CommandsConfig.HTML = &CommandConfig{}
appConfig.CommandsConfig.Office = &CommandConfig{}
appConfig.CommandsConfig.Merge = &CommandConfig{}
appConfig.CommandsConfig.Markdown.Timeout = fileConfig.Commands.Markdown.Timeout
appConfig.CommandsConfig.HTML.Timeout = fileConfig.Commands.HTML.Timeout
appConfig.CommandsConfig.Office.Timeout = fileConfig.Commands.Office.Timeout
appConfig.CommandsConfig.Merge.Timeout = fileConfig.Commands.Merge.Timeout
tmplMarkdown, err := getCommandTemplate(fileConfig.Commands.Markdown.Template, "Markdown")
if err != nil {
return err
}
tmplHTML, err := getCommandTemplate(fileConfig.Commands.HTML.Template, "HTML")
if err != nil {
return err
}
tmplOffice, err := getCommandTemplate(fileConfig.Commands.Office.Template, "Office")
if err != nil {
return err
}
tmplMerge, err := getCommandTemplate(fileConfig.Commands.Merge.Template, "Merge")
if err != nil {
return err
}
appConfig.CommandsConfig.Markdown.Template = tmplMarkdown
appConfig.CommandsConfig.HTML.Template = tmplHTML
appConfig.CommandsConfig.Office.Template = tmplOffice
appConfig.CommandsConfig.Merge.Template = tmplMerge
return nil
}
// getCommandTemplate is a simple helper for parsing a command template as defined by the user.
// If the user gives us a wrong template, throws an error.
func getCommandTemplate(command string, commandName string) (*template.Template, error) {
t, err := template.New(commandName).Parse(command)
if err != nil {
return nil, err
}
return t, nil
}

View File

@@ -1,77 +0,0 @@
package config
import (
"path/filepath"
"testing"
)
func TestNewAppConfig(t *testing.T) {
var path string
// case 1: uses an empty configuration file path.
if _, err := NewAppConfig(""); err == nil {
t.Error("AppConfig should not have been instantiated by using an empty configuration file path")
}
// case 2: uses a broken configuration file.
path, _ = filepath.Abs("../../_tests/configurations/broken-gotenberg.yml")
if _, err := NewAppConfig(path); err == nil {
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
}
// case 3: uses a configuration file with a wrong logging level.
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-level-gotenberg.yml")
if _, err := NewAppConfig(path); err == nil {
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
}
// case 4: uses a configuration file with a wrong logging format.
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-format-gotenberg.yml")
if _, err := NewAppConfig(path); err == nil {
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
}
// case 5: uses a configuration file with a wrong markdown command template.
path, _ = filepath.Abs("../../_tests/configurations/wrong-markdown-command-template-gotenberg.yml")
if _, err := NewAppConfig(path); err == nil {
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
}
// case 6: uses a configuration file with a wrong HTML command template.
path, _ = filepath.Abs("../../_tests/configurations/wrong-html-command-template-gotenberg.yml")
if _, err := NewAppConfig(path); err == nil {
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
}
// case 7: uses a configuration file with a wrong Office command template.
path, _ = filepath.Abs("../../_tests/configurations/wrong-office-command-template-gotenberg.yml")
if _, err := NewAppConfig(path); err == nil {
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
}
// case 8: uses a configuration file with a wrong merge command template.
path, _ = filepath.Abs("../../_tests/configurations/wrong-merge-command-template-gotenberg.yml")
if _, err := NewAppConfig(path); err == nil {
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
}
// case 9: uses a correct configuration file.
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
if _, err := NewAppConfig(path); err != nil {
t.Errorf("AppConfig should have been instantiated with '%s'", path)
}
}
func TestWrongLoggingLevelError(t *testing.T) {
err := &wrongLoggingLevelError{}
if err.Error() != wrongLoggingLevelErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLoggingLevelErrorMessage)
}
}
func TestWrongLoggingFormatError(t *testing.T) {
err := &wrongLoggingFormatError{}
if err.Error() != wrongLoggingFormatErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLoggingFormatErrorMessage)
}
}

View File

@@ -1,75 +0,0 @@
// Package context provides functions for setting and getting values from
// a request's context.
package context
import (
"context"
"net/http"
"github.com/thecodingmachine/gotenberg/app/converter"
)
type key uint32
const (
converterKey key = iota
resultFilePathKey
)
// WithConverter populates a request's context with the given converter
// and returns the updated request.
func WithConverter(r *http.Request, converter *converter.Converter) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, converterKey, converter)
r = r.WithContext(ctx)
return r
}
type converterNotFoundError struct{}
const converterNotFoundErrorMessage = "The converter was not found in request context"
func (e *converterNotFoundError) Error() string {
return converterNotFoundErrorMessage
}
// GetConverter returns the converter if found in
// the request's context. Otherwise throws an error.
func GetConverter(r *http.Request) (*converter.Converter, error) {
c, ok := r.Context().Value(converterKey).(*converter.Converter)
if !ok {
return nil, &converterNotFoundError{}
}
return c, nil
}
// WithResultFilePath populates a request's context with the given result file path
// and returns the updated request.
func WithResultFilePath(r *http.Request, resultFilePath string) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, resultFilePathKey, resultFilePath)
r = r.WithContext(ctx)
return r
}
type resultFilePathNotFoundError struct{}
const resultFilePathNotFoundErrorMessage = "The result file path was not found in request context"
func (e *resultFilePathNotFoundError) Error() string {
return resultFilePathNotFoundErrorMessage
}
// GetResultFilePath returns the result file path if found in
// the request's context. Otherwise throws an error.
func GetResultFilePath(r *http.Request) (string, error) {
path, ok := r.Context().Value(resultFilePathKey).(string)
if !ok {
return "", &resultFilePathNotFoundError{}
}
return path, nil
}

View File

@@ -1,68 +0,0 @@
package context
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/thecodingmachine/gotenberg/app/converter"
)
func TestWithConverter(t *testing.T) {
req := WithConverter(httptest.NewRequest(http.MethodPost, "/", nil), &converter.Converter{})
if c, _ := req.Context().Value(converterKey).(*converter.Converter); c == nil {
t.Errorf("Context returned a wrong converter: got '%v' want not nil", c)
}
}
func TestGetConverter(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", nil)
// case 1: uses a request without a converter entry in its context.
if _, err := GetConverter(req); err == nil {
t.Error("Context should not have a converter entry")
}
// case 2: uses a request with a converter entry in its context.
req = WithConverter(req, &converter.Converter{})
if _, err := GetConverter(req); err != nil {
t.Error("Context should have a converter entry")
}
}
func TestWithResultFilePath(t *testing.T) {
filePath := "file.pdf"
req := WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), filePath)
if path, _ := req.Context().Value(resultFilePathKey).(string); path != filePath {
t.Errorf("Context returned a wrong result file path: got '%s' want '%s'", path, filePath)
}
}
func TestGetResultFilePath(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", nil)
// case 1: uses a request without a result file path entry in its context.
if _, err := GetResultFilePath(req); err == nil {
t.Error("Context should not have a result file path entry")
}
// case 2: uses a request with a result file path entry in its context.
req = WithResultFilePath(req, "file.pdf")
if _, err := GetResultFilePath(req); err != nil {
t.Error("Context should have a result file path entry")
}
}
func TestConverterNotFoundError(t *testing.T) {
err := &converterNotFoundError{}
if err.Error() != converterNotFoundErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), converterNotFoundErrorMessage)
}
}
func TestResultFilePathNotFoundError(t *testing.T) {
err := &resultFilePathNotFoundError{}
if err.Error() != resultFilePathNotFoundErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), resultFilePathNotFoundErrorMessage)
}
}

View File

@@ -1,105 +0,0 @@
// Package converter implements a solution for converting one or more files to PDF.
package converter
import (
"fmt"
"io"
"net/http"
"os"
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
"github.com/thecodingmachine/gotenberg/app/converter/process"
"github.com/satori/go.uuid"
)
// Converter handles conversion into PDF of files coming from a request.
type Converter struct {
files []*gfile.File
workingDir string
}
// NoFileToConvertError is raided when a request has not file attached to it.
type NoFileToConvertError struct{}
const noFileToConvertErrorMessage = "No file to convert"
func (e *NoFileToConvertError) Error() string {
return noFileToConvertErrorMessage
}
// NewConverter instantiates a converter by parsing a request.
func NewConverter(r *http.Request) (*Converter, error) {
c := &Converter{
workingDir: fmt.Sprintf("./%s/", uuid.NewV4().String()),
}
if err := os.Mkdir(c.workingDir, 0666); err != nil {
return nil, err
}
reader, err := r.MultipartReader()
if err != nil {
return c, err
}
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
fileName := part.FileName()
if fileName == "" {
continue
}
f, err := gfile.NewFile(c.workingDir, part, fileName)
if err != nil {
return c, err
}
c.files = append(c.files, f)
}
if len(c.files) == 0 {
return c, &NoFileToConvertError{}
}
return c, nil
}
// Convert converts its associated files to PDF. If more than one file,
// it will merge all of them into one unique PDF file.
// Returns the new file path or an error if something bad happened.
func (c *Converter) Convert() (string, error) {
var filesPaths []string
for _, f := range c.files {
if f.Type != gfile.PDFType {
path, err := process.Unconv(c.workingDir, f)
if err != nil {
return "", err
}
filesPaths = append(filesPaths, path)
} else {
filesPaths = append(filesPaths, f.Path)
}
}
if len(filesPaths) == 1 {
return filesPaths[0], nil
}
path, err := process.Merge(c.workingDir, filesPaths)
if err != nil {
return "", err
}
return path, nil
}
// Clear removes all file inside its working directory.
func (c *Converter) Clear() error {
return os.RemoveAll(c.workingDir)
}

View File

@@ -1,147 +0,0 @@
package converter
import (
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/thecodingmachine/gotenberg/app/config"
"github.com/thecodingmachine/gotenberg/app/converter/process"
)
func makeRequest(filesPaths ...string) *http.Request {
r, w := io.Pipe()
mpw := multipart.NewWriter(w)
go func() {
var part io.Writer
defer w.Close()
if len(filesPaths) == 0 {
part, _ = mpw.CreateFormField("foo")
part.Write([]byte("bar"))
} else {
for _, filePath := range filesPaths {
file, _ := os.Open(filePath)
defer file.Close()
fileInfo, _ := file.Stat()
part, _ = mpw.CreateFormFile("files", fileInfo.Name())
io.Copy(part, file)
}
}
mpw.Close()
}()
req := httptest.NewRequest(http.MethodPost, "/", r)
req.Header.Set("Content-Type", mpw.FormDataContentType())
return req
}
func loadCommandConfigs(configurationFilePath string) {
path, _ := filepath.Abs(configurationFilePath)
c, _ := config.NewAppConfig(path)
process.Load(c.CommandsConfig)
}
func TestNewConverter(t *testing.T) {
var (
path string
oPath string
)
// case 1: uses a request with a single file.
path, _ = filepath.Abs("../../_tests/file.docx")
if _, err := NewConverter(makeRequest(path)); err != nil {
t.Errorf("Converter should have been instantiated with '%s'", path)
}
// case 2: uses a request with wrong file type.
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
if _, err := NewConverter(makeRequest(path)); err == nil {
t.Errorf("Converter should not have been instantiated with '%s'", path)
}
// case 3: uses a request with two files.
path, _ = filepath.Abs("../../_tests/file.pdf")
oPath, _ = filepath.Abs("../../_tests/file.docx")
if _, err := NewConverter(makeRequest(path, oPath)); err != nil {
t.Errorf("Converter should have been instantiated with '%s' and '%s'", path, oPath)
}
// case 4: uses a request with one Office file type and one wrong file type.
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
oPath, _ = filepath.Abs("../../_tests/file.docx")
if _, err := NewConverter(makeRequest(path, oPath)); err == nil {
t.Errorf("Converter should not have been instantiated with '%s' and '%s'", path, oPath)
}
// case 5: uses a request with no file.
if _, err := NewConverter(makeRequest()); err == nil {
t.Error("Converter should not have been instantiated with no file")
}
}
func TestConvert(t *testing.T) {
var (
path string
oPath string
c *Converter
)
loadCommandConfigs("../../_tests/configurations/gotenberg.yml")
// case 1: uses a request with a single file.
path, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path))
if _, err := c.Convert(); err != nil {
t.Errorf("Converter should have been able to convert '%s' to PDF", path)
}
// case 2: uses a request with two files.
path, _ = filepath.Abs("../../_tests/file.pdf")
oPath, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path, oPath))
if _, err := c.Convert(); err != nil {
t.Errorf("Converter should have been able to convert '%s' and '%s' to PDF", path, oPath)
}
loadCommandConfigs("../../_tests/configurations/timeout-gotenberg.yml")
// case 3: uses a request with a single file and a configuration with an unsuitable timeout for the conversion commands.
path, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path))
if _, err := c.Convert(); err == nil {
t.Errorf("Converter should not have been able to convert '%s' to PDF", path)
}
loadCommandConfigs("../../_tests/configurations/merge-timeout-gotenberg.yml")
// case 4: uses a request with two files and a configuration with an unsuitable timeout for the merge command.
path, _ = filepath.Abs("../../_tests/file.pdf")
oPath, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path, oPath))
if _, err := c.Convert(); err == nil {
t.Errorf("Converter should not have been able to merge '%s' and '%s' into PDF", path, oPath)
}
}
func TestClear(t *testing.T) {
path, _ := filepath.Abs("../../_tests/file.docx")
c, _ := NewConverter(makeRequest(path))
if err := c.Clear(); err != nil {
t.Error("Converter should have been able to clear itself")
}
}
func TestNoFileToConvertError(t *testing.T) {
err := &NoFileToConvertError{}
if err.Error() != noFileToConvertErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), noFileToConvertErrorMessage)
}
}

View File

@@ -1,98 +0,0 @@
// Package file implements a solution for handling files coming from a request.
package file
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/satori/go.uuid"
)
// File represents a file which has been created
// from a request.
type File struct {
// Type is the kind of file.
Type Type
// Path is the file path.
Path string
}
// Type represents what kind of file we're dealing with.
type Type uint32
const (
// PDFType represents a... PDF file.
PDFType Type = iota
// MarkdownType represents a... Markdown file.
MarkdownType
// HTMLType represents an... HTML file.
HTMLType
// OfficeType represents an... Office document.
OfficeType
)
// filesTypes associates a file extension with its file kind counterpart.
var filesTypes = map[string]Type{
".pdf": PDFType,
".md": MarkdownType,
".htm": HTMLType,
".html": HTMLType,
".doc": OfficeType,
".docx": OfficeType,
".odt": OfficeType,
".xls": OfficeType,
".xlsx": OfficeType,
".ods": OfficeType,
".ppt": OfficeType,
".pptx": OfficeType,
".odp": OfficeType,
}
type fileTypeNotFoundError struct {
fileName string
}
func (e *fileTypeNotFoundError) Error() string {
return fmt.Sprintf("File type was not found for '%s'", e.fileName)
}
// NewFile creates a file in the considered directory.
// Returns a *File instance or an error if something bad happened.
func NewFile(workingDir string, r io.Reader, fileName string) (*File, error) {
ext := filepath.Ext(fileName)
t, ok := filesTypes[ext]
if !ok {
return nil, &fileTypeNotFoundError{fileName: fileName}
}
f := &File{
Path: MakeFilePath(workingDir, ext),
Type: t,
}
file, err := os.Create(f.Path)
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, r)
if err != nil {
return nil, err
}
// resets the read pointer.
file.Seek(0, 0)
return f, nil
}
// MakeFilePath is a simple helper which generates a random file name
// and associates it with the considered directory to make a path.
func MakeFilePath(workingDir string, ext string) string {
return fmt.Sprintf("%s%s%s", workingDir, uuid.NewV4().String(), ext)
}

View File

@@ -1,37 +0,0 @@
package file
import (
"bytes"
"fmt"
"os"
"path/filepath"
"testing"
)
func TestNewFile(t *testing.T) {
workingDir := "test"
os.Mkdir(workingDir, 0666)
// case 1: uses a wrong file name.
if _, err := NewFile(workingDir, new(bytes.Buffer), "file.yml"); err == nil {
t.Error("File should not have been instantiated with an empty buffer")
}
// case 2: uses a reader from a correct file type.
filePath, _ := filepath.Abs("../../../_tests/file.pdf")
r, _ := os.Open(filePath)
defer r.Close()
if _, err := NewFile(workingDir, r, "file.pdf"); err != nil {
t.Errorf("File should have been instantiated using a reader from '%s'", filePath)
}
os.RemoveAll(workingDir)
}
func TestFileTypeNotFoundError(t *testing.T) {
fileName := "file.wp"
err := &fileTypeNotFoundError{fileName: fileName}
expected := fmt.Sprintf("File type was not found for '%s'", fileName)
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}

View File

@@ -1,155 +0,0 @@
// Package process handles all commands executions.
package process
import (
"bytes"
"fmt"
"os/exec"
"sync"
"text/template"
"time"
"github.com/thecodingmachine/gotenberg/app/config"
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
)
type runner struct {
mu sync.Mutex
commandsConfig *config.CommandsConfig
}
var forest = &runner{}
type commandTimeoutError struct {
command string
timeout int
}
func (e *commandTimeoutError) Error() string {
return fmt.Sprintf("The command '%s' has reached the %d second(s) timeout", e.command, e.timeout)
}
// run runs the given command. If timeout is reached or
// something bad happened, returns an error.
func (r *runner) run(command string, timeout int) error {
r.mu.Lock()
defer r.mu.Unlock()
cmd := exec.Command("/bin/sh", "-c", command)
if err := cmd.Start(); err != nil {
return err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
// wait for the process to finish or kill it after a timeout.
select {
case <-time.After(time.Duration(timeout) * time.Second):
if err := cmd.Process.Kill(); err != nil {
return err
}
return &commandTimeoutError{
command: command,
timeout: timeout,
}
case err := <-done:
if err != nil {
return err
}
return nil
}
}
// Load loads the commands configuration coming from the application configuration.
func Load(config *config.CommandsConfig) {
forest.commandsConfig = config
}
// conversionData will be applied to the data-driven templates of conversions commands.
type conversionData struct {
FilePath string
ResultFilePath string
}
type impossibleConversionError struct{}
const impossibleConversionErrorMessage = "Impossible conversion"
func (e *impossibleConversionError) Error() string {
return impossibleConversionErrorMessage
}
// Unconv converts a file to PDF and returns the new file path.
func Unconv(workingDir string, file *gfile.File) (string, error) {
cmdData := &conversionData{
FilePath: file.Path,
ResultFilePath: gfile.MakeFilePath(workingDir, ".pdf"),
}
var (
cmdTimeout int
cmdTemplate *template.Template
)
switch file.Type {
case gfile.MarkdownType:
cmdTimeout = forest.commandsConfig.Markdown.Timeout
cmdTemplate = forest.commandsConfig.Markdown.Template
break
case gfile.HTMLType:
cmdTimeout = forest.commandsConfig.HTML.Timeout
cmdTemplate = forest.commandsConfig.HTML.Template
break
case gfile.OfficeType:
cmdTimeout = forest.commandsConfig.Office.Timeout
cmdTemplate = forest.commandsConfig.Office.Template
break
default:
return "", &impossibleConversionError{}
}
var data bytes.Buffer
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
return "", err
}
err := forest.run(data.String(), cmdTimeout)
if err != nil {
return "", err
}
return cmdData.ResultFilePath, nil
}
// mergeData will be applied to the data-driven template of the merge command.
type mergeData struct {
FilesPaths []string
ResultFilePath string
}
// Merge merges many PDF files to one unique PDF file and returns the new file path.
func Merge(workingDir string, filesPaths []string) (string, error) {
cmdData := &mergeData{
FilesPaths: filesPaths,
ResultFilePath: gfile.MakeFilePath(workingDir, ".pdf"),
}
cmdTimeout := forest.commandsConfig.Merge.Timeout
cmdTemplate := forest.commandsConfig.Merge.Template
var data bytes.Buffer
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
return "", err
}
err := forest.run(data.String(), cmdTimeout)
if err != nil {
return "", err
}
return cmdData.ResultFilePath, nil
}

View File

@@ -1,148 +0,0 @@
package process
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/thecodingmachine/gotenberg/app/config"
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
)
func makeFile(workingDir string, fileName string) *gfile.File {
filePath := fmt.Sprintf("%s%s", "../../../_tests/", fileName)
absPath, _ := filepath.Abs(filePath)
r, _ := os.Open(absPath)
defer r.Close()
f, _ := gfile.NewFile(workingDir, r, fileName)
return f
}
func loadCommandConfigs(configurationFilePath string) {
path, _ := filepath.Abs(configurationFilePath)
c, _ := config.NewAppConfig(path)
Load(c.CommandsConfig)
}
func TestLoad(t *testing.T) {
path, _ := filepath.Abs("../../../_tests/configurations/gotenberg.yml")
c, _ := config.NewAppConfig(path)
Load(c.CommandsConfig)
if c.CommandsConfig != forest.commandsConfig {
t.Error("Commands configuration should have been loaded correctly")
}
}
func TestRun(t *testing.T) {
var cmd string
// case 1: uses a simple command.
cmd = "echo Hello world"
if err := forest.run(cmd, 30); err != nil {
t.Errorf("Command '%s' should have worked", cmd)
}
// case 2: uses a simple command but with an unsuitable timeout.
cmd = "sleep 5"
if err := forest.run(cmd, 0); err == nil {
t.Errorf("Command '%s' should not have worked", cmd)
}
// case 3: uses a broken command.
cmd = "helloworld"
if err := forest.run(cmd, 30); err == nil {
t.Errorf("Command '%s' should not have worked", cmd)
}
}
func TestUnconv(t *testing.T) {
var file *gfile.File
workingDir := "test"
os.Mkdir(workingDir, 0666)
loadCommandConfigs("../../../_tests/configurations/gotenberg.yml")
// case 1: uses an Markdown file type.
file = makeFile(workingDir, "file.md")
if _, err := Unconv(workingDir, file); err != nil {
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
}
// case 2: uses an HTML file type.
file = makeFile(workingDir, "file.html")
if _, err := Unconv(workingDir, file); err != nil {
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
}
// case 3: uses an Office file type.
file = makeFile(workingDir, "file.docx")
if _, err := Unconv(workingDir, file); err != nil {
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
}
// case 4: uses a PDF file type.
file = makeFile(workingDir, "file.pdf")
if _, err := Unconv(workingDir, file); err == nil {
t.Errorf("Converting '%s' to PDF should not have worked", file.Path)
}
loadCommandConfigs("../../../_tests/configurations/timeout-gotenberg.yml")
// case 5: uses a command with an unsuitable timeout.
file = makeFile(workingDir, "file.docx")
if _, err := Unconv(workingDir, makeFile(workingDir, "file.docx")); err == nil {
t.Errorf("Converting '%s' to PDF should have reached timeout", file.Path)
}
os.RemoveAll(workingDir)
}
func TestMerge(t *testing.T) {
workingDir := "test"
os.Mkdir(workingDir, 0666)
loadCommandConfigs("../../../_tests/configurations/gotenberg.yml")
var filesPaths []string
path, _ := filepath.Abs("../../../_tests/file.pdf")
filesPaths = append(filesPaths, path)
filesPaths = append(filesPaths, path)
// case 1: simple merge.
if _, err := Merge(workingDir, filesPaths); err != nil {
t.Error("Merge should have worked")
}
loadCommandConfigs("../../../_tests/configurations/timeout-gotenberg.yml")
// case 2: uses a command with an unsuitable timeout.
if _, err := Merge(workingDir, filesPaths); err == nil {
t.Error("Merge should have reached timeout")
}
os.RemoveAll(workingDir)
}
func TestImpossibleConversionError(t *testing.T) {
err := &impossibleConversionError{}
if err.Error() != impossibleConversionErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), impossibleConversionErrorMessage)
}
}
func TestCommandTimeoutError(t *testing.T) {
err := &commandTimeoutError{
command: "echo hello",
timeout: 30,
}
expected := fmt.Sprintf("The command '%s' has reached the %d second(s) timeout", err.command, err.timeout)
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}

View File

@@ -1,153 +0,0 @@
// Package app implements all functions on which a request will pass through.
package app
import (
"fmt"
"io"
"net/http"
"os"
"github.com/thecodingmachine/gotenberg/app/context"
"github.com/thecodingmachine/gotenberg/app/converter"
ghttp "github.com/thecodingmachine/gotenberg/app/http"
"github.com/thecodingmachine/gotenberg/app/logger"
"github.com/justinas/alice"
)
// GetHandlersChain returns the handlers chaining
// thanks to the alice library.
func GetHandlersChain() http.Handler {
return alice.New(enforceContentLengthHandler, enforceContentTypeHandler, convertHandler).ThenFunc(serveHandler)
}
type requestHasNoContentError struct{}
func (e *requestHasNoContentError) Error() string {
return "Request has not content"
}
// enforeContentLengthHandler checks if the request has content.
func enforceContentLengthHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength == 0 {
e := &requestHasNoContentError{}
http.Error(w, e.Error(), http.StatusBadRequest)
logger.Error(e)
return
}
next.ServeHTTP(w, r)
})
}
// enforceContentTypeHandler checks if the "Content-Type" entry
// from the request's header matches the allowed content type.
func enforceContentTypeHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := ghttp.CheckAuthorizedContentType(r.Header); err != nil {
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
logger.Error(err)
return
}
next.ServeHTTP(w, r)
})
}
// convertHandler is in charge of converting the file(s) from the request to PDF.
func convertHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := converter.NewConverter(r)
if err != nil {
if _, ok := err.(*converter.NoFileToConvertError); ok {
http.Error(w, err.Error(), http.StatusBadRequest)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
logger.Error(err)
if c != nil {
r = context.WithConverter(r, c)
cleanup(r)
}
return
}
r = context.WithConverter(r, c)
path, err := c.Convert()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
r = context.WithResultFilePath(r, path)
next.ServeHTTP(w, r)
})
}
// serveHandler simply serves the created PDF.
func serveHandler(w http.ResponseWriter, r *http.Request) {
path, err := context.GetResultFilePath(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
reader, err := os.Open(path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
defer reader.Close()
resultFileInfo, err := reader.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
done := make(chan error, 1)
go func() {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", resultFileInfo.Name()))
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Length", fmt.Sprintf("%d", resultFileInfo.Size()))
_, err := io.Copy(w, reader)
done <- err
}()
err = <-done
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
}
cleanup(r)
}
// cleanup removes all files created during the conversion.
func cleanup(r *http.Request) {
c, err := context.GetConverter(r)
if err != nil {
logger.Warn(err.Error())
return
}
if err := c.Clear(); err != nil {
logger.Warn(err.Error())
}
}

View File

@@ -1,221 +0,0 @@
package app
import (
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/thecodingmachine/gotenberg/app/config"
"github.com/thecodingmachine/gotenberg/app/context"
"github.com/thecodingmachine/gotenberg/app/converter/process"
"github.com/justinas/alice"
)
func makeRequest(filesPaths ...string) *http.Request {
r, w := io.Pipe()
mpw := multipart.NewWriter(w)
go func() {
var part io.Writer
defer w.Close()
if len(filesPaths) == 0 {
part, _ = mpw.CreateFormField("foo")
part.Write([]byte("bar"))
} else {
for _, filePath := range filesPaths {
file, _ := os.Open(filePath)
defer file.Close()
fileInfo, _ := file.Stat()
part, _ = mpw.CreateFormFile("files", fileInfo.Name())
io.Copy(part, file)
}
}
mpw.Close()
}()
req := httptest.NewRequest(http.MethodPost, "/", r)
req.Header.Set("Content-Type", mpw.FormDataContentType())
return req
}
func loadCommandConfigs(configurationFilePath string) {
path, _ := filepath.Abs(configurationFilePath)
c, _ := config.NewAppConfig(path)
process.Load(c.CommandsConfig)
}
func fakeSuccessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func TestGetHandlersChain(t *testing.T) {
// dumb test to improve code coverage...
if GetHandlersChain() == nil {
t.Errorf("Handler chains should not be nil")
}
}
func TestEnforceContentLengthHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
)
h := alice.New(enforceContentLengthHandler).ThenFunc(fakeSuccessHandler)
// case 1: sends an empty request.
req = httptest.NewRequest(http.MethodPost, "/", nil)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusBadRequest {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusBadRequest)
}
// case 2: sends a real body.
path, _ := filepath.Abs("../_tests/file.docx")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, makeRequest(path))
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
}
func TestEnforceContentTypeHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
)
h := alice.New(enforceContentTypeHandler).ThenFunc(fakeSuccessHandler)
// case 1: sends a wrong content type.
req = httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set("Content-Type", "application/pdf")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusUnsupportedMediaType {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusUnsupportedMediaType)
}
// case 2: sends a good content type.
path, _ := filepath.Abs("../_tests/file.docx")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, makeRequest(path))
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned wrong a status code: got '%v' want '%v'", status, http.StatusOK)
}
}
func TestConvertHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
path string
oPath string
)
h := alice.New(convertHandler).ThenFunc(fakeSuccessHandler)
// case 1: sends a request without body.
req = httptest.NewRequest(http.MethodPost, "/", nil)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
// case 2: sends a request with no file.
req = makeRequest()
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusBadRequest {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusBadRequest)
}
loadCommandConfigs("../_tests/configurations/merge-timeout-gotenberg.yml")
// case 3: sends a request with two files and using an unsuitable timeout for merge commande.
path, _ = filepath.Abs("../_tests/file.pdf")
oPath, _ = filepath.Abs("../_tests/file.docx")
req = makeRequest(path, oPath)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
loadCommandConfigs("../_tests/configurations/gotenberg.yml")
// case 4: sends a request with two files.
path, _ = filepath.Abs("../_tests/file.pdf")
oPath, _ = filepath.Abs("../_tests/file.docx")
req = makeRequest(path, oPath)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
// case 5: sends five requests (almost) simultany.
path, _ = filepath.Abs("../_tests/file.docx")
filesPaths := []string{
path,
path,
path,
path,
path,
}
for i := 0; i < len(filesPaths); i++ {
go func(i int) {
req := makeRequest(filesPaths[i])
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
}(i)
}
}
func TestServeHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
)
h := alice.New().ThenFunc(serveHandler)
// case 1: sends a request without a result file path entry in its context.
req = httptest.NewRequest(http.MethodPost, "/", nil)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
// case 2: sends a request with a wrong result file path entry in its context.
req = context.WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), "file")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
// case 3: sends a request with a correct result file path entry in its context.
path, _ := filepath.Abs("../_tests/file.pdf")
req = context.WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), path)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
}

View File

@@ -1,47 +0,0 @@
// Package http provides functions for detecting a request or a file content type.
package http
import (
"fmt"
"net/http"
"strings"
)
// ContentType is a string which represents a content type.
type ContentType string
// MultipartFormDataContentType represents... the multipart form data content type.
const MultipartFormDataContentType ContentType = "multipart/form-data"
type notAuthorizedContentTypeError struct{}
func (e *notAuthorizedContentTypeError) Error() string {
return fmt.Sprintf("Accepted value for 'Content-Type': %s", MultipartFormDataContentType)
}
// CheckAuthorizedContentType checks if the request header header has an authorized content type.
// If no authorized content type found, throws an error.
func CheckAuthorizedContentType(h http.Header) error {
ct := findContentType(h.Get("Content-Type"), MultipartFormDataContentType)
if ct == "" {
return &notAuthorizedContentTypeError{}
}
return nil
}
// findContentType parses a string representing a content type and tries to find
// one of the given content types.
func findContentType(requestContentType string, contentTypes ...ContentType) ContentType {
for _, ct := range contentTypes {
if i := strings.IndexRune(requestContentType, ';'); i != -1 {
requestContentType = requestContentType[0:i]
}
if requestContentType == string(ct) {
return ct
}
}
return ""
}

View File

@@ -1,38 +0,0 @@
package http
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestCheckAuthorizedContentType(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", nil)
// case 1: uses a request without a content type entry in its header.
if err := CheckAuthorizedContentType(req.Header); err == nil {
t.Error("Function should not have been able to retrieve an authorized content type from request's header")
}
// case 2: uses a request with a content type entry in its header.
req.Header.Set("Content-Type", string(MultipartFormDataContentType))
if err := CheckAuthorizedContentType(req.Header); err != nil {
t.Error("Function should have been able to retrieve an authorized content type from request's header")
}
// case 3: uses a request with a composed content type entry in its header.
req.Header.Set("Content-Type", "multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW")
if err := CheckAuthorizedContentType(req.Header); err != nil {
t.Error("Function should have been able to retrieve an authorized content type from request's header")
}
}
func TestNotAuthorizedContentTypeError(t *testing.T) {
err := &notAuthorizedContentTypeError{}
expected := fmt.Sprintf("Accepted value for 'Content-Type': %s", MultipartFormDataContentType)
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}

View File

@@ -1,79 +0,0 @@
// Package logger implements a simple wrapper of the logrus library.
package logger
import (
"os"
"github.com/sirupsen/logrus"
)
// logger wraps a logrus.Logger instance.
type logger struct {
logger *logrus.Logger
}
// log is our logger instance used across the application.
var log = newLogger()
// newLogger instantiates a logger instance with default values.
func newLogger() *logger {
l := &logger{
logger: logrus.New(),
}
l.logger.Out = os.Stdout
l.logger.Level = logrus.InfoLevel
return l
}
// SetLevel updates the level of messages which will be logged.
func SetLevel(level logrus.Level) {
log.logger.SetLevel(level)
}
// SetFormatter updates the output format.
// When a TTY is not attached, the output will be in the defined format.
func SetFormatter(formatter logrus.Formatter) {
log.logger.Formatter = formatter
}
// Debug is a wrapper of the logrus Debug function.
func Debug(message string) {
log.logger.Debug(message)
}
// Debugf is a wrapper of the logrus Debugf function.
func Debugf(format string, args ...interface{}) {
log.logger.Debugf(format, args)
}
// Info is a wrapper of the logrus Info function.
func Info(message string) {
log.logger.Info(message)
}
// Infof is a wrapper of the logrus Infof function.
func Infof(format string, args ...interface{}) {
log.logger.Infof(format, args)
}
// Warn is a wrapper of the logrus Warn function.
func Warn(message string) {
log.logger.Warn(message)
}
// Error is a wrapper of the logrus Error function.
func Error(err error) {
log.logger.Error(err.Error())
}
// Fatal is a wrapper of the logrus Fatal function.
func Fatal(err error) {
log.logger.Fatal(err.Error())
}
// Panic is a wrapper of the logrus Panic function.
func Panic(err error) {
log.logger.Panic(err.Error())
}

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

116
build/base/Dockerfile Normal file
View File

@@ -0,0 +1,116 @@
FROM debian:buster-slim
# |--------------------------------------------------------------------------
# | Common libraries
# |--------------------------------------------------------------------------
# |
# | Libraries used in the build process of this image.
# |
RUN echo "deb http://httpredir.debian.org/debian/ buster main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y curl wget gnupg ttf-mscorefonts-installer procps
# |--------------------------------------------------------------------------
# | Chrome
# |--------------------------------------------------------------------------
# |
# | Installs Chrome.
# |
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
apt-get update &&\
apt-get -y --allow-unauthenticated install google-chrome-stable
# |--------------------------------------------------------------------------
# | LibreOffice
# |--------------------------------------------------------------------------
# |
# | Installs LibreOffice.
# |
# https://github.com/nextcloud/docker/issues/380
RUN mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
echo "deb http://httpredir.debian.org/debian/ buster-backports main contrib non-free" >> /etc/apt/sources.list &&\
apt-get update &&\
apt-get -t buster-backports -y install libreoffice
# |--------------------------------------------------------------------------
# | Unoconv
# |--------------------------------------------------------------------------
# |
# | Installs unoconv.
# |
ENV UNO_URL=https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv
RUN curl -Ls $UNO_URL -o /usr/bin/unoconv &&\
chmod +x /usr/bin/unoconv &&\
ln -s /usr/bin/python3 /usr/bin/python &&\
unoconv --version
# |--------------------------------------------------------------------------
# | PDFtk
# |--------------------------------------------------------------------------
# |
# | Installs PDFtk as an alternative to pdfcpu for merging PDFs.
# | https://github.com/thecodingmachine/gotenberg/issues/29
# |
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
# |--------------------------------------------------------------------------
# |
# | Installs a handful of fonts.
# | Note: ttf-mscorefonts-installer are installed on top of this Dockerfile.
# |
# Credits:
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html
RUN apt-get install -y \
culmus \
fonts-beng \
fonts-hosny-amiri \
fonts-lklug-sinhala \
fonts-lohit-guru \
fonts-lohit-knda \
fonts-samyak-gujr \
fonts-samyak-mlym \
fonts-samyak-taml \
fonts-sarai \
fonts-sil-abyssinica \
fonts-sil-padauk \
fonts-telu \
fonts-thai-tlwg \
fonts-liberation \
ttf-wqy-zenhei \
fonts-arphic-uming \
fonts-ipafont-mincho \
fonts-ipafont-gothic \
fonts-unfonts-core
COPY build/base/fonts/* /usr/local/share/fonts/
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
# |--------------------------------------------------------------------------
# | Default user
# |--------------------------------------------------------------------------
# |
# | All processes in the Docker container will run as a dedicated
# | non-root user.
# |
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

29
build/base/fonts.conf Normal file
View File

@@ -0,0 +1,29 @@
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>
<match target="font">
<edit mode="assign" name="rgba">
<const>rgb</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hinting">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hintstyle">
<const>hintslight</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="antialias">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="lcdfilter">
<const>lcddefault</const>
</edit>
</match>
</fontconfig>

Binary file not shown.

22
build/docs/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM thecodingmachine/gotenberg:workspace
# |--------------------------------------------------------------------------
# | static
# |--------------------------------------------------------------------------
# |
# | Installs the static site anti-framework general-purpose library,
# | purpose-built commands for various domains.
# |
RUN go get github.com/apex/static/cmd/static-docs
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
WORKDIR /gotenberg/docs
CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ]

View File

@@ -0,0 +1,10 @@
---
title: Introduction
---
[Gotenberg](https://github.com/thecodingmachine/gotenberg/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.
* 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: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries

View File

@@ -0,0 +1,73 @@
---
title: Install
---
Gotenberg is shipped within a Docker image.
**[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:
```bash
$ 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:
```yaml
version: '3'
services:
# your other services
gotenberg:
image: thecodingmachine/gotenberg:6
```
> The API will be available under `gotenberg:3000` in your Docker Compose network.
## Kubernetes
It may also be deployed with Kubernetes.
Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0.2` CPU).
> The more resources are granted, the quicker will be the conversions.
In the deployment specification of the pod, also specify the uid of the user `gotenberg`:
```
securityContext:
privileged: false
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

@@ -0,0 +1,34 @@
---
title: Clients
---
We provide clients in various languages for easing the interactions with the API.
## Go client
```bash
$ go get -u github.com/thecodingmachine/gotenberg-go-client/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`:
```bash
$ composer require php-http/guzzle6-adapter
```
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

@@ -0,0 +1,125 @@
---
title: Environment variables
---
You may customize the API behaviour thanks to environment variables.
## Log level
The API provides structured logging allowing you to have relevant information
about what's going on.
> If a TTY is attached, the log entries are displayed in text format with colors, otherwise in JSON format.
You may customize the severity of the log entries thanks to the environment variable `LOG_LEVEL`.
It accepts one of the following severities: `"DEBUG"`, `"INFO"` (default) and `"ERROR"`.
## Default listen port
By default, the API will listen on port `3000`.
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`
for disabling Google Chrome.
It takes the strings `"0"` or `"1"` as value where `1` means `true`
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
## Default Google Chrome rpcc buffer size
When performing a [HTML](#html), [URL](#url) or [Markdown](#markdown) conversion, the API might return
a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
If so, you may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`.
It takes a string representation of an int as value (e.g. `"1048576"` for 1 MB).
The hard limit is 100 MB and is defined by Google Chrome itself.
> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`.
> See the [rpcc buffer size section](#html.rpcc_buffer_size).
## 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`.
> If LibreOffice (unoconv) is disabled, the following conversion will **not** be available anymore:
> [Office](#office)
## Default wait timeout
By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful.
If unsucessful, it returns a `504` HTTP code.
You may customize this timeout thanks to the environment variable `DEFAULT_WAIT_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
> The default timeout may also be overridden per request thanks to the form field `waitTimeout`.
> See the [timeout section](#timeout).
## Maximum wait timeout
By default, the value of the form field `waitTimeout` cannot be more than 30 seconds.
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
## Default webhook URL timeout
By default, the API will wait 10 seconds before it considers the sending of the resulting PDF to be unsuccessful.
> See the [webhook section](#webhook).
You may customize this timeout thanks to the environment variable `DEFAULT_WEBHOOK_URL_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
> The default timeout may also be overridden per request thanks to the form field `webhookURLTimeout`.
> See the [webhook timeout section](#webhook.timeout).
## Maximum webhook URL timeout
By default, the value of the form field `webhookURLTimeout` cannot be more than 30 seconds.
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WEBHOOK_URL_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
## Maximum wait delay
By default, the value of the form field `waitDelay` cannot be more than 10 seconds.
> See the [wait delay section](#html.wait_delay).
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_DELAY`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).

View File

@@ -0,0 +1,451 @@
---
title: HTML
---
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
The only requirement is to send a file named `index.html`: it is the file
which will be converted to PDF.
For instance:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
```
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
-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)
dest := "result.pdf"
c.Store(req, dest)
```
### 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);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Header and footer
You may also add a header and/or a footer in the resulting PDF.
Respectively, a file named `header.html` and `footer.html`.
Each of them **has to be a complete HTML document**:
```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 allow you to inject printing values:
* `date`: formatted print date
* `title`: document title
* `pageNumber`: current page number
* `totalPage`: total pages in the document
There are some limitations:
* JavaScript is not executed
* external resources are not loaded
* the CSS properties are independant of the ones used in the `index.html` file
* `footer.html` CSS properties override the ones from `header.html`
* only fonts installed in the Docker image are loaded (see the [fonts section](#fonts))
* images only work using a `base64` encoded source (`<img src="data:image/png;base64, iVBORw0K... />`)
* `background-color` and `color` CSS properties require an additional `-webkit-print-color-adjust: exact` CSS property in order to work
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form files=@header.html \
--form files=@footer.html \
-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")
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
```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');
$header = DocumentFactory::makeFromPath('header.html', '/path/to/file');
$footer = DocumentFactory::makeFromPath('footer.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setHeader($header);
$request->setFooter($footer);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Assets
You may also send additional files. For instance: images, fonts, stylesheets and so on.
The only requirement is to make sure that their paths
are on the same level as the `index.html` file.
In other words, this will work:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
<img src="img.png">
</body>
</html>
```
But this won't:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
<img src="/foo/img.png">
</body>
</html>
```
You may also use *remote* paths for Google fonts, images and so on.
> If you want to install fonts directly in the Gotenberg Docker image,
> see to the [fonts section](#fonts).
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form files=@style.css \
--form files=@img.png \
--form files=@font.woff \
-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")
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
```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');
$assets = [
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';
$client->store($request, $dest);
```
## 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 and 100% (`1.0`) page scale.
> Paper size and margins have to be provided in `inches`. Same for margins.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form paperWidth=8.27 \
--form paperHeight=11.69 \
--form marginTop=0 \
--form marginBottom=0 \
--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/v7"
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
```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->setPaperSize(Request::A4);
$request->setMargins(Request::NO_MARGINS);
$request->setLandscape(true);
$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);
```
## Wait delay
In some cases, you may want to wait a certain amount of time to make sure the
page you're trying to generate is fully rendered. For instance, if your page relies
a lot on JavaScript for rendering.
> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds).
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form waitDelay=5.5 \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
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
```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->setWaitDelay(5.5);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Rpcc buffer size
The API might return a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
If so, you may increase this buffer size with a form field named `googleChromeRpccBufferSize`.
It takes an int as value (e.g. `1048576` for 1 MB).
The hard limit is 100 MB and is defined by Google Chrome itself.
> You may also define this value globally: see the [environment variables](#environment_variables.default_google_chrome_rpcc_buffer_size) section.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form googleChromeRpccBufferSize=1048576 \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
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
```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->setGoogleChromeRpccBufferSize(1048576);
$dest = 'result.pdf';
$client->store($request, $dest);
```

View File

@@ -0,0 +1,106 @@
---
title: URL
---
Gotenberg provides the endpoint `/convert/url` for remote URL conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
This endpoint does not accept an `index.html` file nor assets files but a form field
named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions.
> **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
$ curl --request POST \
--url http://localhost:3000/convert/url \
--header 'Content-Type: multipart/form-data' \
--form remoteURL=https://google.com \
--form marginTop=0 \
--form marginBottom=0 \
--form marginLeft=0 \
--form marginRight=0 \
-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.Margins(gotenberg.NoMargins)
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->setMargins(Request::NO_MARGINS);
$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

@@ -0,0 +1,70 @@
---
title: Markdown
---
Gotenberg provides the endpoint `/convert/markdown` for Markdown conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
Markdown conversions work the same as HTML conversions.
Only difference is that you have access to the Go template function `toHTML`
in the file `index.html`. This function will convert a given markdown file to HTML.
For instance:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
{{ toHTML .DirPath "file.md" }}
</body>
</html>
```
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/markdown \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form files=@file.md \
-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")
markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file")
req := gotenberg.NewMarkdownRequest(index, markdown)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\MarkdownRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$markdowns = [
DocumentFactory::makeFromPath('file.md', '/path/to/file'),
];
$request = new MarkdownRequest($index, $markdowns);
$dest = 'result.pdf';
$client->store($request, $dest);
```

View File

@@ -0,0 +1,167 @@
---
title: Office
---
Gotenberg provides the endpoint `/convert/office` for Office document conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
You may send one or more Office documents. 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 resulting PDF.
> **Attention:** Gotenberg merges the PDF files alphabetically.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form files=@document2.docx \
-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")
doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file")
req := gotenberg.NewOfficeRequest(doc, doc2)
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'),
DocumentFactory::makeFromPath('document2.docx', '/path/to/file'),
];
$request = new OfficeRequest($files);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Orientation
You may also customize the resulting PDF format.
By default, it will be rendered with `portrait` orientation.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form landscape=true \
-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.Landscape(true)
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->setLandscape(true);
$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

@@ -0,0 +1,55 @@
---
title: Merge
---
Gotenberg provides the endpoint `/merge` for merging PDFs.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
Nothing fancy here: you may send one or more PDF files and the API
will merge them and return the resulting PDF file.
> **Attention:** Gotenberg merges the PDF files alphabetically.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/merge \
--header 'Content-Type: multipart/form-data' \
--form files=@file.pdf \
--form files=@file2.pdf \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
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
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\MergeRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath('file.pdf', '/path/to/file'),
DocumentFactory::makeFromPath('file2.pdf', '/path/to/file'),
];
$request = new MergeRequest($files);
$dest = 'result.pdf';
$client->store($request, $dest);
```

View File

@@ -0,0 +1,52 @@
---
title: Timeout
---
All endpoints accept a form field named `waitTimeout`.
The API will wait the given **seconds** before it considers the conversion to be unsucessful.
If unsucessful, it returns a `504` HTTP code.
It takes a float as value (e.g `2.5` for 2.5 seconds).
> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section.
## Examples
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form waitTimeout=2.5
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
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
```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->setWaitTimeout(2.5);
$dest = 'result.pdf';
$client->store($request, $dest);
```

View File

@@ -0,0 +1,149 @@
---
title: Webhook
---
All endpoints accept a form field named `webhookURL`.
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
to given URL.
By doing so, your requests to the API will be over before the conversions are actually done!
## Examples
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form webhookURL='http://myapp.com/webhook/'
```
### 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/")
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/');
$resp = $client->post($request);
```
## Timeout
If a `webhookURL` is provided, you may also send a form field named `webhookURLTimeout`.
The API will wait the given **seconds** before it considers the sending of the resulting PDF to be unsucessful.
It takes a float as value (e.g `2.5` for 2.5 seconds).
> You may also define this value globally: see the [environment variables](#environment_variables.default_webhook_url_timeout) section.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form webhookURL='http://myapp.com/webhook/' \
--form webhookURLTimeout=2.5
```
### 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.WebhookURLTimeout(2.5)
resp, _ := c.Post(req)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/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

@@ -0,0 +1,49 @@
---
title: Result filename
---
All endpoints accept a form field named `resultFilename`.
If provided, the API will return the resulting PDF file with the given filename.
Otherwise a random filename is used.
> **Attention:** this feature does not work if the form field `webhookURL` is given.
## Examples
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form resultFilename='foo.pdf'
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
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
```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->setResultFilename('foo.pdf');
$resp = $client->post($request);
```

View File

@@ -0,0 +1,53 @@
---
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 extent.
For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk
instances as there are requests. The limitation here is the available memory and CPU usage.
On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel.
Indeed, Google Chrome misbehaves if there are too many concurrent conversions.
**The more concurrent requests, the more `504` HTTP codes the API will return.**
> See our [load testing use case](https://github.com/thecodingmachine/gotenberg/tree/master/loadtesting) for more details about the API behaviour under heavy load.
## Strategies
### Increase timeout
You may increase the conversion timeout. In other words, you accept that a conversion takes more time
if the API is under heavy load.
> See [timeout section](#timeout).
### Scaling
The API being stateless, you may scale it as much as you want.
For instance, using the following Docker Compose file:
```yaml
version: '3'
services:
# your other services
gotenberg:
image: thecodingmachine/gotenberg:6
```
You may now launch your services using:
```bash
$ docker-compose up --scale gotenberg=your_number_of_instances
```
When requesting the Gotenberg service with your client(s), Docker will automatically
redirect a request to a Gotenberg container according to the round-robin strategy.

View File

@@ -0,0 +1,12 @@
---
title: Ping
---
Gotenberg provides the endpoint `/ping` for checking the API availability with
a simple `GET` request.
Currently this endpoint does nothing special. A better way to monitor
Gotenberg would be by checking the memory usage.
Also, as the API uses under the hood intricate programs, you should
restart your Gotenberg instances from time to time to ensure a nominal behaviour.

View File

@@ -0,0 +1,17 @@
---
title: Fonts
---
By default, a handful of fonts are installed. Asian characters are also supported out of the box.
If you wish to use more fonts, you will have to create your own image:
```Dockerfile
FROM thecodingmachine/gotenberg:6
USER root
RUN apt-get -y install yourfonts
USER gotenberg
```

View File

@@ -0,0 +1,12 @@
---
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 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)

22
build/lint/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
ARG GOLANGCI_LINT_VERSION
FROM golangci/golangci-lint:v$GOLANGCI_LINT_VERSION
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
# Define our working directory outside of $GOPATH (we're using go modules).
WORKDIR /gotenberg/lint
# Copy our code source.
COPY . .
# Install module dependencies.
RUN go mod download &&\
go mod verify
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit", "--disable=gomnd", "--disable=goimports" ]

62
build/package/Dockerfile Normal file
View File

@@ -0,0 +1,62 @@
# |--------------------------------------------------------------------------
# | Binary
# |--------------------------------------------------------------------------
# |
# | Builds Gotenberg binary.
# |
FROM thecodingmachine/gotenberg:workspace AS workspace
ARG VERSION
ENV GOOS=linux \
GOARCH=amd64 \
CGO_ENABLED=0
# Define our workding outside of $GOPATH (we're using go modules).
WORKDIR /gotenberg/package
# Install module dependencies.
COPY go.mod go.sum ./
RUN go mod download &&\
go mod verify
# Copy our source code.
COPY internal ./internal
COPY cmd ./cmd
# Build our binary.
RUN go build -o gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
FROM thecodingmachine/gotenberg:base
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
# |--------------------------------------------------------------------------
# | Tini
# |--------------------------------------------------------------------------
# |
# | An helper for reaping zombie processes.
# |
ARG TINI_VERSION
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static /tini
RUN chmod +x /tini
ENTRYPOINT [ "/tini", "--" ]
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
COPY --from=workspace /gotenberg/package/gotenberg /usr/local/bin/
USER gotenberg
WORKDIR /gotenberg
EXPOSE 3000
CMD [ "gotenberg" ]

16
build/tests/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM thecodingmachine/gotenberg:workspace
# Define our workding outside of $GOPATH (we're using go modules).
USER gotenberg
WORKDIR /gotenberg/tests
# Install module dependencies.
COPY --chown=gotenberg:gotenberg go.mod go.sum ./
RUN go mod download &&\
go mod verify
# Copy our code source.
COPY --chown=gotenberg:gotenberg . .
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]

View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -xe
# Make sure the user running the
# tests is the Gotenberg user.
CURRENT_USER=$(whoami)
if [ "$CURRENT_USER" != "gotenberg" ]; then
exit 1
fi
# Start Google Chrome headless.
go run test/cmd/chrome.go
# Run our tests.
if [ "$CODE_COVERAGE" = "1" ]; then
go test -race -coverprofile=coverage.txt -covermode=atomic ./...
else
go test -race -cover ./...
fi

View File

@@ -0,0 +1,52 @@
ARG GOLANG_VERSION
FROM golang:${GOLANG_VERSION}-stretch as golang
FROM thecodingmachine/gotenberg:base
# |--------------------------------------------------------------------------
# | Common libraries
# |--------------------------------------------------------------------------
# |
# | Libraries used in the build process of this image.
# |
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
g++ \
gcc \
libc6-dev \
make \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# |--------------------------------------------------------------------------
# | Golang
# |--------------------------------------------------------------------------
# |
# | Installs Golang.
# |
COPY --from=golang /usr/local/go /usr/local/go
ENV GOPATH /gotenberg/go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" &&\
chmod -R 777 "$GOPATH"
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
# Make sure the Gotenber user is able to
# call the Go binary.
USER gotenberg
RUN go version &&\
go env
USER root

64
cmd/gotenberg/main.go Normal file
View File

@@ -0,0 +1,64 @@
package main
import (
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp"
"github.com/thecodingmachine/gotenberg/internal/pkg/chrome"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
// version will be set on build time.
// nolint: gochecknoglobals
var version = "snapshot"
func main() {
const op string = "main"
config, err := conf.FromEnv()
systemLogger := xlog.New(config.LogLevel(), "system")
if err != nil {
systemLogger.FatalOp(op, err)
}
systemLogger.InfoOpf(op, "Gotenberg %s", version)
systemLogger.DebugOpf(op, "configuration: %+v", config)
if !config.DisableGoogleChrome() {
// start Google Chrome headless.
if err := chrome.Start(systemLogger, config.GoogleChromeIgnoreCertificateErrors()); err != nil {
systemLogger.FatalOp(op, err)
}
}
// create our API.
srv := xhttp.New(config)
// run our API in a goroutine so that it doesn't block.
go func() {
systemLogger.InfoOpf(op, "http server started on port '%d'", config.DefaultListenPort())
if err := srv.Start(fmt.Sprintf(":%d", config.DefaultListenPort())); err != nil {
if errors.Is(err, http.ErrServerClosed) {
systemLogger.FatalOp(op, err)
}
}
}()
quit := make(chan os.Signal, 1)
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(quit, os.Interrupt)
// block until we receive our signal.
<-quit
// create a deadline to wait for.
ctx, cancel := xcontext.WithTimeout(systemLogger, 120)
defer cancel()
// doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
systemLogger.InfoOp(op, "shutting down http server...")
if err := srv.Shutdown(ctx); err != nil {
systemLogger.FatalOp(op, err)
}
systemLogger.InfoOp(op, "bye!")
os.Exit(0)
}

1797
docs/index.html Executable file

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

342
docs/theme/gotenberg/css/index.css vendored Normal file
View File

@@ -0,0 +1,342 @@
:root {
--ease: cubic-bezier(.82, 0, .12, 1);
--width: 800px;
--header-height: 400px;
--tracking: 0.05rem;
--tracking-medium: 0.5rem;
--tracking-large: 0.8rem;
--dark: #000;
--blue: #33A9FF;
--light-gray: #fafafa;
--bg: #fff;
--fg: #868E96;
--fg-dark: #212529;
--selection-bg: var(--blue);
--selection-fg: white;
}
* {
box-sizing: border-box;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 16px;
font-weight: 300;
background: var(--bg);
color: var(--fg-dark);
line-height: 1;
margin: 0;
padding: 0;
}
::selection {
background: var(--selection-bg);
color: var(--selection-fg);
}
h1, h2, h3, h4 {
margin-top: 75px;
margin-bottom: 0;
font-size: 1.2rem;
font-weight: 600;
line-height: 1.5rem;
color: var(--fg-dark);
}
h1 {
margin-top: 0;
font-size: 1.5rem;
}
h1 + p {
font-size: 1.5rem;
line-height: 1.6;
}
h3 {
font-size: 1.0rem;
font-style: italic;
}
a {
color: inherit;
text-decoration: none;
}
p {
margin: 25px 0;
line-height: 1.6;
color: var(--fg-color-light);
}
li strong,
p strong {
color: var(--fg-dark);
font-weight: 500;
}
li a,
p a {
color: var(--fg-dark);
font-weight: 400;
padding-bottom: 3px;
border-bottom: 1px dotted #ddd;
}
li a:hover,
p a:hover {
color: var(--blue);
border-bottom: none;
}
p a:hover {
border-bottom-color: var(--color);
}
ul {
margin: 50px 0 50px 30px;
padding: 0;
}
ul ul {
margin: 10px 0 10px 30px;
}
ul li {
margin: 5px 0;
color: var(--fg-color-light);
line-height: 1.5em;
}
ul li strong {
color: var(--fg-color);
font-weight: 500;
}
pre {
background: var(--light-gray);
color: var(--dark);
padding: 30px;
border-radius: 2px;
overflow: auto;
font: "Source Code Pro", Menlo, monospace;
font-size: .8em;
line-height: 1.5em;
}
blockquote {
margin: 0;
padding: 0 1em;
color: var(--fg-dark);
border-left: 0.25em solid var(--dark);
}
li > code,
p > code {
border: 1px solid #DEE2E6;
font-size: 0.75rem;
padding: 3px 10px;
border-radius: 3px;
white-space: nowrap;
font-weight: 600;
font-family: inherit;
}
details > summary {
cursor: pointer;
outline: none;
user-select: none;
}
details > p {
border-left: 3px solid var(--dark);
padding-left: 15px;
}
.Wrapper {
display: flex;
justify-content: center;
}
.Container {
width: var(--width);
display: flex;
flex-direction: column;
}
.Content-wrapper {
display: flex;
}
.Header {
display: flex;
justify-content: center;
align-items: center;
height: var(--header-height);
}
.Sidebar {
flex: 1 1 auto;
}
.Content {
width: 75%;
}
.Content img {
max-width: 100%;
}
.Menu {
position: -webkit-sticky;
position: sticky;
top: 50px;
color: var(--fg-dark);
}
.Menu > .item {
padding-bottom: 15px;
}
.Menu > .item > a {
position: relative;
user-select: none;
font-weight: 400;
transition: color 200ms;
color: var(--fg);
}
.Menu > .item > a.active {
color: var(--fg-dark);
}
.Menu > .item > a:before {
content: "";
position: absolute;
width: 100%;
height: 1px;
bottom: -5px;
left: 0;
background-color: var(--fg-dark);
visibility: hidden;
transform: scaleX(0);
transform-origin: left center;
transition: all 250ms var(--ease);
}
.Menu > .item > a:hover {
color: var(--fg-dark);
}
.Menu > .item > a:hover:before {
visibility: visible;
transform: scaleX(1);
}
.Page {
margin-top: 100px;
padding-top: 50px;
}
.Page:first-child {
margin-top: 0;
padding-top: 0;
}
.Title {
margin: 5px 0;
line-height: 2.2em;
}
.Title.center {
margin-left: auto;
margin-right: auto;
text-align: center;
max-width: 500px;
}
.Title.margin {
margin-bottom: 80px;
}
.Title > span {
display: block;
}
.Title .subtext {
color: var(--fg-color-light);
font-size: 0.8rem;
text-transform: uppercase;
display: none;
}
.Title .text {
letter-spacing: var(--tracking);
text-transform: uppercase;
font-weight: bold;
font-size: 16px;
}
.Title.small .text {
font-size: 14px;
text-transform: none;
letter-spacing: normal;
line-height: 2rem;
}
.Anchor {
position: relative;
margin-left: -14px;
opacity: 0.15;
display: inline-block;
width: 14px;
height: 14px;
}
.Anchor svg {
position: absolute;
right: 5px;
top: 0;
}
.Anchor svg path {
pointer-events: all
}
.Anchor:hover {
opacity: 1;
}
.Footer {
height: 100px;
}
@media screen and (max-width: 850px) {
html, body {
--header-height: 300px;
--width: 80%;
font-size: 14px;
}
.Content-wrapper {
display: flex;
flex-direction: column;
}
.Sidebar {
margin-bottom: 30px;
}
.Content {
width: 100%;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/theme/gotenberg/img/favicon-128.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/theme/gotenberg/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

46
docs/theme/gotenberg/js/index.js vendored Normal file
View File

@@ -0,0 +1,46 @@
/**
* Setup.
*/
const items = document.querySelectorAll('.Page')
const links = document.querySelectorAll('.Menu a')
/**
* Check if `el` is out out of view.
*/
function isBelowScroll(el) {
return el.getBoundingClientRect().bottom > 0
}
/**
* Activate item `i`.
*/
function activateItem(i) {
links.forEach(e => e.classList.remove('active'))
links[i].classList.add('active')
}
/**
* Activate the correct menu item for the
* contents in the viewport.
*/
function activate() {
let i = 0
for (; i < items.length; i++) {
if (isBelowScroll(items[i])) {
break
}
}
activateItem(i)
}
/**
* Activate scroll spy thingy.
*/
window.addEventListener('scroll', e => activate())

68
docs/theme/gotenberg/views/index.html vendored Normal file
View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-10196481-9"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-10196481-9');
</script>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} &#183; {{.Subtitle}}</title>
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="theme/gotenberg/img/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="theme/gotenberg/img/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="theme/gotenberg/img/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="theme/gotenberg/img/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="theme/gotenberg/img/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="theme/gotenberg/img/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="theme/gotenberg/img/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="theme/gotenberg/img/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-128.png" sizes="128x128" />
<link rel="stylesheet" href="theme/gotenberg/css/index.css">
</head>
<body>
<div class="Wrapper">
<div class="Container">
<div class="Header">
<div class="Title center">
<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>
</div>
<div class="Content-wrapper">
<div class="Sidebar">
<div class="Menu">
{{range .Pages}}
<div class="item">
<a href="#{{.Slug}}">{{.Title}}</a>
</div>
{{end}}
</div>
</div>
<div class="Content">
{{range .Pages}}
<div class="Page" id="{{.Slug}}">
<h1>{{.Title}}</h1>
{{.Content}}
</div>
{{end}}
<div class="Footer">
</div>
<script src="theme/gotenberg/js/index.js"></script>
</div>
</div>
</div>
</body>
</html>

26
go.mod Normal file
View File

@@ -0,0 +1,26 @@
module github.com/thecodingmachine/gotenberg
go 1.14
require (
github.com/dustin/go-humanize v1.0.0
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.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.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-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
)

Some files were not shown because too many files have changed in this diff Show More