Compare commits

...

138 Commits

Author SHA1 Message Date
Julien Neuhart
b76990ec16 feat(chromium): new flags chromium-allow-insecure-localhost and chromium-host-resolver-rules 2022-09-01 18:04:00 +02:00
Julien Neuhart
9bc93c5cf5 chore: remove ioutil package 2022-09-01 18:04:00 +02:00
Julien Neuhart
1d8ae79f84 feat(chromium): add ReturnAsStream support 2022-09-01 18:04:00 +02:00
Julien Neuhart
4269aa2422 chore: update Go dependencies 2022-09-01 18:04:00 +02:00
Julien Neuhart
c887dc1ec8 chore: upgrade golangci-lint to version from version 1.48 to 1.49 2022-09-01 18:04:00 +02:00
Julien Neuhart
8bbacd691d chore: update Golang versionin go.mod file 2022-08-19 15:24:09 +02:00
Julien Neuhart
d8f3f85fb7 fix: replace depracted io/ioutil package with os package 2022-08-19 15:01:31 +02:00
Julien Neuhart
159c0b3350 fix: Golang Docker image for tests version 2022-08-19 14:52:36 +02:00
Julien Neuhart
45788f3235 chore: upgrade Golang dependencies 2022-08-19 14:11:36 +02:00
Julien Neuhart
4c85c6ccdf chore: upgrade from Golang 1.18 to 1.19, golangci-lint from 1.46 to 1.48 2022-08-19 14:03:32 +02:00
Julien Neuhart
08408ae005 chore: update Go dependencies 2022-06-07 15:08:04 +02:00
Julien Neuhart
d470279fb7 chore: update golangci-lint version 2022-06-07 14:43:33 +02:00
Julien Neuhart
8260458f8f Revert "fix(chromium): regex only accepts tmp folder by default (#429)"
This reverts commit 27018fa8e2.
2022-03-22 10:48:01 +01:00
Julien Neuhart
0512dd1e86 fix(tests): add -buildvcs=false as a temporary fix for go 1.18 bug (see https://github.com/golang/go/issues/51723) 2022-03-21 17:13:35 +01:00
Julien Neuhart
4972019cb9 chore: update golang-ci-lint 2022-03-21 16:46:46 +01:00
Julien Neuhart
5e615cab52 chore: upgrade go to 1.18 and update Go dependencies 2022-03-21 16:46:15 +01:00
Julien Neuhart
64ea2b6d3b fix(libreoffice): start the long-running listener on the first conversion (fixes #420) 2022-03-21 16:39:52 +01:00
Gabor Szabad
27018fa8e2 fix(chromium): regex only accepts tmp folder by default (#429) 2022-02-25 13:52:27 +01:00
Julien Neuhart
263d634801 chore: rename MockContext to ContextMock and move it to a dedicated go file 2022-02-09 16:09:48 +01:00
Julien Neuhart
c61c33f71c chore: update LICENSE 2022-02-09 10:41:50 +01:00
Julien Neuhart
bfb30b8fed chore: remove Go Report Card from README (not working) 2022-02-08 19:46:53 +01:00
Julien Neuhart
ca3e27937e chore(uno): update uno-listener-restart-threshold flag description 2022-02-08 19:46:31 +01:00
Julien Neuhart
50c34a4186 fix(uno): repare not working test 2022-02-08 19:45:39 +01:00
Julien Neuhart
84875aaf82 fix(chromium): remove local files from extra ling tags & extra script tags (closes #418) 2022-02-08 19:45:10 +01:00
Julien Neuhart
b7a8b95820 fix(uno): close tcp connexions to the LibreOffice listener, better health check that take the restart into account 2022-02-08 16:12:41 +01:00
Julien Neuhart
5cad5ca11a fix: PDFtk new binary URL 2022-02-08 11:33:55 +01:00
Julien Neuhart
7598de29dd chore: upgrade golangci-lint ot v1.44.0 2022-02-08 11:33:55 +01:00
Julien Neuhart
af6ffe67c8 chore: upgrade PDFtk to v3.3.2 2022-02-08 11:33:55 +01:00
Julien Neuhart
6944c4b3bf chore: upgrade Noto Color Emoji font to v2.034 2022-02-08 11:33:55 +01:00
Julien Neuhart
24817d5707 feat: add more PDF formats, drastically improve LibreOffice long-running instance management 2022-02-08 11:33:55 +01:00
Julien Neuhart
3d855712ee chore: update Go dependencies 2022-02-08 11:33:55 +01:00
Julien Neuhart
872263d253 feat: switch from top to htop 2022-01-05 15:46:20 +01:00
Julien Neuhart
1c491fb1c5 fix: use bullseye-backports distribution instead of sid for installing LibreOffice (fix #403) 2022-01-05 14:59:38 +01:00
Julien Neuhart
29374c579a chore: update Go dependencies 2021-12-30 17:45:16 +01:00
Julien Neuhart
206d7b6ca9 fix(chromium): typo in godoc <link> -> <script> 2021-12-27 17:49:06 +01:00
Julien Neuhart
ab6ca8c894 chore(README): improve Gotenberg description 2021-12-27 17:34:10 +01:00
Kaijia Feng
e7f5fde051 feat: add Noto CJK fonts 2021-12-27 17:21:20 +01:00
Julien Neuhart
030b358b0e fix: rework api and webhook module timeout properties (fixes #396) 2021-12-17 19:13:14 +01:00
Julien Neuhart
4b1791d35c fix(release): add noto-emoji arg 2021-12-16 17:49:48 +01:00
Julien Neuhart
b788cf823d chore: update Go dependencies 2021-12-08 17:22:24 +01:00
Julien Neuhart
11cbc0288f chore: update Go dependencies 2021-12-08 17:17:40 +01:00
Julien Neuhart
2269c80b02 feat: start a unoconv listener by default, but allow stateless mode as before with the --unoconv-disable-listener flag. Also improve the shutdown process 2021-12-08 17:17:40 +01:00
Julien Neuhart
c922cc042e chore: improve some tests 2021-12-08 17:17:40 +01:00
Julien Neuhart
9cb6f44f72 feat: improve prometheus startup message 2021-12-08 17:17:40 +01:00
Julien Neuhart
f376cd5ab1 feat: improve 'logger' field from logger 2021-12-08 15:41:08 +01:00
Julien Neuhart
8b8a02b68c fix(qpdf): typo in godoc 2021-12-08 14:39:20 +01:00
Rodrigo Delduca
5759b1336a feat: add qpdf engine (#380)
Co-authored-by: Andrei Regiani <andrei.cpp@gmail.com>
2021-12-07 19:48:41 +01:00
Julien Neuhart
76f17cb6d7 fix(pdfengines): selection of PDF engines and add a new module interface for modules that want to print system message on startup 2021-12-07 17:20:25 +01:00
Julien Neuhart
7149c421cf fix(pdfengines): improve --pdfengines-engines flag description 2021-11-28 17:05:25 +01:00
Julien Neuhart
353c599767 chore: align <p> tags from README 2021-11-28 15:49:49 +01:00
Julien Neuhart
285c43dab7 fix(chromium): form field 'failOnConsoleException' does not work if JavaScript is disabled 2021-11-24 17:33:55 +01:00
Julien Neuhart
bd5933f4c2 chore: upgrade golangci-lint to 1.43.0 2021-11-24 17:33:55 +01:00
Julien Neuhart
0faac08237 chore(unoconv): improve code coverage 2021-11-24 17:33:55 +01:00
Julien Neuhart
14f94b1566 feat(chromium): add validation of 'emulatedMediaType' form field at the handler level 2021-11-24 17:33:55 +01:00
Julien Neuhart
415e2ff265 feat(chromium): add 'extraLinkTags' and 'extraScriptTags' form fields, add auto-detection of files for URL endpoint, disable some functionalities if JavaScript is disabled 2021-11-24 17:33:55 +01:00
Julien Neuhart
26e97358d1 chore: update Go dependencies 2021-11-24 17:33:55 +01:00
Julien Neuhart
0ed6fb33c5 feat(libreoffice): add high verbosity if debug log level 2021-11-24 17:33:55 +01:00
Julien Neuhart
df98e7512b feat(chromium): add new form field 'failOnConsoleExceptions' (fixes #262) 2021-11-24 17:33:55 +01:00
Julien Neuhart
99ede6477f fix(api): typo an handler -> a handler 2021-11-24 17:33:55 +01:00
Julien Neuhart
ee0debe14a feat(chromium): add --chromium-disable-javascript property (fixes #175) 2021-11-24 17:33:55 +01:00
Julien Neuhart
dd1e11f102 feat(chromium): add emulated media type 2021-11-24 17:33:55 +01:00
Julien Neuhart
ed47493459 feat(chromium): add 'waitForExpression' form field, deprecate 'waitWindowStatus' form field 2021-11-24 17:33:55 +01:00
Julien Neuhart
5b8391067d chore: reorganize useAgent form field 2021-11-24 17:33:55 +01:00
Julien Neuhart
4f41a59ceb chore: update Go dependencies 2021-11-24 17:33:55 +01:00
Julien Neuhart
4525cb38e4 feat(chromium): deprecate --chromium-user-agent property, add userAgent form field 2021-11-24 17:33:55 +01:00
Julien Neuhart
704dfc25e6 feat(chromium): add --chromium-disable-web-security property 2021-11-24 17:33:55 +01:00
Julien Neuhart
7022ebe4ca feat: add module property --chromium-proxy-server 2021-11-12 17:36:15 +01:00
Julien Neuhart
094863f0ea chore: update Go dependencies 2021-11-12 17:36:15 +01:00
Julien Neuhart
a6282125cf chore: update Go dependencies 2021-11-02 17:38:26 +01:00
Julien Neuhart
63387a09af chore: update Go dependencies 2021-10-22 15:48:37 +02:00
Julien Neuhart
d4c47cecf2 feat: add metrics system, move webhook feature to dedicated module (#372) 2021-10-19 18:35:24 +02:00
Oliver Grof
b839069af1 feat: add OpenAPI specification for API v7 (#362) 2021-10-15 11:30:32 +02:00
Julien Neuhart
e3c98100cc chore: update the README introduction 2021-09-27 18:47:26 +02:00
Killian Meersman
afcce04650 feat: add HTTP2 (h2c) support (#366) 2021-09-27 12:42:36 +02:00
Julien Neuhart
5fb35c2d48 fix: chromium exact colors (#361) 2021-09-21 18:51:52 +02:00
Julien Neuhart
dfe89f0d83 fix: add gotenberg flag for chromium flag --allow-file-access-from-files (#360) 2021-09-21 18:39:13 +02:00
Julien Neuhart
c500ec071e feat: support armhf & i386 architectures (#355) 2021-09-21 18:38:11 +02:00
Julien Neuhart
6e47f16fe1 fix: filename case (#351) 2021-09-15 17:41:03 +02:00
Julien Neuhart
13e6c2dbeb fix: add latest version of noto color emoji front (#345) 2021-09-09 20:08:07 +02:00
remoteexception
9d27dfb41c fix: add more supported file extensions for unovonv (#340)
* Add supported file extensions for unovonv

* Fix expected file extensions length

Co-authored-by: Jana Storch <jana.storch@dpdhl.com>
2021-09-09 20:07:45 +02:00
remoteexception
9529905875 fix: only use webhook ports between 1025 and 65536 (#346)
- ports between 0 and 1024 can only be used by root in some OS

Co-authored-by: Jana Storch <jana.storch@dpdhl.com>
2021-09-09 20:06:49 +02:00
Snyk bot
eef7fc4b51 fix: build/Dockerfile to reduce vulnerabilities (#336)
The
2021-09-03 22:25:54 +02:00
Julien Neuhart
a88435f6b6 feat: add Live Demo link in the README 2021-08-29 14:38:10 +02:00
Julien Neuhart
0b22598783 fix: typo in api module godoc -> remove pointer for middleware 2021-08-28 17:59:18 +02:00
Julien Neuhart
40cc019a51 fix: typo Docker Registry -> Docker Repository 2021-08-27 19:14:17 +02:00
Julien Neuhart
781094ab05 fix: typo arch64 -> amd64 2021-08-27 18:50:06 +02:00
Julien Neuhart
21cfe2e535 Merge branch 'main' of github.com:gotenberg/gotenberg 2021-08-27 18:48:08 +02:00
Julien Neuhart
4f72684080 chore: upgrade LibreOffice to version 7.2 (#335) 2021-08-27 18:47:48 +02:00
Julien Neuhart
5ac514366e Merge branch 'main' of github.com:gotenberg/gotenberg 2021-08-27 14:49:56 +02:00
Julien Neuhart
d5d6285eef fix: use google chrome stable on amd64 (#333) 2021-08-27 14:48:09 +02:00
Julien Neuhart
0e34b41bce Merge branch 'main' of github.com:gotenberg/gotenberg 2021-08-27 13:48:22 +02:00
Julien Neuhart
e157066bcc chore: upgrade Go version to 1.17 (#334) 2021-08-27 13:46:44 +02:00
Julien Neuhart
f43a63ac6e chore: upgrade Go version to 1.17 2021-08-27 12:54:07 +02:00
Julien Neuhart
a5828fa01a fix: add --disable-gpu flag to Chromium / chore: upgrade base image to bullseye (#331) 2021-08-27 09:41:59 +02:00
Julien Neuhart
662d3ff795 chore: use echo.WrapHandler for health route 2021-08-25 17:12:54 +02:00
Julien Neuhart
a97f05db3b chore: add markdown table sample & test 2021-08-24 19:24:16 +02:00
Julien Neuhart
5a439ca70c fix: remove internal package and move it to cmd (#321) 2021-08-23 18:33:26 +02:00
Julien Neuhart
551faca3a3 fix: wrong version number in startup message & version LABEL 2021-08-23 12:10:34 +02:00
Julien Neuhart
c25ffe9639 feat: add stale bot 2021-08-22 17:48:34 +02:00
Julien Neuhart
900021ec1b fix: typo in test error 2021-08-22 17:24:20 +02:00
Julien Neuhart
cc1ad16688 feat: add version 7.0.0 - #319 2021-08-22 17:13:46 +02:00
Julien Neuhart
fabac9fcd9 Delete CNAME 2021-08-22 17:09:56 +02:00
Julien Neuhart
da170b9b16 feat: Chromium underlying requests are not tested againt the allow/deny lists 2021-08-22 16:35:04 +02:00
Julien Neuhart
0e417430b8 chore: update issues URLs to new repository 2021-08-22 13:27:40 +02:00
Julien Neuhart
0cfac31bca fix: add missing go.sum file 2021-08-22 13:17:13 +02:00
Julien Neuhart
0f5e8fd314 feat: add 7.x source code 2021-08-22 12:52:44 +02:00
Julien Neuhart
e457155950 chore: remove 6.x source code 2021-08-22 12:50:17 +02:00
Julien Neuhart
5a98790dd3 Update Google Analytics 2021-07-25 15:27:55 +02:00
Julien Neuhart
e188f65e7c Update README link to documentation 2021-07-23 18:27:23 +02:00
Julien Neuhart
6bf3ea3c46 Create CNAME 2021-07-23 18:13:17 +02:00
Julien Neuhart
fa33ac57fb Merge pull request #314 from AlexBondor/master
Add tags on openapi.yaml endpoints
2021-07-15 09:20:11 +02:00
Alexandru Bondor
0bed9eea09 Update openapi.yaml
Fix tags
2021-07-09 21:02:39 +03:00
Alexandru Bondor
36e9f7a8cb Update openapi.yaml
Add tags on endpoints
2021-07-09 21:00:34 +03:00
Julien Neuhart
b155dbef6a Merge pull request #312 from j2L4e/patch-1
Dockerfile: Use https everywhere
2021-07-02 14:43:21 +02:00
Jan Lohage
e48b428725 Dockerfile: Use https everywhere 2021-07-02 13:36:41 +02:00
Julien Neuhart
68884c9126 Merge pull request #305 from thecodingmachine/dependabot/add-v2-config-file
Upgrade to GitHub-native Dependabot
2021-05-14 12:02:51 +02:00
Julien Neuhart
a713976f9c Merge branch 'master' into dependabot/add-v2-config-file 2021-05-14 11:54:49 +02:00
Julien Neuhart
51d7306380 Merge pull request #308 from thecodingmachine/dependencies-update
Update golang dependencies
2021-05-14 11:50:50 +02:00
Julien Neuhart
0b83f75ec8 Update golang dependencies 2021-05-14 11:41:40 +02:00
dependabot-preview[bot]
0a80635943 Upgrade to GitHub-native Dependabot 2021-04-29 21:11:47 +00:00
Julien Neuhart
e6a5f4b203 Merge pull request #303 from thecodingmachine/missing-fonts
Add missing fonts from 6.4.1
2021-04-15 09:50:36 +02:00
Julien Neuhart
061b2fdbcb Remove font duplicate 2021-04-15 09:37:57 +02:00
Julien Neuhart
7922e32618 Add missing fonts from 6.4.1 2021-04-15 09:34:12 +02:00
Julien Neuhart
0949d10da6 Merge pull request #296 from bendavies/reduce-docker-image-size
Reduce the docker image size
2021-03-31 10:53:29 +02:00
Ben Davies
0c8ad9262e use only curl 2021-03-30 20:30:43 +01:00
Ben Davies
f841aef82d install ttf-mscorefonts-installer 3.8 2021-03-30 20:29:55 +01:00
Ben Davies
d42e51315d Delete the apt-get lists after installing something.
https://github.com/hadolint/hadolint/wiki/DL3009
2021-03-30 18:00:10 +01:00
Ben Davies
e761b73cfd Avoid additional packages by specifying --no-install-recommends.
https://github.com/hadolint/hadolint/wiki/DL3015
2021-03-30 18:00:06 +01:00
Julien Neuhart
1a9c9a7767 Merge pull request #273 from owsy/pdftk
Upgrade PDFtk
2021-01-07 18:05:36 +01:00
Rhodri Pugh
eb7ce7cef1 use pdftk 3.2.2 2021-01-07 16:17:24 +00:00
Julien Neuhart
68fd8dcd76 Merge pull request #274 from thecodingmachine/github-actions
GitHub Actions
2021-01-07 16:31:54 +01:00
Julien Neuhart
ce8bb2567c Fix typo in publish script 2021-01-07 16:19:21 +01:00
Julien Neuhart
ab3d9bc120 Remove TravisCI, add GitHub Actions 2021-01-07 16:18:48 +01:00
Julien Neuhart
432ec161c1 Add warning in documentation about public facing Gotenberg instances 2020-12-30 15:47:54 +01:00
Julien Neuhart
fcaf4a9b51 Merge pull request #260 from karakanb/add-openapi-docs
Add the first version of the OpenAPI documentation
2020-11-30 15:04:06 +01:00
Burak Karakan
cbb7e6599b Add OpenAPI docs to the links section 2020-11-22 19:59:41 +01:00
Burak Karakan
8f89a144ca Add the first version of the OpenAPI documentation 2020-11-22 19:58:34 +01:00
Julien Neuhart
50bd05b351 Update 15-links.md 2020-11-18 17:30:38 +01:00
298 changed files with 70586 additions and 14475 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
.git

View File

@@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neuhart.julien@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
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/

View File

@@ -1,39 +0,0 @@
# Contributing
Hi! Thank you for considering contributing to Gotenberg. You'll
find below useful information about how to contribute to the Gotenberg project.
## Contributing code
### Install from sources
1. Install and run the latest version of Docker
2. Verify your Go version (>= 1.14)
3. Fork this repository
4. Clone it outside of your `GOPATH` (we're using Go modules)
### Working with git
1. Create your feature branch (`git checkout -b my-new-feature`)
2. Commit your changes (`git commit -am 'Add some feature'`)
3. Push to the branch (`git push origin my-new-feature`)
4. Create a new pull request
### Testing
1. Run all linters (`make lint`)
2. Run all tests (`make tests`)
## Reporting bugs and feature request
Your issue or feature request may already be reported!
Please search on the [issue tracker](../../../issues) before creating one.
If you do not find any relevant issue or feature request, feel free to
add a new one!
## Additional resources
* [Code of conduct](CODE_OF_CONDUCT.md)
* [Issue template](ISSUE_TEMPLATE.md)
* [Pull request template](PULL_REQUEST_TEMPLATE.md)

View File

@@ -1,35 +0,0 @@
Your issue may already be reported!
Please search on the [issue tracker](../../../issues) before creating one.
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
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 -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
* Operating System and version:
* Link to your project:

View File

@@ -1,45 +0,0 @@
A similar PR may already be submitted!
Please search among the [pull requests](../../../pulls) before creating one.
Thanks for submitting a pull request! Please provide enough information so that others can review your pull request:
For more information, see the [CONTRIBUTING](CONTRIBUTING.md) guide.
**Summary**
<!-- Summary of the PR -->
This PR fixes/implements the following **bugs/features**
* [ ] Bug 1
* [ ] Bug 2
* [ ] Feature 1
* [ ] Feature 2
* [ ] Breaking changes
<!-- You can skip this if you're fixing a typo or adding an app to the Showcase. -->
Explain the **motivation** for making this change. What existing problem does the pull request solve?
<!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. -->
**Test plan (required)**
Demonstrate the code is solid. Example: The exact commands you ran and their output.
<!-- Make sure tests pass on Travis. -->
**Closing issues**
<!-- Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). -->
Fixes #
**Checklist**
- [ ] Have you followed the guidelines in our [CONTRIBUTING](CONTRIBUTING.md) guide?
- [ ] Have you lint your code locally prior to submission (`make lint`)?
- [ ] Have you written new tests for your core changes, as applicable?
- [ ] Have you successfully ran tests with your changes locally (`make tests`)?
- [ ] Have you updated the documentation (Markdown files under `build > docs > content` and then `make doc`)?
- [ ] I have squashed any insignificant commits
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code

18
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- bug
- documentation
- enhancement
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -0,0 +1,27 @@
name: Continuous Delivery
on:
release:
types: [ published ]
jobs:
release:
name: Release Docker image
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Checkout source code
uses: actions/checkout@v2
- name: Log in to Docker Hub Container Registry
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
run: |
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }}
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }} DOCKER_REPOSITORY=thecodingmachine

View File

@@ -0,0 +1,43 @@
name: Continuous Integration
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Run linters
uses: golangci/golangci-lint-action@v2
with:
version: v1.49.0
tests:
needs:
- Lint
name: Tests
# TODO: once arm64 actions are available, also run the tests on this architecture.
# See: https://github.com/actions/virtual-environments/issues/2552#issuecomment-771478000.
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Checkout source code
uses: actions/checkout@v2
- name: Build testing environment
run: make build build-tests
- name: Run tests
run: |
make tests-once
bash <(curl -s https://codecov.io/bash)

View File

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

View File

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

4
.gitignore vendored
View File

@@ -1,2 +1,2 @@
.idea
coverage.txt
/coverage.html
/coverage.txt

27
.golangci.yml Normal file
View File

@@ -0,0 +1,27 @@
linters:
disable-all: true
enable:
- bodyclose
- errcheck
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- misspell
- prealloc
- staticcheck
- typecheck
- unconvert
- unused
run:
deadline: 5m
issues-exit-code: 1
tests: false
output:
format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true

View File

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

View File

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

204
Makefile
View File

@@ -1,66 +1,156 @@
GOLANG_VERSION=1.14
VERSION=snapshot
DOCKER_USER=
DOCKER_PASSWORD=
DOCKER_REGISTRY=thecodingmachine
.PHONY: help
help: ## Show the help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: it
it: build build-tests ## Initialize the development environment
GOLANG_VERSION=1.19
DOCKER_REPOSITORY=gotenberg
GOTENBERG_VERSION=snapshot
GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001
GOLANGCI_LINT_VERSION=1.27.0
CODE_COVERAGE=0
TINI_VERSION=0.19.0
MAXIMUM_WAIT_TIMEOUT=30.0
MAXIMUM_WAIT_DELAY=10.0
MAXIMUM_WEBHOOK_URL_TIMEOUT=30.0
DEFAULT_WAIT_TIMEOUT=10.0
DEFAULT_WEBHOOK_URL_TIMEOUT=10.0
DEFAULT_LISTEN_PORT=3000
DISABLE_GOOGLE_CHROME=0
DISABLE_UNOCONV=0
LOG_LEVEL=INFO
ROOT_PATH=/
DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576
GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=0
NOTO_COLOR_EMOJI_VERSION=v2.034 # See https://github.com/googlefonts/noto-emoji/releases.
PDFTK_VERSION=v3.3.2 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
GOLANGCI_LINT_VERSION=v1.49.0 # See https://github.com/golangci/golangci-lint/releases.
# build the base Docker image.
base:
docker build --build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) --build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) -t $(DOCKER_REGISTRY)/gotenberg:base -f build/base/Dockerfile .
.PHONY: build
build: ## Build the Gotenberg's Docker image
docker build \
--build-arg GOLANG_VERSION=$(GOLANG_VERSION) \
--build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) \
--build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) \
--build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) \
--build-arg NOTO_COLOR_EMOJI_VERSION=$(NOTO_COLOR_EMOJI_VERSION) \
--build-arg PDFTK_VERSION=$(PDFTK_VERSION) \
-t $(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
-f build/Dockerfile .
# build the workspace Docker image.
workspace:
make base
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:workspace -f build/workspace/Dockerfile .
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
API_PORT=3000
API_PORT_FROM_ENV=
API_TIMEOUT=30s
API_ROOT_PATH=/
API_TRACE_HEADER=Gotenberg-Trace
API_DISABLE_HEALTH_CHECK_LOGGING=false
CHROMIUM_INCOGNITO=false
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
CHROMIUM_DISABLE_WEB_SECURITY=false
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
CHROMIUM_HOST_RESOLVER_RULES=
CHROMIUM_PROXY_SERVER=
CHROMIUM_ALLOW_LIST=
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
CHROMIUM_DISABLE_JAVASCRIPT=false
CHROMIUM_DISABLE_ROUTES=false
LIBREOFFICE_DISABLES_ROUTES=false
LOG_LEVEL=info
LOG_FORMAT=auto
PDFENGINES_ENGINES=
PDFENGINES_DISABLE_ROUTES=false
PROMETHEUS_NAMESPACE=gotenberg
PROMETHEUS_COLLECT_INTERVAL=1s
PROMETHEUS_DISABLE_ROUTE_LOGGING=false
PROMETHEUS_DISABLE_COLLECT=false
UNO_LISTENER_START_TIMEOUT=10s
UNO_LISTENER_RESTART_THRESHOLD=10
WEBHOOK_ALLOW_LIST=
WEBHOOK_DENY_LIST=
WEBHOOK_ERROR_ALLOW_LIST=
WEBHOOK_ERROR_DENY_LIST=
WEBHOOK_MAX_RETRY=4
WEBHOOK_RETRY_MIN_WAIT=1s
WEBHOOK_RETRY_MAX_WAIT=30s
WEBHOOK_CLIENT_TIMEOUT=30s
WEBHOOK_DISABLE=false
# gofmt and goimports all go files.
fmt:
.PHONY: run
run: ## Start a Gotenberg container
docker run --rm -it \
-p $(API_PORT):$(API_PORT) \
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
gotenberg \
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
--api-port=$(API_PORT) \
--api-port-from-env=$(API_PORT_FROM_ENV) \
--api-timeout=$(API_TIMEOUT) \
--api-root-path=$(API_ROOT_PATH) \
--api-trace-header=$(API_TRACE_HEADER) \
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
--chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLES_ROUTES) \
--log-level=$(LOG_LEVEL) \
--log-format=$(LOG_FORMAT) \
--pdfengines-engines=$(PDFENGINES_ENGINES) \
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
--uno-listener-start-timeout=$(UNO_LISTENER_START_TIMEOUT) \
--uno-listener-restart-threshold=$(UNO_LISTENER_RESTART_THRESHOLD) \
--webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \
--webhook-deny-list=$(WEBHOOK_DENY_LIST) \
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
--webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
--webhook-retry-min-wait=$(WEBHOOK_RETRY_MIN_WAIT) \
--webhook-retry-max-wait=$(WEBHOOK_RETRY_MAX_WAIT) \
--webhook-client-timeout=$(WEBHOOK_CLIENT_TIMEOUT) \
--webhook-disable=$(WEBHOOK_DISABLE)
.PHONY: build-tests
build-tests: ## Build the tests' Docker image
docker build \
--build-arg GOLANG_VERSION=$(GOLANG_VERSION) \
--build-arg DOCKER_REPOSITORY=$(DOCKER_REPOSITORY) \
--build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) \
--build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) \
-t $(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \
-f test/Dockerfile .
.PHONY: tests
tests: ## Start the testing environment
docker run --rm -it \
-v $(PWD):/tests \
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \
bash
.PHONY: tests-once
tests-once: ## Run the tests once (prefer the "tests" command while developing)
docker run --rm \
-v $(PWD):/tests \
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \
gotest
.PHONY: fmt
fmt: ## Format the code and "optimize" the dependencies
go fmt ./...
go mod tidy
# run all linters.
lint:
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:lint -f build/lint/Dockerfile .
docker run --rm $(DOCKER_REGISTRY)/gotenberg:lint
.PHONY: godoc
godoc: ## Run a webserver with Gotenberg godoc (go get golang.org/x/tools/cmd/godoc)
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v7)
godoc -http=:6060
# run all tests.
tests:
make workspace
./scripts/tests.sh $(DOCKER_REGISTRY) $(CODE_COVERAGE)
# generate documentation.
doc:
make workspace
docker build -t $(DOCKER_REGISTRY)/gotenberg:docs -f build/docs/Dockerfile .
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REGISTRY)/gotenberg:docs
# build Gotenberg Docker image.
image:
make workspace
docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
# start the API using previously built Docker image.
gotenberg:
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e ROOT_PATH=$(ROOT_PATH) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -e GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=$(GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REGISTRY)/gotenberg:$(VERSION)
# publish Gotenberg images according to version.
publish:
make workspace
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
.PHONY: release
release: ## Build the Gotenberg's Docker image for many platforms, then push it to a Docker repository
./scripts/release.sh \
$(GOLANG_VERSION) \
$(GOTENBERG_VERSION) \
$(GOTENBERG_USER_GID) \
$(GOTENBERG_USER_UID) \
$(NOTO_COLOR_EMOJI_VERSION) \
$(PDFTK_VERSION) \
$(DOCKER_REPOSITORY)

View File

@@ -1,45 +1,45 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/69229423-ac731300-0b85-11ea-8c2e-2cc00ecdb269.PNG" alt="Gotenberg logo" width="250" height="250" />
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
<h3 align="center">Gotenberg</h3>
<p align="center">A Docker-powered stateless API for PDF files</p>
<p align="center"><a href="https://gotenberg.dev/docs/about">Documentation</a> &#183; 🔥 <a href="https://gotenberg.dev/docs/get-started/live-demo">Live Demo</a></p>
</p>
<h3 align="center">Gotenberg</h3>
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
<p align="center"><a href="https://thecodingmachine.github.io/gotenberg">Documentation</a> &#183; <a href="/.github/CONTRIBUTING.md">Contributing</a></p>
---
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from various sources. Each time, we ended up using some well known libraries and kind of lost time by reimplementing a solution from a project to another project. Meh.
Gotenberg provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice for converting
numerous document formats (HTML, Markdown, Word, Excel, etc.) into PDF files, and more!
## Features
* HTML and Markdown conversions using Google Chrome headless
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beautiful PDFs!
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
## Quick start
## Quick Start
Open a terminal and run the following command:
```bash
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
```
docker run --rm -p 3000:3000 gotenberg/gotenberg:7
```
The API is now available on your host at `http://localhost:3000`.
Alternatively, using the historic Docker repository from our sponsor [TheCodingMachine](https://www.thecodingmachine.com):
Head to the [documentation](https://thecodingmachine.github.io/gotenberg)
to learn how to interact with it!
```
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:7
```
The API is now available on your host at http://localhost:3000.
Head to the [documentation](https://gotenberg.dev/docs/about) to learn how to interact with it 🚀
## Sponsors
<p align="center">
<a href="https://thecodingmachine.com">
<img src="https://user-images.githubusercontent.com/8983173/130324668-9d6e7b35-53a3-49c7-a574-38190d2bd6b0.png" alt="TheCodingMachine Logo" width="429" height="210" />
</a>
</p>
## Badges
[![Docker pulls](https://img.shields.io/docker/pulls/gotenberg/gotenberg)](https://hub.docker.com/r/gotenberg/gotenberg)
[![Docker pulls](https://img.shields.io/docker/pulls/thecodingmachine/gotenberg)](https://hub.docker.com/r/thecodingmachine/gotenberg)
[![Docker image layers](https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:6.svg)](https://microbadger.com/images/thecodingmachine/gotenberg:6)
[![Travis CI](https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master)](https://travis-ci.org/thecodingmachine/gotenberg)
[![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)
---
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
</p>
[![Continuous Integration](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/gotenberg/gotenberg.svg)](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
[![Codecov](https://codecov.io/gh/gotenberg/gotenberg/branch/main/graph/badge.svg)](https://codecov.io/gh/gotenberg/gotenberg)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

171
build/Dockerfile Normal file
View File

@@ -0,0 +1,171 @@
# Note: ARG instructions do not create additional layers.
# Instead, next layers will concatenate them.
ARG GOLANG_VERSION
# Note: we have to repeat ARG instructions in each build stage that uses them.
ARG GOTENBERG_VERSION
FROM golang:$GOLANG_VERSION AS builder
ENV CGO_ENABLED 0
# Define the working directory outside of $GOPATH (we're using go modules).
WORKDIR /home
# Install module dependencies.
COPY go.mod go.sum ./
RUN go mod download &&\
go mod verify
# Copy the source code.
COPY cmd ./cmd
COPY pkg ./pkg
# Build the binary.
ARG GOTENBERG_VERSION
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v7/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
FROM debian:11-slim
ARG GOTENBERG_VERSION
LABEL author="Julien Neuhart" \
description="A Docker-powered stateless API for PDF files." \
github="https://github.com/gotenberg/gotenberg" \
version="$GOTENBERG_VERSION" \
website="https://gotenberg.dev"
# Improve fonts subpixel hinting and smoothing.
# Credits:
# https://github.com/arachnys/athenapdf/issues/69.
# https://github.com/arachnys/athenapdf/commit/ba25a8d80a25d08d58865519c4cd8756dc9a336d.
COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
# Simple wrapper around Java and PDFtk.
COPY build/pdftk.sh /usr/bin/pdftk
# Setup the Docker image.
ARG GOTENBERG_USER_GID
ARG GOTENBERG_USER_UID
ARG NOTO_COLOR_EMOJI_VERSION
ARG PDFTK_VERSION
# Script for installing either Google Chrome stable on amd64 architecture or
# Chromium on other architectures.
# See https://github.com/gotenberg/gotenberg/issues/328.
COPY build/install-chromium.sh /tmp/install-chromium.sh
RUN \
# Create a non-root user.
# All processes in the Docker container will run with this dedicated user.
groupadd --gid "$GOTENBERG_USER_GID" gotenberg &&\
useradd --uid "$GOTENBERG_USER_UID" --gid gotenberg --shell /bin/bash --home /home/gotenberg --no-create-home gotenberg &&\
mkdir /home/gotenberg &&\
chown gotenberg: /home/gotenberg &&\
# Install dependencies required for the next instructions or debugging.
# Note: tini is a helper for reaping zombie processes.
apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg htop tini python3 default-jre-headless &&\
ln -s /usr/bin/htop /usr/bin/top &&\
# Install fonts.
# 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.
curl -o ./ttf-mscorefonts-installer_3.8_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8_all.deb &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
./ttf-mscorefonts-installer_3.8_all.deb \
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 \
ttf-wqy-zenhei \
fonts-arphic-ukai \
fonts-arphic-uming \
fonts-ipafont-mincho \
fonts-ipafont-gothic \
fonts-unfonts-core \
# LibreOffice recommends.
fonts-crosextra-caladea \
fonts-crosextra-carlito \
fonts-dejavu \
fonts-dejavu-extra \
fonts-liberation \
fonts-liberation2 \
fonts-linuxlibertine \
fonts-noto-cjk \
fonts-noto-core \
fonts-noto-mono \
fonts-noto-ui-core \
fonts-sil-gentium \
fonts-sil-gentium-basic &&\
rm -f ./ttf-mscorefonts-installer_3.8_all.deb &&\
# Add Color and Black-and-White Noto emoji font.
# Credits:
# https://github.com/gotenberg/gotenberg/pull/325.
# https://github.com/googlefonts/noto-emoji.
curl -Ls "https://github.com/googlefonts/noto-emoji/raw/$NOTO_COLOR_EMOJI_VERSION/fonts/NotoColorEmoji.ttf" -o /usr/local/share/fonts/NotoColorEmoji.ttf &&\
# Install Google Chrome / Chromium.
/tmp/install-chromium.sh &&\
# Install LibreOffice.
# Note: we use the bullseye-backports distribution to get the latest LibreOffice version.
# See:
# https://github.com/gotenberg/gotenberg/pull/322.
# https://github.com/gotenberg/gotenberg/issues/403.
echo "deb https://httpredir.debian.org/debian/ bullseye-backports main contrib non-free" >> /etc/apt/sources.list &&\
apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bullseye-backports libreoffice &&\
# Download unoconv (Python script).
curl -Ls https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv -o /usr/bin/unoconv &&\
chmod +x /usr/bin/unoconv &&\
# unoconv will look for the Python binary, which has to be at version 3.
ln -s /usr/bin/python3 /usr/bin/python &&\
# Download PDFtk.
# See https://github.com/gotenberg/gotenberg/pull/273. \
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
chmod a+x /usr/bin/pdftk-all.jar &&\
# Download QPDF.
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
# See https://github.com/nextcloud/docker/issues/380.
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
# Cleanup.
# Note: the Debian image does automatically a clean after each install thanks to a hook.
# Therefore, there is no need for apt-get clean.
# See https://stackoverflow.com/a/24417119/3248473.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
# Print versions of main dependencies.
chromium --version &&\
libreoffice --version &&\
unoconv --version &&\
pdftk --version &&\
qpdf --version
# Copy the Gotenberg binary from the builder stage.
COPY --from=builder /home/gotenberg /usr/bin/
# Environment variables required by modules or else.
ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
ENV UNOCONV_BIN_PATH /usr/bin/unoconv
ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin
ENV PDFTK_BIN_PATH /usr/bin/pdftk
ENV QPDF_BIN_PATH /usr/bin/qpdf
USER gotenberg
WORKDIR /home/gotenberg
# Default API port.
EXPOSE 3000
ENTRYPOINT [ "/usr/bin/tini", "--" ]
CMD [ "gotenberg" ]

15
build/Dockerfile.cloudrun Normal file
View File

@@ -0,0 +1,15 @@
# Note: ARG instructions do not create additional layers.
# Instead, next layers will concatenate them.
ARG DOCKER_REPOSITORY
ARG GOTENBERG_VERSION
FROM $DOCKER_REPOSITORY/gotenberg:$GOTENBERG_VERSION
USER root
# For security reasons, the non-root user gotenberg does not own the Tini binary by default.
# However, some providers like Cloud Run from Google Cloud cannot start a Docker container in that case.
# See https://github.com/gotenberg/gotenberg/issues/90#issuecomment-543551353.
RUN chown gotenberg: /usr/bin/tini
USER gotenberg

View File

@@ -1,113 +0,0 @@
FROM debian:buster-slim
# |--------------------------------------------------------------------------
# | Common libraries
# |--------------------------------------------------------------------------
# |
# | Libraries used in the build process of this image.
# |
RUN echo "deb http://httpredir.debian.org/debian/ buster main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y curl wget gnupg ttf-mscorefonts-installer procps
# |--------------------------------------------------------------------------
# | Chrome
# |--------------------------------------------------------------------------
# |
# | Installs Chrome.
# |
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
apt-get update &&\
apt-get -y --allow-unauthenticated install google-chrome-stable
# |--------------------------------------------------------------------------
# | LibreOffice
# |--------------------------------------------------------------------------
# |
# | Installs LibreOffice.
# |
# https://github.com/nextcloud/docker/issues/380
RUN mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
echo "deb http://httpredir.debian.org/debian/ buster-backports main contrib non-free" >> /etc/apt/sources.list &&\
apt-get update &&\
apt-get -t buster-backports -y install libreoffice
# |--------------------------------------------------------------------------
# | Unoconv
# |--------------------------------------------------------------------------
# |
# | Installs unoconv.
# |
ENV UNO_URL=https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv
RUN curl -Ls $UNO_URL -o /usr/bin/unoconv &&\
chmod +x /usr/bin/unoconv &&\
ln -s /usr/bin/python3 /usr/bin/python &&\
unoconv --version
# |--------------------------------------------------------------------------
# | PDFtk
# |--------------------------------------------------------------------------
# |
# | Installs PDFtk as an alternative to pdfcpu for merging PDFs.
# | https://github.com/thecodingmachine/gotenberg/issues/29
# |
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.
# |
ARG GOTENBERG_USER_GID=1001
ARG GOTENBERG_USER_UID=1001
RUN groupadd --gid ${GOTENBERG_USER_GID} gotenberg \
&& useradd --uid ${GOTENBERG_USER_UID} --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \
&& mkdir /gotenberg \
&& chown gotenberg: /gotenberg

Binary file not shown.

View File

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

View File

@@ -1,10 +0,0 @@
---
title: Introduction
---
[Gotenberg](https://github.com/thecodingmachine/gotenberg/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.
* HTML and Markdown conversions using Google Chrome headless
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries

View File

@@ -1,71 +0,0 @@
---
title: Install
---
Gotenberg is shipped within a Docker image.
You may start it with:
```bash
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
```
> The API will be available at [http://localhost:3000](http://localhost:3000).
The image uses a dedicated non-root user called `gotenberg` with uid and gid `1001`.
If you wish to change those uid and gid, you will have to:
* clone the project
* re-build the image
* publish the image in your own Docker registry
For instance:
```bash
$ git clone https://github.com/thecodingmachine/gotenberg.git
$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=version
```
> `master` branch is always up-to-date with the latest version of the API.
## Docker Compose
You may also add it in your Docker Compose stack:
```yaml
version: '3'
services:
# your other services
gotenberg:
image: thecodingmachine/gotenberg:6
```
> The API will be available under `gotenberg:3000` in your Docker Compose network.
## Kubernetes
It may also be deployed with Kubernetes.
Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0.2` CPU).
> The more resources are granted, the quicker will be the conversions.
In the deployment specification of the pod, also specify the uid of the user `gotenberg`:
```
securityContext:
privileged: false
runAsUser: 1001
```
## Cloud Run (Google Cloud)
If you're looking for cost savings, you might be interested by [Cloud Run](https://cloud.google.com/run).
However, according to some users, doing asynchronous conversion (with a webhook) might not working.
In the following examples, we will assume your
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).

View File

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

View File

@@ -1,125 +0,0 @@
---
title: Environment variables
---
You may customize the API behaviour thanks to environment variables.
## Log level
The API provides structured logging allowing you to have relevant information
about what's going on.
> If a TTY is attached, the log entries are displayed in text format with colors, otherwise in JSON format.
You may customize the severity of the log entries thanks to the environment variable `LOG_LEVEL`.
It accepts one of the following severities: `"DEBUG"`, `"INFO"` (default) and `"ERROR"`.
## Default listen port
By default, the API will listen on port `3000`.
You may customize this value with the environment variable `DEFAULT_LISTEN_PORT`.
This environment variable accepts any string that can be turned into a port number.
## Root path
By default, the API root path is `/`.
You may customize this value with the environment variable `ROOT_PATH`.
This environment variable accepts a string starting and ending with `/`.
For instance, `/gotenberg/` is a valid value while `gotenberg` is not.
> This is useful if you wish to do service discovery via URL paths.
## Disable Google Chrome
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`
for disabling Google Chrome.
It takes the strings `"0"` or `"1"` as value where `1` means `true`
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
## Default Google Chrome rpcc buffer size
When performing a [HTML](#html), [URL](#url) or [Markdown](#markdown) conversion, the API might return
a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
If so, you may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`.
It takes a string representation of an int as value (e.g. `"1048576"` for 1 MB).
The hard limit is 100 MB and is defined by Google Chrome itself.
> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`.
> See the [rpcc buffer size section](#html.rpcc_buffer_size).
## Google Chrome ignore certificate errors
When performing a [URL](#url) conversion, Google Chrome will not accept certificate errors.
You may allow insecure connections by setting the `GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS` environment variable to `"1"`.
**You should be careful with this feature and only enable it in your development environment.**
## Disable LibreOffice (unoconv)
You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`.
> If LibreOffice (unoconv) is disabled, the following conversion will **not** be available anymore:
> [Office](#office)
## Default wait timeout
By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful.
If unsucessful, it returns a `504` HTTP code.
You may customize this timeout thanks to the environment variable `DEFAULT_WAIT_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
> The default timeout may also be overridden per request thanks to the form field `waitTimeout`.
> See the [timeout section](#timeout).
## Maximum wait timeout
By default, the value of the form field `waitTimeout` cannot be more than 30 seconds.
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
## Default webhook URL timeout
By default, the API will wait 10 seconds before it considers the sending of the resulting PDF to be unsuccessful.
> See the [webhook section](#webhook).
You may customize this timeout thanks to the environment variable `DEFAULT_WEBHOOK_URL_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
> The default timeout may also be overridden per request thanks to the form field `webhookURLTimeout`.
> See the [webhook timeout section](#webhook.timeout).
## Maximum webhook URL timeout
By default, the value of the form field `webhookURLTimeout` cannot be more than 30 seconds.
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WEBHOOK_URL_TIMEOUT`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
## Maximum wait delay
By default, the value of the form field `waitDelay` cannot be more than 10 seconds.
> See the [wait delay section](#html.wait_delay).
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_DELAY`.
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).

View File

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

View File

@@ -1,106 +0,0 @@
---
title: URL
---
Gotenberg provides the endpoint `/convert/url` for remote URL conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
This endpoint does not accept an `index.html` file nor assets files but a form field
named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions.
> **Attention:** when converting a website to PDF, you should remove all margins.
> If not, some of the content of the page might be hidden.
> **Attention:** if you try to convert a URL from a Docker Compose service named `app` (i.e. `removeURL` = `http://app/an/entrypoint`),
> the resulting PDF will be blank. Make sure to rename your service to avoid this issue.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/url \
--header 'Content-Type: multipart/form-data' \
--form remoteURL=https://google.com \
--form marginTop=0 \
--form marginBottom=0 \
--form marginLeft=0 \
--form marginRight=0 \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := gotenberg.NewURLRequest("https://google.com")
req.Margins(gotenberg.NoMargins)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\URLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$request = new URLRequest('https://google.com');
$request->setMargins(Request::NO_MARGINS);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Custom HTTP headers
You may send your own HTTP headers to the `remoteURL`.
For instance, by adding the HTTP header `Gotenberg-Remoteurl-Your-Header` to your request,
the API will send a request to the `remoteURL` with the HTTP header `Your-Header`.
> **Attention:** the API uses a canonical format for the HTTP headers:
> it transforms the first
> letter and any letter following a hyphen to upper case;
> the rest are converted to lowercase. For example, the
> canonical key for `accept-encoding` is `Accept-Encoding`.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/url \
--header 'Content-Type: multipart/form-data' \
--header 'Gotenberg-Remoteurl-Your-Header: Foo' \
--form remoteURL=https://google.com \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := gotenberg.NewURLRequest("https://google.com")
req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\URLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$request = new URLRequest('https://google.com');
$request->addRemoteURLHTTPHeader('Your-Header', 'Foo')
$dest = 'result.pdf';
$client->store($request, $dest);
```

View File

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

View File

@@ -1,167 +0,0 @@
---
title: Office
---
Gotenberg provides the endpoint `/convert/office` for Office document conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
You may send one or more Office documents. Following file extensions are accepted:
* `.txt`
* `.rtf`
* `.fodt`
* `.doc`
* `.docx`
* `.odt`
* `.xls`
* `.xlsx`
* `.ods`
* `.ppt`
* `.pptx`
* `.odp`
All files will be merged into a single resulting PDF.
> **Attention:** Gotenberg merges the PDF files alphabetically.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form files=@document2.docx \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file")
req := gotenberg.NewOfficeRequest(doc, doc2)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\OfficeRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
DocumentFactory::makeFromPath('document2.docx', '/path/to/file'),
];
$request = new OfficeRequest($files);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Orientation
You may also customize the resulting PDF format.
By default, it will be rendered with `portrait` orientation.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form landscape=true \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
req := gotenberg.NewOfficeRequest(doc)
req.Landscape(true)
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\OfficeRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
];
$request = new OfficeRequest($files);
$request->setLandscape(true);
$dest = 'result.pdf';
$client->store($request, $dest);
```
## Page ranges
You may specify the page ranges to convert.
The format is the same as the one from the print options
of LibreOffice, e.g. `1-1` or `1-4`.
> **Attention:** if more than one document, the page ranges will be
> applied for each document.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form pageRanges='1-3' \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
req := gotenberg.NewOfficeRequest(doc)
req.PageRanges("1-3")
dest := "result.pdf"
c.Store(req, dest)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\OfficeRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
];
$request = new OfficeRequest($files);
$request->setPageRanges('1-3');
$dest = 'result.pdf';
$client->store($request, $dest);
```

View File

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

View File

@@ -1,52 +0,0 @@
---
title: Timeout
---
All endpoints accept a form field named `waitTimeout`.
The API will wait the given **seconds** before it considers the conversion to be unsucessful.
If unsucessful, it returns a `504` HTTP code.
It takes a float as value (e.g `2.5` for 2.5 seconds).
> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section.
## Examples
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form waitTimeout=2.5
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.WaitTimeout(2.5)
resp, _ := c.Post(req)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setWaitTimeout(2.5);
$dest = 'result.pdf';
$client->store($request, $dest);
```

View File

@@ -1,149 +0,0 @@
---
title: Webhook
---
All endpoints accept a form field named `webhookURL`.
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
to given URL.
By doing so, your requests to the API will be over before the conversions are actually done!
## Examples
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form webhookURL='http://myapp.com/webhook/'
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.WebhookURL("http://myapp.com/webhook/")
resp, _ := c.Post(req)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setWebhookURL('http://myapp.com/webhook/');
$resp = $client->post($request);
```
## Timeout
If a `webhookURL` is provided, you may also send a form field named `webhookURLTimeout`.
The API will wait the given **seconds** before it considers the sending of the resulting PDF to be unsucessful.
It takes a float as value (e.g `2.5` for 2.5 seconds).
> You may also define this value globally: see the [environment variables](#environment_variables.default_webhook_url_timeout) section.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form webhookURL='http://myapp.com/webhook/' \
--form webhookURLTimeout=2.5
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.WebhookURL("http://myapp.com/webhook/")
req.WebhookURLTimeout(2.5)
resp, _ := c.Post(req)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setWebhookURL('http://myapp.com/webhook/');
$request->setWebhookURLTimeout(2.5);
$resp = $client->post($request);
```
## Custom HTTP headers
You may send your own HTTP headers to the `webhookURL`.
For instance, by adding the HTTP header `Gotenberg-Webhookurl-Your-Header` to your request,
the API will send a request to the `webhookURL` with the HTTP header `Your-Header`.
> **Attention:** the API uses a canonical format for the HTTP headers:
> it transforms the first
> letter and any letter following a hyphen to upper case;
> the rest are converted to lowercase. For example, the
> canonical key for `accept-encoding` is `Accept-Encoding`.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--header 'Gotenberg-Webhookurl-Your-Header: Foo' \
--form files=@index.html \
--form webhookURL='http://myapp.com/webhook/'
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.WebhookURL("http://myapp.com/webhook/")
req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
resp, _ := c.Post(req)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setWebhookURL('http://myapp.com/webhook/');
$request->addWebhookURLHTTPHeader('Your-Header', 'Foo');
$resp = $client->post($request);
```

View File

@@ -1,49 +0,0 @@
---
title: Result filename
---
All endpoints accept a form field named `resultFilename`.
If provided, the API will return the resulting PDF file with the given filename.
Otherwise a random filename is used.
> **Attention:** this feature does not work if the form field `webhookURL` is given.
## Examples
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form resultFilename='foo.pdf'
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg-go-client/v7"
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
req := gotenberg.NewHTMLRequest(index)
req.ResultFilename("foo.pdf")
resp, _ := c.Post(req)
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
$request = new HTMLRequest($index);
$request->setResultFilename('foo.pdf');
$resp = $client->post($request);
```

View File

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

View File

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

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

@@ -1,11 +0,0 @@
---
title: Links
---
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
</p>
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
* Thanks to [@mafredri](https://github.com/mafredri) for its help and its wonderful [cdp](https://github.com/mafredri/cdp) library

15
build/install-chromium.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -e
ARCH=$(dpkg --print-architecture)
if [[ "$ARCH" == "amd64" ]]; then
curl 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 -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends --allow-unauthenticated google-chrome-stable
mv /usr/bin/google-chrome-stable /usr/bin/chromium
else
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium
fi

View File

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

View File

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

3
build/pdftk.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
exec java -jar /usr/bin/pdftk-all.jar "$@"

View File

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

View File

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

View File

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

158
cmd/gotenberg.go Normal file
View File

@@ -0,0 +1,158 @@
package gotenbergcmd
import (
"context"
"fmt"
"os"
"os/signal"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
)
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.
// Credits: https://github.com/labstack/echo/blob/v4.3.0/echo.go#L240.
const banner = `
_____ __ __
/ ___/__ / /____ ___ / / ___ _______ _
/ (_ / _ \/ __/ -_) _ \/ _ \/ -_) __/ _ '/
\___/\___/\__/\__/_//_/_.__/\__/_/ \_, /
/___/
A Docker-powered stateless API for PDF files.
Version: %s
-------------------------------------------------------
`
// Version is the... version of the Gotenberg application. We set it at the
// build stage of the Docker image.
var Version = "snapshot"
// Run starts the Gotenberg application. Call this in the main of your program.
func Run() {
fmt.Printf(banner, Version)
// Create the root FlagSet and adds the modules flags to it.
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
descriptors := gotenberg.GetModuleDescriptors()
var modsInfo string
for _, desc := range descriptors {
fs.AddFlagSet(desc.FlagSet)
modsInfo += desc.ID + " "
}
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
// Parse the flags...
err := fs.Parse(os.Args[1:])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// ...and create a wrapper around those.
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
// Get the graceful shutdown duration.
gracefulShutdownDuration := parsedFlags.MustDuration("gotenberg-graceful-shutdown-duration")
ctx := gotenberg.NewContext(parsedFlags, descriptors)
// Start application modules.
apps, err := ctx.Modules(new(gotenberg.App))
if err != nil {
fmt.Printf("[FATAL] %s\n", err)
os.Exit(1)
}
for _, a := range apps {
go func(app gotenberg.App) {
id := app.(gotenberg.Module).Descriptor().ID
err = app.Start()
if err != nil {
fmt.Printf("[FATAL] starting %s: %s\n", id, err)
os.Exit(1)
}
startupMessage := app.StartupMessage()
if startupMessage == "" {
fmt.Printf("[SYSTEM] %s: application started\n", id)
return
}
fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage)
}(a.(gotenberg.App))
}
// Get modules that want to print system messages.
sysLoggers, err := ctx.Modules(new(gotenberg.SystemLogger))
if err != nil {
fmt.Printf("[FATAL] %s\n", err)
os.Exit(1)
}
for _, l := range sysLoggers {
go func(logger gotenberg.SystemLogger) {
id := logger.(gotenberg.Module).Descriptor().ID
for _, message := range logger.SystemMessages() {
fmt.Printf("[SYSTEM] %s: %s\n", id, message)
}
}(l.(gotenberg.SystemLogger))
}
quit := make(chan os.Signal, 1)
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C).
signal.Notify(quit, os.Interrupt)
// Block until we receive our signal.
<-quit
gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration)
defer cancel()
forceQuit := make(chan os.Signal, 1)
signal.Notify(forceQuit, os.Interrupt)
go func() {
// In case of force quit, cancel the context.
<-forceQuit
cancel()
}()
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
eg, _ := errgroup.WithContext(gracefulShutdownCtx)
for _, a := range apps {
eg.Go(func(app gotenberg.App) func() error {
return func() error {
id := app.(gotenberg.Module).Descriptor().ID
err = app.Stop(gracefulShutdownCtx)
if err != nil {
return fmt.Errorf("stopping %s: %w", id, err)
}
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
return nil
}
}(a.(gotenberg.App)))
}
err = eg.Wait()
if err != nil {
fmt.Printf("[FATAL] %v\n", err)
os.Exit(1)
}
os.Exit(0)
}

View File

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

File diff suppressed because it is too large Load Diff

730
docs/openapi.yaml Normal file
View File

@@ -0,0 +1,730 @@
openapi: 3.0.3
info:
title: Gotenberg
version: 7.x
license:
name: MIT
url: 'https://github.com/gotenberg/gotenberg/blob/main/LICENSE'
contact:
url: 'https://github.com/gotenberg/gotenberg'
description: >-
A Docker-powered stateless API for PDF files.
externalDocs:
url: https://gotenberg.dev
servers:
- url: 'http://localhost:3000'
description: Local server with the default Docker image and port
tags:
- name: chromium
description: Operations of the Chromium module
externalDocs:
url: https://gotenberg.dev/docs/modules/chromium
- name: libreoffice
description: Operations of the Libreoffice module
externalDocs:
url: https://gotenberg.dev/docs/modules/libreoffice
- name: pdfengines
description: Operations of the PDF Engines module
externalDocs:
url: https://gotenberg.dev/docs/modules/pdf-engines
paths:
/forms/chromium/convert/url:
post:
tags:
- chromium
summary: Convert the contents of a given URL to PDF
externalDocs:
url: https://gotenberg.dev/docs/modules/chromium
description: >-
Send a URL in your API request via the `url` form field
Send a remote URL in your API request via the `remoteURL` parameter, and
get the resulting PDF file. The API will fetch the given URL and render
the page to PDF using the underlying headless Chrome instance.
You can optionally include `header.html` and `footer.html` files as part of the request as well.
See externalDocs for more details.
parameters:
- in: header
name: Gotenberg-Output-Filename
description: >-
By default, the API generates a UUID filename.
However, you may also specify the filename per request,
thanks to the Gotenberg-Output-Filename header.
Caution! The API adds the file extension automatically; you don't have to set it.
schema:
type: string
required: false
- in: header
name: Gotenberg-Trace
description: >-
The trace, or request ID, identifies a request in the logs.
By default, the API generates a UUID trace for each request.
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
schema:
type: string
required: false
requestBody:
required: true
description: >-
The request must be `multipart/form-data` that includes a `url` form field.
The API uses a headless Chrome instance to do the conversion, therefore print
parameter such as margins and paper size are also accepted as optional parameters.
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/URLConvertRequestBody'
examples: { }
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
'400':
description: Bad Request
/forms/chromium/convert/html:
post:
tags:
- chromium
summary: Convert a given HTML file to PDF
externalDocs:
url: https://gotenberg.dev/docs/modules/chromium
description: >-
Send an HTML file called `index.html` as a multipart form request, and
get the resulting PDF file. You can optionally include `header.html` and
`footer.html` files as part of the request as well.
See externalDocs for more details.
parameters:
- in: header
name: Gotenberg-Output-Filename
description: >-
By default, the API generates a UUID filename.
However, you may also specify the filename per request,
thanks to the Gotenberg-Output-Filename header.
Caution! The API adds the file extension automatically; you don't have to set it.
schema:
type: string
required: false
- in: header
name: Gotenberg-Trace
description: >-
The trace, or request ID, identifies a request in the logs.
By default, the API generates a UUID trace for each request.
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
schema:
type: string
required: false
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/HTMLConvertRequestBody'
description: >-
The request body must have an `index.html` file in the `files` array,
as well as all the referred resources on the same level as the
`index.html` file. The request can also include `header.html` and
`footer.html`, given the limitations in the API description above.
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
'400':
description: Bad Request
/forms/chromium/convert/markdown:
post:
tags:
- chromium
summary: Convert a Markdown file to PDF
externalDocs:
url: https://gotenberg.dev/docs/modules/chromium
description: >-
Accepts an HTML file called `index.html` plus markdown files as a multipart
form request and embeds the markdown files into the HTML file using the Golang template
function `toHTML`.
The API will convert the markdown to HTML and embed it into your `index.html` file,
then render the resulting page. You can include your own styling and more in your HTML file.
Refer to the HTML conversion page for all the options you can use when converting
Markdown documents as well. You can optionally include `header.html` and
`footer.html` files as part of the request as well.
See externalDocs for more details.
parameters:
- in: header
name: Gotenberg-Output-Filename
description: >-
By default, the API generates a UUID filename.
However, you may also specify the filename per request,
thanks to the Gotenberg-Output-Filename header.
Caution! The API adds the file extension automatically; you don't have to set it.
schema:
type: string
required: false
- in: header
name: Gotenberg-Trace
description: >-
The trace, or request ID, identifies a request in the logs.
By default, the API generates a UUID trace for each request.
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
schema:
type: string
required: false
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/MarkdownConvertRequestBody'
description: >-
The request body must have an `index.html` file in the `files` array,
as well as all the referred markdown resources on the same level as
the `index.html` file.
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
'400':
description: Bad Request
/forms/libreoffice/convert:
post:
tags:
- libreoffice
summary: Convert an Office document to PDF
externalDocs:
url: https://gotenberg.dev/docs/modules/libreoffice
description: >-
This route accepts multipart/form-data requests and files with the following extensions:
.bib .doc .xml .docx .fodt .html .ltx .txt .odt .ott .pdb .pdf .psw .rtf
.sdw .stw .sxw .uot .vor .wps .epub .png .bmp .emf .eps .fodg .gif .jpg
.met .odd .otg .pbm .pct .pgm .ppm .ras .std .svg .svm .swf .sxd .sxw
.tiff .xhtml .xpm .fodp .potm .pot .pptx .pps .ppt .pwp .sda .sdd .sti
.sxi .uop .wmf .csv .dbf .dif .fods .ods .ots .pxl .sdc .slk .stc .sxc
.uos .xls .xlt .xlsx .tif .jpeg .odp
By default, if you send more than one file to convert, the route returns a ZIP archive of the
resulting PDF files. However, you may prefer to merge all the PDF files into an individual PDF file.
> **Attention:** The files will be merged alphabetically for the
resulting PDF.
You may also specify the page ranges to convert from the incoming Office
documents. The expected format is the same as the one from the print
options of LibreOffice, e.g. `1-1` or `1-4`.
> **Attention:** if more than one document, the page ranges will be
applied for each document.
See externalDocs for more details.
parameters:
- in: header
name: Gotenberg-Output-Filename
description: >-
By default, the API generates a UUID filename.
However, you may also specify the filename per request,
thanks to the Gotenberg-Output-Filename header.
Caution! The API adds the file extension automatically; you don't have to set it.
schema:
type: string
required: false
- in: header
name: Gotenberg-Trace
description: >-
The trace, or request ID, identifies a request in the logs.
By default, the API generates a UUID trace for each request.
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
schema:
type: string
required: false
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/OfficeConvertRequestBody'
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
'400':
description: Bad Request, e.g. Both 'pdfFormat' and 'nativePdfA1aFormat' form values are provided
/forms/pdfengines/merge:
post:
tags:
- pdfengines
summary: Merge multiple PDFs into a single PDF
externalDocs:
url: https://gotenberg.dev/docs/modules/pdf-engines
description: >-
You can send multiple PDF files to this endpoint, the API will merge
them into a single PDF and return the resulting PDF file.
> **Attention:** The PDF files will be merged alphabetically.
parameters:
- in: header
name: Gotenberg-Output-Filename
description: >-
By default, the API generates a UUID filename.
However, you may also specify the filename per request,
thanks to the Gotenberg-Output-Filename header.
Caution! The API adds the file extension automatically; you don't have to set it.
schema:
type: string
required: false
- in: header
name: Gotenberg-Trace
description: >-
The trace, or request ID, identifies a request in the logs.
By default, the API generates a UUID trace for each request.
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
schema:
type: string
required: false
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
files:
type: array
items:
type: string
format: binary
pdfFormat:
type: string
description: The PDF format of the resulting PDF
example: PDF/A-1a
required:
- files
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
'400':
description: Bad Request
/forms/pdfengines/convert:
post:
tags:
- pdfengines
summary: Convert PDFs into the given formats
externalDocs:
url: https://gotenberg.dev/docs/modules/pdf-engines
description: >-
This route accepts PDF files and a form field pdfFormat for converting them into the specified format.
parameters:
- in: header
name: Gotenberg-Output-Filename
description: >-
By default, the API generates a UUID filename.
However, you may also specify the filename per request,
thanks to the Gotenberg-Output-Filename header.
Caution! The API adds the file extension automatically; you don't have to set it.
schema:
type: string
required: false
- in: header
name: Gotenberg-Trace
description: >-
The trace, or request ID, identifies a request in the logs.
By default, the API generates a UUID trace for each request.
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
schema:
type: string
required: false
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
files:
type: array
items:
type: string
format: binary
pdfFormat:
type: string
description: The PDF format of the resulting PDF
example: PDF/A-1a
required:
- files
- pdfFormat
responses:
'200':
$ref: '#/components/responses/SuccessfulPDF'
'400':
description: >-
Bad Request, e.g. Invalid form data: no form file found for extensions: [.pdf]; form value 'pdfFormat' is required
components:
schemas:
HTMLConvertRequestBody:
title: HTML Conversion Request Body
type: object
properties:
files:
type: array
description: >-
List of HTML files to be converted to PDF. An `index.html` file is
required, and any other resources that are referenced through the
HTML file must be included as well. All the referenced files must be
on the same level as the `index.html` file.
items:
type: string
format: binary
marginTop:
type: number
example: 0
default: 1
description: Top margin for the page in inches.
marginBottom:
type: number
example: 0
default: 1
description: Bottom margin for the page in inches.
marginLeft:
type: number
example: 0
default: 1
description: Left margin for the page in inches.
marginRight:
type: number
example: 0
default: 1
description: Right margin for the page in inches.
paperWidth:
type: number
example: 8.27
description: >-
Paper width to be used while rendering the PDF. The default page
size is A4.
paperHeight:
type: number
example: 11.69
description: >-
Paper height to be used while rendering the PDF. The default page
size is A4.
preferCssPageSize:
type: boolean
description: >-
Define whether to prefer page size as defined by CSS (default false)
default: false
printBackground:
type: boolean
description: >-
Print the background graphics (default false)
default: false
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
scale:
type: number
minimum: 0.1
maximum: 2.0
example: 1.5
description: >-
The scale of the page rendering
default: 1.0
waitDelay:
type: string
example: 5s
description: >-
When the page relies on JavaScript for rendering, and you don't have access to the page's code,
you may want to wait a certain amount of time to make sure Chromium has fully rendered the page
you're trying to generate.
waitWindowStatus:
type: string
example: done
description: >-
If you have access to the page's code, you may set the window status and tell Gotenberg to wait for a specific value.
For instance
await promises()
window.status = 'ready'
Prefer this option over waitDelay.
extraHttpHeaders:
type: string
description: HTTP headers to send by Chromium while loading the HTML document (JSON format)
nativePageRanges:
type: string
example: 1-4
description: >-
The page ranges to be converted to PDF for the incoming Office
documents.
pdfFormat:
type: string
description: >-
The PDF format of the resulting PDF.
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
example: PDF/A-1a
required:
- files
MarkdownConvertRequestBody:
title: Markdown Conversion Request Body
type: object
properties:
files:
type: array
items:
type: string
format: binary
marginTop:
type: number
example: 0
default: 1
description: Top margin for the page in inches.
marginBottom:
type: number
example: 0
default: 1
description: Bottom margin for the page in inches.
marginLeft:
type: number
example: 0
default: 1
description: Left margin for the page in inches.
marginRight:
type: number
example: 0
default: 1
description: Right margin for the page in inches.
paperWidth:
type: number
example: 8.27
description: >-
Paper width to be used while rendering the PDF. The default page
size is A4.
paperHeight:
type: number
example: 11.69
description: >-
Paper height to be used while rendering the PDF. The default page
size is A4.
preferCssPageSize:
type: boolean
description: >-
Define whether to prefer page size as defined by CSS (default false)
default: false
printBackground:
type: boolean
description: >-
Print the background graphics (default false)
default: false
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
scale:
type: number
minimum: 0.1
maximum: 2.0
example: 1.5
description: >-
The scale of the page rendering
default: 1.0
waitDelay:
type: string
example: 5s
description: >-
When the page relies on JavaScript for rendering, and you don't have access to the page's code,
you may want to wait a certain amount of time to make sure Chromium has fully rendered the page
you're trying to generate.
waitWindowStatus:
type: string
example: done
description: >-
If you have access to the page's code, you may set the window status and tell Gotenberg to wait for a specific value.
For instance
await promises()
window.status = 'ready'
Prefer this option over waitDelay.
extraHttpHeaders:
type: string
description: HTTP headers to send by Chromium while loading the HTML document (JSON format)
nativePageRanges:
type: string
example: 1-4
description: >-
The page ranges to be converted to PDF for the incoming Office
documents.
pdfFormat:
type: string
description: >-
The PDF format of the resulting PDF.
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
example: PDF/A-1a
required:
- files
URLConvertRequestBody:
title: URL Conversion Request Body
type: object
properties:
url:
type: string
example: 'https://google.com'
files:
description: Optional files named header.html and footer.html
type: array
items:
type: string
format: binary
marginTop:
type: number
example: 0
default: 1
description: Top margin for the page in inches.
marginBottom:
type: number
example: 0
default: 1
description: Bottom margin for the page in inches.
marginLeft:
type: number
example: 0
default: 1
description: Left margin for the page in inches.
marginRight:
type: number
example: 0
default: 1
description: Right margin for the page in inches.
paperWidth:
type: number
example: 8.27
description: >-
Paper width to be used while rendering the PDF. The default page
size is A4.
paperHeight:
type: number
example: 11.69
description: >-
Paper height to be used while rendering the PDF. The default page
size is A4.
preferCssPageSize:
type: boolean
description: >-
Define whether to prefer page size as defined by CSS (default false)
default: false
printBackground:
type: boolean
description: >-
Print the background graphics (default false)
default: false
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
scale:
type: number
minimum: 0.1
maximum: 2.0
example: 1.5
description: >-
The scale of the page rendering
default: 1.0
waitDelay:
type: string
example: 5s
description: >-
When the page relies on JavaScript for rendering, and you don't have access to the page's code,
you may want to wait a certain amount of time to make sure Chromium has fully rendered the page
you're trying to generate.
waitWindowStatus:
type: string
example: done
description: >-
If you have access to the page's code, you may set the window status and tell Gotenberg to wait for a specific value.
For instance
await promises()
window.status = 'ready'
Prefer this option over waitDelay.
extraHttpHeaders:
type: string
description: HTTP headers to send by Chromium while loading the HTML document (JSON format)
nativePageRanges:
type: string
example: 1-4
description: >-
The page ranges to be converted to PDF for the incoming Office
documents.
pdfFormat:
type: string
description: >-
The PDF format of the resulting PDF.
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
example: PDF/A-1a
required:
- url
OfficeConvertRequestBody:
title: Office Conversion Request Body
type: object
properties:
files:
type: array
items:
type: string
format: binary
nativePageRanges:
type: string
example: 1-4
description: >-
The page ranges to be converted to PDF for the incoming Office
documents. **If there are multiple files sent to the API, this page
range will apply to all of the documents**. Empty means all pages.
nativePdfA1aFormat:
type: boolean
description: >-
Use unoconv to convert the resulting PDF to the 'PDF/A-1a' format.
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
pdfFormat:
type: string
description: >-
The PDF format of the resulting PDF.
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
example: PDF/A-1a
landscape:
type: boolean
example: true
default: false
description: >-
The default orientation for rendering the page is "portrait" mode.
By sending "landscape" parameter, you can ask the output to be
landscape.
merge:
type: boolean
description: >-
Merge all PDF files into an individual PDF file.
required:
- files
MergeFilesRequestBody:
title: Merge Files Request Body
type: object
properties:
files:
type: array
items:
type: string
format: binary
required:
- files
securitySchemes: { }
responses:
SuccessfulPDF:
description: Resulting PDF file from the conversion.
content:
application/pdf:
schema:
type: string
format: binary

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

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

View File

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

84
go.mod
View File

@@ -1,26 +1,66 @@
module github.com/thecodingmachine/gotenberg
module github.com/gotenberg/gotenberg/v7
go 1.14
go 1.19
require (
github.com/dustin/go-humanize v1.0.0
github.com/kr/text v0.2.0 // indirect
github.com/labstack/echo/v4 v4.1.16
github.com/labstack/gommon v0.3.0
github.com/mafredri/cdp v0.28.0
github.com/mattn/go-isatty v0.0.12
github.com/microcosm-cc/bluemonday v1.0.2
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
github.com/alexliesenfeld/health v0.6.0
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf
github.com/chromedp/chromedp v0.8.5
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/labstack/echo/v4 v4.8.0
github.com/labstack/gommon v0.3.1
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.19
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pdfcpu/pdfcpu v0.3.13
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/prometheus/client_golang v1.13.0
github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/pflag v1.0.5
github.com/ulikunitz/xz v0.5.10 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0
go.uber.org/zap v1.23.0
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
golang.org/x/text v0.3.7
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

625
go.sum
View File

@@ -1,102 +1,605 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexliesenfeld/health v0.6.0 h1:HRBTCgybNSe4lqGEk7nU82c3bjwh9W+3b46W6UvD4CQ=
github.com/alexliesenfeld/health v0.6.0/go.mod h1:N4NDIeQtlWumG+6z1ne1v62eQxktz5ylEgGgH9emdMw=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf h1:e0oJZmGJidTRZ0FvWXNhdj9OaOMN30Yf+T3K2Tf3L+s=
github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0=
github.com/chromedp/chromedp v0.8.5 h1:HAVg54yQFcn7sg5reVjXtoI1eQaFxhjAjflHACicUFw=
github.com/chromedp/chromedp v0.8.5/go.mod h1:xal2XY5Di7m/bzlGwtoYpmgIOfDqCakOIVg5OfdkPZ4=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ=
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94=
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/mafredri/cdp v0.28.0 h1:v/LWC3GpactA1EaS737bl+eNUhvOaDh+FzvyoDxNM5o=
github.com/mafredri/cdp v0.28.0/go.mod h1:11586MgpyJuQR4qiXQ/7HBame2FtGIBzymaUHZcQ0PY=
github.com/mafredri/go-lint v0.0.0-20180911205320-920981dfc79e/go.mod h1:k/zdyxI3q6dup24o8xpYjJKTCf2F7rfxLp6w/efTiWs=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/labstack/echo/v4 v4.8.0 h1:wdc6yKVaHxkNOEdz4cRZs1pQkwSXPiRjq69yWP4QQS8=
github.com/labstack/echo/v4 v4.8.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c=
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/pdfcpu/pdfcpu v0.3.13 h1:VFon2Yo1PJt+sA57vPAeXWGLSZ7Ux3Jl4h02M0+s3dg=
github.com/pdfcpu/pdfcpu v0.3.13/go.mod h1:UJc5xsXg0fpmjp1zOPdyYcAQArc/Zf3V0nv5URe+9fg=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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 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.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200330183114-f8bfb4ee3038 h1:FUufmZ17Dp41BwLDPii6R8vp0QtIquTiDIxcbFlY1ek=
golang.org/x/tools v0.0.0-20200330183114-f8bfb4ee3038/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

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

View File

@@ -1,383 +0,0 @@
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/chrome"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
)
func pingEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "ping")
}
func mergeEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "merge")
}
func htmlEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/html")
}
func urlEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/url")
}
func markdownEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/markdown")
}
func officeEndpoint(config conf.Config) string {
return fmt.Sprintf("%s%s", config.RootPath(), "convert/office")
}
func isMultipartFormDataEndpoint(config conf.Config, path string) bool {
var multipartFormDataEndpoints []string
multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint(config))
if !config.DisableGoogleChrome() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
htmlEndpoint(config),
urlEndpoint(config),
markdownEndpoint(config),
)
}
if !config.DisableUnoconv() {
multipartFormDataEndpoints = append(
multipartFormDataEndpoints,
officeEndpoint(config),
)
}
for _, endpoint := range multipartFormDataEndpoints {
if endpoint == path {
return true
}
}
return false
}
// pingHandler is the handler for healthcheck.
func pingHandler(c echo.Context) error {
const op string = "xhttp.pingHandler"
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
config := ctx.Config()
logger.DebugOp(op, "handling ping request...")
if !config.DisableGoogleChrome() {
_, err := chrome.IsViable(logger)
if err != nil {
return xerror.New(op, err)
}
}
return nil
}
// 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
}
opts.CustomHTTPHeaders = resource.RemoteURLCustomHTTPHeaders(r)
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.DebugOpf(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
return convertSync(ctx, p, filename, fpath)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
logger.DebugOpf(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.DebugOpf(
op,
"no '%s' found, using generated filename '%s'",
resource.RemoteURLArgKey,
filename,
)
if err := ctx.Attachment(fpath, filename); err != nil {
return err
}
return nil
}
logger.DebugOpf(
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()
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()
logger.DebugOpf(
op,
"preparing to send result file '%s' to '%s'...",
filename,
webhookURL,
)
httpClient := &http.Client{
Timeout: xtime.Duration(webhookURLTimeout),
}
req, err := http.NewRequest(http.MethodPost, webhookURL, f)
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
req.Header.Set(echo.HeaderContentType, "application/pdf")
// set custom headers (if any).
customHTTPHeaders := resource.WebhookURLCustomHTTPHeaders(r)
if len(customHTTPHeaders) > 0 {
for key, value := range customHTTPHeaders {
req.Header.Set(key, value)
logger.DebugOpf(op, "set '%s' to custom HTTP header '%s'", value, key)
}
} else {
logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...")
}
// send the result file.
logger.DebugOpf(
op,
"sending result file '%s' to '%s'...",
filename,
webhookURL,
)
resp, err := httpClient.Do(req) /* #nosec */
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
defer resp.Body.Close()
logger.DebugOpf(
op,
"result file '%s' sent to '%s'",
filename,
webhookURL,
)
}()
return nil
}

View File

@@ -1,635 +0,0 @@
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)
endpoint := pingEndpoint(config)
req := httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodPost, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
}
func TestMergeHandler(t *testing.T) {
config := conf.DefaultConfig()
srv := New(config)
endpoint := mergeEndpoint(config)
// should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is is > config.MaximumWaitTimeout().
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 400 as "waitTimeout" form field
// value is invalid.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
req = httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
// should return 504.
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
req = httptest.NewRequest(http.MethodPost, endpoint, 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 := htmlEndpoint(config)
// should return 200.
body, contentType := test.HTMLMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
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 := urlEndpoint(config)
// should return 200.
body, contentType := test.URLMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
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 := markdownEndpoint(config)
// should return 200.
body, contentType := test.MarkdownMultipartForm(t, nil)
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
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 := officeEndpoint(config)
// should return 200.
// note: increased the wait timeout as it happens to timeout from time to time.
body, contentType := test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "30"})
req := httptest.NewRequest(http.MethodPost, endpoint, body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// should return 405 as Method is wrong.
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
// should return 415 as Content-Type is wrong.
body, _ = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, endpoint, body)
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
// should return 400 as "waitTimeout" form field
// value is < 0.
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
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) {
customHeaderRealKey := http.CanonicalHeaderKey("MyCustomHeader")
customHeaderKey := fmt.Sprintf("%s%s", resource.WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderRealKey)
customHeaderValue := "foo"
status := make(chan error, 2)
rcv := echo.New()
rcv.POST("/foo", func(c echo.Context) error {
if c.Request().Header.Get(echo.HeaderContentType) != "application/pdf" {
status <- fmt.Errorf("wrong Content-type: got '%s' want '%s'", c.Request().Header.Get(echo.HeaderContentType), "application/pdf")
return nil
}
if c.Request().Header.Get(customHeaderRealKey) != customHeaderValue {
status <- fmt.Errorf("wrong '%s': got '%s' want '%s'", customHeaderRealKey, c.Request().Header.Get(customHeaderRealKey), customHeaderValue)
return nil
}
body, err := ioutil.ReadAll(c.Request().Body)
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(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
req.Header.Set(customHeaderKey, customHeaderValue)
test.AssertStatusCode(t, http.StatusOK, srv, req)
err := <-status
assert.NoError(t, err)
}
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(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get(echo.HeaderContentDisposition))
}

View File

@@ -1,153 +0,0 @@
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(config) && 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(config conf.Config) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := context.MustCastFromEchoContext(c)
err := next(ctx)
// we do not want to log healthcheck requests if
// log level is not set to DEBUG.
isDebug := ctx.Path() == pingEndpoint(config)
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

@@ -1,114 +0,0 @@
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
}
pageRanges, err := r.StringArg(resource.PageRangesArgKey, "")
if err != nil {
return printer.ChromePrinterOptions{}, err
}
googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
scale, err := resource.ScaleArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
return printer.ChromePrinterOptions{
WaitTimeout: waitTimeout,
WaitDelay: waitDelay,
HeaderHTML: headerHTML,
FooterHTML: footerHTML,
PaperWidth: paperWidth,
PaperHeight: paperHeight,
MarginTop: marginTop,
MarginBottom: marginBottom,
MarginLeft: marginLeft,
MarginRight: marginRight,
Landscape: landscape,
PageRanges: pageRanges,
RpccBufferSize: googleChromeRpccBufferSize,
CustomHTTPHeaders: make(map[string]string),
Scale: scale,
}, 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
}
pageRanges, err := r.StringArg(resource.PageRangesArgKey, "")
if err != nil {
return printer.OfficePrinterOptions{}, err
}
return printer.OfficePrinterOptions{
WaitTimeout: waitTimeout,
Landscape: landscape,
PageRanges: pageRanges,
}, nil
}
opts, err := resolver()
if err != nil {
return opts, xerror.New(op, err)
}
return opts, nil
}

View File

@@ -1,210 +0,0 @@
package context
import (
"fmt"
"io"
"net/http"
"path/filepath"
"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/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 custom headers from request.
for key, value := range ctx.Request().Header {
r.WithCustomHTTPHeader(key, value[0])
}
// retrieve form values from request.
for _, key := range resource.ArgKeys() {
r.WithArg(key, ctx.FormValue(string(key)))
}
// 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()
// avoid directory traversal.
filename := filepath.Base(fh.Filename)
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).ErrorOpf(op, "request failed")
return err
}
if isDebug {
ctx.logger.WithFields(fields).DebugOpf(op, "request handled")
return nil
}
ctx.logger.WithFields(fields).InfoOpf(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

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

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

View File

@@ -1,319 +0,0 @@
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"
// PageRangesArgKey is the key
// of the argument "pageRanges".
PageRangesArgKey ArgKey = "pageRanges"
// GoogleChromeRpccBufferSizeArgKey is the key
// of the argument "googleChromeRpccBufferSize".
GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize"
// ScaleArgKey is the key
// of the argument "scale".
ScaleArgKey ArgKey = "scale"
)
/*
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,
PageRangesArgKey,
GoogleChromeRpccBufferSizeArgKey,
ScaleArgKey,
}
}
/*
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
}
/*
ScaleArg is a helper for retrieving
the "scale" argument as float64.
*/
func ScaleArg(r Resource, config conf.Config) (float64, error) {
const op string = "resource.ScaleArg"
opts := printer.DefaultChromePrinterOptions(config)
result, err := r.Float64Arg(
ScaleArgKey,
opts.Scale,
xassert.Float64NotInferiorTo(0.1),
xassert.Float64NotSuperiorTo(2.0),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}

View File

@@ -1,396 +0,0 @@
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,
PageRangesArgKey,
GoogleChromeRpccBufferSizeArgKey,
ScaleArgKey,
}
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)
}
func TestScaleArg(t *testing.T) {
const (
resourceDirectoryName string = "foo"
defaultValue float64 = 1.0
)
var expected float64
logger := test.DebugLogger()
config := conf.DefaultConfig()
r, err := New(logger, resourceDirectoryName)
assert.Nil(t, err)
// argument does not exist.
expected = defaultValue
v, err := ScaleArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// argument exist.
expected = 0.5
r.WithArg(ScaleArgKey, "0.5")
v, err = ScaleArg(r, config)
assert.Nil(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is < 0.1.
expected = defaultValue
r.WithArg(ScaleArgKey, "-1.0")
v, err = ScaleArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as argument
// value is > 2.0.
expected = defaultValue
r.WithArg(ScaleArgKey, "2.1")
v, err = ScaleArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// should not be OK as
// argument value is invalid.
expected = defaultValue
r.WithArg(ScaleArgKey, "foo")
v, err = ScaleArg(r, config)
test.AssertError(t, err)
assert.Equal(t, expected, v)
// finally...
err = r.Close()
assert.Nil(t, err)
}

View File

@@ -1,8 +0,0 @@
/*
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

@@ -1,91 +0,0 @@
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()
if err := out.Chmod(0600); 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

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

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

View File

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

View File

@@ -1,258 +0,0 @@
package resource
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
/*
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
customHeaders map[string]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.DebugOpf(op, "resource directory '%s' created", directoryName)
return Resource{
logger: logger,
dirPath: dirPath,
customHeaders: make(map[string]string),
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.DebugOpf(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
return nil
}
if err := os.RemoveAll(r.dirPath); err != nil {
return xerror.New(op, err)
}
r.logger.DebugOpf(op, "resource directory '%s' removed", r.dirPath)
return nil
}
// WithCustomHTTPHeader add a new custom header to the Resource.
// Given key should be in canonical format.
func (r *Resource) WithCustomHTTPHeader(key string, value string) {
const op string = "resource.Resource.WithCustomHTTPHeader"
// should already be in canonical format.
canonicalKey := http.CanonicalHeaderKey(key)
if strings.Contains(canonicalKey, RemoteURLCustomHTTPHeaderCanonicalBaseKey) ||
strings.Contains(canonicalKey, WebhookURLCustomHTTPHeaderCanonicalBaseKey) {
r.customHeaders[canonicalKey] = value
r.logger.DebugOpf(op, "added '%s' with value '%s' to resource custom HTTP headers", canonicalKey, value)
return
}
r.logger.DebugOpf(op, "skipping '%s' as it is not a custom HTTP header...", canonicalKey)
}
// WithArg add a new argument to the Resource.
func (r *Resource) WithArg(key ArgKey, value string) {
const op string = "resource.Resource.WithArg"
r.args[key] = value
r.logger.DebugOpf(op, "added '%s' with value '%s' to resource args", key, value)
}
// WithFile add a new file to the Resource.
func (r *Resource) WithFile(filename string, in io.Reader) error {
const op string = "resource.Resource.WithFile"
resolver := func() error {
// see https://github.com/thecodingmachine/gotenberg/issues/104.
normalized, err := normalize.String(filename)
if err != nil {
return err
}
fpath := fmt.Sprintf("%s/%s", r.dirPath, normalized)
file := file{fpath: fpath}
if err := file.write(in); err != nil {
return err
}
r.files[filename] = file
r.logger.DebugOpf(op, "resource file '%s' created", filename)
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
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

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

@@ -1,31 +0,0 @@
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(config))
srv.Use(cleanupMiddleware())
srv.Use(errorMiddleware())
srv.GET(pingEndpoint(config), pingHandler)
srv.POST(mergeEndpoint(config), mergeHandler)
if config.DisableGoogleChrome() && config.DisableUnoconv() {
return srv
}
if !config.DisableGoogleChrome() {
srv.POST(htmlEndpoint(config), htmlHandler)
srv.POST(urlEndpoint(config), urlHandler)
srv.POST(markdownEndpoint(config), markdownHandler)
}
if !config.DisableUnoconv() {
srv.POST(officeEndpoint(config), officeHandler)
}
return srv
}

View File

@@ -1,171 +0,0 @@
package xhttp
import (
"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(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 404.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// URL endpoint should return 404.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Markdown endpoint should return 404.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Office endpoint should return 200.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), 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(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 200.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// URL endpoint should return 200.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Markdown endpoint should return 200.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Office endpoint should return 404.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// finally...
os.Setenv(conf.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(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 404.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// URL endpoint should return 404.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Markdown endpoint should return 404.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// Office endpoint should return 404.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
// finally...
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
os.Setenv(conf.DisableUnoconvEnvVar, "0")
}
func TestCustomRootPath(t *testing.T) {
os.Setenv(conf.RootPathEnvVar, "/foo/")
config, err := conf.FromEnv()
assert.Nil(t, err)
srv := New(config)
// Ping endpoint should return 200.
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Merge endpoint should return 200.
body, contentType := test.MergeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// HTML endpoint should return 200.
body, contentType = test.HTMLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// URL endpoint should return 200.
body, contentType = test.URLMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Markdown endpoint should return 200.
body, contentType = test.MarkdownMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// Office endpoint should return 200.
body, contentType = test.OfficeMultipartForm(t, nil)
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
req.Header.Set(echo.HeaderContentType, contentType)
test.AssertStatusCode(t, http.StatusOK, srv, req)
// finally...
os.Setenv(conf.RootPathEnvVar, "/")
}

View File

@@ -1,186 +0,0 @@
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, ignoreCertificateErrors bool) error {
const op string = "chrome.Start"
logger.DebugOp(op, "starting new Google Chrome headless process on port 9222...")
resolver := func() error {
cmd, err := cmd(logger, ignoreCertificateErrors)
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.
isViable, _ := IsViable(logger)
if !isViable {
return restart(logger, cmd.Process, ignoreCertificateErrors)
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func cmd(logger xlog.Logger, ignoreCertificateErrors bool) (*exec.Cmd, error) {
const op string = "chrome.cmd"
binary := "google-chrome-stable"
args := []string{
"--no-sandbox",
"--headless",
// see https://github.com/thecodingmachine/gotenberg/issues/157.
"--disable-dev-shm-usage",
// See https://github.com/puppeteer/puppeteer/issues/661
// and https://github.com/puppeteer/puppeteer/issues/2410.
"--font-render-hinting=none",
"--remote-debugging-port=9222",
"--disable-gpu",
"--disable-translate",
"--disable-extensions",
"--disable-background-networking",
"--safebrowsing-disable-auto-update",
"--disable-sync",
"--disable-default-apps",
"--hide-scrollbars",
"--metrics-recording-only",
"--mute-audio",
"--no-first-run",
}
if ignoreCertificateErrors {
args = append(args, "--ignore-certificate-errors")
}
cmd, err := xexec.Command(logger, binary, args...)
if err != nil {
return nil, xerror.New(op, err)
}
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, ignoreCertificateErrors bool) 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, ignoreCertificateErrors)
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.
isViable, _ := IsViable(logger)
if !isViable {
return restart(logger, cmd.Process, ignoreCertificateErrors)
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// IsViable checks if Google Chrome is healthy.
func IsViable(logger xlog.Logger) (bool, error) {
const (
op string = "chrome.IsViable"
maxViabilityTests int = 20
)
viable := func() (bool, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
endpoint := "http://localhost:9222"
logger.DebugOpf(
op,
"checking Google Chrome headless process viability via endpoint '%s/json/version'",
endpoint,
)
v, err := devtool.New(endpoint).Version(ctx)
if err != nil {
logger.DebugOpf(
op,
"Google Chrome headless is not viable as endpoint returned '%v'",
err.Error(),
)
return false, err
}
logger.DebugOpf(
op,
"Google Chrome headless is viable as endpoint returned '%v'",
v,
)
return true, nil
}
result := false
var err error
for i := 0; i < maxViabilityTests && !result; i++ {
warmup(logger)
result, err = viable()
}
return result, err
}
func warmup(logger xlog.Logger) {
const (
op string = "chrome.warmup"
seconds float64 = 0.5
)
warmupTime := xtime.Duration(seconds)
logger.DebugOpf(
op,
"waiting '%v' for allowing Google Chrome to warmup",
warmupTime,
)
time.Sleep(warmupTime)
}

View File

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

View File

@@ -1,293 +0,0 @@
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"
// RootPathEnvVar contains the name
// of the environment variable "ROOT_PATH".
RootPathEnvVar string = "ROOT_PATH"
// DefaultGoogleChromeRpccBufferSizeEnvVar contains the name
// of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE".
DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE"
// GoogleChromeIgnoreCertificateErrorsEnvVar contains the name
// of the environment variable "GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS".
GoogleChromeIgnoreCertificateErrorsEnvVar string = "GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS"
)
// Config contains the application
// configuration.
type Config struct {
maximumWaitTimeout float64
maximumWaitDelay float64
maximumWebhookURLTimeout float64
defaultWaitTimeout float64
defaultWebhookURLTimeout float64
defaultListenPort int64
disableGoogleChrome bool
disableUnoconv bool
googleChromeIgnoreCertificateErrors bool
logLevel xlog.Level
rootPath string
maximumGoogleChromeRpccBufferSize int64
defaultGoogleChromeRpccBufferSize int64
}
// DefaultConfig returns the default
// configuration.
func DefaultConfig() Config {
return Config{
maximumWaitTimeout: 30.0,
maximumWaitDelay: 10.0,
maximumWebhookURLTimeout: 30.0,
defaultWaitTimeout: 10.0,
defaultWebhookURLTimeout: 10.0,
defaultListenPort: 3000,
disableGoogleChrome: false,
disableUnoconv: false,
logLevel: xlog.InfoLevel,
rootPath: "/",
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
googleChromeIgnoreCertificateErrors: false,
}
}
/*
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
}
rootPath, err := xassert.StringFromEnv(
RootPathEnvVar,
c.rootPath,
xassert.StringStartWith("/"),
xassert.StringEndWith("/"),
)
c.rootPath = rootPath
if err != nil {
return c, err
}
defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv(
DefaultGoogleChromeRpccBufferSizeEnvVar,
c.defaultGoogleChromeRpccBufferSize,
xassert.Int64NotInferiorTo(0),
xassert.Int64NotSuperiorTo(c.MaximumGoogleChromeRpccBufferSize()),
)
c.defaultGoogleChromeRpccBufferSize = defaultGoogleChromeRpccBufferSize
if err != nil {
return c, err
}
googleChromeIgnoreCertificateErrors, err := xassert.BoolFromEnv(
GoogleChromeIgnoreCertificateErrorsEnvVar,
c.googleChromeIgnoreCertificateErrors,
)
c.googleChromeIgnoreCertificateErrors = googleChromeIgnoreCertificateErrors
if err != nil {
return c, err
}
return c, nil
}
result, err := resolver()
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
}
// RootPath returns the rooth path from
// the configuration.
func (c Config) RootPath() string {
return c.rootPath
}
// MaximumGoogleChromeRpccBufferSize returns the maximum
// Google Chrome rpcc buffer size from the configuration.
func (c Config) MaximumGoogleChromeRpccBufferSize() int64 {
return c.maximumGoogleChromeRpccBufferSize
}
// DefaultGoogleChromeRpccBufferSize returns the default
// Google Chrome rpcc buffer size from the configuration.
func (c Config) DefaultGoogleChromeRpccBufferSize() int64 {
return c.defaultGoogleChromeRpccBufferSize
}
func (c Config) GoogleChromeIgnoreCertificateErrors() bool {
return c.googleChromeIgnoreCertificateErrors
}

View File

@@ -1,435 +0,0 @@
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 TestRootPathFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// ROOT_PATH correctly set.
os.Setenv(RootPathEnvVar, "/foo/")
expected = DefaultConfig()
expected.rootPath = "/foo/"
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(RootPathEnvVar)
// ROOT_PATH wrongly set.
os.Setenv(RootPathEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(RootPathEnvVar)
}
func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) {
var (
expected Config
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 TestGoogleChromeIgnoreCertificateErrorsFromEnv(t *testing.T) {
var (
expected Config
result Config
err error
)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS correctly set to true.
os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "1")
expected = DefaultConfig()
expected.googleChromeIgnoreCertificateErrors = true
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS correctly set to false.
os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "0")
expected = DefaultConfig()
expected.googleChromeIgnoreCertificateErrors = false
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS wrongly set.
os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "foo")
expected = DefaultConfig()
result, err = FromEnv()
test.AssertError(t, err)
assert.Equal(t, expected, result)
os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar)
// GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS not set at all.
expected = DefaultConfig()
expected.googleChromeIgnoreCertificateErrors = false
result, err = FromEnv()
assert.Nil(t, err)
assert.Equal(t, expected, result)
}
func TestGetters(t *testing.T) {
result := DefaultConfig()
assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout())
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.rootPath, result.RootPath())
assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize())
assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize())
assert.Equal(t, result.googleChromeIgnoreCertificateErrors, result.GoogleChromeIgnoreCertificateErrors())
}

View File

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

View File

@@ -1,9 +0,0 @@
/*
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

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