Compare commits

...

153 Commits
3.1.2 ... 6.0.2

Author SHA1 Message Date
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
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
175 changed files with 8718 additions and 2751 deletions

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

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

@@ -42,4 +42,4 @@ Fixes #
- [ ] Have you successfully ran tests with your changes locally (`make tests`)?
- [ ] Have you updated the documentation (`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

14
.github/workflows/publish.yml vendored Normal file
View File

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

12
.github/workflows/push.yml vendored Normal file
View File

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

2
.gitignore vendored
View File

@@ -0,0 +1,2 @@
.idea
coverage.txt

View File

@@ -17,9 +17,10 @@ stages:
jobs:
include:
- stage: tests
script: make lint
- stage: tests
script: make tests
script:
- make lint
- make tests CODE_COVERAGE=1
- bash <(curl -s https://codecov.io/bash)
- stage: publish
if: tag IS present
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS

View File

@@ -1,37 +1,63 @@
GOLANG_VERSION=1.11.2
GOLANG_VERSION=1.13
VERSION=snapshot
DOCKER_USER=
DOCKER_PASSWORD=
DOCKER_REPOSITORY=thecodingmachine
GOLANGCI_LINT_VERSION=1.19.1
CODE_COVERAGE=0
TINI_VERSION=0.18.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
DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576
# generate documentation.
doc:
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:docs -f build/docs/Dockerfile .
docker run --rm -it -v "$(PWD):/docs" thecodingmachine/gotenberg:docs
# build the base Docker image.
base:
docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
# build the workspace Docker image.
workspace:
make base
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:workspace -f build/workspace/Dockerfile .
# gofmt and goimports all go files.
fmt:
go fmt ./...
go mod tidy
# run all linters.
lint:
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:lint -f build/lint/Dockerfile .
docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg:lint
make workspace
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile .
docker run --rm $(DOCKER_REPOSITORY)/gotenberg:lint
# run all tests.
tests:
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:tests -f build/tests/Dockerfile .
docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg:tests
make workspace
./scripts/tests.sh $(DOCKER_REPOSITORY) $(CODE_COVERAGE)
# build Docker image.
# generate documentation.
doc:
make workspace
docker build -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile .
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REPOSITORY)/gotenberg:docs
# build Gotenberg Docker image.
image:
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
make workspace
docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
# start the API using previously built Docker image.
gotenberg:
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
# publish Gotenberg images according to version.
publish:
./scripts/publish.sh $(GOLANG_VERSION) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
make workspace
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REPOSITORY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)

View File

@@ -13,17 +13,15 @@ At TheCodingMachine, we build a lot of web applications (intranets, extranets an
* HTML and Markdown conversions using Google Chrome headless
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* Performance :zap:: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
* Easily interact with the API using our [Go](/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
## Quick start
Open a terminal and run the following command:
```bash
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:3
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
```
The API is now available on your host at `http://localhost:3000`.
@@ -33,9 +31,10 @@ to learn how to interact with it!
## Badges
[![Docker image layers](https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:3.svg)](https://microbadger.com/images/thecodingmachine/gotenberg:3)
[![Docker image layers](https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:6.svg)](https://microbadger.com/images/thecodingmachine/gotenberg:6)
[![Travis CI](https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master)](https://travis-ci.org/thecodingmachine/gotenberg)
[![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)
---

View File

@@ -1,4 +1,4 @@
FROM debian:9.5-slim
FROM debian:buster-slim
# |--------------------------------------------------------------------------
# | Common libraries
@@ -7,43 +7,48 @@ FROM debian:9.5-slim
# | Libraries used in the build process of this image.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
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 python3-pip
# |--------------------------------------------------------------------------
# | PM2
# |--------------------------------------------------------------------------
# |
# | Installs PM2 for launching programs in background and with failure
# | recovering. In our case: Chrome (headless) and Office (headless).
# |
RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
apt-get install -y curl wget gnupg ttf-mscorefonts-installer procps
# |--------------------------------------------------------------------------
# | Chrome
# |--------------------------------------------------------------------------
# |
# | Installs Chrome for HTML and Markdown conversions.
# | Installs Chrome.
# |
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
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 install google-chrome-stable
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 and LibreOffice for Office documents conversions.
# | Installs unoconv.
# |
RUN pip3 install unoconv &&\
# https://github.com/nextcloud/docker/issues/380
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
apt-get -y install libreoffice
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
@@ -53,4 +58,53 @@ RUN pip3 install unoconv &&\
# | https://github.com/thecodingmachine/gotenberg/issues/29
# |
RUN apt-get -y install pdftk
RUN apt-get -y install 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.
# |
RUN groupadd --gid 1001 gotenberg \
&& useradd --uid 1001 --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.

View File

@@ -1,6 +1,4 @@
ARG GOLANG_VERSION
FROM golang:${GOLANG_VERSION}-stretch
FROM thecodingmachine/gotenberg:workspace
# |--------------------------------------------------------------------------
# | static
@@ -19,6 +17,6 @@ RUN go get github.com/apex/static/cmd/static-docs
# | Last instructions of this build.
# |
WORKDIR /docs
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

@@ -6,7 +6,5 @@ title: Introduction
* 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)
* Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
* Failure prevention: PM2 automatically restarts previous processes if they fail
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries

View File

@@ -4,15 +4,19 @@ title: Install
Gotenberg is shipped within a Docker image.
> It uses a dedicated non-root user called `gotenberg` with uid and gid `1001`.
You may start it with:
```bash
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:3
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
```
> The API will be available at [http://localhost:3000](http://localhost:3000).
Or add it in your Docker Compose stack:
## Docker Compose
You may also add it in your Docker Compose stack:
```yaml
version: '3'
@@ -22,32 +26,26 @@ services:
# your others services
gotenberg:
image: thecodingmachine/gotenberg:3
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 `1001` of the user `gotenberg`:
```
securityContext:
privileged: false
runAsUser: 1001
```
In the following examples, we will assume your
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
## Go client
```bash
$ go get -u github.com/thecodingmachine/gotenberg
```
## PHP client
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
```bash
$ composer require php-http/guzzle6-adapter
```
Then the PHP client:
```bash
$ composer require thecodingmachine/gotenberg-php-client
```

View File

@@ -0,0 +1,25 @@
---
title: Clients
---
We provide clients in various languages for easing the interactions with the API.
## Go client
```bash
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v6
```
## PHP client
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
```bash
$ composer require php-http/guzzle6-adapter
```
Then the PHP client:
```bash
$ composer require thecodingmachine/gotenberg-php-client
```

View File

@@ -0,0 +1,105 @@
---
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.
## 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).
## 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

@@ -39,7 +39,7 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
@@ -95,8 +95,14 @@ The following classes allow you to inject printing values:
* `pageNumber`: current page number
* `totalPage`: total pages in the document
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
> Also, `footer.html` CSS properties override the ones from `header.html`.
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... />`)
### cURL
@@ -113,13 +119,13 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetHeader("header.html")
req.SetFooter("footer.html")
req.Header("header.html")
req.Footer("footer.html")
dest := "result.pdf"
c.Store(req, dest)
}
@@ -185,7 +191,7 @@ But this won't:
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](#office.fonts).
> see to the [fonts section](#fonts).
### cURL
@@ -203,16 +209,12 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetAssets([]string{
"font.woff",
"img.gif",
"style.css",
})
req.Assets("font.woff", "img.gif", "style.css")
dest := "result.pdf"
c.Store(req, dest)
}
@@ -234,8 +236,8 @@ $assets = [
];
$request = new HTMLRequest($index);
$request->setAssets($assets);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
$dest = "result.pdf";
$client->store($request, $dest);
```
## Paper size, margins, orientation
@@ -244,8 +246,7 @@ You may also customize the resulting PDF format.
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
> Paper size and margins have to be provided in `inches`.
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
> Paper size and margins have to be provided in `inches`. Same for margins.
### cURL
@@ -255,7 +256,7 @@ $ curl --request POST \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form paperWidth=8.27 \
--form paperHeight=11.27 \
--form paperHeight=11.69 \
--form marginTop=0 \
--form marginBottom=0 \
--form marginLeft=0 \
@@ -267,14 +268,14 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetPaperSize(gotenberg.A4)
req.SetMargins(gotenberg.NoMargins)
req.SetLandscape(true)
req.PaperSize(gotenberg.A4)
req.Margins(gotenberg.NoMargins)
req.Landscape(true)
dest := "result.pdf"
c.Store(req, dest)
}
@@ -294,6 +295,107 @@ $request = new HTMLRequest($index);
$request->setPaperSize(Request::A4);
$request->setMargins(Request::NO_MARGINS);
$request->setLandscape(true);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
```
$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/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.WaitDelay(5.5)
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$request = new HTMLRequest($index);
$request->setWaitDelay(5.5);
$dest = "result.pdf";
$client->store($request, $dest);
```
## 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/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
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', 'index.html');
$request = new HTMLRequest($index);
$request->setGoogleChromeRpccBufferSize(1048576);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,56 @@
---
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.
### 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/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := gotenberg.NewURLRequest("https://google.com")
req.Margins(gotenberg.NoMargins)
dest := "result.pdf"
c.Store(req, dest)
}
```
### 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);
```

View File

@@ -42,11 +42,11 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewMarkdownRequest("index.html", []string{"file.md"})
req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
dest := "result.pdf"
c.Store(req, dest)
}
@@ -65,6 +65,6 @@ $markdowns = [
DocumentFactory::makeFromPath('file.md', 'file.md'),
];
$request = new MarkdownRequest($index, $markdowns);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -12,6 +12,7 @@ You may send one or more Office documents. Following file extensions are accepte
* `.txt`
* `.rtf`
* `.fodt`
* `.doc`
* `.docx`
* `.odt`
@@ -24,9 +25,7 @@ You may send one or more Office documents. Following file extensions are accepte
All files will be merged into a single resulting PDF.
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
> That's why for Office conversions, the API does only one conversion at a time.
> See the [scalability section](#scalability) to find how to mitigate this issue.
> **Attention:** Gotenberg merges the PDF files alphabetically.
### cURL
@@ -42,11 +41,11 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx", "document2.docx"})
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
dest := "result.pdf"
c.Store(req, dest)
}
@@ -65,18 +64,15 @@ $files = [
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
];
$request = new OfficeRequest($files);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
$dest = "result.pdf";
$client->store($request, $dest);
```
## Paper size and orientation
## Orientation
You may also customize the resulting PDF format.
By default, it will be rendered with `A4` size and `portrait` orientation.
> Paper size has to be provided in `inches`.
> Also, you have to set both `paperWidth` and `paperHeight`.
By default, it will be rendered with `portrait` orientation.
### cURL
@@ -85,8 +81,6 @@ $ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form paperWidth=8.27 \
--form paperHeight=11.27 \
--form landscape=true \
-o result.pdf
```
@@ -94,13 +88,12 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx"})
req.SetPaperSize(gotenberg.A4)
req.SetLandscape(true)
req, _ := gotenberg.NewOfficeRequest("document.docx")
req.Landscape(true)
dest := "result.pdf"
c.Store(req, dest)
}
@@ -118,20 +111,7 @@ $files = [
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
];
$request = new OfficeRequest($files);
$request->setPaperSize(Request::A4);
$request->setLandscape(true);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
```
## Fonts
By default, only `ttf-mscorefonts` fonts are installed.
If you wish to use more fonts, you will have to create your own image:
```Dockerfile
FROM thecodingmachine/gotenberg:3
RUN apt-get -y install yourfonts
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -1,27 +0,0 @@
---
title: Scalability
---
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 others services
gotenberg:
image: thecodingmachine/gotenberg:3
```
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

@@ -8,9 +8,11 @@ It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
Nothing special here: you may send one or more PDF files and the API
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
@@ -25,11 +27,11 @@ $ curl --request POST \
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewMergeRequest([]string{"file.pdf", "file2.pdf"})
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
dest := "result.pdf"
c.Store(req, dest)
}
@@ -48,6 +50,6 @@ $files = [
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
];
$request = new MergeRequest($files);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
```
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,53 @@
---
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/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.WaitTimeout(2.5)
resp, _ := c.Post(req)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$request = new HTMLRequest($index);
$request->setWaitTimeout(2.5);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,101 @@
---
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/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
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', 'index.html');
$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.
### 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/' \
--form webhookURLTimeout=2.5
```
#### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.WebhookURL("http://myapp.com/webhook/")
req.WebhookURLTimeout(2.5)
resp, _ := c.Post(req)
}
```
#### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$request = new HTMLRequest($index);
$request->setWebhookURL('http://myapp.com/webhook/');
$request->setWebhookURLTimeout(2.5);
$resp = $client->post($request);
```

View File

@@ -1,13 +1,13 @@
---
title: Webhook
title: Result filename
---
All endpoints accept a form field named `webhookURL`.
All endpoints accept a form field named `resultFilename`.
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
to given URL.
If provided, the API will return the resulting PDF file with the given filename.
Otherwise a random filename is used.
By doing so, your requests to the API will be over before the conversions are actually done!
> **Attention:** this feature does not work if the form field `webhookURL` is given.
## Examples
@@ -18,19 +18,18 @@ $ 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 resultFilename='foo.pdf'
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
import "github.com/thecodingmachine/gotenberg-go-client/v6"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetWebhookURL("http://myapp.com/webhook/")
dest := "result.pdf"
req.ResultFilename("foo.pdf")
resp, _ := c.Post(req)
}
```
@@ -41,10 +40,11 @@ func main() {
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$request = new HTMLRequest($index);
$request->setWebhookURL('http://myapp.com/webhook/');
$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 extend.
For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk
instances are there are requests. The limitation here is the available memory and CPU usage.
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 others 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

@@ -4,5 +4,6 @@ title: Links
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
* Thanks to [@mafredri](https://github.com/mafredri) for its help and its wonderful [cdp](https://github.com/mafredri/cdp) library
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).

View File

@@ -1,6 +1,4 @@
ARG GOLANG_VERSION
FROM golang:${GOLANG_VERSION}-stretch
FROM thecodingmachine/gotenberg:workspace
# |--------------------------------------------------------------------------
# | GolangCI-Lint
@@ -10,7 +8,7 @@ FROM golang:${GOLANG_VERSION}-stretch
# | than gometalinter.
# |
ENV GOLANGCI_LINT_VERSION 1.12.3
ARG GOLANGCI_LINT_VERSION
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
golangci-lint --version
@@ -22,14 +20,17 @@ RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.s
# | Last instructions of this build.
# |
# Define our workding outside of $GOPATH (we're using go modules).
WORKDIR /lint
# Copy our module dependencies definitions.
COPY go.mod .
COPY go.sum .
# Define our working directory outside of $GOPATH (we're using go modules).
USER gotenberg
WORKDIR /gotenberg/lint
# Install module dependencies.
RUN go mod download
COPY --chown=gotenberg:gotenberg go.mod go.sum ./
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=lll", "--disable=errcheck", "--disable=gosec", "--disable=gochecknoglobals", "--disable=gochecknoinits" ]
RUN go mod download &&\
go mod verify
# Copy our code source.
COPY --chown=gotenberg:gotenberg . .
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen" ]

View File

@@ -1,5 +1,3 @@
ARG GOLANG_VERSION
# |--------------------------------------------------------------------------
# | Binary
# |--------------------------------------------------------------------------
@@ -7,7 +5,7 @@ ARG GOLANG_VERSION
# | Buils Gotenberg binary.
# |
FROM golang:${GOLANG_VERSION}-stretch AS golang
FROM thecodingmachine/gotenberg:workspace AS workspace
ARG VERSION
@@ -16,13 +14,37 @@ ENV GOOS=linux \
CGO_ENABLED=0
# Define our workding outside of $GOPATH (we're using go modules).
WORKDIR /gotenberg
WORKDIR /gotenberg/package
# Install module dependencies.
COPY go.mod go.sum ./
RUN go mod download &&\
go mod verify
# Copy our source code.
COPY . .
COPY internal ./internal
COPY cmd ./cmd
# Build our binary.
RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
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 /tini
RUN chmod +x /tini
ENTRYPOINT [ "/tini", "--" ]
# |--------------------------------------------------------------------------
# | Final touch
@@ -31,12 +53,9 @@ RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/g
# | Last instructions of this build.
# |
FROM thecodingmachine/gotenberg:base
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
COPY --from=workspace /gotenberg/package/gotenberg /usr/local/bin/
USER gotenberg
WORKDIR /gotenberg
EXPOSE 3000

View File

@@ -1,48 +1,16 @@
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 install -y git gcc
# |--------------------------------------------------------------------------
# | Golang
# |--------------------------------------------------------------------------
# |
# | Installs Golang.
# |
COPY --from=golang /usr/local/go /usr/local/go
RUN export PATH="/usr/local/go/bin:$PATH" &&\
go version
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
FROM thecodingmachine/gotenberg:workspace
# Define our workding outside of $GOPATH (we're using go modules).
WORKDIR /tests
# Copy our module dependencies definitions.
COPY go.mod .
COPY go.sum .
USER gotenberg
WORKDIR /gotenberg/tests
# Install module dependencies.
RUN go mod download
COPY --chown=gotenberg:gotenberg go.mod go.sum ./
ENTRYPOINT ["build/tests/docker-entrypoint.sh"]
RUN go mod download &&\
go mod verify
# Copy our code source.
COPY --chown=gotenberg:gotenberg . .
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]

View File

@@ -2,24 +2,19 @@
set -xe
# Testing PM2 processes launch separatly for avoiding
# spending to much time on each tests depending on
# them.
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeLaunch
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvLaunch
# Make sure the user running the
# tests is the Gotenberg user.
CURRENT_USER=$(whoami)
if [ "$CURRENT_USER" != "gotenberg" ]; then
exit 1
fi
# Running others tests.
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/printer
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
# Start Google Chrome headless.
go run test/cmd/chrome.go
# Finally testing processes shutdown.
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
# Testing Go client.
go build -o /usr/local/bin/gotenberg cmd/gotenberg/main.go
gotenberg &
sleep 10
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/pkg
sleep 5 # allows Gotenberg to remove generated files (concurrent requests).
# 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

View File

@@ -2,20 +2,62 @@ package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"github.com/thecodingmachine/gotenberg/internal/app/api"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
"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() {
notify.Println(fmt.Sprintf("Gotenberg %s", version))
if err := api.Start(); err != nil {
notify.ErrPrintln(err)
os.Exit(1)
const op string = "main"
config, err := conf.FromEnv()
systemLogger := xlog.New(config.LogLevel(), "system")
if err != nil {
systemLogger.FatalOp(op, err)
}
systemLogger.InfofOp(op, "Gotenberg %s", version)
systemLogger.DebugfOp(op, "configuration: %+v", config)
if !config.DisableGoogleChrome() {
// start Google Chrome headless.
if err := chrome.Start(systemLogger); 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.InfofOp(op, "http server started on port '%d'", config.DefaultListenPort())
if err := srv.Start(fmt.Sprintf(":%d", config.DefaultListenPort())); err != nil {
if 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)
}

File diff suppressed because it is too large Load Diff

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.

Before

Width:  |  Height:  |  Size: 923 B

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

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

View File

@@ -1,12 +1,32 @@
<!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-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>

38
go.mod
View File

@@ -1,21 +1,27 @@
module github.com/thecodingmachine/gotenberg
go 1.13
require (
github.com/derekparker/delve v1.1.0
github.com/google/go-cmp v0.2.0 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/hhrutter/pdfcpu v0.1.19
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73
github.com/labstack/gommon v0.2.8
github.com/mafredri/cdp v0.20.0
github.com/microcosm-cc/bluemonday v1.0.1
github.com/pkg/errors v0.8.0 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/google/go-cmp v0.3.1 // indirect
github.com/gorilla/websocket v1.4.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo/v4 v4.1.10
github.com/labstack/gommon v0.3.0
github.com/mafredri/cdp v0.24.2
github.com/mattn/go-isatty v0.0.9
github.com/microcosm-cc/bluemonday v1.0.2
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad // indirect
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

112
go.sum
View File

@@ -1,55 +1,81 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hhrutter/pdfcpu v0.1.18 h1:GX3d6tZxmQlmB5X9VUyOtx7+HZZpR+8ISLqBineP6dc=
github.com/hhrutter/pdfcpu v0.1.18/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
github.com/hhrutter/pdfcpu v0.1.19 h1:J6GjNFDPjurQWallPW9gmz3CcNqBfvgB7Dad9T3e8Hc=
github.com/hhrutter/pdfcpu v0.1.19/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73 h1:oQvA3Xy8274W5Ldgo6Nogy4lX8XO5AaIBsjF5Ggk0IU=
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73/go.mod h1:rS0D1UPvC8/3sXjhSwEq+K1olh7ipbDhjDWATN2KSgA=
github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=
github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/mafredri/cdp v0.20.0 h1:dRq16FwXCAAbolR4W2iyRNsL14Bp6jFHLbRp2EkICVs=
github.com/mafredri/cdp v0.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.1.10 h1:/yhIpO50CBInUbE/nHJtGIyhBv0dJe2cDAYxc3V3uMo=
github.com/labstack/echo/v4 v4.1.10/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/mafredri/cdp v0.24.2 h1:Rzhj/EQw9opbiwUpNML7P+4Hvf0/nSYPaDbiCEpILOM=
github.com/mafredri/cdp v0.24.2/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -1,134 +0,0 @@
package api
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
// Start starts the API server on port 3000.
func Start() error {
e := setup()
// start Chrome headless and
// unoconv listener with PM2.
chrome := &pm2.Chrome{}
unoconv := &pm2.Unoconv{}
if err := chrome.Launch(); err != nil {
return err
}
if err := unoconv.Launch(); err != nil {
return err
}
// run our API in a goroutine so that it doesn't block.
go func() {
notify.Println("http server started on port 3000")
if err := e.Start(":3000"); err != nil {
e.Logger.Fatalf("%v", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(quit, os.Interrupt)
// block until we receive our signal.
<-quit
// create a deadline to wait for.
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
// doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
notify.Println("shutting down http server... (Ctrl+C to force)")
return e.Shutdown(ctx)
}
func setup() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(middleware.Logger())
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if err := next(c); err != nil {
// TODO should return a better HTTP status code
// than 500 for some cases.
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}
return nil
}
})
e.POST("/merge", merge)
g := e.Group("/convert")
g.POST("/html", convertHTML)
g.POST("/markdown", convertMarkdown)
g.POST("/office", convertOffice)
return e
}
func newContext(r *resource) (context.Context, context.CancelFunc) {
webhookURL := r.webhookURL()
if webhookURL == "" {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
return ctx, cancel
}
return context.Background(), nil
}
func print(c echo.Context, p printer.Printer, r *resource) error {
baseFilename, err := rand.Get()
if err != nil {
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
}
filename := fmt.Sprintf("%s.pdf", baseFilename)
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
if r.webhookURL() == "" {
defer r.removeAll()
// if no webhook URL given, run conversion
// and directly return the resulting PDF file
// or an error.
if err := p.Print(fpath); err != nil {
return err
}
return c.Attachment(fpath, filename)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
go func() {
defer r.removeAll()
if err := p.Print(fpath); err != nil {
c.Logger().Errorf("%v", err)
return
}
f, err := os.Open(fpath)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer f.Close()
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer resp.Body.Close()
}()
return nil
}
func hijackErr(err error, r *resource) error {
if r != nil {
defer r.removeAll()
}
return err
}

View File

@@ -1,9 +0,0 @@
/*
Package api starts a HTTP server on port 3000.
It accepts POST requests with a multipart/form-data Content-Type
for converting HTML, Markdown and Office documents to PDF.
It is also able to merge a list of PDF files.
*/
package api

View File

@@ -1,51 +0,0 @@
package api
import (
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
func convertHTML(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
p := &printer.HTML{Context: ctx}
indexPath, err := r.filePath("index.html")
if err != nil {
return hijackErr(err, r)
}
p.WithLocalURL(indexPath)
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
p.MarginLeft = paperMargins[2]
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
return print(c, p, r)
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestHTML(t *testing.T) {
body, contentType := test.HTMLTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertHTML(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -1,50 +0,0 @@
package api
import (
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
func convertMarkdown(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
indexPath, err := r.filePath("index.html")
if err != nil {
return hijackErr(err, r)
}
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
p.MarginLeft = paperMargins[2]
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
return print(c, p, r)
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestMarkdown(t *testing.T) {
body, contentType := test.MarkdownTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertMarkdown(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -1,65 +0,0 @@
package api
import (
"errors"
"fmt"
"net/http"
"os"
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
func merge(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
fpaths, err := r.filePaths([]string{".pdf"})
if err != nil {
return hijackErr(err, r)
}
if len(fpaths) == 0 {
return hijackErr(errors.New("no suitable PDF files to merge"), r)
}
baseFilename, err := rand.Get()
if err != nil {
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
}
filename := fmt.Sprintf("%s.pdf", baseFilename)
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
if r.webhookURL() == "" {
defer r.removeAll()
// if no webhook URL given, run merge
// and directly return the resulting PDF file
// or an error.
if err := printer.Merge(fpaths, fpath); err != nil {
return err
}
return c.Attachment(fpath, filename)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
go func() {
defer r.removeAll()
if err := printer.Merge(fpaths, fpath); err != nil {
c.Logger().Errorf("%v", err)
return
}
f, err := os.Open(fpath)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer f.Close()
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer resp.Body.Close()
}()
return nil
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestMerge(t *testing.T) {
body, contentType := test.PDFTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/merge", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, merge(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -1,53 +0,0 @@
package api
import (
"errors"
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
var officeExts = []string{
".txt",
".rtf",
".doc",
".docx",
".odt",
".xls",
".xlsx",
".ods",
".ppt",
".pptx",
".odp",
}
func convertOffice(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
fpaths, err := r.filePaths(officeExts)
if err != nil {
return hijackErr(err, r)
}
if len(fpaths) == 0 {
return hijackErr(errors.New("no suitable office documents to convert"), r)
}
p := &printer.Office{Context: ctx, FilePaths: fpaths}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
return print(c, p, r)
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestOffice(t *testing.T) {
body, contentType := test.OfficeTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertOffice(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -1,184 +0,0 @@
package api
import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
const (
webhookURL string = "webhookURL"
paperWidth string = "paperWidth"
paperHeight string = "paperHeight"
marginTop string = "marginTop"
marginBottom string = "marginBottom"
marginLeft string = "marginLeft"
marginRight string = "marginRight"
landscape string = "landscape"
)
// resource facilitates storing and accessing
// data from a multipart/form-data request.
type resource struct {
values map[string]string
dirPath string
}
func newResource(c echo.Context) (*resource, error) {
v := make(map[string]string)
v[webhookURL] = c.FormValue(webhookURL)
v[paperWidth] = c.FormValue(paperWidth)
v[paperHeight] = c.FormValue(paperHeight)
v[marginTop] = c.FormValue(marginTop)
v[marginBottom] = c.FormValue(marginBottom)
v[marginLeft] = c.FormValue(marginLeft)
v[marginRight] = c.FormValue(marginRight)
v[landscape] = c.FormValue(landscape)
dirPath, err := rand.Get()
if err != nil {
return nil, err
}
if err := os.MkdirAll(dirPath, 0755); err != nil {
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
}
r := &resource{values: v, dirPath: dirPath}
form, err := c.MultipartForm()
if err != nil {
return r, fmt.Errorf("getting multipart form: %v", err)
}
for _, files := range form.File {
for _, fh := range files {
in, err := fh.Open()
if err != nil {
return r, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
}
defer in.Close()
if err := r.writeFile(fh.Filename, in); err != nil {
return r, err
}
}
}
return r, nil
}
func (r *resource) writeFile(filename string, in io.Reader) error {
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
out, err := os.Create(fpath)
if err != nil {
return fmt.Errorf("%s: creating new file: %v", fpath, err)
}
defer out.Close()
if err := out.Chmod(0644); err != nil {
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
}
if _, err := io.Copy(out, in); err != nil {
return fmt.Errorf("%s: writing file: %v", fpath, err)
}
if _, err := out.Seek(0, 0); err != nil {
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
}
return nil
}
func (r *resource) filePath(filename string) (string, error) {
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
_, err := os.Stat(fpath)
if os.IsNotExist(err) {
return "", fmt.Errorf("%s: file does not exist", filename)
}
absPath, err := filepath.Abs(fpath)
if err != nil {
return "", fmt.Errorf("%s: getting absolute path: %v", fpath, err)
}
return absPath, nil
}
func (r *resource) filePaths(exts []string) ([]string, error) {
var fpaths []string
err := filepath.Walk(r.dirPath, func(path string, info os.FileInfo, _ error) error {
if info.IsDir() {
return nil
}
fpath, err := r.filePath(info.Name())
if err != nil {
return err
}
for _, ext := range exts {
if filepath.Ext(fpath) == ext {
fpaths = append(fpaths, fpath)
return nil
}
}
return nil
})
if err != nil {
return nil, err
}
return fpaths, nil
}
func (r *resource) paperSize() ([2]float64, error) {
defaultSize := [2]float64{8.27, 11.7}
widthStr := r.values[paperWidth]
heightStr := r.values[paperHeight]
if widthStr == "" || heightStr == "" {
return defaultSize, nil
}
width, err := strconv.ParseFloat(widthStr, 64)
if err != nil {
return defaultSize, fmt.Errorf("paper width: %v", err)
}
height, err := strconv.ParseFloat(heightStr, 64)
if err != nil {
return defaultSize, fmt.Errorf("paper height: %v", err)
}
return [2]float64{width, height}, nil
}
func (r *resource) paperMargins() ([4]float64, error) {
defaultMargins := [4]float64{1, 1, 1, 1}
topStr := r.values[marginTop]
bottomStr := r.values[marginBottom]
leftStr := r.values[marginLeft]
rightStr := r.values[marginRight]
if topStr == "" || bottomStr == "" || leftStr == "" || rightStr == "" {
return defaultMargins, nil
}
top, err := strconv.ParseFloat(topStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin top: %v", err)
}
bottom, err := strconv.ParseFloat(bottomStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin bottom: %v", err)
}
left, err := strconv.ParseFloat(leftStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin left: %v", err)
}
right, err := strconv.ParseFloat(rightStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin right: %v", err)
}
return [4]float64{top, bottom, left, right}, nil
}
func (r *resource) landscape() (bool, error) {
landscapeStr := r.values[landscape]
if landscapeStr == "" {
return false, nil
}
landscape, err := strconv.ParseBool(landscapeStr)
if err != nil {
return false, fmt.Errorf("landscape: %v", err)
}
return landscape, nil
}
func (r *resource) webhookURL() string { return r.values[webhookURL] }
func (r *resource) removeAll() error { return os.RemoveAll(r.dirPath) }

View File

@@ -0,0 +1,3 @@
// Package xhttp defines our own implementation
// of echo.Echo.
package xhttp

View File

@@ -0,0 +1,327 @@
package xhttp
import (
"fmt"
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
)
const (
pingEndpoint string = "/ping"
mergeEndpoint string = "/merge"
convertGroupEndpoint string = "/convert"
htmlEndpoint string = "/html"
urlEndpoint string = "/url"
markdownEndpoint string = "/markdown"
officeEndpoint string = "/office"
)
func isMultipartFormDataEndpoint(config conf.Config, path string) bool {
var multipartFormDataEndpoints []string
multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint)
if !config.DisableGoogleChrome() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint),
fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint),
fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint),
)
}
if !config.DisableUnoconv() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint),
)
}
for _, endpoint := range multipartFormDataEndpoints {
if endpoint == path {
return true
}
}
return false
}
// pingHandler is the handler for healthcheck.
func pingHandler(c echo.Context) error {
const op string = "xhttp.pingHandler"
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling ping request...")
return nil
}
// mergeHandler is the handler for merging
// PDF files.
func mergeHandler(c echo.Context) error {
const op string = "xhttp.mergeHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling merge request...")
r := ctx.MustResource()
opts, err := mergePrinterOptions(r, ctx.Config())
if err != nil {
return xerror.New(op, err)
}
fpaths, err := r.Fpaths(".pdf")
if err != nil {
return err
}
p := printer.NewMergePrinter(logger, fpaths, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// htmlHandler is the handler for converting
// HTML to PDF.
func htmlHandler(c echo.Context) error {
const op string = "xhttp.htmlHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling HTML request...")
r := ctx.MustResource()
opts, err := chromePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
fpath, err := r.Fpath("index.html")
if err != nil {
return err
}
p := printer.NewHTMLPrinter(logger, fpath, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// urlHandler is the handler for converting
// a URL to PDF.
func urlHandler(c echo.Context) error {
const op string = "xhttp.urlHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling URL request...")
r := ctx.MustResource()
opts, err := chromePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
if !r.HasArg(resource.RemoteURLArgKey) {
return xerror.Invalid(
op,
fmt.Sprintf("'%s' not found or empty", resource.RemoteURLArgKey),
nil,
)
}
remoteURL, err := r.StringArg(resource.RemoteURLArgKey, "")
if err != nil {
return err
}
p := printer.NewURLPrinter(logger, remoteURL, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// markdownHandler is the handler for converting
// Markdown to PDF.
func markdownHandler(c echo.Context) error {
const op string = "xhttp.markdownHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling Markdown request...")
r := ctx.MustResource()
opts, err := chromePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
fpath, err := r.Fpath("index.html")
if err != nil {
return err
}
p, err := printer.NewMarkdownPrinter(logger, fpath, opts)
if err != nil {
return err
}
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// officeHandler is the handler for converting
// Office documents to PDF.
func officeHandler(c echo.Context) error {
const op string = "xhttp.officeHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling Office request...")
r := ctx.MustResource()
opts, err := officePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
fpaths, err := r.Fpaths(
".txt",
".rtf",
".fodt",
".doc",
".docx",
".odt",
".xls",
".xlsx",
".ods",
".ppt",
".pptx",
".odp",
)
if err != nil {
return err
}
p := printer.NewOfficePrinter(logger, fpaths, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func convert(ctx context.Context, p printer.Printer) error {
const op string = "xhttp.convert"
resolver := func() error {
logger := ctx.XLogger()
r := ctx.MustResource()
baseFilename := xrand.Get()
filename := fmt.Sprintf("%s.pdf", baseFilename)
fpath := fmt.Sprintf("%s/%s", r.DirPath(), filename)
// if no webhook URL given, run conversion
// and directly return the resulting PDF file
// or an error.
if !r.HasArg(resource.WebhookURLArgKey) {
logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
return convertSync(ctx, p, filename, fpath)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
return convertAsync(ctx, p, filename, fpath)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func convertSync(ctx context.Context, p printer.Printer, filename, fpath string) error {
const op = "xhttp.convertSync"
resolver := func() error {
logger := ctx.XLogger()
r := ctx.MustResource()
if err := p.Print(fpath); err != nil {
return err
}
if !r.HasArg(resource.ResultFilenameArgKey) {
logger.DebugfOp(
op,
"no '%s' found, using generated filename '%s'",
resource.RemoteURLArgKey,
filename,
)
if err := ctx.Attachment(fpath, filename); err != nil {
return err
}
return nil
}
logger.DebugfOp(
op,
"'%s' found, so not using generated filename",
resource.ResultFilenameArgKey,
)
filename, err := r.StringArg(resource.ResultFilenameArgKey, filename)
if err != nil {
return err
}
if err := ctx.Attachment(fpath, filename); err != nil {
return err
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string) error {
const op = "xhttp.convertAsync"
logger := ctx.XLogger()
r := ctx.MustResource()
webhookURL, err := r.StringArg(resource.WebhookURLArgKey, "")
if err != nil {
return xerror.New(op, err)
}
webhookURLTimeout, err := resource.WebhookURLTimeoutArg(r, ctx.Config())
if err != nil {
return xerror.New(op, err)
}
go func() {
defer r.Close() // nolint: errcheck
if err := p.Print(fpath); err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
f, err := os.Open(fpath)
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
defer f.Close() // nolint: errcheck
logger.DebugfOp(
op,
"sending result file '%s' to '%s'",
filename,
webhookURL,
)
httpClient := &http.Client{
Timeout: xtime.Duration(webhookURLTimeout),
}
resp, err := httpClient.Post(webhookURL, "application/pdf", f) /* #nosec */
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
defer resp.Body.Close() // nolint: errcheck
}()
return nil
}

View File

@@ -0,0 +1,624 @@
package xhttp
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/test"
)
func TestPingHandler(t *testing.T) {
// should return 200.
config := conf.DefaultConfig()
srv := New(config)
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodPost, pingEndpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
}
func TestMergeHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
// should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, mergeEndpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is is > config.MaximumWaitTimeout().
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is invalid.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 504.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
}
func TestHTMLHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint)
// should return 200.
body, contentType := test.HTMLMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is is > config.MaximumWaitTimeout().
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 504.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
// should return 400 as "waitDelay" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitDelay" form field
// value is is > config.MaximumWaitDelay().
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitDelay" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperWidth" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperWidth" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperHeight" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperHeight" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginTop" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginTop" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginBottom" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginBottom" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginLeft" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginLeft" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginRight" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginRight" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "landscape" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is is > config.MaximumGoogleChromeRpccBufferSize().
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is invalid.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
}
func TestURLHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint)
// should return 200.
body, contentType := test.URLMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is is > config.MaximumWaitTimeout().
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 504.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
// should return 400 as "waitDelay" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitDelay" form field
// value is is > config.MaximumWaitDelay().
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitDelay" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperWidth" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperWidth" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperHeight" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperHeight" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginTop" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginTop" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginBottom" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginBottom" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginLeft" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginLeft" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginRight" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginRight" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "landscape" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is is > config.MaximumGoogleChromeRpccBufferSize().
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is invalid.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
}
func TestMarkdownHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint)
// should return 200.
body, contentType := test.MarkdownMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is is > config.MaximumWaitTimeout().
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 504.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
// should return 400 as "waitDelay" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitDelay" form field
// value is is > config.MaximumWaitDelay().
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitDelay" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperWidth" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperWidth" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperHeight" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "paperHeight" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginTop" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginTop" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginBottom" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginBottom" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginLeft" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginLeft" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginRight" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "marginRight" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "landscape" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is is > config.MaximumGoogleChromeRpccBufferSize().
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "googleChromeRpccBufferSize" form field
// value is invalid.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
}
func TestOfficeHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint)
// should return 200.
body, contentType := test.OfficeMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is is > config.MaximumWaitTimeout().
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is invalid.
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 504.
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
// should return 400 as "landscape" form field
// value is invalid.
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
}
func TestWebhook(t *testing.T) {
status := make(chan error, 2)
rcv := echo.New()
rcv.POST("/foo", func(c echo.Context) error {
if c.Request().Header.Get("Content-type") != "application/pdf" {
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
return nil
}
body, err := ioutil.ReadAll(c.Request().Body)
if err != nil {
status <- err
return nil
}
if body == nil || len(body) == 0 {
status <- errors.New("empty body")
return nil
}
status <- nil
return nil
})
go func() {
rcv.Start(":3001")
}()
config := conf.DefaultConfig()
srv := New(config)
// our custom server should receive the PDF.
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"})
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
err := <-status
assert.NoError(t, err)
}
func TestResultFilename(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"})
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
}

View File

@@ -0,0 +1,153 @@
package xhttp
import (
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
)
// contextMiddleware extends the default echo.Context with
// our custom context.Context.
func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// generate a unique identifier for the request.
trace := xrand.Get()
// create the logger for this request using
// the previous identifier as trace.
logger := xlog.New(config.LogLevel(), trace)
// extend the current echo context with our custom
// context.
ctx := context.New(c, logger, config)
// if it's not a multipart/form-data request,
// there is no need to create a Resource.
if !isMultipartFormDataEndpoint(config, ctx.Path()) {
// validate method for healthcheck endpoint.
if ctx.Path() == pingEndpoint && ctx.Request().Method != http.MethodGet {
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
return ctx.LogRequestResult(err, false)
}
return next(ctx)
}
// validate method.
if ctx.Request().Method != http.MethodPost {
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
return ctx.LogRequestResult(err, false)
}
// validate Content-Type.
contentType := ctx.Request().Header.Get("Content-Type")
if !strings.Contains(contentType, "multipart/form-data") {
err := doErr(ctx, echo.NewHTTPError(http.StatusUnsupportedMediaType))
return ctx.LogRequestResult(err, false)
}
// it's a multipart/form-data request, create a
// Resource.
if err := ctx.WithResource(trace); err != nil {
err = doCleanup(ctx, err)
err = doErr(ctx, err)
return ctx.LogRequestResult(err, false)
}
return next(ctx)
}
}
}
// loggerMiddleware logs the result of a request.
func loggerMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := context.MustCastFromEchoContext(c)
err := next(ctx)
// we do not want to log healthcheck requests if
// log level is not set to DEBUG.
isDebug := ctx.Path() == pingEndpoint
return ctx.LogRequestResult(err, isDebug)
}
}
}
// cleanupMiddleware removes a resource.Resource
// at the end of a request.
func cleanupMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := next(c)
ctx := context.MustCastFromEchoContext(c)
return doCleanup(ctx, err)
}
}
}
// errorMiddleware handles errors (if any).
func errorMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := context.MustCastFromEchoContext(c)
err := next(ctx)
if err == nil {
// so far so good!
return nil
}
return doErr(ctx, err)
}
}
}
func doCleanup(ctx context.Context, err error) error {
const op string = "xhttp.cleanup"
if !ctx.HasResource() {
// nothing to remove.
return err
}
r := ctx.MustResource()
// if a webhook URL has been given,
// do not remove the resource.Resource here because
// we don't know if the result file has been
// generated or sent.
if r.HasArg(resource.WebhookURLArgKey) {
return err
}
// a resource.Resource is associated with our custom context.
if resourceErr := r.Close(); resourceErr != nil {
xerr := xerror.New(op, resourceErr)
ctx.XLogger().ErrorOp(xerror.Op(xerr), xerr)
}
return err
}
func doErr(ctx context.Context, err error) error {
// if it's an error from echo
// like 404 not found and so on.
if echoHTTPErr, ok := err.(*echo.HTTPError); ok {
// required to have a correct status code.
ctx.Error(echoHTTPErr)
return echoHTTPErr
}
// we log the initial error before returning
// the HTTP error.
errOp := xerror.Op(err)
logger := ctx.XLogger()
logger.ErrorOp(errOp, err)
// handle our custom HTTP error.
var httpErr error
errCode := xerror.Code(err)
errMessage := xerror.Message(err)
switch errCode {
case xerror.InvalidCode:
httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage)
case xerror.TimeoutCode:
httpErr = echo.NewHTTPError(http.StatusGatewayTimeout, errMessage)
default:
httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage)
}
// required to have a correct status code.
ctx.Error(httpErr)
return httpErr
}

View File

@@ -0,0 +1,98 @@
package xhttp
import (
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
)
func mergePrinterOptions(r resource.Resource, config conf.Config) (printer.MergePrinterOptions, error) {
const op string = "xhttp.mergePrinterOptions"
waitTimeout, err := resource.WaitTimeoutArg(r, config)
if err != nil {
return printer.MergePrinterOptions{}, xerror.New(op, err)
}
return printer.MergePrinterOptions{
WaitTimeout: waitTimeout,
}, nil
}
func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.ChromePrinterOptions, error) {
const op string = "xhttp.chromePrinterOptions"
resolver := func() (printer.ChromePrinterOptions, error) {
waitTimeout, err := resource.WaitTimeoutArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
waitDelay, err := resource.WaitDelayArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
headerHTML, footerHTML,
err := resource.HeaderFooterContents(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
paperWidth, paperHeight,
err := resource.PaperSizeArgs(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
marginTop, marginBottom, marginLeft, marginRight,
err := resource.MarginArgs(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
return printer.ChromePrinterOptions{
WaitTimeout: waitTimeout,
WaitDelay: waitDelay,
HeaderHTML: headerHTML,
FooterHTML: footerHTML,
PaperWidth: paperWidth,
PaperHeight: paperHeight,
MarginTop: marginTop,
MarginBottom: marginBottom,
MarginLeft: marginLeft,
MarginRight: marginRight,
Landscape: landscape,
RpccBufferSize: googleChromeRpccBufferSize,
}, nil
}
opts, err := resolver()
if err != nil {
return opts, xerror.New(op, err)
}
return opts, nil
}
func officePrinterOptions(r resource.Resource, config conf.Config) (printer.OfficePrinterOptions, error) {
const op string = "xhttp.officePrinterOptions"
resolver := func() (printer.OfficePrinterOptions, error) {
waitTimeout, err := resource.WaitTimeoutArg(r, config)
if err != nil {
return printer.OfficePrinterOptions{}, err
}
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
if err != nil {
return printer.OfficePrinterOptions{}, err
}
return printer.OfficePrinterOptions{
WaitTimeout: waitTimeout,
Landscape: landscape,
}, nil
}
opts, err := resolver()
if err != nil {
return opts, xerror.New(op, err)
}
return opts, nil
}

View File

@@ -0,0 +1,208 @@
package context
import (
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
// Context extends the default echo.Context.
type Context struct {
echo.Context
logger xlog.Logger
config conf.Config
resource resource.Resource
startTime time.Time
}
// New creates a new Context.
func New(c echo.Context, logger xlog.Logger, config conf.Config) Context {
return Context{
c,
logger,
config,
resource.Resource{},
time.Now(),
}
}
/*
MustCastFromEchoContext cast an echo.Context
to our custom Context.
It panics if casting goes wrong.
*/
func MustCastFromEchoContext(c echo.Context) Context {
const op string = "context.MustCastFromEchoContext"
ctx, ok := c.(Context)
if !ok {
panic(fmt.Sprintf("%s: unable to cast an echo.Context to our custom context.Context", op))
}
return ctx
}
/*
XLogger returns the xlog.Logger associated
with the Context.
This method should be used instead of the
default Logger() method coming from
the echo.Context.
*/
func (ctx Context) XLogger() xlog.Logger {
return ctx.logger
}
// Config returns the conf.Config associated
// with the Context.
func (ctx Context) Config() conf.Config {
return ctx.config
}
// WithResource creates a resource.Resource and
// adds it to the Context.
func (ctx *Context) WithResource(directoryName string) error {
const op string = "context.Context.WithResource"
resolver := func() (resource.Resource, error) {
r, err := resource.New(ctx.logger, directoryName)
if err != nil {
return r, err
}
// retrieve form values from request.
for _, key := range resource.ArgKeys() {
r.WithArg(key, ctx.FormValue(string(key)))
}
// write form files from request.
form, err := ctx.MultipartForm()
if err != nil {
/*
(very) special case: one and
only one file has been sent
and it is empty.
*/
if strings.Contains(err.Error(), io.EOF.Error()) {
return r, xerror.Invalid(op, "one file has been sent but it is empty: does it exist?", err)
}
return r, err
}
for _, files := range form.File {
for _, fh := range files {
in, err := fh.Open()
if err != nil {
return r, err
}
defer in.Close() // nolint: errcheck
filename, err := normalize.String(fh.Filename)
if err != nil {
return r, err
}
if err := r.WithFile(filename, in); err != nil {
return r, err
}
}
}
return r, nil
}
resource, err := resolver()
ctx.resource = resource
if err != nil {
return xerror.New(op, err)
}
return nil
}
// HasResource returns true if the Context
// has a resource.Resource.
func (ctx Context) HasResource() bool {
return !reflect.DeepEqual(ctx.resource, resource.Resource{})
}
/*
MustResource returns the resource.Resource
associated with the Context.
It panics if no resource.Resource.
*/
func (ctx Context) MustResource() resource.Resource {
const op string = "context.Context.MustResource"
if !ctx.HasResource() {
panic(fmt.Sprintf("%s: unable to retrieve the resource.Resource from our custom context.Context", op))
}
return ctx.resource
}
/*
LogRequestResult logs the result of a request.
This method should only be used by a middleware!
If an error is given, returns the exact same error.
*/
func (ctx Context) LogRequestResult(err error, isDebug bool) error {
const op string = "context.Context.LogRequestResult"
req := ctx.Request()
resp := ctx.Response()
stopTime := time.Now()
fields := map[string]interface{}{
"remote_ip": ctx.RealIP(),
"host": req.Host,
"uri": req.RequestURI,
"method": req.Method,
"path": path(req),
"referer": req.Referer(),
"user_agent": req.UserAgent(),
"status": resp.Status,
"latency": lantency(ctx.startTime, stopTime),
"latency_human": latencyHuman(ctx.startTime, stopTime),
"bytes_in": bytesIn(req),
"bytes_out": bytesOut(resp),
}
if err != nil {
ctx.logger.WithFields(fields).ErrorfOp(op, "request failed")
return err
}
if isDebug {
ctx.logger.WithFields(fields).DebugfOp(op, "request handled")
return nil
}
ctx.logger.WithFields(fields).InfofOp(op, "request handled")
return nil
}
func path(r *http.Request) string {
path := r.URL.Path
if path == "" {
path = "/"
}
return path
}
func lantency(startTime time.Time, stopTime time.Time) string {
return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10)
}
func latencyHuman(startTime time.Time, stopTime time.Time) string {
return stopTime.Sub(startTime).String()
}
func bytesIn(r *http.Request) string {
bytesIn := r.Header.Get(echo.HeaderContentLength)
if bytesIn == "" {
bytesIn = "0"
}
return bytesIn
}
func bytesOut(r *echo.Response) string {
return strconv.FormatInt(r.Size, 10)
}

View File

@@ -0,0 +1,78 @@
package context
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/test"
)
func TestMustCastFromEchoContext(t *testing.T) {
// should be OK.
ctx := New(
test.DummyEchoContext(),
test.DebugLogger(),
conf.DefaultConfig(),
)
assert.NotPanics(t, func() {
result := MustCastFromEchoContext(ctx)
assert.Equal(t, ctx, result)
})
// should not be OK.
assert.Panics(t, func() {
MustCastFromEchoContext(test.DummyEchoContext())
})
}
func TestLogRequestResult(t *testing.T) {
ctx := New(
test.DummyEchoContext(),
test.DebugLogger(),
conf.DefaultConfig(),
)
// Info log.
err := ctx.LogRequestResult(nil, false)
assert.Nil(t, err)
// Debug log.
err = ctx.LogRequestResult(nil, true)
assert.Nil(t, err)
// Error log.
err = ctx.LogRequestResult(errors.New("foo"), true)
assert.NotNil(t, err)
}
func TestGetters(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
config := conf.DefaultConfig()
ctx := New(
test.DummyEchoContext(),
logger,
config,
)
// Logger.
assert.Equal(t, logger, ctx.XLogger())
// Config.
assert.Equal(t, config, ctx.Config())
// Context should not have a resource.Resource.
assert.Equal(t, false, ctx.HasResource())
assert.Panics(t, func() {
ctx.MustResource()
})
// Context should have a resource.Resource.
ctx = New(
test.EchoContextMultipart(t),
logger,
config,
)
err := ctx.WithResource(resourceDirectoryName)
assert.Nil(t, err)
assert.Equal(t, true, ctx.HasResource())
assert.NotPanics(t, func() {
r := ctx.MustResource()
err = r.Close()
assert.Nil(t, err)
})
}

View File

@@ -0,0 +1,7 @@
/*
Package context extends the default echo.Context.
All functions return our standard xerror.Error
in case of error.
*/
package context

View File

@@ -0,0 +1,292 @@
package resource
import (
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
)
// ArgKey is a type for
// arguments' keys.
type ArgKey string
const (
// ResultFilenameArgKey is the key
// of the argument "resultFilename".
ResultFilenameArgKey ArgKey = "resultFilename"
// WaitTimeoutArgKey is the key
// of the argument "waitTimeout".
WaitTimeoutArgKey ArgKey = "waitTimeout"
// WebhookURLArgKey is the key
// of the argument "webhookURL".
WebhookURLArgKey ArgKey = "webhookURL"
// WebhookURLTimeoutArgKey is the key
// of the argument "webhookURLTimeout".
WebhookURLTimeoutArgKey ArgKey = "webhookURLTimeout"
// RemoteURLArgKey is the key
// of the argument "remoteURL".
RemoteURLArgKey ArgKey = "remoteURL"
// WaitDelayArgKey is the key
// of the argument "waitDelay".
WaitDelayArgKey ArgKey = "waitDelay"
// PaperWidthArgKey is the key
// of the argument "paperWidth".
PaperWidthArgKey ArgKey = "paperWidth"
// PaperHeightArgKey is the key
// of the argument "paperHeight".
PaperHeightArgKey ArgKey = "paperHeight"
// MarginTopArgKey is the key
// of the argument "marginTop".
MarginTopArgKey ArgKey = "marginTop"
// MarginBottomArgKey is the key
// of the argument "marginBottom".
MarginBottomArgKey ArgKey = "marginBottom"
// MarginLeftArgKey is the key
// of the argument "marginLeft".
MarginLeftArgKey ArgKey = "marginLeft"
// MarginRightArgKey is the key
// of the argument "marginRight".
MarginRightArgKey ArgKey = "marginRight"
// LandscapeArgKey is the key
// of the argument "landscape".
LandscapeArgKey ArgKey = "landscape"
// GoogleChromeRpccBufferSizeArgKey is the key
// of the argument "googleChromeRpccBufferSize".
GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize"
)
/*
ArgKeys returns a slice
containing all available
arguments' keys.
*/
func ArgKeys() []ArgKey {
return []ArgKey{
ResultFilenameArgKey,
WaitTimeoutArgKey,
WebhookURLArgKey,
WebhookURLTimeoutArgKey,
RemoteURLArgKey,
WaitDelayArgKey,
PaperWidthArgKey,
PaperHeightArgKey,
MarginTopArgKey,
MarginBottomArgKey,
MarginLeftArgKey,
MarginRightArgKey,
LandscapeArgKey,
GoogleChromeRpccBufferSizeArgKey,
}
}
/*
WaitTimeoutArg is a helper for retrieving
the "waitTimeout" argument as float64.
It also validates it against the application
configuration.
*/
func WaitTimeoutArg(r Resource, config conf.Config) (float64, error) {
const op string = "resource.WaitTimeoutArg"
result, err := r.Float64Arg(
WaitTimeoutArgKey,
config.DefaultWaitTimeout(),
xassert.Float64NotInferiorTo(0),
xassert.Float64NotSuperiorTo(config.MaximumWaitTimeout()),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
WaitDelayArg is a helper for retrieving
the "waitDelay" argument as float64.
It also validates it against the application
configuration.
*/
func WaitDelayArg(r Resource, config conf.Config) (float64, error) {
const (
op string = "resource.WaitDelayArg"
defaultWaitDelay float64 = 0.0
)
result, err := r.Float64Arg(
WaitDelayArgKey,
defaultWaitDelay,
xassert.Float64NotInferiorTo(0.0),
xassert.Float64NotSuperiorTo(config.MaximumWaitDelay()),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
WebhookURLTimeoutArg is a helper for retrieving
the "webhookURLTimeout" argument as float64.
It also validates it against the application
configuration.
*/
func WebhookURLTimeoutArg(r Resource, config conf.Config) (float64, error) {
const op string = "resource.WebhookURLTimeoutArg"
result, err := r.Float64Arg(
WebhookURLTimeoutArgKey,
config.DefaultWebhookURLTimeout(),
xassert.Float64NotInferiorTo(0),
xassert.Float64NotSuperiorTo(config.MaximumWebhookURLTimeout()),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
PaperSizeArgs is a helper for retrieving
the "paperWidth" and "paperHeight" arguments
as float64.
*/
func PaperSizeArgs(r Resource, config conf.Config) (float64, float64, error) {
const op string = "resource.PaperSizeArgs"
opts := printer.DefaultChromePrinterOptions(config)
resolver := func() (float64, float64, error) {
paperWidth, err := r.Float64Arg(
PaperWidthArgKey,
opts.PaperWidth,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return opts.PaperWidth,
opts.PaperHeight,
err
}
paperHeight, err := r.Float64Arg(
PaperHeightArgKey,
opts.PaperHeight,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return opts.PaperWidth,
opts.PaperHeight,
err
}
return paperWidth,
paperHeight,
nil
}
paperWidth, paperHeight,
err := resolver()
if err != nil {
return paperWidth,
paperHeight,
xerror.New(op, err)
}
return paperWidth,
paperHeight,
nil
}
/*
MarginArgs is a helper for retrieving
the "marginTop", "marginBottom", "marginLeft"
and "marginRight" arguments as float64.
*/
func MarginArgs(r Resource, config conf.Config) (float64, float64, float64, float64, error) {
const op string = "resource.MarginArgs"
opts := printer.DefaultChromePrinterOptions(config)
resolver := func() (float64, float64, float64, float64, error) {
marginTop, err := r.Float64Arg(
MarginTopArgKey,
opts.MarginTop,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return opts.MarginTop,
opts.MarginBottom,
opts.MarginLeft,
opts.MarginRight,
err
}
marginBottom, err := r.Float64Arg(
MarginBottomArgKey,
opts.MarginBottom,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return opts.MarginTop,
opts.MarginBottom,
opts.MarginLeft,
opts.MarginRight,
err
}
marginLeft, err := r.Float64Arg(
MarginLeftArgKey,
opts.MarginLeft,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return opts.MarginTop,
opts.MarginBottom,
opts.MarginLeft,
opts.MarginRight,
err
}
marginRight, err := r.Float64Arg(
MarginRightArgKey,
opts.MarginRight,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return opts.MarginTop,
opts.MarginBottom,
opts.MarginLeft,
opts.MarginRight,
err
}
return marginTop,
marginBottom,
marginLeft,
marginRight,
nil
}
marginTop, marginBottom, marginLeft, marginRight,
err := resolver()
if err != nil {
return marginTop,
marginBottom,
marginLeft,
marginRight,
xerror.New(op, err)
}
return marginTop,
marginBottom,
marginLeft,
marginRight,
nil
}
/*
GoogleChromeRpccBufferSizeArg is a helper for retrieving
the "googleChromeRpccBufferSize" argument as int64.
It also validates it against the application
configuration.
*/
func GoogleChromeRpccBufferSizeArg(r Resource, config conf.Config) (int64, error) {
const op string = "resource.GoogleChromeRpccBufferSizeArg"
result, err := r.Int64Arg(
GoogleChromeRpccBufferSizeArgKey,
config.DefaultGoogleChromeRpccBufferSize(),
xassert.Int64NotInferiorTo(0.0),
xassert.Int64NotSuperiorTo(config.MaximumGoogleChromeRpccBufferSize()),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}

View File

@@ -0,0 +1,347 @@
package resource
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/test"
)
func TestArgKeys(t *testing.T) {
expected := []ArgKey{
ResultFilenameArgKey,
WaitTimeoutArgKey,
WebhookURLArgKey,
WebhookURLTimeoutArgKey,
RemoteURLArgKey,
WaitDelayArgKey,
PaperWidthArgKey,
PaperHeightArgKey,
MarginTopArgKey,
MarginBottomArgKey,
MarginLeftArgKey,
MarginRightArgKey,
LandscapeArgKey,
GoogleChromeRpccBufferSizeArgKey,
}
assert.Equal(t, expected, ArgKeys())
}
func TestWaitTimeoutArg(t *testing.T) {
const resourceDirectoryName string = "foo"
var expected float64
logger := test.DebugLogger()
config := conf.DefaultConfig()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// argument does not exist.
expected = config.DefaultWaitTimeout()
v, err := WaitTimeoutArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// argument exist.
expected = 5.0
r.WithArg(WaitTimeoutArgKey, "5.0")
v, err = WaitTimeoutArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is < 0.
expected = config.DefaultWaitTimeout()
r.WithArg(WaitTimeoutArgKey, "-1.0")
v, err = WaitTimeoutArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is > config.MaximumWaitTimeout().
expected = config.DefaultWaitTimeout()
r.WithArg(WaitTimeoutArgKey, "31.0")
v, err = WaitTimeoutArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as
// argument value is invalid.
expected = config.DefaultWaitTimeout()
r.WithArg(WaitTimeoutArgKey, "foo")
v, err = WaitTimeoutArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestWaitDelayArg(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue float64 = 0.0
)
var expected float64
logger := test.DebugLogger()
config := conf.DefaultConfig()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// argument does not exist.
expected = defaultValue
v, err := WaitDelayArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// argument exist.
expected = 5.0
r.WithArg(WaitDelayArgKey, "5.0")
v, err = WaitDelayArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is < 0.
expected = defaultValue
r.WithArg(WaitDelayArgKey, "-1.0")
v, err = WaitDelayArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is > config.MaximumWaitDelay().
expected = defaultValue
r.WithArg(WaitDelayArgKey, "31.0")
v, err = WaitDelayArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as
// argument value is invalid.
expected = defaultValue
r.WithArg(WaitDelayArgKey, "foo")
v, err = WaitDelayArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestWebhookURLTimeoutArg(t *testing.T) {
const resourceDirectoryName string = "foo"
var expected float64
logger := test.DebugLogger()
config := conf.DefaultConfig()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// argument does not exist.
expected = config.DefaultWebhookURLTimeout()
v, err := WebhookURLTimeoutArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// argument exist.
expected = 5.0
r.WithArg(WebhookURLTimeoutArgKey, "5.0")
v, err = WebhookURLTimeoutArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is < 0.
expected = config.DefaultWebhookURLTimeout()
r.WithArg(WebhookURLTimeoutArgKey, "-1.0")
v, err = WebhookURLTimeoutArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is > config.MaximumWebhookURLTimeout().
expected = config.DefaultWebhookURLTimeout()
r.WithArg(WebhookURLTimeoutArgKey, "31.0")
v, err = WebhookURLTimeoutArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as
// argument value is invalid.
expected = config.DefaultWebhookURLTimeout()
r.WithArg(WebhookURLTimeoutArgKey, "foo")
v, err = WebhookURLTimeoutArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestPaperSizeArgs(t *testing.T) {
const resourceDirectoryName string = "foo"
var expected float64
logger := test.DebugLogger()
config := conf.DefaultConfig()
opts := printer.DefaultChromePrinterOptions(config)
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// arguments do not exist.
width, height, err := PaperSizeArgs(r, config)
assert.Nil(t, err)
assert.Equal(t, opts.PaperWidth, width)
assert.Equal(t, opts.PaperHeight, height)
// arguments exist.
expected = 5.0
r.WithArg(PaperWidthArgKey, "5.0")
r.WithArg(PaperHeightArgKey, "5.0")
width, height, err = PaperSizeArgs(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, width)
assert.Equal(t, expected, height)
// should not be OK as arguments
// value are < 0.
expected = opts.PaperWidth
r.WithArg(PaperWidthArgKey, "-1.0")
width, _, err = PaperSizeArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, width)
r.WithArg(PaperWidthArgKey, "5.0")
expected = opts.PaperHeight
r.WithArg(PaperHeightArgKey, "-1.0")
_, height, err = PaperSizeArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, height)
r.WithArg(PaperHeightArgKey, "5.0")
// should not be OK as
// arguments value are invalids.
expected = opts.PaperWidth
r.WithArg(PaperWidthArgKey, "foo")
width, _, err = PaperSizeArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, width)
r.WithArg(PaperWidthArgKey, "5.0")
expected = opts.PaperHeight
r.WithArg(PaperHeightArgKey, "foo")
_, height, err = PaperSizeArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, height)
r.WithArg(PaperHeightArgKey, "5.0")
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestMarginArgs(t *testing.T) {
const resourceDirectoryName string = "foo"
var expected float64
logger := test.DebugLogger()
config := conf.DefaultConfig()
opts := printer.DefaultChromePrinterOptions(config)
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// arguments do not exist.
top, bottom, left, right, err := MarginArgs(r, config)
assert.Nil(t, err)
assert.Equal(t, opts.MarginTop, top)
assert.Equal(t, opts.MarginBottom, bottom)
assert.Equal(t, opts.MarginLeft, left)
assert.Equal(t, opts.MarginRight, right)
// arguments exist.
expected = 5.0
r.WithArg(MarginTopArgKey, "5.0")
r.WithArg(MarginBottomArgKey, "5.0")
r.WithArg(MarginLeftArgKey, "5.0")
r.WithArg(MarginRightArgKey, "5.0")
top, bottom, left, right, err = MarginArgs(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, top)
assert.Equal(t, expected, bottom)
assert.Equal(t, expected, left)
assert.Equal(t, expected, right)
// should not be OK as arguments
// value are < 0.
expected = opts.MarginTop
r.WithArg(MarginTopArgKey, "-1.0")
top, _, _, _, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, top)
r.WithArg(MarginTopArgKey, "5.0")
expected = opts.MarginBottom
r.WithArg(MarginBottomArgKey, "-1.0")
_, bottom, _, _, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, bottom)
r.WithArg(MarginBottomArgKey, "5.0")
expected = opts.MarginLeft
r.WithArg(MarginLeftArgKey, "-1.0")
_, _, left, _, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, left)
r.WithArg(MarginLeftArgKey, "5.0")
expected = opts.MarginRight
r.WithArg(MarginRightArgKey, "-1.0")
_, _, _, right, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, right)
r.WithArg(MarginRightArgKey, "5.0")
// should not be OK as
// arguments value are invalids.
expected = opts.MarginTop
r.WithArg(MarginTopArgKey, "foo")
top, _, _, _, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, top)
r.WithArg(MarginTopArgKey, "5.0")
expected = opts.MarginBottom
r.WithArg(MarginBottomArgKey, "foo")
_, bottom, _, _, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, bottom)
r.WithArg(MarginBottomArgKey, "5.0")
expected = opts.MarginLeft
r.WithArg(MarginLeftArgKey, "foo")
_, _, left, _, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, left)
r.WithArg(MarginLeftArgKey, "5.0")
expected = opts.MarginRight
r.WithArg(MarginRightArgKey, "foo")
_, _, _, right, err = MarginArgs(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, right)
r.WithArg(MarginRightArgKey, "5.0")
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestGoogleChromeRpccBufferSizeArg(t *testing.T) {
const resourceDirectoryName string = "foo"
var expected int64
logger := test.DebugLogger()
config := conf.DefaultConfig()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// argument does not exist.
expected = config.DefaultGoogleChromeRpccBufferSize()
v, err := GoogleChromeRpccBufferSizeArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// argument exist.
expected = 10
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "10")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is < 0.
expected = config.DefaultGoogleChromeRpccBufferSize()
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "-1")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is > config.MaximumGoogleChromeRpccBufferSize().
expected = config.DefaultGoogleChromeRpccBufferSize()
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "104857601")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as
// argument value is invalid.
expected = config.DefaultGoogleChromeRpccBufferSize()
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "foo")
v, err = GoogleChromeRpccBufferSizeArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,8 @@
/*
Package resource helps managing
arguments and files for a conversion.
All functions return our standard xerror.Error
in case of error.
*/
package resource

View File

@@ -0,0 +1,91 @@
package resource
import (
"io"
"io/ioutil"
"os"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
)
// file represents a file within the resource.
type file struct {
fpath string
}
// write writes given content to the
// resourceFile location.
func (f file) write(in io.Reader) error {
const op string = "resource.file.write"
resolver := func() error {
out, err := os.Create(f.fpath)
if err != nil {
return err
}
defer out.Close() // nolint: errcheck
if err := out.Chmod(0644); err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
return err
}
if _, err := out.Seek(0, 0); err != nil {
return err
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// content returns the string content of
// the file.
func (f file) content() (string, error) {
const op string = "resource.file.content"
b, err := ioutil.ReadFile(f.fpath)
if err != nil {
return "", xerror.New(op, err)
}
return string(b), nil
}
/*
HeaderFooterContents is a helper for retrieving
the content of the files "header.html"
and "footer.html".
*/
func HeaderFooterContents(r Resource, config conf.Config) (string, string, error) {
const op string = "resource.HeaderFooterContents"
opts := printer.DefaultChromePrinterOptions(config)
resolver := func() (string, string, error) {
headerHTML, err := r.Fcontent("header.html", opts.HeaderHTML)
if err != nil {
return opts.HeaderHTML,
opts.FooterHTML,
err
}
footerHTML, err := r.Fcontent("footer.html", opts.FooterHTML)
if err != nil {
return opts.HeaderHTML,
opts.FooterHTML,
err
}
return headerHTML,
footerHTML,
nil
}
headerHTML, footerHTML,
err := resolver()
if err != nil {
return headerHTML,
footerHTML,
xerror.New(op, err)
}
return headerHTML,
footerHTML,
nil
}

View File

@@ -0,0 +1,46 @@
package resource
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/test"
)
func TestHeaderFooterContents(t *testing.T) {
const resourceDirectoryName string = "foo"
var expected string
logger := test.DebugLogger()
config := conf.DefaultConfig()
opts := printer.DefaultChromePrinterOptions(config)
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// arguments do not exist.
header, footer, err := HeaderFooterContents(r, config)
assert.Nil(t, err)
assert.Equal(t, opts.HeaderHTML, header)
assert.Equal(t, opts.FooterHTML, footer)
// arguments exist.
expected = "Gutenberg"
fpath := test.OfficeFpaths(t)[2]
f1, err := os.Open(fpath)
assert.Nil(t, err)
defer f1.Close() // nolint: errcheck
err = r.WithFile("header.html", f1)
assert.Nil(t, err)
f2, err := os.Open(fpath)
assert.Nil(t, err)
defer f2.Close() // nolint: errcheck
err = r.WithFile("footer.html", f2)
assert.Nil(t, err)
header, footer, err = HeaderFooterContents(r, config)
assert.Nil(t, err)
assert.Contains(t, header, expected)
assert.Contains(t, footer, expected)
// finally...
err = r.Close()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,227 @@
package resource
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
/*
TemporaryDirectory is the directory
where all the resources directory
are located.
*/
const TemporaryDirectory string = "tmp"
// Resource helps managing
// arguments and files for a conversion.
type Resource struct {
logger xlog.Logger
dirPath string
args map[ArgKey]string
files map[string]file
}
// New creates a Resource where its files will
// be located in the given directory name.
func New(logger xlog.Logger, directoryName string) (Resource, error) {
const op string = "resource.New"
resolver := func() (string, error) {
dirPath := fmt.Sprintf("%s/%s", TemporaryDirectory, directoryName)
if err := os.MkdirAll(dirPath, 0755); err != nil {
return "", err
}
absDirPath, err := filepath.Abs(dirPath)
if err != nil {
return "", err
}
return absDirPath, nil
}
dirPath, err := resolver()
if err != nil {
return Resource{}, xerror.New(op, err)
}
logger.DebugfOp(op, "resource directory '%s' created", directoryName)
return Resource{
logger: logger,
dirPath: dirPath,
args: make(map[ArgKey]string),
files: make(map[string]file),
}, nil
}
// Close removes the working directory of the
// Resource if it exists.
func (r Resource) Close() error {
const op string = "resource.Resource.Close"
if _, err := os.Stat(r.dirPath); os.IsNotExist(err) {
r.logger.DebugfOp(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
return nil
}
if err := os.RemoveAll(r.dirPath); err != nil {
return xerror.New(op, err)
}
r.logger.DebugfOp(op, "resource directory '%s' removed", r.dirPath)
return nil
}
// WithArg add a new argument to the Resource.
func (r *Resource) WithArg(key ArgKey, value string) {
const op string = "resource.Resource.WithArg"
r.args[key] = value
r.logger.DebugfOp(op, "added '%s' with value '%s' to resource args", key, value)
}
// WithFile add a new file to the Resource.
func (r *Resource) WithFile(filename string, in io.Reader) error {
const op string = "resource.Resource.WithFile"
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
file := file{fpath: fpath}
if err := file.write(in); err != nil {
return xerror.New(op, err)
}
r.files[filename] = file
r.logger.DebugfOp(op, "resource file '%s' created", filename)
return nil
}
// DirPath returns the directory path
// of the Resource.
func (r Resource) DirPath() string {
return r.dirPath
}
// HasArg returns true if given key exists
// among the Resource and its value is not empty.
func (r Resource) HasArg(key ArgKey) bool {
if v, ok := r.args[key]; ok {
return v != ""
}
return false
}
/*
StringArg returns the value of the
argument identified by given key.
It works in the same manner as xassert.String.
*/
func (r Resource) StringArg(key ArgKey, defaultValue string, rules ...xassert.RuleString) (string, error) {
const op string = "resource.Resource.StringArg"
result, err := xassert.String(string(key), r.args[key], defaultValue, rules...)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
Int64Arg returns the int64 representation of the
argument identified by given key.
It works in the same manner as xassert.Int64.
*/
func (r Resource) Int64Arg(key ArgKey, defaultValue int64, rules ...xassert.RuleInt64) (int64, error) {
const op string = "resource.Resource.Int64Arg"
result, err := xassert.Int64(string(key), r.args[key], defaultValue, rules...)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
Float64Arg returns the float64 representation of the
argument identified by given key.
It works in the same manner as xassert.Float64.
*/
func (r Resource) Float64Arg(key ArgKey, defaultValue float64, rules ...xassert.RuleFloat64) (float64, error) {
const op string = "resource.Resource.Float64Arg"
result, err := xassert.Float64(string(key), r.args[key], defaultValue, rules...)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
BoolArg returns the boolean representation of the
argument identified by given key.
It works in the same manner as xassert.Bool.
*/
func (r Resource) BoolArg(key ArgKey, defaultValue bool) (bool, error) {
const op string = "resource.Resource.BoolArg"
result, err := xassert.Bool(string(key), r.args[key], defaultValue)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
// Fpath returns the path of the given filename.
// This filename should exist whithin the Resource.
func (r Resource) Fpath(filename string) (string, error) {
const op string = "resource.Resource.Fpath"
file, ok := r.files[filename]
if !ok {
return "", xerror.Invalid(
op,
fmt.Sprintf("resource file '%s' does not exist", filename),
nil,
)
}
return file.fpath, nil
}
/*
Fpaths returns the paths of the files
having one of the given file extensions.
It should found at least one path.
*/
func (r Resource) Fpaths(exts ...string) ([]string, error) {
const op string = "resource.Resource.Fpaths"
var fpaths []string
for filename, file := range r.files {
for _, ext := range exts {
if filepath.Ext(filename) == ext {
fpaths = append(fpaths, file.fpath)
}
}
}
if len(fpaths) == 0 {
return nil, xerror.Invalid(
op,
fmt.Sprintf("no resource file found for extensions '%v'", exts),
nil,
)
}
return fpaths, nil
}
/*
Fcontent returns the string content of the
given filename.
If filename does not exist within the Resource,
returns the default value.
*/
func (r Resource) Fcontent(filename, defaultValue string) (string, error) {
const op string = "resource.Resource.Fcontent"
file, ok := r.files[filename]
if !ok {
return defaultValue, nil
}
content, err := file.content()
if err != nil {
return "", xerror.New(op, err)
}
return content, nil
}

View File

@@ -0,0 +1,287 @@
package resource
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestStringArg(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue string = "FOO"
)
var expected string
rule := xassert.StringOneOf([]string{"FOO", "BAR"})
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// empty value, result should be equal
// to the default value.
r.WithArg(ResultFilenameArgKey, "")
v, err := r.StringArg(ResultFilenameArgKey, defaultValue)
assert.Nil(t, err)
assert.Equal(t, defaultValue, v)
// result should be equal to given value
// as it is one of "FOO" and "BAR".
expected = "BAR"
r.WithArg(ResultFilenameArgKey, expected)
v, err = r.StringArg(ResultFilenameArgKey, defaultValue, rule)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as given value is not
// one of "FOO" and "BAR".
expected = defaultValue
r.WithArg(ResultFilenameArgKey, "BAZ")
v, err = r.StringArg(ResultFilenameArgKey, defaultValue, rule)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestInt64Arg(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue int64 = 10
)
var expected int64
rule := xassert.Int64NotInferiorTo(6)
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// empty value, result should be equal
// to the default value.
r.WithArg(WaitTimeoutArgKey, "")
v, err := r.Int64Arg(WaitTimeoutArgKey, defaultValue)
expected = defaultValue
assert.Equal(t, expected, v)
assert.Nil(t, err)
// result should be equal to given value
// but as integer.
r.WithArg(WaitTimeoutArgKey, "5")
v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue)
expected = 5
assert.Equal(t, expected, v)
assert.Nil(t, err)
// should not be OK as given value is not
// a string representation of an integer.
r.WithArg(WaitTimeoutArgKey, "foo")
v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue)
expected = defaultValue
assert.Equal(t, expected, v)
test.AssertError(t, err)
// should not be OK as given value does not
// validate the rule x >= 6.
r.WithArg(WaitTimeoutArgKey, "5")
v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue, rule)
expected = defaultValue
assert.Equal(t, expected, v)
test.AssertError(t, err)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestFloat64Arg(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue float64 = 10.0
)
var expected float64
rule := xassert.Float64NotInferiorTo(6.0)
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// empty value, result should be equal
// to the default value.
r.WithArg(WaitTimeoutArgKey, "")
v, err := r.Float64Arg(WaitTimeoutArgKey, defaultValue)
expected = defaultValue
assert.Equal(t, expected, v)
assert.Nil(t, err)
// result should be equal to given value
// but as float.
r.WithArg(WaitTimeoutArgKey, "5.5")
v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue)
expected = 5.5
assert.Equal(t, expected, v)
assert.Nil(t, err)
// should not be OK as given value is not
// a string representation of a float.
r.WithArg(WaitTimeoutArgKey, "foo")
v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue)
expected = defaultValue
assert.Equal(t, expected, v)
test.AssertError(t, err)
// should not be OK as given value does not
// validate the rule x >= 6.
r.WithArg(WaitTimeoutArgKey, "5.0")
v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue, rule)
expected = defaultValue
assert.Equal(t, expected, v)
test.AssertError(t, err)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestBoolArg(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue bool = true
)
var expected bool
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// empty value, result should be equal
// to the default value.
r.WithArg(LandscapeArgKey, "")
v, err := r.BoolArg(LandscapeArgKey, defaultValue)
expected = defaultValue
assert.Equal(t, expected, v)
assert.Nil(t, err)
// result should be equal to given value
// but as boolean.
r.WithArg(LandscapeArgKey, "1")
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
expected = true
assert.Equal(t, expected, v)
assert.Nil(t, err)
r.WithArg(LandscapeArgKey, "true")
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
expected = true
assert.Equal(t, expected, v)
assert.Nil(t, err)
r.WithArg(LandscapeArgKey, "0")
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
expected = false
assert.Equal(t, expected, v)
assert.Nil(t, err)
r.WithArg(LandscapeArgKey, "false")
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
expected = false
assert.Equal(t, expected, v)
assert.Nil(t, err)
// should not be OK as given value is not
// a string representation of a boolean.
r.WithArg(LandscapeArgKey, "foo")
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
expected = defaultValue
assert.Equal(t, expected, v)
test.AssertError(t, err)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestFpath(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// file exists.
fpath := test.MergeFpaths(t)[0]
f, err := os.Open(fpath)
assert.Nil(t, err)
filename := "foo.pdf"
err = r.WithFile(filename, f)
assert.Nil(t, err)
absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName))
assert.Nil(t, err)
expected := fmt.Sprintf("%s/%s", absDirPath, filename)
v, err := r.Fpath(filename)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as file does
// not exist.
_, err = r.Fpath("bar.pdf")
test.AssertError(t, err)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestFpaths(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
fpath := test.MergeFpaths(t)[0]
f, err := os.Open(fpath)
assert.Nil(t, err)
defer f.Close() // nolint: errcheck
filename := "foo.pdf"
err = r.WithFile(filename, f)
assert.Nil(t, err)
// file extension exists.
absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName))
assert.Nil(t, err)
expected := []string{
fmt.Sprintf("%s/%s", absDirPath, filename),
}
fpaths, err := r.Fpaths(".pdf")
assert.Nil(t, err)
assert.Equal(t, expected, fpaths)
// should not be OK as file extension
// does not exist.
_, err = r.Fpaths(".html")
test.AssertError(t, err)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestFcontent(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue string = "Gutenberg"
)
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
filename := "foo.txt"
// file does not exist, expecting
// value.
v, err := r.Fcontent(filename, defaultValue)
assert.Nil(t, err)
assert.Equal(t, defaultValue, v)
// file exists.
fpath := test.OfficeFpaths(t)[2]
f, err := os.Open(fpath)
assert.Nil(t, err)
defer f.Close() // nolint: errcheck
err = r.WithFile(filename, f)
assert.Nil(t, err)
v, err = r.Fcontent(filename, defaultValue)
assert.Nil(t, err)
assert.Contains(t, v, defaultValue)
// finally...
err = r.Close()
assert.Nil(t, err)
}
func TestGetters(t *testing.T) {
const resourceDirectoryName string = "foo"
logger := test.DebugLogger()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// has arg.
assert.Equal(t, false, r.HasArg(LandscapeArgKey))
r.WithArg(LandscapeArgKey, "foo")
assert.Equal(t, true, r.HasArg(LandscapeArgKey))
// directory path.
absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName))
assert.Nil(t, err)
assert.Equal(t, absDirPath, r.DirPath())
// finally...
err = r.Close()
assert.Nil(t, err)
}

View File

@@ -0,0 +1,32 @@
package xhttp
import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
)
// New returns a custom echo.Echo.
func New(config conf.Config) *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
srv.Use(contextMiddleware(config))
srv.Use(loggerMiddleware())
srv.Use(cleanupMiddleware())
srv.Use(errorMiddleware())
srv.GET(pingEndpoint, pingHandler)
srv.POST(mergeEndpoint, mergeHandler)
if config.DisableGoogleChrome() && config.DisableUnoconv() {
return srv
}
g := srv.Group(convertGroupEndpoint)
if !config.DisableGoogleChrome() {
g.POST(htmlEndpoint, htmlHandler)
g.POST(urlEndpoint, urlHandler)
g.POST(markdownEndpoint, markdownHandler)
}
if !config.DisableUnoconv() {
g.POST(officeEndpoint, officeHandler)
}
return srv
}

View File

@@ -0,0 +1,135 @@
package xhttp
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/test"
)
func TestNonExistingEndpoint(t *testing.T) {
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// "/" endpoint should return 404.
req := httptest.NewRequest(http.MethodGet, "/", nil)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
}
func TestDisableChromeEndpoints(t *testing.T) {
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 404.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// URL endpoint should return 404.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Markdown endpoint should return 404.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Office endpoint should return 200.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// finally...
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
}
func TestDisableUnoconvEndpoints(t *testing.T) {
os.Setenv(conf.DisableUnoconvEnvVar, "1")
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 200.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// URL endpoint should return 200.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Markdown endpoint should return 200.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Office endpoint should return 404.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// finally...
os.Setenv(conf.DisableUnoconvEnvVar, "0")
}
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
os.Setenv(conf.DisableUnoconvEnvVar, "1")
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 404.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// URL endpoint should return 404.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Markdown endpoint should return 404.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Office endpoint should return 404.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// finally...
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
os.Setenv(conf.DisableUnoconvEnvVar, "0")
}

View File

@@ -0,0 +1,170 @@
package chrome
import (
"context"
"os"
"os/exec"
"strings"
"syscall"
"time"
"github.com/mafredri/cdp/devtool"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
)
// Start starts Google Chrome headless in background.
func Start(logger xlog.Logger) error {
const op string = "chrome.Start"
logger.DebugOp(op, "starting new Google Chrome headless process on port 9222...")
resolver := func() error {
cmd, err := cmd(logger)
if err != nil {
return err
}
// we try to start the process.
xexec.LogBeforeExecute(logger, cmd)
if err := cmd.Start(); err != nil {
return err
}
// if the process failed to start correctly,
// we have to restart it.
if !isViable(logger) {
return restart(logger, cmd.Process)
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func cmd(logger xlog.Logger) (*exec.Cmd, error) {
const op string = "chrome.cmd"
binary := "google-chrome-stable"
args := []string{
"--no-sandbox",
"--headless",
// see https://github.com/GoogleChrome/puppeteer/issues/2410.
"--font-render-hinting=medium",
"--remote-debugging-port=9222",
"--disable-gpu",
"--disable-translate",
"--disable-extensions",
"--disable-background-networking",
"--safebrowsing-disable-auto-update",
"--disable-sync",
"--disable-default-apps",
"--hide-scrollbars",
"--metrics-recording-only",
"--mute-audio",
"--no-first-run",
}
cmd, err := xexec.Command(logger, binary, args...)
if err != nil {
return nil, xerror.New(op, err)
}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return cmd, nil
}
func kill(logger xlog.Logger, proc *os.Process) error {
const op string = "chrome.kill"
logger.DebugOp(op, "killing Google Chrome headless process using port 9222...")
resolver := func() error {
err := syscall.Kill(-proc.Pid, syscall.SIGKILL)
if err == nil {
return nil
}
if strings.Contains(err.Error(), "no such process") {
return nil
}
return err
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func restart(logger xlog.Logger, proc *os.Process) error {
const op string = "chrome.restart"
logger.DebugOp(op, "restarting Google Chrome headless process using port 9222...")
resolver := func() error {
// kill the existing process first.
if err := kill(logger, proc); err != nil {
return err
}
cmd, err := cmd(logger)
if err != nil {
return err
}
// we try to restart the process.
xexec.LogBeforeExecute(logger, cmd)
if err := cmd.Start(); err != nil {
return err
}
// if the process failed to restart correctly,
// we have to restart it again.
if !isViable(logger) {
return restart(logger, cmd.Process)
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func isViable(logger xlog.Logger) bool {
const (
op string = "chrome.isViable"
maxViabilityTests int = 20
)
viable := func() bool {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
endpoint := "http://localhost:9222"
logger.DebugfOp(
op,
"checking Google Chrome headless process viability via endpoint '%s/json/version'",
endpoint,
)
v, err := devtool.New(endpoint).Version(ctx)
if err != nil {
logger.DebugfOp(
op,
"Google Chrome headless is not viable as endpoint returned '%v'",
err.Error(),
)
return false
}
logger.DebugfOp(
op,
"Google Chrome headless is viable as endpoint returned '%v'",
v,
)
return true
}
result := false
for i := 0; i < maxViabilityTests && !result; i++ {
warmup(logger)
result = viable()
}
return result
}
func warmup(logger xlog.Logger) {
const op string = "chrome.warmup"
warmupTime := xtime.Duration(0.5)
logger.DebugfOp(
op,
"waiting '%v' for allowing Google Chrome to warmup",
warmupTime,
)
time.Sleep(warmupTime)
}

View File

@@ -0,0 +1,3 @@
// Package chrome helps starting
// Google Chrome headless in background.
package chrome

255
internal/pkg/conf/conf.go Normal file
View File

@@ -0,0 +1,255 @@
package conf
import (
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
const (
// MaximumWaitTimeoutEnvVar contains the name
// of the environment variable "MAXIMUM_WAIT_TIMEOUT".
MaximumWaitTimeoutEnvVar string = "MAXIMUM_WAIT_TIMEOUT"
// MaximumWaitDelayEnvVar contains the name
// of the environment variable "MAXIMUM_WAIT_DELAY".
MaximumWaitDelayEnvVar string = "MAXIMUM_WAIT_DELAY"
// MaximumWebhookURLTimeoutEnvVar contains the name
// of the environment variable "MAXIMUM_WEBHOOK_URL_TIMEOUT".
MaximumWebhookURLTimeoutEnvVar string = "MAXIMUM_WEBHOOK_URL_TIMEOUT"
// DefaultWaitTimeoutEnvVar contains the name
// of the environment variable "DEFAULT_WAIT_TIMEOUT".
DefaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_TIMEOUT"
// DefaultWebhookURLTimeoutEnvVar contains the name
// of the environment variable "DEFAULT_WEBHOOK_URL_TIMEOUT".
DefaultWebhookURLTimeoutEnvVar string = "DEFAULT_WEBHOOK_URL_TIMEOUT"
// DefaultListenPortEnvVar contains the name
// of the environment variable "DEFAULT_LISTEN_PORT".
DefaultListenPortEnvVar string = "DEFAULT_LISTEN_PORT"
// DisableGoogleChromeEnvVar contains the name
// of the environment variable "DISABLE_GOOGLE_CHROME".
DisableGoogleChromeEnvVar string = "DISABLE_GOOGLE_CHROME"
// DisableUnoconvEnvVar contains the name
// of the environment variable "DISABLE_UNOCONV".
DisableUnoconvEnvVar string = "DISABLE_UNOCONV"
// LogLevelEnvVar contains the name
// of the environment variable "LOG_LEVEL".
LogLevelEnvVar string = "LOG_LEVEL"
// DefaultGoogleChromeRpccBufferSizeEnvVar contains the name
// of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE".
DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE"
)
// Config contains the application
// configuration.
type Config struct {
maximumWaitTimeout float64
maximumWaitDelay float64
maximumWebhookURLTimeout float64
defaultWaitTimeout float64
defaultWebhookURLTimeout float64
defaultListenPort int64
disableGoogleChrome bool
disableUnoconv bool
logLevel xlog.Level
maximumGoogleChromeRpccBufferSize int64
defaultGoogleChromeRpccBufferSize int64
}
// DefaultConfig returns the default
// configuration.
func DefaultConfig() Config {
return Config{
maximumWaitTimeout: 30.0,
maximumWaitDelay: 10.0,
maximumWebhookURLTimeout: 30.0,
defaultWaitTimeout: 10.0,
defaultWebhookURLTimeout: 10.0,
defaultListenPort: 3000,
disableGoogleChrome: false,
disableUnoconv: false,
logLevel: xlog.InfoLevel,
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
}
}
/*
FromEnv returns a Conf according
to environment variables.
*/
func FromEnv() (Config, error) {
const op string = "conf.FromEnv"
resolver := func() (Config, error) {
c := DefaultConfig()
maximumWaitTimeout, err := xassert.Float64FromEnv(
MaximumWaitTimeoutEnvVar,
c.maximumWaitTimeout,
xassert.Float64NotInferiorTo(0.0),
)
c.maximumWaitTimeout = maximumWaitTimeout
if err != nil {
return c, err
}
maximumWaitDelay, err := xassert.Float64FromEnv(
MaximumWaitDelayEnvVar,
c.maximumWaitDelay,
xassert.Float64NotInferiorTo(0.0),
)
c.maximumWaitDelay = maximumWaitDelay
if err != nil {
return c, err
}
maximumWebhookURLTimeout, err := xassert.Float64FromEnv(
MaximumWebhookURLTimeoutEnvVar,
c.maximumWebhookURLTimeout,
xassert.Float64NotInferiorTo(0.0),
)
c.maximumWebhookURLTimeout = maximumWebhookURLTimeout
if err != nil {
return c, err
}
defaultWaitTimeout, err := xassert.Float64FromEnv(
DefaultWaitTimeoutEnvVar,
c.defaultWaitTimeout,
xassert.Float64NotInferiorTo(0.0),
xassert.Float64NotSuperiorTo(c.maximumWaitTimeout),
)
c.defaultWaitTimeout = defaultWaitTimeout
if err != nil {
return c, err
}
defaultWebhookURLTimeout, err := xassert.Float64FromEnv(
DefaultWebhookURLTimeoutEnvVar,
c.defaultWebhookURLTimeout,
xassert.Float64NotInferiorTo(0.0),
xassert.Float64NotSuperiorTo(c.defaultWebhookURLTimeout),
)
c.defaultWebhookURLTimeout = defaultWebhookURLTimeout
if err != nil {
return c, err
}
defaultListenPort, err := xassert.Int64FromEnv(
DefaultListenPortEnvVar,
c.defaultListenPort,
xassert.Int64NotInferiorTo(0),
xassert.Int64NotSuperiorTo(65535),
)
c.defaultListenPort = defaultListenPort
if err != nil {
return c, err
}
disableGoogleChrome, err := xassert.BoolFromEnv(
DisableGoogleChromeEnvVar,
c.disableGoogleChrome,
)
c.disableGoogleChrome = disableGoogleChrome
if err != nil {
return c, err
}
disableUnoconv, err := xassert.BoolFromEnv(
DisableUnoconvEnvVar,
c.disableUnoconv,
)
c.disableUnoconv = disableUnoconv
if err != nil {
return c, err
}
logLevel, err := xassert.StringFromEnv(
LogLevelEnvVar,
string(c.logLevel),
xassert.StringOneOf(xlog.Levels()),
)
c.logLevel = xlog.MustParseLevel(logLevel)
if err != nil {
return c, err
}
defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv(
DefaultGoogleChromeRpccBufferSizeEnvVar,
c.defaultGoogleChromeRpccBufferSize,
xassert.Int64NotInferiorTo(0),
xassert.Int64NotSuperiorTo(c.MaximumGoogleChromeRpccBufferSize()),
)
c.defaultGoogleChromeRpccBufferSize = defaultGoogleChromeRpccBufferSize
if err != nil {
return c, err
}
return c, nil
}
result, err := resolver()
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
// MaximumWaitTimeout returns the maximum
// wait timeout from the configuration.
func (c Config) MaximumWaitTimeout() float64 {
return c.maximumWaitTimeout
}
// MaximumWaitDelay returns the maximum
// wait timeout from the configuration.
func (c Config) MaximumWaitDelay() float64 {
return c.maximumWaitDelay
}
// MaximumWebhookURLTimeout returns the maximum
// webhook URL wait timeout from the configuration.
func (c Config) MaximumWebhookURLTimeout() float64 {
return c.maximumWebhookURLTimeout
}
// DefaultWaitTimeout returns the default
// wait timeout from the configuration.
func (c Config) DefaultWaitTimeout() float64 {
return c.defaultWaitTimeout
}
// DefaultWebhookURLTimeout returns the default
// webhook URL wait timeout from the configuration.
func (c Config) DefaultWebhookURLTimeout() float64 {
return c.defaultWebhookURLTimeout
}
// DefaultListenPort returns the default
// listen port from the configuration.
func (c Config) DefaultListenPort() int64 {
return c.defaultListenPort
}
/*
DisableGoogleChrome returns true if
Google Chrome is disabled in the
configuration.
*/
func (c Config) DisableGoogleChrome() bool {
return c.disableGoogleChrome
}
/*
DisableUnoconv returns true if
Unoconv is disabled in the
configuration.
*/
func (c Config) DisableUnoconv() bool {
return c.disableUnoconv
}
// LogLevel returns the xlog.Level from
// the configuration.
func (c Config) LogLevel() xlog.Level {
return c.logLevel
}
// MaximumGoogleChromeRpccBufferSize returns the maximum
// Google Chrome rpcc buffer size from the configuration.
func (c Config) MaximumGoogleChromeRpccBufferSize() int64 {
return c.maximumGoogleChromeRpccBufferSize
}
// DefaultGoogleChromeRpccBufferSize returns the default
// Google Chrome rpcc buffer size from the configuration.
func (c Config) DefaultGoogleChromeRpccBufferSize() int64 {
return c.defaultGoogleChromeRpccBufferSize
}

View File

@@ -0,0 +1,373 @@
package conf
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/test"
)
func TestEmptyFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// no environment variables set,
// values should be equal to default config.
expected = DefaultConfig()
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
}
func TestMaximumWaitTimeoutFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// MAXIMUM_WAIT_TIMEOUT correctly set.
os.Setenv(MaximumWaitTimeoutEnvVar, "10.0")
expected = DefaultConfig()
expected.maximumWaitTimeout = 10.0
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWaitTimeoutEnvVar)
// MAXIMUM_WAIT_TIMEOUT wrongly set.
os.Setenv(MaximumWaitTimeoutEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWaitTimeoutEnvVar)
// MAXIMUM_WAIT_TIMEOUT < 0.
os.Setenv(MaximumWaitTimeoutEnvVar, "-1.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWaitTimeoutEnvVar)
}
func TestMaximumWaitDelayFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// MAXIMUM_WAIT_DELAY correctly set.
os.Setenv(MaximumWaitDelayEnvVar, "10.0")
expected = DefaultConfig()
expected.maximumWaitDelay = 10.0
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWaitDelayEnvVar)
// MAXIMUM_WAIT_DELAY wrongly set.
os.Setenv(MaximumWaitDelayEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWaitDelayEnvVar)
// MAXIMUM_WAIT_DELAY < 0.
os.Setenv(MaximumWaitDelayEnvVar, "-1.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWaitDelayEnvVar)
}
func TestMaximumWebhookURLTimeoutFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// MAXIMUM_WEBHOOK_URL_TIMEOUT correctly set.
os.Setenv(MaximumWebhookURLTimeoutEnvVar, "10.0")
expected = DefaultConfig()
expected.maximumWebhookURLTimeout = 10.0
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWebhookURLTimeoutEnvVar)
// MAXIMUM_WEBHOOK_URL_TIMEOUT wrongly set.
os.Setenv(MaximumWebhookURLTimeoutEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWebhookURLTimeoutEnvVar)
// MAXIMUM_WEBHOOK_URL_TIMEOUT < 0.
os.Setenv(MaximumWebhookURLTimeoutEnvVar, "-1.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(MaximumWebhookURLTimeoutEnvVar)
}
func TestDefaultWaitTimeoutFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// DEFAULT_WAIT_TIMEOUT correctly set.
os.Setenv(DefaultWaitTimeoutEnvVar, "10.0")
expected = DefaultConfig()
expected.defaultWaitTimeout = 10.0
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWaitTimeoutEnvVar)
// DEFAULT_WAIT_TIMEOUT wrongly set.
os.Setenv(DefaultWaitTimeoutEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWaitTimeoutEnvVar)
// DEFAULT_WAIT_TIMEOUT < 0.
os.Setenv(DefaultWaitTimeoutEnvVar, "-1.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWaitTimeoutEnvVar)
// DEFAULT_WAIT_TIMEOUT > MAXIMUM_WAIT_TIMEOUT.
os.Setenv(DefaultWaitTimeoutEnvVar, "40.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWaitTimeoutEnvVar)
}
func TestDefaultWebhookURLTimeoutFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// DEFAULT_WEBHOOK_URL_TIMEOUT correctly set.
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "10.0")
expected = DefaultConfig()
expected.defaultWebhookURLTimeout = 10.0
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
// DEFAULT_WEBHOOK_URL_TIMEOUT wrongly set.
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
// DEFAULT_WEBHOOK_URL_TIMEOUT < 0.
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "-1.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
// DEFAULT_WEBHOOK_URL_TIMEOUT > MAXIMUM_WEBHOOK_URL_TIMEOUT.
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "40.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
}
func TestDefaultListenPortFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// DEFAULT_LISTEN_PORT correctly set.
os.Setenv(DefaultListenPortEnvVar, "80")
expected = DefaultConfig()
expected.defaultListenPort = 80
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultListenPortEnvVar)
// DEFAULT_LISTEN_PORT wrongly set.
os.Setenv(DefaultListenPortEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultListenPortEnvVar)
// DEFAULT_LISTEN_PORT < 0.
os.Setenv(DefaultListenPortEnvVar, "-1.0")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultListenPortEnvVar)
// DEFAULT_LISTEN_PORT > 65535.
os.Setenv(DefaultListenPortEnvVar, "65536")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultListenPortEnvVar)
}
func TestDisableGoogleChromeFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// DISABLE_GOOGLE_CHROME correctly set.
os.Setenv(DisableGoogleChromeEnvVar, "1")
expected = DefaultConfig()
expected.disableGoogleChrome = true
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DisableGoogleChromeEnvVar)
os.Setenv(DisableGoogleChromeEnvVar, "0")
expected = DefaultConfig()
expected.disableGoogleChrome = false
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DisableGoogleChromeEnvVar)
// DISABLE_GOOGLE_CHROME wrongly set.
os.Setenv(DisableGoogleChromeEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DisableGoogleChromeEnvVar)
}
func TestDisableUnoconvFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// DISABLE_UNOCONV correctly set.
os.Setenv(DisableUnoconvEnvVar, "1")
expected = DefaultConfig()
expected.disableUnoconv = true
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DisableUnoconvEnvVar)
os.Setenv(DisableUnoconvEnvVar, "0")
expected = DefaultConfig()
expected.disableUnoconv = false
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DisableUnoconvEnvVar)
// DISABLE_UNOCONV wrongly set.
os.Setenv(DisableUnoconvEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DisableUnoconvEnvVar)
}
func TestLogLevelFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// LOG_LEVEL correctly set.
os.Setenv(LogLevelEnvVar, "DEBUG")
expected = DefaultConfig()
expected.logLevel = xlog.DebugLevel
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(LogLevelEnvVar)
os.Setenv(LogLevelEnvVar, "INFO")
expected = DefaultConfig()
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(LogLevelEnvVar)
os.Setenv(LogLevelEnvVar, "ERROR")
expected = DefaultConfig()
expected.logLevel = xlog.ErrorLevel
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(LogLevelEnvVar)
// LOG_LEVEL wrongly set.
os.Setenv(LogLevelEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(LogLevelEnvVar)
}
func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE correctly set.
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "100")
expected = DefaultConfig()
expected.defaultGoogleChromeRpccBufferSize = 100
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE wrongly set.
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE < 0.
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "-1")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE > 100 MB (maximumGoogleChromeRpccBufferSize).
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "104857601")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
}
func TestGetters(t *testing.T) {
result := DefaultConfig()
assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout())
assert.Equal(t, result.maximumWaitDelay, result.MaximumWaitDelay())
assert.Equal(t, result.maximumWebhookURLTimeout, result.MaximumWebhookURLTimeout())
assert.Equal(t, result.defaultWaitTimeout, result.DefaultWaitTimeout())
assert.Equal(t, result.defaultWebhookURLTimeout, result.DefaultWebhookURLTimeout())
assert.Equal(t, result.defaultListenPort, result.DefaultListenPort())
assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome())
assert.Equal(t, result.disableUnoconv, result.DisableUnoconv())
assert.Equal(t, result.logLevel, result.LogLevel())
assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize())
assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize())
}

3
internal/pkg/conf/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Package conf gathers all
// configuration data.
package conf

View File

@@ -0,0 +1,9 @@
/*
Package normalize helps removing special
characters from a string.
Fixes: https://github.com/thecodingmachine/gotenberg/issues/104
Credits: https://medium.com/@swdream/golang-remove-all-accents-in-string-319abf6a7f5b
*/
package normalize

View File

@@ -0,0 +1,21 @@
package normalize
import (
"unicode"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
// String normalizes given string.
func String(str string) (string, error) {
const op string = "normalize.String"
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
result, _, err := transform.String(t, str)
if err != nil {
return "", xerror.New(op, err)
}
return result, nil
}

View File

@@ -0,0 +1,17 @@
package normalize
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestString(t *testing.T) {
const (
toNormalize string = "analise da aplicação"
expected string = "analise da aplicacao"
)
v, err := String(toNormalize)
assert.Nil(t, err)
assert.Equal(t, expected, v)
}

View File

@@ -1,5 +0,0 @@
/*
Package notify is used across the application
to display nice outputs to the user.
*/
package notify

View File

@@ -1,30 +0,0 @@
package notify
import (
"fmt"
"os"
"github.com/labstack/gommon/color"
)
var (
stdout *color.Color
stderr *color.Color
)
func init() {
stdout = color.New()
stdout.SetOutput(os.Stdout)
stderr = color.New()
stderr.SetOutput(os.Stderr)
}
// Println prints a message to stdout.
func Println(message string) {
stdout.Printf("⇨ %s\n", message)
}
// ErrPrintln prints an error to stderr.
func ErrPrintln(err error) {
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
}

View File

@@ -1,71 +0,0 @@
package pm2
import (
"context"
"fmt"
"time"
"github.com/mafredri/cdp/devtool"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
)
// Chrome facilitates starting or shutting down
// Chrome headless with PM2.
type Chrome struct{}
// Launch starts Chrome headless with PM2.
func (c *Chrome) Launch() error {
return launch(c)
}
// Shutdown stops Chrome headless and
// removes it from the list of PM2
// processes.
func (c *Chrome) Shutdown() error {
return shutdown(c)
}
func (c *Chrome) getArgs() []string {
return []string{
"--no-sandbox",
"--headless",
"--remote-debugging-port=9222",
"--disable-gpu",
"--disable-translate",
"--disable-extensions",
"--disable-background-networking",
"--safebrowsing-disable-auto-update",
"--disable-sync",
"--disable-default-apps",
"--hide-scrollbars",
"--metrics-recording-only",
"--mute-audio",
"--no-first-run",
}
}
func (c *Chrome) getName() string {
return "google-chrome-stable"
}
func (c *Chrome) getFullname() string {
return "Chrome headless"
}
func (c *Chrome) isViable() bool {
// check if Chrome is correctly running.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := devtool.New("http://localhost:9222").Version(ctx)
return err == nil
}
func (c *Chrome) warmup() {
notify.Println(fmt.Sprintf("warming-up %s", c.getFullname()))
time.Sleep(5 * time.Second)
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Process(new(Chrome))
)

View File

@@ -1,19 +0,0 @@
package pm2
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestChromeLaunch(t *testing.T) {
p := &Chrome{}
err := p.Launch()
require.Nil(t, err)
}
func TestChromeShutdown(t *testing.T) {
p := &Chrome{}
err := p.Shutdown()
require.Nil(t, err)
}

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