Compare commits
362 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b76990ec16 | ||
|
|
9bc93c5cf5 | ||
|
|
1d8ae79f84 | ||
|
|
4269aa2422 | ||
|
|
c887dc1ec8 | ||
|
|
8bbacd691d | ||
|
|
d8f3f85fb7 | ||
|
|
159c0b3350 | ||
|
|
45788f3235 | ||
|
|
4c85c6ccdf | ||
|
|
08408ae005 | ||
|
|
d470279fb7 | ||
|
|
8260458f8f | ||
|
|
0512dd1e86 | ||
|
|
4972019cb9 | ||
|
|
5e615cab52 | ||
|
|
64ea2b6d3b | ||
|
|
27018fa8e2 | ||
|
|
263d634801 | ||
|
|
c61c33f71c | ||
|
|
bfb30b8fed | ||
|
|
ca3e27937e | ||
|
|
50c34a4186 | ||
|
|
84875aaf82 | ||
|
|
b7a8b95820 | ||
|
|
5cad5ca11a | ||
|
|
7598de29dd | ||
|
|
af6ffe67c8 | ||
|
|
6944c4b3bf | ||
|
|
24817d5707 | ||
|
|
3d855712ee | ||
|
|
872263d253 | ||
|
|
1c491fb1c5 | ||
|
|
29374c579a | ||
|
|
206d7b6ca9 | ||
|
|
ab6ca8c894 | ||
|
|
e7f5fde051 | ||
|
|
030b358b0e | ||
|
|
4b1791d35c | ||
|
|
b788cf823d | ||
|
|
11cbc0288f | ||
|
|
2269c80b02 | ||
|
|
c922cc042e | ||
|
|
9cb6f44f72 | ||
|
|
f376cd5ab1 | ||
|
|
8b8a02b68c | ||
|
|
5759b1336a | ||
|
|
76f17cb6d7 | ||
|
|
7149c421cf | ||
|
|
353c599767 | ||
|
|
285c43dab7 | ||
|
|
bd5933f4c2 | ||
|
|
0faac08237 | ||
|
|
14f94b1566 | ||
|
|
415e2ff265 | ||
|
|
26e97358d1 | ||
|
|
0ed6fb33c5 | ||
|
|
df98e7512b | ||
|
|
99ede6477f | ||
|
|
ee0debe14a | ||
|
|
dd1e11f102 | ||
|
|
ed47493459 | ||
|
|
5b8391067d | ||
|
|
4f41a59ceb | ||
|
|
4525cb38e4 | ||
|
|
704dfc25e6 | ||
|
|
7022ebe4ca | ||
|
|
094863f0ea | ||
|
|
a6282125cf | ||
|
|
63387a09af | ||
|
|
d4c47cecf2 | ||
|
|
b839069af1 | ||
|
|
e3c98100cc | ||
|
|
afcce04650 | ||
|
|
5fb35c2d48 | ||
|
|
dfe89f0d83 | ||
|
|
c500ec071e | ||
|
|
6e47f16fe1 | ||
|
|
13e6c2dbeb | ||
|
|
9d27dfb41c | ||
|
|
9529905875 | ||
|
|
eef7fc4b51 | ||
|
|
a88435f6b6 | ||
|
|
0b22598783 | ||
|
|
40cc019a51 | ||
|
|
781094ab05 | ||
|
|
21cfe2e535 | ||
|
|
4f72684080 | ||
|
|
5ac514366e | ||
|
|
d5d6285eef | ||
|
|
0e34b41bce | ||
|
|
e157066bcc | ||
|
|
f43a63ac6e | ||
|
|
a5828fa01a | ||
|
|
662d3ff795 | ||
|
|
a97f05db3b | ||
|
|
5a439ca70c | ||
|
|
551faca3a3 | ||
|
|
c25ffe9639 | ||
|
|
900021ec1b | ||
|
|
cc1ad16688 | ||
|
|
fabac9fcd9 | ||
|
|
da170b9b16 | ||
|
|
0e417430b8 | ||
|
|
0cfac31bca | ||
|
|
0f5e8fd314 | ||
|
|
e457155950 | ||
|
|
5a98790dd3 | ||
|
|
e188f65e7c | ||
|
|
6bf3ea3c46 | ||
|
|
fa33ac57fb | ||
|
|
0bed9eea09 | ||
|
|
36e9f7a8cb | ||
|
|
b155dbef6a | ||
|
|
e48b428725 | ||
|
|
68884c9126 | ||
|
|
a713976f9c | ||
|
|
51d7306380 | ||
|
|
0b83f75ec8 | ||
|
|
0a80635943 | ||
|
|
e6a5f4b203 | ||
|
|
061b2fdbcb | ||
|
|
7922e32618 | ||
|
|
0949d10da6 | ||
|
|
0c8ad9262e | ||
|
|
f841aef82d | ||
|
|
d42e51315d | ||
|
|
e761b73cfd | ||
|
|
1a9c9a7767 | ||
|
|
eb7ce7cef1 | ||
|
|
68fd8dcd76 | ||
|
|
ce8bb2567c | ||
|
|
ab3d9bc120 | ||
|
|
432ec161c1 | ||
|
|
fcaf4a9b51 | ||
|
|
cbb7e6599b | ||
|
|
8f89a144ca | ||
|
|
50bd05b351 | ||
|
|
4e7f5401ec | ||
|
|
3fd56e8c36 | ||
|
|
daf028ab2b | ||
|
|
5467620b32 | ||
|
|
a3cae0f094 | ||
|
|
f74eea4314 | ||
|
|
04e930a766 | ||
|
|
89ac13e805 | ||
|
|
0b86d38517 | ||
|
|
7e76809860 | ||
|
|
4d46b98e7f | ||
|
|
6c1a46aee5 | ||
|
|
a7cef5a1c6 | ||
|
|
8a218a6626 | ||
|
|
542fe18dcd | ||
|
|
25f18ac951 | ||
|
|
5540438595 | ||
|
|
b25456835a | ||
|
|
f86091382f | ||
|
|
dbdf8871cc | ||
|
|
1e6926ee2d | ||
|
|
24cbe98ac2 | ||
|
|
c590b026d6 | ||
|
|
d28221aec9 | ||
|
|
df0c6c8c2d | ||
|
|
876f0f9a14 | ||
|
|
f9f0fc1d67 | ||
|
|
42e357341a | ||
|
|
edc5402aad | ||
|
|
3f61ffa6fb | ||
|
|
1c22b889fe | ||
|
|
e80f5939b2 | ||
|
|
c068088cc1 | ||
|
|
1767f371f7 | ||
|
|
5d5caf64fa | ||
|
|
e095b9419a | ||
|
|
a5ef072f56 | ||
|
|
5f6a2456d4 | ||
|
|
b0f24889f4 | ||
|
|
0a69f0fd51 | ||
|
|
43c0142682 | ||
|
|
1ff16cc54b | ||
|
|
3609cf54c0 | ||
|
|
198038f5ea | ||
|
|
b5a3df85a8 | ||
|
|
6e56c6e0d4 | ||
|
|
568f3d6007 | ||
|
|
6b505a1d06 | ||
|
|
acc7e91c55 | ||
|
|
8e43df8d95 | ||
|
|
ca81b382d3 | ||
|
|
a60d7976f5 | ||
|
|
46cf63da37 | ||
|
|
099079b962 | ||
|
|
5b8121516f | ||
|
|
23eacaa78a | ||
|
|
659292a3f1 | ||
|
|
f88756d3a8 | ||
|
|
81e7bab22a | ||
|
|
1eeb5fc2b0 | ||
|
|
126cdd73e4 | ||
|
|
2251267ae4 | ||
|
|
f3e4b6d9a2 | ||
|
|
b22379537b | ||
|
|
feb5bb0653 | ||
|
|
ebfe99be79 | ||
|
|
49c1b939d5 | ||
|
|
84a37f124b | ||
|
|
f0f48f4ddf | ||
|
|
cf15e3a9a1 | ||
|
|
927f98c66b | ||
|
|
782f6ac27e | ||
|
|
5140e4ec9a | ||
|
|
e255f9bfc5 | ||
|
|
9e9b369dc2 | ||
|
|
765dc734a4 | ||
|
|
3abe8a1103 | ||
|
|
73333074fd | ||
|
|
a415786e1c | ||
|
|
a3a1077dea | ||
|
|
58a812c2c9 | ||
|
|
7e69ec4367 | ||
|
|
4623c01896 | ||
|
|
e85322de62 | ||
|
|
090ff3da13 | ||
|
|
fe068b1bfb | ||
|
|
d3b0a685b9 | ||
|
|
245c04fc5a | ||
|
|
94acb0c6ae | ||
|
|
ad4a16cc4e | ||
|
|
dae39113f6 | ||
|
|
1e1cb8fe57 | ||
|
|
606254d941 | ||
|
|
8fd51b9e9e | ||
|
|
f90442cfb1 | ||
|
|
2a44c0f65a | ||
|
|
cd8d2b5e95 | ||
|
|
8d89b2e80a | ||
|
|
3255f8af0c | ||
|
|
ca0784fa59 | ||
|
|
827060b6ce | ||
|
|
60c069cb1f | ||
|
|
70dfea5876 | ||
|
|
ab475d65ee | ||
|
|
fa9963a737 | ||
|
|
d65d9c9a4c | ||
|
|
6f8c7a613f | ||
|
|
1b45c927b2 | ||
|
|
ab1cc896de | ||
|
|
4b91600e35 | ||
|
|
69e43e40eb | ||
|
|
930622ea3c | ||
|
|
7ac104c3c6 | ||
|
|
c47dc5ecdb | ||
|
|
a3f05c1146 | ||
|
|
04b1fa378d | ||
|
|
cbc9d9ab8c | ||
|
|
7f61c63425 | ||
|
|
3d379dec58 | ||
|
|
9968a816db | ||
|
|
8f721cd12a | ||
|
|
d45f2e1ec7 | ||
|
|
7e0f1794a5 | ||
|
|
20959fc68f | ||
|
|
a942224f46 | ||
|
|
fef16f6b28 | ||
|
|
1abc4d0365 | ||
|
|
862761d46f | ||
|
|
94743a16be | ||
|
|
f6729834b6 | ||
|
|
1b4c435900 | ||
|
|
24e6a86bf5 | ||
|
|
88f74d5e9c | ||
|
|
171f93662f | ||
|
|
c5429f7efa | ||
|
|
36847e6a06 | ||
|
|
438fd7d825 | ||
|
|
767b61a3bb | ||
|
|
dd2542dcd6 | ||
|
|
f8b63d487a | ||
|
|
fcd0feb7f6 | ||
|
|
8287cf386b | ||
|
|
8415bee5da | ||
|
|
30eb48321c | ||
|
|
7cff2e01a4 | ||
|
|
fa94341b60 | ||
|
|
a1b34d5e7b | ||
|
|
14ef3cf07c | ||
|
|
db6cddefd4 | ||
|
|
95c6ecb724 | ||
|
|
fc27f6b3f0 | ||
|
|
2ce0bb2caa | ||
|
|
94e1c006ab | ||
|
|
fc2e3586a2 | ||
|
|
bca8b6b606 | ||
|
|
bb6a3471d5 | ||
|
|
09d07e4890 | ||
|
|
8826ad8990 | ||
|
|
b350edc250 | ||
|
|
af326c0b1e | ||
|
|
edc346a6b3 | ||
|
|
1e3fc97f2b | ||
|
|
51b78ef5f3 | ||
|
|
4e4036382b | ||
|
|
daf7cb9d6b | ||
|
|
08c97f280f | ||
|
|
9c836a34ee | ||
|
|
e839430270 | ||
|
|
cf5e530ed9 | ||
|
|
49ac5e647f | ||
|
|
5a7b3ded20 | ||
|
|
bcf06f0d69 | ||
|
|
4f3fe28905 | ||
|
|
f695861731 | ||
|
|
2fee966fc8 | ||
|
|
6964124c1b | ||
|
|
f9e2d91e48 | ||
|
|
2c483e7f7c | ||
|
|
3d078108a3 | ||
|
|
794967bd9f | ||
|
|
554474776c | ||
|
|
81c5f9cd4e | ||
|
|
e4735401a7 | ||
|
|
758c3be3c5 | ||
|
|
2be9ae7868 | ||
|
|
3ee4540f7d | ||
|
|
84c1175379 | ||
|
|
ad4d87a138 | ||
|
|
5feff0d0ec | ||
|
|
08e7a2c065 | ||
|
|
a524521fdb | ||
|
|
0b2312a407 | ||
|
|
c7d9e24c00 | ||
|
|
0d827e490e | ||
|
|
6402125233 | ||
|
|
7bfbda4490 | ||
|
|
f6b357691c | ||
|
|
0c1e4e6888 | ||
|
|
c7ecdcf625 | ||
|
|
957b9b1cf4 | ||
|
|
1b4eed1a6e | ||
|
|
acd47b870f | ||
|
|
9ab8d03697 | ||
|
|
9c6f23d7cc | ||
|
|
9af00d6d58 | ||
|
|
186c09f29b | ||
|
|
70b185a37a | ||
|
|
5af01505c2 | ||
|
|
5f6ab2fc08 | ||
|
|
c8f7ea934c | ||
|
|
a4aa8dafac | ||
|
|
8ca9866440 | ||
|
|
97eff7f21d | ||
|
|
e30fd8eb9b | ||
|
|
6fa48c9222 | ||
|
|
c36c7144cb | ||
|
|
bbd0a893b1 | ||
|
|
cfea2b8d9d | ||
|
|
0fcbe821cd | ||
|
|
ca41164980 | ||
|
|
cf20e2a510 | ||
|
|
02f1231e7d | ||
|
|
6846e7941f | ||
|
|
719287c3a6 |
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.git
|
||||||
46
.github/CODE_OF_CONDUCT.md
vendored
@@ -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/
|
|
||||||
39
.github/CONTRIBUTING.md
vendored
@@ -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.12)
|
|
||||||
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)
|
|
||||||
32
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,32 +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.
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
45
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 (`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
@@ -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
|
||||||
27
.github/workflows/continuous_delivery.yml
vendored
Normal 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
|
||||||
43
.github/workflows/continuous_integration.yml
vendored
Normal 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)
|
||||||
2
.gitignore
vendored
@@ -0,0 +1,2 @@
|
|||||||
|
/coverage.html
|
||||||
|
/coverage.txt
|
||||||
27
.golangci.yml
Normal 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
|
||||||
25
.travis.yml
@@ -1,25 +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
|
|
||||||
- stage: tests
|
|
||||||
script: make tests
|
|
||||||
- stage: publish
|
|
||||||
if: tag IS present
|
|
||||||
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS
|
|
||||||
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019 TheCodingMachine
|
Copyright (c) 2022 Julien Neuhart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
182
Makefile
@@ -1,38 +1,156 @@
|
|||||||
GOLANG_VERSION=1.12
|
.PHONY: help
|
||||||
VERSION=snapshot
|
help: ## Show the help
|
||||||
DOCKER_USER=
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
DOCKER_PASSWORD=
|
|
||||||
|
|
||||||
# generate documentation.
|
.PHONY: it
|
||||||
doc:
|
it: build build-tests ## Initialize the development environment
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:docs -f build/docs/Dockerfile .
|
|
||||||
docker run --rm -it -v "$(PWD):/docs" thecodingmachine/gotenberg:docs
|
|
||||||
|
|
||||||
# gofmt and goimports all go files.
|
GOLANG_VERSION=1.19
|
||||||
fmt:
|
DOCKER_REPOSITORY=gotenberg
|
||||||
|
GOTENBERG_VERSION=snapshot
|
||||||
|
GOTENBERG_USER_GID=1001
|
||||||
|
GOTENBERG_USER_UID=1001
|
||||||
|
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.
|
||||||
|
|
||||||
|
.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 .
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
.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 fmt ./...
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
# run all linters.
|
.PHONY: godoc
|
||||||
lint:
|
godoc: ## Run a webserver with Gotenberg godoc (go get golang.org/x/tools/cmd/godoc)
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:lint -f build/lint/Dockerfile .
|
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v7)
|
||||||
docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg:lint
|
godoc -http=:6060
|
||||||
|
|
||||||
# run all tests.
|
.PHONY: release
|
||||||
tests:
|
release: ## Build the Gotenberg's Docker image for many platforms, then push it to a Docker repository
|
||||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
./scripts/release.sh \
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:tests -f build/tests/Dockerfile .
|
$(GOLANG_VERSION) \
|
||||||
docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg:tests
|
$(GOTENBERG_VERSION) \
|
||||||
|
$(GOTENBERG_USER_GID) \
|
||||||
# build Docker image.
|
$(GOTENBERG_USER_UID) \
|
||||||
image:
|
$(NOTO_COLOR_EMOJI_VERSION) \
|
||||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
$(PDFTK_VERSION) \
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
$(DOCKER_REPOSITORY)
|
||||||
|
|
||||||
# start the API using previously built Docker image.
|
|
||||||
gotenberg:
|
|
||||||
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
|
|
||||||
|
|
||||||
# publish Gotenberg images according to version.
|
|
||||||
publish:
|
|
||||||
./scripts/publish.sh $(GOLANG_VERSION) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
|
||||||
|
|||||||
58
README.md
@@ -1,43 +1,45 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
|
<img src="https://user-images.githubusercontent.com/8983173/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> · 🔥 <a href="https://gotenberg.dev/docs/get-started/live-demo">Live Demo</a></p>
|
||||||
</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> · <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
|
## Quick Start
|
||||||
|
|
||||||
* HTML and Markdown conversions using Google Chrome headless
|
|
||||||
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
|
||||||
* Performance :zap:: Google Chrome and LibreOffice (unoconv) started once in the background thanks to PM2
|
|
||||||
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
|
|
||||||
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
|
||||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
Open a terminal and run the following command:
|
Open a terminal and run the following command:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
|
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
|
## Badges
|
||||||
|
|
||||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:5)
|
[](https://hub.docker.com/r/gotenberg/gotenberg)
|
||||||
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
[](https://hub.docker.com/r/thecodingmachine/gotenberg)
|
||||||
[](https://godoc.org/github.com/thecodingmachine/gotenberg)
|
[](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
|
||||||
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
|
||||||
|
[](https://codecov.io/gh/gotenberg/gotenberg)
|
||||||
---
|
|
||||||
|
|
||||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
|
||||||
|
|||||||
171
build/Dockerfile
Normal 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
@@ -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
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
FROM thecodingmachine/gotenberg:3.2.0 AS hack
|
|
||||||
FROM debian:9.5-slim
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Common libraries
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Libraries used in the build process of this image.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y curl wget python3-pip ttf-mscorefonts-installer
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | PM2
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs PM2 for launching programs in background and with failure
|
|
||||||
# | recovering. In our case: Chrome (headless) and Office (headless).
|
|
||||||
# |
|
|
||||||
|
|
||||||
# Yep, this is dirty. The following script does not work anymore (see https://github.com/Unitech/pm2/issues/4127):
|
|
||||||
#RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
|
|
||||||
|
|
||||||
# Installing PM2 with Node.js and npm breaks something which prevents Google Chrome to work as expected.
|
|
||||||
# So we just copy all PM2 related files from a previous version of Gotenberg.
|
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
|
||||||
apt-get install -y nodejs
|
|
||||||
|
|
||||||
COPY --from=hack /usr/bin/pm2 /usr/bin/pm2
|
|
||||||
COPY --from=hack /usr/share/pm2 /usr/share/pm2
|
|
||||||
COPY --from=hack /etc/default/pm2 /etc/default/pm2
|
|
||||||
COPY --from=hack /etc/systemd/system/pm2.service /etc/systemd/system/pm2.service
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | 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
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Unoconv
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs unoconv and LibreOffice.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN pip3 install unoconv &&\
|
|
||||||
# https://github.com/nextcloud/docker/issues/380
|
|
||||||
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
|
||||||
apt-get -y install libreoffice
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | 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 \
|
|
||||||
ttf-liberation \
|
|
||||||
ttf-wqy-zenhei \
|
|
||||||
fonts-arphic-uming \
|
|
||||||
fonts-ipafont-mincho \
|
|
||||||
fonts-ipafont-gothic \
|
|
||||||
fonts-unfonts-core
|
|
||||||
|
|
||||||
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
ARG GOLANG_VERSION
|
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | 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 /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." ]
|
|
||||||
@@ -1,12 +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)
|
|
||||||
* Performance: Google Chrome and LibreOffice (unoconv) started once in the background thanks to PM2
|
|
||||||
* Failure prevention: PM2 automatically restarts previous processes if they fail
|
|
||||||
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
|
||||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
|
||||||
@@ -1,42 +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:5
|
|
||||||
```
|
|
||||||
|
|
||||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
|
||||||
|
|
||||||
## Docker Compose
|
|
||||||
|
|
||||||
You may also add it in your Docker Compose stack:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# your others services
|
|
||||||
|
|
||||||
gotenberg:
|
|
||||||
image: thecodingmachine/gotenberg:5
|
|
||||||
```
|
|
||||||
|
|
||||||
> 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).
|
|
||||||
Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoconv).
|
|
||||||
|
|
||||||
> The more resources are granted, the quicker will be the conversions.
|
|
||||||
|
|
||||||
In the following examples, we will assume your
|
|
||||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
|
||||||
@@ -1,25 +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/v5
|
|
||||||
```
|
|
||||||
|
|
||||||
## PHP client
|
|
||||||
|
|
||||||
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require php-http/guzzle6-adapter
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the PHP client:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require thecodingmachine/gotenberg-php-client
|
|
||||||
```
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
title: Environment variables
|
|
||||||
---
|
|
||||||
|
|
||||||
You may customize the API behaviour thanks to environment variables.
|
|
||||||
|
|
||||||
## Disable Google Chrome
|
|
||||||
|
|
||||||
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`.
|
|
||||||
|
|
||||||
It takes the strings `"0"` or `"1"` as value.
|
|
||||||
|
|
||||||
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
|
|
||||||
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
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).
|
|
||||||
@@ -1,342 +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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
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', 'index.html');
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$dirPath = "/foo";
|
|
||||||
$filename = $client->store($request, $dirPath);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
|
||||||
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
|
||||||
|
|
||||||
### 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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
req.Header("header.html")
|
|
||||||
req.Footer("footer.html")
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
```php
|
|
||||||
use TheCodingMachine\Gotenberg\Client;
|
|
||||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
|
||||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
|
||||||
|
|
||||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
|
||||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
|
||||||
$header = DocumentFactory::makeFromPath('header.html', 'header.html');
|
|
||||||
$footer = DocumentFactory::makeFromPath('footer.html', 'footer.html');
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$request->setHeader($header);
|
|
||||||
$request->setFooter($footer);
|
|
||||||
$dirPath = "/foo";
|
|
||||||
$filename = $client->store($request, $dirPath);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 others 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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
req.Assets("font.woff", "img.gif", "style.css")
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
```php
|
|
||||||
use TheCodingMachine\Gotenberg\Client;
|
|
||||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
|
||||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
|
||||||
|
|
||||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
|
||||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
|
||||||
$assets = [
|
|
||||||
DocumentFactory::makeFromPath('style.css', 'style.css'),
|
|
||||||
DocumentFactory::makeFromPath('img.png', 'img.png'),
|
|
||||||
DocumentFactory::makeFromPath('font.woff', 'font.woff'),
|
|
||||||
];
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$request->setAssets($assets);
|
|
||||||
$dest = "result.pdf";
|
|
||||||
$client->store($request, $dest);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Paper size, margins, orientation
|
|
||||||
|
|
||||||
You may also customize the resulting PDF format.
|
|
||||||
|
|
||||||
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
|
|
||||||
|
|
||||||
> Paper size and margins have to be provided in `inches`. 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.27 \
|
|
||||||
--form marginTop=0 \
|
|
||||||
--form marginBottom=0 \
|
|
||||||
--form marginLeft=0 \
|
|
||||||
--form marginRight=0 \
|
|
||||||
--form landscape=true \
|
|
||||||
-o result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
req.PaperSize(gotenberg.A4)
|
|
||||||
req.Margins(gotenberg.NoMargins)
|
|
||||||
req.Landscape(true)
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
```php
|
|
||||||
use TheCodingMachine\Gotenberg\Client;
|
|
||||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
|
||||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
|
||||||
use TheCodingMachine\Gotenberg\Request;
|
|
||||||
|
|
||||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
|
||||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$request->setPaperSize(Request::A4);
|
|
||||||
$request->setMargins(Request::NO_MARGINS);
|
|
||||||
$request->setLandscape(true);
|
|
||||||
$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.
|
|
||||||
|
|
||||||
> 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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
req.WaitDelay(5.5)
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
```php
|
|
||||||
use TheCodingMachine\Gotenberg\Client;
|
|
||||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
|
||||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
|
||||||
use TheCodingMachine\Gotenberg\Request;
|
|
||||||
|
|
||||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
|
||||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$request->setWaitDelay(5.5);
|
|
||||||
$dest = "result.pdf";
|
|
||||||
$client->store($request, $dest);
|
|
||||||
```
|
|
||||||
@@ -1,56 +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.
|
|
||||||
|
|
||||||
### 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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := gotenberg.NewURLRequest("https://google.com")
|
|
||||||
req.Margins(gotenberg.NoMargins)
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
```php
|
|
||||||
use TheCodingMachine\Gotenberg\Client;
|
|
||||||
use TheCodingMachine\Gotenberg\URLRequest;
|
|
||||||
|
|
||||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
|
||||||
$request = new URLRequest('https://google.com');
|
|
||||||
$request->setMargins(Request::NO_MARGINS);
|
|
||||||
$dest = "result.pdf";
|
|
||||||
$client->store($request, $dest);
|
|
||||||
```
|
|
||||||
@@ -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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
|
|
||||||
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', 'index.html');
|
|
||||||
$markdowns = [
|
|
||||||
DocumentFactory::makeFromPath('file.md', 'file.md'),
|
|
||||||
];
|
|
||||||
$request = new MarkdownRequest($index, $markdowns);
|
|
||||||
$dest = "result.pdf";
|
|
||||||
$client->store($request, $dest);
|
|
||||||
```
|
|
||||||
@@ -1,119 +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:** currently, `unoconv` cannot perform concurrent conversions.
|
|
||||||
> That's why for Office conversions, the API does only one conversion at a time.
|
|
||||||
> See the [scalability section](#scalability) to find how to mitigate this issue.
|
|
||||||
|
|
||||||
### 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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
|
|
||||||
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', 'document.docx'),
|
|
||||||
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
|
|
||||||
];
|
|
||||||
$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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewOfficeRequest("document.docx")
|
|
||||||
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', 'document.docx'),
|
|
||||||
];
|
|
||||||
$request = new OfficeRequest($files);
|
|
||||||
$request->setLandscape(true);
|
|
||||||
$dest = "result.pdf";
|
|
||||||
$client->store($request, $dest);
|
|
||||||
```
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
title: Merge
|
|
||||||
---
|
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/convert/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/convert/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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
|
|
||||||
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', 'file.pdf'),
|
|
||||||
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
|
|
||||||
];
|
|
||||||
$request = new MergeRequest($files);
|
|
||||||
$dest = "result.pdf";
|
|
||||||
$client->store($request, $dest);
|
|
||||||
```
|
|
||||||
@@ -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.
|
|
||||||
|
|
||||||
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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
req.WaitTimeout(2.5)
|
|
||||||
resp, _ := c.Post(req)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
```php
|
|
||||||
use TheCodingMachine\Gotenberg\Client;
|
|
||||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
|
||||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
|
||||||
use TheCodingMachine\Gotenberg\Request;
|
|
||||||
|
|
||||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
|
||||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$request->setWaitTimeout(2.5);
|
|
||||||
$dest = "result.pdf";
|
|
||||||
$client->store($request, $dest);
|
|
||||||
```
|
|
||||||
@@ -1,49 +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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
req.WebhookURL("http://myapp.com/webhook/")
|
|
||||||
resp, _ := c.Post(req)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
```php
|
|
||||||
use TheCodingMachine\Gotenberg\Client;
|
|
||||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
|
||||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
|
||||||
|
|
||||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
|
||||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
|
||||||
$resp = $client->post($request);
|
|
||||||
```
|
|
||||||
@@ -1,50 +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/v5"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
|
||||||
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', 'index.html');
|
|
||||||
$request = new HTMLRequest($index);
|
|
||||||
$request->setResultFilename('foo.pdf');
|
|
||||||
$resp = $client->post($request);
|
|
||||||
```
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: Scalability
|
|
||||||
---
|
|
||||||
|
|
||||||
The API being stateless, you may scale it as much as you want.
|
|
||||||
|
|
||||||
For instance, using the following Docker Compose file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# your others services
|
|
||||||
|
|
||||||
gotenberg:
|
|
||||||
image: thecodingmachine/gotenberg:5
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
title: Ping
|
|
||||||
---
|
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/ping` for checking the API availability with
|
|
||||||
a simple `GET` request.
|
|
||||||
|
|
||||||
This feature is especially useful for liveness/readiness probes in Kubernetes:
|
|
||||||
|
|
||||||
* [Pod lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes)
|
|
||||||
* [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/)
|
|
||||||
@@ -1,13 +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:5
|
|
||||||
|
|
||||||
RUN apt-get -y install yourfonts
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: Links
|
|
||||||
---
|
|
||||||
|
|
||||||
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
|
|
||||||
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
|
|
||||||
|
|
||||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
|
||||||
15
build/install-chromium.sh
Executable 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
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
ARG GOLANG_VERSION
|
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | GolangCI-Lint
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs GolangCI-Lint, a linters Runner for Go. 5x faster
|
|
||||||
# | than gometalinter.
|
|
||||||
# |
|
|
||||||
|
|
||||||
ENV GOLANGCI_LINT_VERSION 1.16.0
|
|
||||||
|
|
||||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
|
||||||
golangci-lint --version
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Final touch
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Last instructions of this build.
|
|
||||||
# |
|
|
||||||
|
|
||||||
# Define our workding outside of $GOPATH (we're using go modules).
|
|
||||||
WORKDIR /lint
|
|
||||||
|
|
||||||
# Copy our module dependencies definitions.
|
|
||||||
COPY go.mod .
|
|
||||||
COPY go.sum .
|
|
||||||
|
|
||||||
# Install module dependencies.
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
ARG GOLANG_VERSION
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Binary
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Buils Gotenberg binary.
|
|
||||||
# |
|
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
|
||||||
|
|
||||||
ARG VERSION
|
|
||||||
|
|
||||||
ENV GOOS=linux \
|
|
||||||
GOARCH=amd64 \
|
|
||||||
CGO_ENABLED=0
|
|
||||||
|
|
||||||
# Define our workding outside of $GOPATH (we're using go modules).
|
|
||||||
WORKDIR /gotenberg
|
|
||||||
|
|
||||||
# Copy our source code.
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build our binary.
|
|
||||||
RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Final touch
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Last instructions of this build.
|
|
||||||
# |
|
|
||||||
|
|
||||||
FROM thecodingmachine/gotenberg:base
|
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
|
|
||||||
|
|
||||||
WORKDIR /gotenberg
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
CMD [ "gotenberg" ]
|
|
||||||
3
build/pdftk.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
exec java -jar /usr/bin/pdftk-all.jar "$@"
|
||||||
@@ -1,48 +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 install -y git gcc
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Golang
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs Golang.
|
|
||||||
# |
|
|
||||||
|
|
||||||
COPY --from=golang /usr/local/go /usr/local/go
|
|
||||||
|
|
||||||
RUN export PATH="/usr/local/go/bin:$PATH" &&\
|
|
||||||
go version
|
|
||||||
|
|
||||||
ENV GOPATH /go
|
|
||||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Final touch
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Last instructions of this build.
|
|
||||||
# |
|
|
||||||
|
|
||||||
# Define our workding outside of $GOPATH (we're using go modules).
|
|
||||||
WORKDIR /tests
|
|
||||||
|
|
||||||
# Copy our module dependencies definitions.
|
|
||||||
COPY go.mod .
|
|
||||||
COPY go.sum .
|
|
||||||
|
|
||||||
# Install module dependencies.
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
# Testing PM2 processes launch separatly for avoiding
|
|
||||||
# spending to much time on each tests depending on
|
|
||||||
# them.
|
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
|
||||||
|
|
||||||
# Running others tests.
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
|
||||||
|
|
||||||
# Finally testing processes shutdown.
|
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
|
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
|
||||||
158
cmd/gotenberg.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -1,125 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
// Gotenberg modules.
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/standard"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set on build time.
|
|
||||||
// nolint: gochecknoglobals
|
|
||||||
var version = "snapshot"
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultWaitTimeoutEnvVar = "DEFAULT_WAIT_TIMEOUT"
|
|
||||||
disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME"
|
|
||||||
disableUnoconvEnvVar = "DISABLE_UNOCONV"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mustParseEnvVar() *api.Options {
|
|
||||||
opts := api.DefaultOptions()
|
|
||||||
if os.Getenv(defaultWaitTimeoutEnvVar) != "" {
|
|
||||||
defaultWaitTimeout, err := strconv.ParseFloat(os.Getenv(defaultWaitTimeoutEnvVar), 64)
|
|
||||||
if err != nil {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want float got %v", defaultWaitTimeoutEnvVar, err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.DefaultWaitTimeout = defaultWaitTimeout
|
|
||||||
}
|
|
||||||
if v, ok := os.LookupEnv(disableGoogleChromeEnvVar); ok {
|
|
||||||
if v != "1" && v != "0" {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", defaultWaitTimeoutEnvVar, v))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.EnableChromeEndpoints = v != "1"
|
|
||||||
}
|
|
||||||
if v, ok := os.LookupEnv(disableUnoconvEnvVar); ok {
|
|
||||||
if v != "1" && v != "0" {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableUnoconvEnvVar, v))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.EnableUnoconvEndpoints = v != "1"
|
|
||||||
}
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustStartProcesses(opts *api.Options) []pm2.Process {
|
|
||||||
var processes []pm2.Process
|
|
||||||
if opts.EnableChromeEndpoints {
|
|
||||||
processes = append(processes, pm2.NewChrome())
|
|
||||||
}
|
|
||||||
if opts.EnableUnoconvEndpoints {
|
|
||||||
processes = append(processes, pm2.NewUnoconv())
|
|
||||||
}
|
|
||||||
for _, p := range processes {
|
|
||||||
notify.Printf("starting %s with PM2...", p.Fullname())
|
|
||||||
if err := p.Start(); err != nil {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return processes
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustStartAPI(srv *echo.Echo) {
|
|
||||||
notify.Print("http server started on port 3000")
|
|
||||||
if err := srv.Start(":3000"); err != nil {
|
|
||||||
if err != http.ErrServerClosed {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustShutdownProcesses(processes []pm2.Process) {
|
|
||||||
for _, p := range processes {
|
|
||||||
notify.Printf("shutting down %s with PM2... (Ctrl+C to force)", p.Fullname())
|
|
||||||
if err := p.Shutdown(); err != nil {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustShutdownAPI(srv *echo.Echo) {
|
|
||||||
// create a deadline to wait for.
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
// doesn't block if no connections, but will otherwise wait
|
|
||||||
// until the timeout deadline.
|
|
||||||
notify.Print("shutting down http server... (Ctrl+C to force)")
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
notify.Printf("Gotenberg %s", version)
|
gotenbergcmd.Run()
|
||||||
opts := mustParseEnvVar()
|
|
||||||
srv := api.New(opts)
|
|
||||||
processes := mustStartProcesses(opts)
|
|
||||||
// run our API in a goroutine so that it doesn't block.s
|
|
||||||
go func() {
|
|
||||||
mustStartAPI(srv)
|
|
||||||
}()
|
|
||||||
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
|
|
||||||
mustShutdownAPI(srv)
|
|
||||||
mustShutdownProcesses(processes)
|
|
||||||
notify.Print("bye!")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|||||||
1238
docs/index.html
730
docs/openapi.yaml
Normal 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
|
||||||
342
docs/theme/gotenberg/css/index.css
vendored
@@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-57x57.png
vendored
|
Before Width: | Height: | Size: 7.0 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-60x60.png
vendored
|
Before Width: | Height: | Size: 7.5 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-72x72.png
vendored
|
Before Width: | Height: | Size: 9.9 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-76x76.png
vendored
|
Before Width: | Height: | Size: 11 KiB |
BIN
docs/theme/gotenberg/img/favicon-128.png
vendored
|
Before Width: | Height: | Size: 24 KiB |
BIN
docs/theme/gotenberg/img/favicon-16x16.png
vendored
|
Before Width: | Height: | Size: 917 B |
BIN
docs/theme/gotenberg/img/favicon-196x196.png
vendored
|
Before Width: | Height: | Size: 53 KiB |
BIN
docs/theme/gotenberg/img/favicon-32x32.png
vendored
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
docs/theme/gotenberg/img/favicon-96x96.png
vendored
|
Before Width: | Height: | Size: 16 KiB |
BIN
docs/theme/gotenberg/img/favicon.ico
vendored
|
Before Width: | Height: | Size: 34 KiB |
BIN
docs/theme/gotenberg/img/mstile-144x144.png
vendored
|
Before Width: | Height: | Size: 30 KiB |
BIN
docs/theme/gotenberg/img/mstile-150x150.png
vendored
|
Before Width: | Height: | Size: 95 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x150.png
vendored
|
Before Width: | Height: | Size: 182 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x310.png
vendored
|
Before Width: | Height: | Size: 372 KiB |
BIN
docs/theme/gotenberg/img/mstile-70x70.png
vendored
|
Before Width: | Height: | Size: 24 KiB |
46
docs/theme/gotenberg/js/index.js
vendored
@@ -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())
|
|
||||||
67
docs/theme/gotenberg/views/index.html
vendored
@@ -1,67 +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}} · {{.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/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.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>
|
|
||||||
81
go.mod
@@ -1,23 +1,66 @@
|
|||||||
module github.com/thecodingmachine/gotenberg
|
module github.com/gotenberg/gotenberg/v7
|
||||||
|
|
||||||
go 1.12
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/alexliesenfeld/health v0.6.0
|
||||||
github.com/google/go-cmp v0.2.0 // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
github.com/gorilla/websocket v1.4.0 // indirect
|
github.com/chromedp/cdproto v0.0.0-20220827030233-358ed4af73cf
|
||||||
github.com/labstack/echo/v4 v4.0.0
|
github.com/chromedp/chromedp v0.8.5
|
||||||
github.com/labstack/gommon v0.2.8
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/mafredri/cdp v0.22.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/mattn/go-colorable v0.1.1 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.7 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1
|
github.com/klauspost/compress v1.15.9 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
github.com/labstack/echo/v4 v4.8.0
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/labstack/gommon v0.3.1
|
||||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
github.com/mholt/archiver/v3 v3.5.1
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
|
github.com/microcosm-cc/bluemonday v1.0.19
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // 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
|
||||||
)
|
)
|
||||||
|
|||||||
634
go.sum
@@ -1,53 +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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/mafredri/cdp v0.22.0 h1:BV17j8hXLDWczo2SZIAFuOjMpQMIOq5DOcd9sgB2hv0=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/mafredri/cdp v0.22.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
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-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/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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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/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-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-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-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.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-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/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-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-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=
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options allows to customize the behaviour
|
|
||||||
// of the API.
|
|
||||||
type Options struct {
|
|
||||||
DefaultWaitTimeout float64
|
|
||||||
EnableChromeEndpoints bool
|
|
||||||
EnableUnoconvEndpoints bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultOptions returns default options.
|
|
||||||
func DefaultOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
DefaultWaitTimeout: 10,
|
|
||||||
EnableChromeEndpoints: true,
|
|
||||||
EnableUnoconvEndpoints: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns an API.
|
|
||||||
func New(opts *Options) *echo.Echo {
|
|
||||||
api := echo.New()
|
|
||||||
api.HideBanner = true
|
|
||||||
api.HidePort = true
|
|
||||||
api.Use(middleware.Logger())
|
|
||||||
api.GET("/ping", func(c echo.Context) error { return nil })
|
|
||||||
g := api.Group("/convert")
|
|
||||||
g.Use(handleContext(opts))
|
|
||||||
g.Use(handleError())
|
|
||||||
g.POST("/merge", merge)
|
|
||||||
if !opts.EnableChromeEndpoints && !opts.EnableUnoconvEndpoints {
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
if opts.EnableChromeEndpoints {
|
|
||||||
g.POST("/html", convertHTML)
|
|
||||||
g.POST("/url", convertURL)
|
|
||||||
g.POST("/markdown", convertMarkdown)
|
|
||||||
}
|
|
||||||
if opts.EnableUnoconvEndpoints {
|
|
||||||
g.POST("/office", convertOffice)
|
|
||||||
}
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultWaitTimeout(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.DefaultWaitTimeout = 0
|
|
||||||
srv := New(opts)
|
|
||||||
// testing if timeout.
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
// testing if no timeout.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "10"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisableChromeEndpoints(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.EnableChromeEndpoints = false
|
|
||||||
srv := New(opts)
|
|
||||||
// Ping.
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Merge.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// HTML.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Markdown.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// URL.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Office.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisableUnoconvEndpoints(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.EnableUnoconvEndpoints = false
|
|
||||||
srv := New(opts)
|
|
||||||
// Ping.
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Merge.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// HTML.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Markdown.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// URL.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Office.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
}
|
|
||||||
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.EnableChromeEndpoints = false
|
|
||||||
opts.EnableUnoconvEndpoints = false
|
|
||||||
srv := New(opts)
|
|
||||||
// Ping.
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Merge.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// HTML.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Markdown.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// URL.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Office.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
// Package api helps managing the HTTP server behind Gotenberg.
|
|
||||||
package api
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
type errBadRequest struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *errBadRequest) Error() string {
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func merge(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.mergePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpaths, err := ctx.resource.fpaths(".pdf")
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewMerge(fpaths, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertHTML(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.chromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpath, err := ctx.resource.fpath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewHTML(fpath, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertMarkdown(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.chromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpath, err := ctx.resource.fpath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p, err := printer.NewMarkdown(fpath, opts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertURL(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.chromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
remote, err := ctx.resource.get(remoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewURL(remote, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertOffice(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.officePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpaths, err := ctx.resource.fpaths(
|
|
||||||
".txt",
|
|
||||||
".rtf",
|
|
||||||
".fodt",
|
|
||||||
".doc",
|
|
||||||
".docx",
|
|
||||||
".odt",
|
|
||||||
".xls",
|
|
||||||
".xlsx",
|
|
||||||
".ods",
|
|
||||||
".ppt",
|
|
||||||
".pptx",
|
|
||||||
".odp",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewOffice(fpaths, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convert(ctx *resourceContext, p printer.Printer) error {
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
|
||||||
fpath := fmt.Sprintf("%s/%s", ctx.resource.formFilesDirPath, filename)
|
|
||||||
// if no webhook URL given, run conversion
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or an error.
|
|
||||||
if !ctx.resource.has(webhookURL) {
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if ctx.resource.has(resultFilename) {
|
|
||||||
filename, err = ctx.resource.get(resultFilename)
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctx.Attachment(fpath, filename)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
defer ctx.resource.close() // nolint: errcheck
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close() // nolint: errcheck
|
|
||||||
webhook, err := ctx.resource.get(webhookURL)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := http.Post(webhook, "application/pdf", f) /* #nosec */
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() // nolint: errcheck
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
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/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestURL(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConcurrent(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.DefaultWaitTimeout = 30
|
|
||||||
srv := New(opts)
|
|
||||||
// Merge.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// HTML.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// Markdown.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// URL.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// Office.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWebhook(t *testing.T) {
|
|
||||||
status := make(chan error, 2)
|
|
||||||
rcv := echo.New()
|
|
||||||
rcv.POST("/foo", func(c echo.Context) error {
|
|
||||||
if c.Request().Header.Get("Content-type") != "application/pdf" {
|
|
||||||
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(c.Request().Body)
|
|
||||||
if err != nil {
|
|
||||||
status <- err
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if body == nil || len(body) == 0 {
|
|
||||||
status <- errors.New("empty body")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
status <- nil
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
go func() {
|
|
||||||
rcv.Start(":3001")
|
|
||||||
}()
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{webhookURL: "http://localhost:3001/foo"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
err := <-status
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResultFilename(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resultFilename: "foo.pdf"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleContext(opts *Options) echo.MiddlewareFunc {
|
|
||||||
// middleware for extending default context with our
|
|
||||||
// custom constext.
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
ctx := &resourceContext{c, opts, nil}
|
|
||||||
r, err := newResource(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if resourceErr := r.close(); resourceErr != nil {
|
|
||||||
c.Logger().Error(resourceErr)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ctx.resource = r
|
|
||||||
return next(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleError() echo.MiddlewareFunc {
|
|
||||||
// middleware for handling errors and removing resources
|
|
||||||
// once the request has been handled.
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
err := next(c)
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
// if a webhookURL has been given,
|
|
||||||
// do not remove the resources here because
|
|
||||||
// we don't know if the result file has been
|
|
||||||
// generated or sent.
|
|
||||||
if !ctx.resource.has(webhookURL) {
|
|
||||||
if resourceErr := ctx.resource.close(); resourceErr != nil {
|
|
||||||
c.Logger().Error(resourceErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*echo.HTTPError); ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, ok := err.(*errBadRequest); ok {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
|
||||||
return echo.NewHTTPError(http.StatusRequestTimeout)
|
|
||||||
}
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
resultFilename string = "resultFilename"
|
|
||||||
waitTimeout string = "waitTimeout"
|
|
||||||
webhookURL string = "webhookURL"
|
|
||||||
remoteURL string = "remoteURL"
|
|
||||||
waitDelay string = "waitDelay"
|
|
||||||
paperWidth string = "paperWidth"
|
|
||||||
paperHeight string = "paperHeight"
|
|
||||||
marginTop string = "marginTop"
|
|
||||||
marginBottom string = "marginBottom"
|
|
||||||
marginLeft string = "marginLeft"
|
|
||||||
marginRight string = "marginRight"
|
|
||||||
landscape string = "landscape"
|
|
||||||
)
|
|
||||||
|
|
||||||
type resource struct {
|
|
||||||
formValues map[string]string
|
|
||||||
formFilesDirPath string
|
|
||||||
opts *Options
|
|
||||||
}
|
|
||||||
|
|
||||||
type resourceContext struct {
|
|
||||||
echo.Context
|
|
||||||
opts *Options
|
|
||||||
resource *resource
|
|
||||||
}
|
|
||||||
|
|
||||||
func newResource(ctx *resourceContext) (*resource, error) {
|
|
||||||
r := &resource{
|
|
||||||
formValues: formValues(ctx),
|
|
||||||
opts: ctx.opts,
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
r.formFilesDirPath = dirPath
|
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
|
||||||
}
|
|
||||||
if err := formFiles(ctx, dirPath); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formValues(ctx *resourceContext) map[string]string {
|
|
||||||
v := make(map[string]string)
|
|
||||||
v[resultFilename] = ctx.FormValue(resultFilename)
|
|
||||||
v[waitTimeout] = ctx.FormValue(waitTimeout)
|
|
||||||
v[webhookURL] = ctx.FormValue(webhookURL)
|
|
||||||
v[remoteURL] = ctx.FormValue(remoteURL)
|
|
||||||
v[waitDelay] = ctx.FormValue(waitDelay)
|
|
||||||
v[paperWidth] = ctx.FormValue(paperWidth)
|
|
||||||
v[paperHeight] = ctx.FormValue(paperHeight)
|
|
||||||
v[marginTop] = ctx.FormValue(marginTop)
|
|
||||||
v[marginBottom] = ctx.FormValue(marginBottom)
|
|
||||||
v[marginLeft] = ctx.FormValue(marginLeft)
|
|
||||||
v[marginRight] = ctx.FormValue(marginRight)
|
|
||||||
v[landscape] = ctx.FormValue(landscape)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func formFiles(ctx *resourceContext, dirPath string) error {
|
|
||||||
form, err := ctx.MultipartForm()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting multipart form: %v", err)
|
|
||||||
}
|
|
||||||
for _, files := range form.File {
|
|
||||||
for _, fh := range files {
|
|
||||||
in, err := fh.Open()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
|
||||||
}
|
|
||||||
defer in.Close() // nolint: errcheck
|
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
defer out.Close() // nolint: errcheck
|
|
||||||
if err := out.Chmod(0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := out.Seek(0, 0); err != nil {
|
|
||||||
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) close() error {
|
|
||||||
if _, err := os.Stat(r.formFilesDirPath); os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return os.RemoveAll(r.formFilesDirPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
|
||||||
|
|
||||||
func (r *resource) chromePrinterOptions() (*printer.ChromeOptions, error) {
|
|
||||||
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
delay, err := r.float64(waitDelay, 0.0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
header, err := r.content("header.html", defaultHeaderFooterHTML)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
footer, err := r.content("footer.html", defaultHeaderFooterHTML)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
width, err := r.float64(paperWidth, 8.27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
height, err := r.float64(paperHeight, 11.7)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
top, err := r.float64(marginTop, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bottom, err := r.float64(marginBottom, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
left, err := r.float64(marginLeft, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
right, err := r.float64(marginRight, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
landscape, err := r.bool(landscape, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &printer.ChromeOptions{
|
|
||||||
WaitTimeout: timeout,
|
|
||||||
WaitDelay: delay,
|
|
||||||
HeaderHTML: header,
|
|
||||||
FooterHTML: footer,
|
|
||||||
PaperWidth: width,
|
|
||||||
PaperHeight: height,
|
|
||||||
MarginTop: top,
|
|
||||||
MarginBottom: bottom,
|
|
||||||
MarginLeft: left,
|
|
||||||
MarginRight: right,
|
|
||||||
Landscape: landscape,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) officePrinterOptions() (*printer.OfficeOptions, error) {
|
|
||||||
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
landscape, err := r.bool(landscape, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &printer.OfficeOptions{
|
|
||||||
WaitTimeout: timeout,
|
|
||||||
Landscape: landscape,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) mergePrinterOptions() (*printer.MergeOptions, error) {
|
|
||||||
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &printer.MergeOptions{
|
|
||||||
WaitTimeout: timeout,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) has(key string) bool {
|
|
||||||
v, ok := r.formValues[key]
|
|
||||||
if ok {
|
|
||||||
ok = v != ""
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) hasFile(filename string) bool {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
|
||||||
_, err := os.Stat(fpath)
|
|
||||||
return !os.IsNotExist(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) get(key string) (string, error) {
|
|
||||||
v, ok := r.formValues[key]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("form value %s does not exist", key)
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) float64(key string, defaultValue float64) (float64, error) {
|
|
||||||
if !r.has(key) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
v, err := r.get(key)
|
|
||||||
if err != nil {
|
|
||||||
return 0.0, err
|
|
||||||
}
|
|
||||||
f, err := strconv.ParseFloat(v, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0.0, fmt.Errorf("form value %s: %v", key, err)
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) bool(key string, defaultValue bool) (bool, error) {
|
|
||||||
if !r.has(key) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
v, err := r.get(key)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
b, err := strconv.ParseBool(v)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("form value %s: %v", key, err)
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) fpath(filename string) (string, error) {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
|
||||||
_, err := os.Stat(fpath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return "", fmt.Errorf("%s: form file does not exist", filename)
|
|
||||||
}
|
|
||||||
absPath, err := filepath.Abs(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: getting absolute path: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return absPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) content(filename string, defaultValue string) (string, error) {
|
|
||||||
if !r.hasFile(filename) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
fpath, err := r.fpath(filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: reading form file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) fpaths(exts ...string) ([]string, error) {
|
|
||||||
var fpaths []string
|
|
||||||
err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error {
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fpath, err := r.fpath(info.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, ext := range exts {
|
|
||||||
if filepath.Ext(fpath) == ext {
|
|
||||||
fpaths = append(fpaths, fpath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(fpaths) == 0 {
|
|
||||||
return nil, fmt.Errorf("no form files found for extensions: %v", exts)
|
|
||||||
}
|
|
||||||
return fpaths, nil
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/*
|
|
||||||
Package notify helps displaying nice outputs
|
|
||||||
to the user.
|
|
||||||
*/
|
|
||||||
package notify
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package notify
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/gommon/color"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Print prints a message to stdout.
|
|
||||||
func Print(message string) {
|
|
||||||
stdout := color.New()
|
|
||||||
stdout.SetOutput(os.Stdout)
|
|
||||||
stdout.Printf("⇨ %s\n", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf prints a formatted message to stdout.
|
|
||||||
func Printf(format string, a ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, a...)
|
|
||||||
Print(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WarnPrint prints a warning to stderr.
|
|
||||||
func WarnPrint(err error) {
|
|
||||||
stderr := color.New()
|
|
||||||
stderr.SetOutput(os.Stderr)
|
|
||||||
stderr.Printf("%s\n", color.Yellow(fmt.Sprintf("⇨ warn: %v", err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrPrint prints an error to stderr.
|
|
||||||
func ErrPrint(err error) {
|
|
||||||
stderr := color.New()
|
|
||||||
stderr.SetOutput(os.Stderr)
|
|
||||||
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
|
||||||
)
|
|
||||||
|
|
||||||
type chrome struct {
|
|
||||||
manager *processManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChrome retruns a Google Chrome
|
|
||||||
// headless process.
|
|
||||||
func NewChrome() Process {
|
|
||||||
return &chrome{
|
|
||||||
manager: &processManager{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) Fullname() string {
|
|
||||||
return "Google Chrome headless"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) Start() error {
|
|
||||||
return p.manager.start(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) Shutdown() error {
|
|
||||||
return p.manager.shutdown(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) args() []string {
|
|
||||||
return []string{
|
|
||||||
"--no-sandbox",
|
|
||||||
"--headless",
|
|
||||||
"--remote-debugging-port=9222",
|
|
||||||
"--disable-gpu",
|
|
||||||
"--disable-translate",
|
|
||||||
"--disable-extensions",
|
|
||||||
"--disable-background-networking",
|
|
||||||
"--safebrowsing-disable-auto-update",
|
|
||||||
"--disable-sync",
|
|
||||||
"--disable-default-apps",
|
|
||||||
"--hide-scrollbars",
|
|
||||||
"--metrics-recording-only",
|
|
||||||
"--mute-audio",
|
|
||||||
"--no-first-run",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) name() string {
|
|
||||||
return "google-chrome-stable"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) viable() bool {
|
|
||||||
// check if Google Chrome is correctly running.
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
_, err := devtool.New("http://localhost:9222").Version(ctx)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) warmup() {
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Process(new(chrome))
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChromeStart(t *testing.T) {
|
|
||||||
p := NewChrome()
|
|
||||||
err := p.Start()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChromeShutdown(t *testing.T) {
|
|
||||||
p := NewChrome()
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
Package pm2 facilitates starting external
|
|
||||||
processes on which our API depends.
|
|
||||||
|
|
||||||
For instance, it may start Google Chrome headless and
|
|
||||||
unoconv listener with PM2.
|
|
||||||
|
|
||||||
The PM2 process manager launch those processes and keep
|
|
||||||
them running in the background. If for some reason they
|
|
||||||
crash, it will also restart them.
|
|
||||||
*/
|
|
||||||
package pm2
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
stoppedState = iota
|
|
||||||
runningState
|
|
||||||
errorState
|
|
||||||
)
|
|
||||||
|
|
||||||
// Process is a type that can start or
|
|
||||||
// shutdown a process with PM2.
|
|
||||||
type Process interface {
|
|
||||||
Fullname() string
|
|
||||||
Start() error
|
|
||||||
Shutdown() error
|
|
||||||
args() []string
|
|
||||||
name() string
|
|
||||||
viable() bool
|
|
||||||
warmup()
|
|
||||||
}
|
|
||||||
|
|
||||||
type processManager struct {
|
|
||||||
heuristicState int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *processManager) start(p Process) error {
|
|
||||||
if err := m.pm2(p, "start"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.warmup()
|
|
||||||
if !p.viable() {
|
|
||||||
attempts := 0
|
|
||||||
for attempts < 5 && !p.viable() {
|
|
||||||
if err := m.pm2(p, "restart"); err != nil {
|
|
||||||
m.heuristicState = errorState
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.warmup()
|
|
||||||
attempts++
|
|
||||||
}
|
|
||||||
if !p.viable() {
|
|
||||||
m.heuristicState = errorState
|
|
||||||
return fmt.Errorf("failed to launch %s", p.Fullname())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.heuristicState = runningState
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *processManager) shutdown(p Process) error {
|
|
||||||
if m.heuristicState != runningState {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := m.pm2(p, "stop"); err != nil {
|
|
||||||
m.heuristicState = errorState
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.heuristicState = stoppedState
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *processManager) pm2(p Process, cmdName string) error {
|
|
||||||
cmdArgs := []string{
|
|
||||||
cmdName,
|
|
||||||
p.name(),
|
|
||||||
}
|
|
||||||
if cmdName == "start" {
|
|
||||||
cmdArgs = append(cmdArgs, "--interpreter none", "--")
|
|
||||||
cmdArgs = append(cmdArgs, p.args()...)
|
|
||||||
}
|
|
||||||
cmd := exec.Command(
|
|
||||||
"pm2",
|
|
||||||
cmdArgs...,
|
|
||||||
)
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
type unoconv struct {
|
|
||||||
manager *processManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUnoconv retruns a unoconv listener
|
|
||||||
// process.
|
|
||||||
func NewUnoconv() Process {
|
|
||||||
return &unoconv{
|
|
||||||
manager: &processManager{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) Fullname() string {
|
|
||||||
return "unoconv listener"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) Start() error {
|
|
||||||
return p.manager.start(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) Shutdown() error {
|
|
||||||
return p.manager.shutdown(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) args() []string {
|
|
||||||
return []string{
|
|
||||||
"--listener",
|
|
||||||
"--verbose",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) name() string {
|
|
||||||
return "unoconv"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) viable() bool {
|
|
||||||
// TODO find a way to check if
|
|
||||||
// the unoconv listener
|
|
||||||
// is correctly started?
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) warmup() {
|
|
||||||
// let's do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Process(new(unoconv))
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnoconvStart(t *testing.T) {
|
|
||||||
p := NewUnoconv()
|
|
||||||
err := p.Start()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconvShutdown(t *testing.T) {
|
|
||||||
p := NewUnoconv()
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mafredri/cdp"
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
|
||||||
"github.com/mafredri/cdp/protocol/network"
|
|
||||||
"github.com/mafredri/cdp/protocol/page"
|
|
||||||
"github.com/mafredri/cdp/protocol/target"
|
|
||||||
"github.com/mafredri/cdp/rpcc"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
|
||||||
|
|
||||||
type chrome struct {
|
|
||||||
url string
|
|
||||||
opts *ChromeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChromeOptions helps customizing the
|
|
||||||
// Google Chrome printer behaviour.
|
|
||||||
type ChromeOptions struct {
|
|
||||||
WaitTimeout float64
|
|
||||||
WaitDelay float64
|
|
||||||
HeaderHTML string
|
|
||||||
FooterHTML string
|
|
||||||
PaperWidth float64
|
|
||||||
PaperHeight float64
|
|
||||||
MarginTop float64
|
|
||||||
MarginBottom float64
|
|
||||||
MarginLeft float64
|
|
||||||
MarginRight float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) Print(destination string) error {
|
|
||||||
duration := time.Duration(p.opts.WaitTimeout+p.opts.WaitDelay) * time.Second
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), duration)
|
|
||||||
defer cancel()
|
|
||||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
|
||||||
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer devtConn.Close() // nolint: errcheck
|
|
||||||
// create a new CDP Client that uses conn.
|
|
||||||
devtClient := cdp.NewClient(devtConn)
|
|
||||||
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new browser context: %v", err)
|
|
||||||
}
|
|
||||||
// create a new blank target with the new browser context.
|
|
||||||
createTargetArgs := target.
|
|
||||||
NewCreateTargetArgs("about:blank").
|
|
||||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
|
||||||
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new blank target: %v", err)
|
|
||||||
}
|
|
||||||
// connect the client to the new target.
|
|
||||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
|
||||||
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connecting client to blank target: %v", err)
|
|
||||||
}
|
|
||||||
defer newContextConn.Close() // nolint: errcheck
|
|
||||||
// create a new CDP Client that uses newContextConn.
|
|
||||||
targetClient := cdp.NewClient(newContextConn)
|
|
||||||
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
|
||||||
// close the target when done.
|
|
||||||
defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck
|
|
||||||
if err := runBatch(
|
|
||||||
// enable all the domain events that we're interested in.
|
|
||||||
func() error { return targetClient.DOM.Enable(ctx) },
|
|
||||||
func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) },
|
|
||||||
func() error { return targetClient.Page.Enable(ctx) },
|
|
||||||
func() error { return targetClient.Runtime.Enable(ctx) },
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.navigate(ctx, targetClient); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
print, err := targetClient.Page.PrintToPDF(
|
|
||||||
ctx,
|
|
||||||
page.NewPrintToPDFArgs().
|
|
||||||
SetPaperWidth(p.opts.PaperWidth).
|
|
||||||
SetPaperHeight(p.opts.PaperHeight).
|
|
||||||
SetMarginTop(p.opts.MarginTop).
|
|
||||||
SetMarginBottom(p.opts.MarginBottom).
|
|
||||||
SetMarginLeft(p.opts.MarginLeft).
|
|
||||||
SetMarginRight(p.opts.MarginRight).
|
|
||||||
SetLandscape(p.opts.Landscape).
|
|
||||||
SetDisplayHeaderFooter(true).
|
|
||||||
SetHeaderTemplate(p.opts.HeaderHTML).
|
|
||||||
SetFooterTemplate(p.opts.FooterHTML).
|
|
||||||
SetPrintBackground(true),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("printing page to PDF: %v", err)
|
|
||||||
}
|
|
||||||
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", destination, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error {
|
|
||||||
// make sure Page events are enabled.
|
|
||||||
if err := client.Page.Enable(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// make sure Network events are enabled.
|
|
||||||
if err := client.Network.Enable(ctx, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// create all clients for events.
|
|
||||||
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer domContentEventFired.Close() // nolint: errcheck
|
|
||||||
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer loadEventFired.Close() // nolint: errcheck
|
|
||||||
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer loadingFinished.Close() // nolint: errcheck
|
|
||||||
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := runBatch(
|
|
||||||
// wait for all events.
|
|
||||||
func() error { _, err := domContentEventFired.Recv(); return err },
|
|
||||||
func() error { _, err := loadEventFired.Recv(); return err },
|
|
||||||
func() error { _, err := loadingFinished.Recv(); return err },
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// wait for a given amount of time (useful for javascript delay).
|
|
||||||
time.Sleep(time.Duration(p.opts.WaitDelay) * time.Second)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runBatch(fn ...func() error) error {
|
|
||||||
// run all functions simultaneously and wait until
|
|
||||||
// execution has completed or an error is encountered.
|
|
||||||
eg := errgroup.Group{}
|
|
||||||
for _, f := range fn {
|
|
||||||
eg.Go(f)
|
|
||||||
}
|
|
||||||
return eg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(chrome))
|
|
||||||
)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/*
|
|
||||||
Package printer contains structs which convert
|
|
||||||
a specific file type to PDF.
|
|
||||||
*/
|
|
||||||
package printer
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewHTML returns an HTML printer.
|
|
||||||
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
|
||||||
URL := fmt.Sprintf("file://%s", fpath)
|
|
||||||
return &chrome{
|
|
||||||
url: URL,
|
|
||||||
opts: opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
|
||||||
"github.com/russross/blackfriday/v2"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewMarkdown returns a Markdown printer.
|
|
||||||
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
|
||||||
tmpl, err := template.
|
|
||||||
New(filepath.Base(fpath)).
|
|
||||||
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
|
||||||
ParseFiles(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: parsing template: %v", fpath, err)
|
|
||||||
}
|
|
||||||
dirPath := filepath.Dir(fpath)
|
|
||||||
data := &templateData{DirPath: dirPath}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: executing template: %v", fpath, err)
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
|
||||||
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: writing file: %v", dst, err)
|
|
||||||
}
|
|
||||||
URL := fmt.Sprintf("file://%s", dst)
|
|
||||||
return &chrome{
|
|
||||||
url: URL,
|
|
||||||
opts: opts,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateData struct {
|
|
||||||
DirPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
|
||||||
b, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
unsafe := blackfriday.Run(b)
|
|
||||||
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
|
||||||
/* #nosec */
|
|
||||||
return template.HTML(content), nil
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type merge struct {
|
|
||||||
ctx context.Context
|
|
||||||
fpaths []string
|
|
||||||
opts *MergeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeOptions helps customizing the
|
|
||||||
// merge printer behaviour.
|
|
||||||
type MergeOptions struct {
|
|
||||||
WaitTimeout float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMerge returns a merge printer.
|
|
||||||
func NewMerge(fpaths []string, opts *MergeOptions) Printer {
|
|
||||||
return &merge{
|
|
||||||
fpaths: fpaths,
|
|
||||||
opts: opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *merge) Print(destination string) error {
|
|
||||||
if p.ctx == nil {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
p.ctx = ctx
|
|
||||||
}
|
|
||||||
var cmdArgs []string
|
|
||||||
cmdArgs = append(cmdArgs, p.fpaths...)
|
|
||||||
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
|
||||||
cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...)
|
|
||||||
_, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("pdtk: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(merge))
|
|
||||||
)
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
type office struct {
|
|
||||||
fpaths []string
|
|
||||||
opts *OfficeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// OfficeOptions helps customizing the
|
|
||||||
// Office printer behaviour.
|
|
||||||
type OfficeOptions struct {
|
|
||||||
WaitTimeout float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOffice returns an Office printer.
|
|
||||||
func NewOffice(fpaths []string, opts *OfficeOptions) Printer {
|
|
||||||
return &office{
|
|
||||||
fpaths: fpaths,
|
|
||||||
opts: opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *office) Print(destination string) error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
fpaths := make([]string, len(p.fpaths))
|
|
||||||
dirPath := filepath.Dir(destination)
|
|
||||||
for i, fpath := range p.fpaths {
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
|
||||||
if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fpaths[i] = tmpDest
|
|
||||||
}
|
|
||||||
if len(fpaths) == 1 {
|
|
||||||
return os.Rename(fpaths[0], destination)
|
|
||||||
}
|
|
||||||
m := &merge{
|
|
||||||
ctx: ctx,
|
|
||||||
fpaths: fpaths,
|
|
||||||
}
|
|
||||||
return m.Print(destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint: gochecknoglobals
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
cmdArgs := []string{
|
|
||||||
"--format",
|
|
||||||
"pdf",
|
|
||||||
}
|
|
||||||
if opts.Landscape {
|
|
||||||
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "--output", destination, fpath)
|
|
||||||
cmd := exec.CommandContext(
|
|
||||||
ctx,
|
|
||||||
"unoconv",
|
|
||||||
cmdArgs...,
|
|
||||||
)
|
|
||||||
_, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unoconv: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(office))
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
// Printer is a type that can create a PDF file from a source.
|
|
||||||
// The source is defined in the underlying implementation.
|
|
||||||
type Printer interface {
|
|
||||||
Print(destination string) error
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
// NewURL returns a URL printer.
|
|
||||||
func NewURL(url string, opts *ChromeOptions) Printer {
|
|
||||||
return &chrome{
|
|
||||||
url: url,
|
|
||||||
opts: opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/*
|
|
||||||
Package rand helps generating a random string.
|
|
||||||
|
|
||||||
It should be used for creating directory and
|
|
||||||
file names in order to avoid collision.
|
|
||||||
*/
|
|
||||||
package rand
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package rand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get returns a random string.
|
|
||||||
func Get() (string, error) {
|
|
||||||
randBytes := make([]byte, 16)
|
|
||||||
_, err := rand.Read(randBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("creating random string: %v", err)
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(randBytes), nil
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package rand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
|
||||||
rand1, err := Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
rand2, err := Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.NotEqual(t, rand1, rand2)
|
|
||||||
}
|
|
||||||
206
pkg/gotenberg/cmd.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cmd wraps an exec.Cmd.
|
||||||
|
type Cmd struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger *zap.Logger
|
||||||
|
process *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command creates a Cmd without a context. It configures the internal
|
||||||
|
// exec.Cmd of Cmd so that we may kill its unix process and all its children
|
||||||
|
// without creating orphans.
|
||||||
|
//
|
||||||
|
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
||||||
|
func Command(logger *zap.Logger, binPath string, args ...string) Cmd {
|
||||||
|
cmd := exec.Command(binPath, args...)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
|
return Cmd{
|
||||||
|
ctx: nil,
|
||||||
|
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
|
||||||
|
process: cmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandContext creates a Cmd with a context. It configures the internal
|
||||||
|
// exec.Cmd of Cmd so that we may kill its unix process and all its children
|
||||||
|
// without creating orphans.
|
||||||
|
//
|
||||||
|
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
||||||
|
func CommandContext(ctx context.Context, logger *zap.Logger, binPath string, args ...string) (Cmd, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return Cmd{}, errors.New("nil context")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, binPath, args...)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
|
return Cmd{
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
|
||||||
|
process: cmd,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the command but does not wait for its completion.
|
||||||
|
func (cmd Cmd) Start() error {
|
||||||
|
err := cmd.pipeOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pipe unix process output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.logger.Debug(fmt.Sprintf("start unix process: %s", strings.Join(cmd.process.Args, " ")))
|
||||||
|
|
||||||
|
err = cmd.process.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start unix process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the command to complete. It should be called when using the
|
||||||
|
// Start method, so that the command does not leak zombies.
|
||||||
|
func (cmd Cmd) Wait() error {
|
||||||
|
err := cmd.process.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for unix process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the command and wait for its completion or until the context
|
||||||
|
// is done. In any case, it kills the unix process and all its children.
|
||||||
|
func (cmd Cmd) Exec() (int, error) {
|
||||||
|
if cmd.ctx == nil {
|
||||||
|
return 10, errors.New("nil context")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
if cmd.process.ProcessState == nil {
|
||||||
|
return 131, fmt.Errorf("start command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("start command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errChan <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-errChan:
|
||||||
|
errProc := cmd.Kill()
|
||||||
|
if errProc != nil {
|
||||||
|
cmd.logger.Error(errProc.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.process.ProcessState == nil {
|
||||||
|
return 131, fmt.Errorf("unix process error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("unix process error: %w", err)
|
||||||
|
case <-cmd.ctx.Done():
|
||||||
|
errProc := cmd.Kill()
|
||||||
|
if errProc != nil {
|
||||||
|
cmd.logger.Error(errProc.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return 62, fmt.Errorf("context done: %w", cmd.ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pipeOutput creates logs entries according to the process stdout and stderr.
|
||||||
|
// It does nothing if the logging level is not debug.
|
||||||
|
func (cmd Cmd) pipeOutput() error {
|
||||||
|
checkedEntry := cmd.logger.Check(zap.DebugLevel, "check for debug level before piping unix process output")
|
||||||
|
if checkedEntry == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.process.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pipe unix process stdout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, err := cmd.process.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unix process sdterr: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// logCommandOutput creates logs entries according to a reader
|
||||||
|
// (either stdout or stderr).
|
||||||
|
logCommandOutput := func(logger *zap.Logger, reader io.ReadCloser) {
|
||||||
|
r := bufio.NewReader(reader)
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, _, err := r.ReadLine()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
|
||||||
|
logger.Error(fmt.Sprintf("pipe unix process output error: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(line) != 0 {
|
||||||
|
logger.Debug(string(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go logCommandOutput(cmd.logger.Named("stdout"), stdout)
|
||||||
|
go logCommandOutput(cmd.logger.Named("stderr"), stderr)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill kills the unix process and all its children without creating orphans.
|
||||||
|
//
|
||||||
|
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
||||||
|
func (cmd Cmd) Kill() error {
|
||||||
|
if cmd.process == nil {
|
||||||
|
// We cannot use the logger here, because for whatever reason using it
|
||||||
|
// result to a panic.
|
||||||
|
// cmd.logger.Debug("no process, skip killing")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := syscall.Kill(-cmd.process.Process.Pid, syscall.SIGKILL)
|
||||||
|
if err == nil {
|
||||||
|
cmd.logger.Debug("unix process killed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the process does not exist anymore, the error is irrelevant.
|
||||||
|
if strings.Contains(err.Error(), "no such process") {
|
||||||
|
cmd.logger.Debug("unix process already killed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("kill unix process: %w", err)
|
||||||
|
}
|
||||||
324
pkg/gotenberg/cmd_test.go
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommand(t *testing.T) {
|
||||||
|
cmd := Command(zap.NewNop(), "foo")
|
||||||
|
|
||||||
|
if !cmd.process.SysProcAttr.Setpgid {
|
||||||
|
t.Error("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandContext(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
expectCommandContextErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil context",
|
||||||
|
expectCommandContextErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
|
||||||
|
|
||||||
|
if err == nil && !cmd.process.SysProcAttr.Setpgid {
|
||||||
|
t.Fatal("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectCommandContextErr && err == nil {
|
||||||
|
t.Error("expected error from CommandContext(), but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectCommandContextErr && err != nil {
|
||||||
|
t.Errorf("expected no error from CommandContext(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_Start(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd Cmd
|
||||||
|
expectStartErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "start error",
|
||||||
|
cmd: Command(zap.NewNop(), "foo"),
|
||||||
|
expectStartErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.cmd.Start()
|
||||||
|
|
||||||
|
if tc.expectStartErr && err == nil {
|
||||||
|
t.Error("expected error from cmd.Start(), but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectStartErr && err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_Wait(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd Cmd
|
||||||
|
expectWaitErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wait error",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectWaitErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.cmd.Wait()
|
||||||
|
|
||||||
|
if tc.expectWaitErr && err == nil {
|
||||||
|
t.Error("expected error from cmd.Wait(), but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectWaitErr && err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.Wait(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_Exec(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd Cmd
|
||||||
|
timeout time.Duration
|
||||||
|
expectExecErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd, err := CommandContext(context.Background(), zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil context",
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
expectExecErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "start error",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd, err := CommandContext(context.Background(), zap.NewNop(), "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectExecErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "context done",
|
||||||
|
cmd: Command(zap.NewNop(), "sleep", "2"),
|
||||||
|
timeout: time.Duration(1) * time.Second,
|
||||||
|
expectExecErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.timeout > 0 {
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tc.cmd.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := tc.cmd.Exec()
|
||||||
|
|
||||||
|
if tc.expectExecErr && err == nil {
|
||||||
|
t.Error("expected error from cmd.Exec(), but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectExecErr && err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.Exec(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_pipeOutput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd Cmd
|
||||||
|
run bool
|
||||||
|
expectPipeOutputErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
|
||||||
|
run: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no debug, no pipe",
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stdout already piped",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
|
|
||||||
|
_, err := cmd.process.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.process.StdoutPipe(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectPipeOutputErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stderr already piped",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
|
|
||||||
|
_, err := cmd.process.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.process.StderrPipe(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectPipeOutputErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.cmd.pipeOutput()
|
||||||
|
|
||||||
|
if tc.run {
|
||||||
|
errStart := tc.cmd.process.Start()
|
||||||
|
if errStart != nil {
|
||||||
|
t.Fatalf("expected no error from tc.cmd.process.Start(), but got: %v", errStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectPipeOutputErr && err == nil {
|
||||||
|
t.Error("expected error from cmd.pipeOutput(), but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPipeOutputErr && err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.pipeOutput(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_Kill(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd Cmd
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||||
|
|
||||||
|
err := cmd.process.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no process",
|
||||||
|
cmd: Cmd{logger: zap.NewNop()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "process already killed",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||||
|
|
||||||
|
err := cmd.process.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.Kill(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
125
pkg/gotenberg/context.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is a struct which helps initializing modules. When provisioning, a
|
||||||
|
// module may use the context to get other modules that it needs internally.
|
||||||
|
type Context struct {
|
||||||
|
flags ParsedFlags
|
||||||
|
descriptors []ModuleDescriptor
|
||||||
|
moduleInstances map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext creates a Context.
|
||||||
|
// In a module, prefer the Provisioner interface to get a Context.
|
||||||
|
func NewContext(
|
||||||
|
flags ParsedFlags,
|
||||||
|
descriptors []ModuleDescriptor,
|
||||||
|
) *Context {
|
||||||
|
return &Context{
|
||||||
|
flags: flags,
|
||||||
|
descriptors: descriptors,
|
||||||
|
moduleInstances: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsedFlags returns the parsed flags.
|
||||||
|
//
|
||||||
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
|
// flags := ctx.ParsedFlags()
|
||||||
|
// m.foo = flags.RequiredString("foo")
|
||||||
|
// }
|
||||||
|
func (ctx Context) ParsedFlags() ParsedFlags {
|
||||||
|
return ctx.flags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module returns a module which satisfies the requested interface.
|
||||||
|
//
|
||||||
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
|
// mod, _ := ctx.Module(new(ModuleInterface))
|
||||||
|
// real := mod.(ModuleInterface)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If the module has not yet been initialized, this method
|
||||||
|
// initializes it. Otherwise, returns the already initialized instance.
|
||||||
|
func (ctx *Context) Module(kind interface{}) (interface{}, error) {
|
||||||
|
mods, err := ctx.Modules(kind)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get module: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mods) != 1 {
|
||||||
|
return nil, fmt.Errorf("expected to have one and only one %s module", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mods[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modules returns the list of modules which satisfies the requested interface.
|
||||||
|
//
|
||||||
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
|
// mods, _ := ctx.Modules(new(ModuleInterface))
|
||||||
|
// for _, mod := range mods {
|
||||||
|
// real := mod.(ModuleInterface)
|
||||||
|
// // ...
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If one or more modules have not yet been initialized, this method
|
||||||
|
// initializes them. Otherwise, returns the already initialized instances.
|
||||||
|
func (ctx *Context) Modules(kind interface{}) ([]interface{}, error) {
|
||||||
|
realKind := reflect.TypeOf(kind).Elem()
|
||||||
|
|
||||||
|
var mods []interface{}
|
||||||
|
|
||||||
|
for _, desc := range ctx.descriptors {
|
||||||
|
newInstance := desc.New()
|
||||||
|
|
||||||
|
if ok := reflect.TypeOf(newInstance).Implements(realKind); ok {
|
||||||
|
// The module implements the requested interface.
|
||||||
|
// We check if it has already been initialized.
|
||||||
|
instance, ok := ctx.moduleInstances[desc.ID]
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
mods = append(mods, instance)
|
||||||
|
} else {
|
||||||
|
err := ctx.loadModule(desc.ID, newInstance)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mods = append(mods, newInstance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mods, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadModule calls the Provision and/or Validate methods of the requested
|
||||||
|
// module if it satisfies the Provisioner and/or Validator interfaces.
|
||||||
|
func (ctx *Context) loadModule(id string, instance interface{}) error {
|
||||||
|
if prov, ok := instance.(Provisioner); ok {
|
||||||
|
// The instance can be provisioned.
|
||||||
|
err := prov.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("provision module %s: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if validator, ok := instance.(Validator); ok {
|
||||||
|
// The instance can be validated.
|
||||||
|
err := validator.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate module %s: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.moduleInstances[id] = instance
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
195
pkg/gotenberg/context_test.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewContext(t *testing.T) {
|
||||||
|
if NewContext(ParsedFlags{}, nil) == nil {
|
||||||
|
t.Error("expected a non-nil value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_ParsedFlags(t *testing.T) {
|
||||||
|
ctx := NewContext(ParsedFlags{}, nil)
|
||||||
|
|
||||||
|
actual := ctx.ParsedFlags()
|
||||||
|
expect := ParsedFlags{}
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected %v but got %v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_Module(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
mods []ModuleDescriptor
|
||||||
|
kind interface{}
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
mods: func() []ModuleDescriptor {
|
||||||
|
mod := struct{ ProtoProvisioner }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.provision = func(ctx *Context) error { return errors.New("foo") }
|
||||||
|
|
||||||
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
|
}(),
|
||||||
|
kind: new(Provisioner),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mods: func() []ModuleDescriptor {
|
||||||
|
mod := struct{ ProtoProvisioner }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.provision = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
|
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
|
||||||
|
}(),
|
||||||
|
kind: new(Provisioner),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mods: func() []ModuleDescriptor {
|
||||||
|
mod := struct{ ProtoProvisioner }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.provision = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
|
}(),
|
||||||
|
kind: new(Provisioner),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
|
||||||
|
ctx := NewContext(ParsedFlags{}, tc.mods)
|
||||||
|
_, err := ctx.Module(tc.kind)
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_Modules(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
mods []ModuleDescriptor
|
||||||
|
kind interface{}
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
mods: func() []ModuleDescriptor {
|
||||||
|
mod := struct{ ProtoProvisioner }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.provision = func(ctx *Context) error { return errors.New("foo") }
|
||||||
|
|
||||||
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
|
}(),
|
||||||
|
kind: new(Provisioner),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mods: func() []ModuleDescriptor {
|
||||||
|
mod := struct{ ProtoProvisioner }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.provision = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
|
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
|
||||||
|
}(),
|
||||||
|
kind: new(Provisioner),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mods: func() []ModuleDescriptor {
|
||||||
|
mod := struct{ ProtoProvisioner }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.provision = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
|
}(),
|
||||||
|
kind: new(Provisioner),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
|
||||||
|
ctx := NewContext(ParsedFlags{}, tc.mods)
|
||||||
|
_, err := ctx.Modules(tc.kind)
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext_loadModule(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
instance interface{}
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
instance: func() interface{} {
|
||||||
|
mod := struct{ ProtoProvisioner }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.provision = func(ctx *Context) error { return errors.New("foo") }
|
||||||
|
|
||||||
|
return mod
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
instance: func() interface{} {
|
||||||
|
mod := struct{ ProtoValidator }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error { return errors.New("foo") }
|
||||||
|
|
||||||
|
return mod
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
instance: func() interface{} {
|
||||||
|
mod := struct{ ProtoValidator }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error { return nil }
|
||||||
|
|
||||||
|
return mod
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
|
||||||
|
ctx := NewContext(ParsedFlags{}, nil)
|
||||||
|
err := ctx.loadModule("foo", tc.instance)
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
pkg/gotenberg/doc.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Package gotenberg provides most of the logic of the module system.
|
||||||
|
//
|
||||||
|
// caddyserver/caddy, licensed under the Apache License 2.0, has significantly
|
||||||
|
// inspired this module system.
|
||||||
|
//
|
||||||
|
// More details are available on https://caddyserver.com/.
|
||||||
|
package gotenberg
|
||||||