mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
344 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
194670c3bf | ||
|
|
c4222e0981 | ||
|
|
61dc4b8bb8 | ||
|
|
df357c1008 | ||
|
|
871b52a288 | ||
|
|
8a4a043e9f | ||
|
|
c0297aa061 | ||
|
|
2aaa2eeaff | ||
|
|
5f62e5bacf | ||
|
|
ab78559710 | ||
|
|
0d75bb1d3e | ||
|
|
6441062e7f | ||
|
|
4827a45f22 | ||
|
|
40eef457ab | ||
|
|
c22a4d109b | ||
|
|
571c3e04c4 | ||
|
|
f9fb97a572 | ||
|
|
e6484bb2d5 | ||
|
|
19f51fdc1f | ||
|
|
25f7ba3ee6 | ||
|
|
a3cd1ff4e1 | ||
|
|
f62c717f79 | ||
|
|
3ed99133d4 | ||
|
|
ab25cc331d | ||
|
|
2648992a9b | ||
|
|
7bcc3d081e | ||
|
|
affd1c45ca | ||
|
|
8c7458812d | ||
|
|
053908c772 | ||
|
|
d623736f9f | ||
|
|
a3e416ff68 | ||
|
|
8bf207b9f7 | ||
|
|
e1520d5e7e | ||
|
|
4f89c9f7c6 | ||
|
|
d34fb92211 | ||
|
|
3f7b006d16 | ||
|
|
a15cf33691 | ||
|
|
4c1699df6c | ||
|
|
404535c512 | ||
|
|
2cedcecc38 | ||
|
|
ba7b02d8d4 | ||
|
|
6aa3c16646 | ||
|
|
5a03e17e00 | ||
|
|
417d145b43 | ||
|
|
e5636a2afa | ||
|
|
5291d2d129 | ||
|
|
c6f6a87499 | ||
|
|
1ef21c6ed6 | ||
|
|
6ee1932cbf | ||
|
|
83cf43a314 | ||
|
|
52c14a79a1 | ||
|
|
152db0551d | ||
|
|
3a2467ccfe | ||
|
|
5a28f3597c | ||
|
|
9ce179f91b | ||
|
|
7aeb072cc3 | ||
|
|
8a652761a3 | ||
|
|
be667575d5 | ||
|
|
59ca8a44a0 | ||
|
|
c9f0d3bfd8 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.git
|
||||||
46
.github/CODE_OF_CONDUCT.md
vendored
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/
|
|
||||||
38
.github/CONTRIBUTING.md
vendored
38
.github/CONTRIBUTING.md
vendored
@@ -1,38 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
Hi! Thank your 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. Fork this repository
|
|
||||||
3. Clone it to your local Go environment (requires *Go* >= 1.11)
|
|
||||||
|
|
||||||
### 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
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
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
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
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
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.42
|
||||||
|
|
||||||
|
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
2
.gitignore
vendored
@@ -0,0 +1,2 @@
|
|||||||
|
/coverage.html
|
||||||
|
/coverage.txt
|
||||||
30
.golangci.yml
Normal file
30
.golangci.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- deadcode
|
||||||
|
- errcheck
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- prealloc
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
|
||||||
|
run:
|
||||||
|
deadline: 5m
|
||||||
|
issues-exit-code: 1
|
||||||
|
tests: false
|
||||||
|
|
||||||
|
output:
|
||||||
|
format: 'colored-line-number'
|
||||||
|
print-issued-lines: true
|
||||||
|
print-linter-name: true
|
||||||
23
.travis.yml
23
.travis.yml
@@ -1,23 +0,0 @@
|
|||||||
sudo: required
|
|
||||||
|
|
||||||
language: generic
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- secure: d6pliM7iR7KJ3TQkr1C0tHi0HgU6Vdmc/ycw11XrXs794/dgj6g9UDrhgcvT2mTzOVIUNouHx7gyEWAlwJv7p8PPyguHJfIsyqMWfaXNPoytTx1ETUiygl1G/UsvXVdry6I5tZF/U+/PJ3hBXoAvo8EOFe0/MxxvP3ghCeeCpeEwEdoYP2jbADGffywVUNI7OEdmS29A28GusXXp2cWCIZYSq5Gn5iu1ZMPaJA6SkRTXpS+iVVLUvlbz5HW09ETFfurTvsdz5Ze4JICiuQt///CR1uwdxD8K7Ils/mkenaMtja9NHA9RdQBa51Azn8iCLsObCTYc52Zb6atbCi/X4ae9M8GA8qaaLigNz5G4v1BHneJylYfWKj5GwqUA3VmRu1mlEw7d2sN2uFRmgIl5kgsYYyOVOLBOKaJWhyBhNnR4U8KeqwUkOnlSdmNZnjfexXXBEcKHen1SozpXwMnzZh8LZuN6BNYd6TVpKmjulGI3vqNMM0c4JJZ9pSm4q5FjbNGQArwCsujDDwY4bCGZSJJTPMXjYOrx4yJ1RY+0TpyV9oDU3ogEqaWnhrkgrphfiYBlDY2bEvrxMTMukLCxJb0Nru6LGCpg4X1P9hu9r0gceqUSnBgdJI4rYTNddenxlEd4j/Thqv835stJWz6YoV+rfvERuSUpntl8xvrk3U8=
|
|
||||||
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- lint
|
|
||||||
- publish
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: lint
|
|
||||||
script: make lint
|
|
||||||
- stage: publish
|
|
||||||
if: tag IS present
|
|
||||||
script: make image VERSION=$TRAVIS_TAG && ./scripts/publish.sh $TRAVIS_TAG $DOCKER_USER $DOCKER_PASS
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 TheCodingMachine
|
Copyright (c) 2021 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
|
||||||
|
|||||||
155
Makefile
155
Makefile
@@ -1,30 +1,135 @@
|
|||||||
VERSION=snapshot
|
.PHONY: help
|
||||||
GOLANG_VERSION=1.11.2
|
help: ## Show the help
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
# generate documentation.
|
.PHONY: it
|
||||||
doc:
|
it: build build-tests ## Initialize the development environment
|
||||||
static-docs --in build/docs --out docs --theme gotenberg --title Gotenberg --subtitle "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF."
|
|
||||||
|
|
||||||
# gofmt and goimports all go files.
|
GOLANG_VERSION=1.17
|
||||||
fmt:
|
DOCKER_REPOSITORY=gotenberg
|
||||||
|
GOTENBERG_VERSION=snapshot
|
||||||
|
GOTENBERG_USER_GID=1001
|
||||||
|
GOTENBERG_USER_UID=1001
|
||||||
|
NOTO_COLOR_EMOJI_VERSION=v2.028 # See https://github.com/googlefonts/noto-emoji/releases.
|
||||||
|
PDFTK_VERSION=1353200058 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||||
|
GOLANGCI_LINT_VERSION=v1.42.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_READ_TIMEOUT=30s
|
||||||
|
API_PROCESS_TIMEOUT=30s
|
||||||
|
API_WRITE_TIMEOUT=30s
|
||||||
|
API_ROOT_PATH=/
|
||||||
|
API_TRACE_HEADER=Gotenberg-Trace
|
||||||
|
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||||
|
API_WEBHOOK_ALLOW_LIST=
|
||||||
|
API_WEBHOOK_DENY_LIST=
|
||||||
|
API_WEBHOOK_ERROR_ALLOW_LIST=
|
||||||
|
API_WEBHOOK_ERROR_DENY_LIST=
|
||||||
|
API_WEBHOOK_MAX_RETRY=4
|
||||||
|
API_WEBHOOK_RETRY_MIN_WAIT=1s
|
||||||
|
API_WEBHOOK_RETRY_MAX_WAIT=30s
|
||||||
|
API_DISABLE_WEBHOOK=false
|
||||||
|
CHROMIUM_USER_AGENT=
|
||||||
|
CHROMIUM_INCOGNITO=false
|
||||||
|
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
||||||
|
CHROMIUM_ALLOW_LIST=
|
||||||
|
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
|
||||||
|
CHROMIUM_DISABLE_ROUTES=false
|
||||||
|
LIBREOFFICE_DISABLES_ROUTES=false
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FORMAT=auto
|
||||||
|
PDFENGINES_ENGINES=
|
||||||
|
PDFENGINES_DISABLE_ROUTES=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-read-timeout=$(API_READ_TIMEOUT) \
|
||||||
|
--api-process-timeout=$(API_PROCESS_TIMEOUT) \
|
||||||
|
--api-write-timeout=$(API_WRITE_TIMEOUT) \
|
||||||
|
--api-root-path=$(API_ROOT_PATH) \
|
||||||
|
--api-trace-header=$(API_TRACE_HEADER) \
|
||||||
|
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||||
|
--api-webhook-allow-list=$(API_WEBHOOK_ALLOW_LIST) \
|
||||||
|
--api-webhook-deny-list=$(API_WEBHOOK_DENY_LIST) \
|
||||||
|
--api-webhook-error-allow-list=$(API_WEBHOOK_ERROR_ALLOW_LIST) \
|
||||||
|
--api-webhook-error-deny-list=$(API_WEBHOOK_ERROR_DENY_LIST) \
|
||||||
|
--api-webhook-max-retry=$(API_WEBHOOK_MAX_RETRY) \
|
||||||
|
--api-webhook-retry-min-wait=$(API_WEBHOOK_RETRY_MIN_WAIT) \
|
||||||
|
--api-webhook-retry-max-wait=$(API_WEBHOOK_RETRY_MAX_WAIT) \
|
||||||
|
--api-disable-webhook=$(API_DISABLE_WEBHOOK) \
|
||||||
|
--chromium-user-agent=$(CHROMIUM_USER_AGENT) \
|
||||||
|
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
||||||
|
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
|
||||||
|
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
|
||||||
|
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \
|
||||||
|
--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)
|
||||||
|
|
||||||
|
.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
|
||||||
|
|
||||||
# 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 linux/amd64 and linux/arm64 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:
|
$(PDFTK_VERSION) \
|
||||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
$(DOCKER_REPOSITORY)
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
|
||||||
|
|
||||||
# start the API using previously built Docker image.
|
|
||||||
gotenberg:
|
|
||||||
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
|
|
||||||
|
|||||||
58
README.md
58
README.md
@@ -1,42 +1,46 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/8983173/49701110-4c8b9280-fbe8-11e8-895c-a4b9c7d2515b.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" />
|
||||||
</p>
|
|
||||||
<h3 align="center">Gotenberg</h3>
|
<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 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>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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 to convert many
|
||||||
|
documents to PDF, transform them, merge them, and more!
|
||||||
|
|
||||||
## Features
|
## Quick Start
|
||||||
|
|
||||||
* HTML and Markdown conversions using Google Chrome headless
|
|
||||||
* Office conversions (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
|
||||||
* Performance :zap:: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
|
||||||
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
|
|
||||||
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
|
||||||
* Easily interact with the API using our [Go](/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
|
||||||
|
|
||||||
## 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:3
|
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:3)
|
[](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?status.svg)
|
[](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)
|
||||||
---
|
[](https://goreportcard.com/report/gotenberg/gotenberg)
|
||||||
|
|
||||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
|
||||||
|
|||||||
163
build/Dockerfile
Normal file
163
build/Dockerfile
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 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 arm64 architecture.
|
||||||
|
# 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: procps for "top" command (useful when debugging processes).
|
||||||
|
# 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 procps tini python3 default-jre-headless &&\
|
||||||
|
# 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-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 sid distribution to get the latest LibreOffice version.
|
||||||
|
# See https://github.com/gotenberg/gotenberg/pull/322.
|
||||||
|
echo "deb https://httpredir.debian.org/debian/ sid main contrib non-free" >> /etc/apt/sources.list &&\
|
||||||
|
apt-get update -qq &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t sid 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/pdftk-java/pdftk/-/jobs/$PDFTK_VERSION/artifacts/raw/build/libs/pdftk-all.jar" &&\
|
||||||
|
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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 PDFTK_BIN_PATH /usr/bin/pdftk
|
||||||
|
|
||||||
|
USER gotenberg
|
||||||
|
WORKDIR /home/gotenberg
|
||||||
|
|
||||||
|
# Default API port.
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/usr/bin/tini", "--" ]
|
||||||
|
CMD [ "gotenberg" ]
|
||||||
15
build/Dockerfile.cloudrun
Normal file
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,45 +0,0 @@
|
|||||||
FROM debian:stretch-slim
|
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | 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
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | PM2
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs PM2 for launching programs in background and with failure
|
|
||||||
# | recovering. In our case: Chrome (headless) and Office (headless).
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Chrome
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs Chrome for HTML and Markdown conversions.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
|
||||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get -y install google-chrome-stable
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Unoconv
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs MS fonts and unoconv for Office documents conversions.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN apt-get -y install ttf-mscorefonts-installer unoconv
|
|
||||||
@@ -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 (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
|
||||||
* Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
|
||||||
* Failure prevention: PM2 automatically restarts previous processes if they fail
|
|
||||||
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
|
||||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
|
||||||
@@ -1,53 +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:3
|
|
||||||
```
|
|
||||||
|
|
||||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
|
||||||
|
|
||||||
Or add it in your Docker Compose stack:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# your others services
|
|
||||||
|
|
||||||
gotenberg:
|
|
||||||
image: thecodingmachine/gotenberg:3
|
|
||||||
```
|
|
||||||
|
|
||||||
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
|
||||||
|
|
||||||
It may also be deployed with Kubernetes.
|
|
||||||
|
|
||||||
In the following examples, we will assume your
|
|
||||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
|
||||||
|
|
||||||
## Go client
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go get -u github.com/thecodingmachine/gotenberg
|
|
||||||
```
|
|
||||||
|
|
||||||
## PHP client
|
|
||||||
|
|
||||||
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require php-http/guzzle6-adapter
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the PHP client:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require thecodingmachine/gotenberg-php-client
|
|
||||||
```
|
|
||||||
@@ -1,257 +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>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/html \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## 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`.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```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
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
HeaderFilePath: "header.html",
|
|
||||||
FooterFilePath: "footer.html",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## 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](#office.fonts).
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```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
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
"style.css",
|
|
||||||
"img.png",
|
|
||||||
"font.woff",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Paper size, margins, orientation
|
|
||||||
|
|
||||||
You may also customize the resulting PDF paper.
|
|
||||||
|
|
||||||
By default, it will be rendered in `A4` size with `1 inch` margins and `portrait` orientation.
|
|
||||||
|
|
||||||
> Paper size and margins have to be provided in `inches`.
|
|
||||||
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```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
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
PaperSize: gotenberg.A4,
|
|
||||||
PaperMargins: gotenberg.NoMargins,
|
|
||||||
Landscape: true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,62 +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` which 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>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/markdown \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html
|
|
||||||
--form files=@file.md
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.MarkdownRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
MarkdownFilePaths: []string{
|
|
||||||
"file.md",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
---
|
|
||||||
title: Office
|
|
||||||
---
|
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/convert/office` for Office documents 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:
|
|
||||||
|
|
||||||
* `.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.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/office \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@document.docx
|
|
||||||
--form files=@document2.docx
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.OfficeRequest{
|
|
||||||
FilePaths: []string{
|
|
||||||
"document.docx",
|
|
||||||
"document2.docx",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Fonts
|
|
||||||
|
|
||||||
By default, only `ttf-mscorefonts` fonts are installed.
|
|
||||||
|
|
||||||
If you wish to use more fonts, you will have to create your own image:
|
|
||||||
|
|
||||||
```Dockerfile
|
|
||||||
FROM thecodingmachine/gotenberg:3
|
|
||||||
|
|
||||||
RUN apt-get -y install yourfonts
|
|
||||||
```
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
title: Merge
|
|
||||||
---
|
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/merge` for merging PDFs.
|
|
||||||
|
|
||||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
|
||||||
|
|
||||||
## Basic
|
|
||||||
|
|
||||||
Nothing special here: you may send one or more PDF files and the API
|
|
||||||
will merge them and return the resulting PDF file.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/merge \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@file.pdf
|
|
||||||
--form files=@file2.pdf
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.MergeRequest{
|
|
||||||
FilePaths: []string{
|
|
||||||
"file.pdf",
|
|
||||||
"file2.pdf",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,45 +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 done before the conversions are actually done!
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```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/'
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
WebHookURL: "http://myapp.com/webhook/",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
resp, err := c.Post(req)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: Scalability
|
|
||||||
---
|
|
||||||
|
|
||||||
The API being stateless, you may scale it as much as you want.
|
|
||||||
|
|
||||||
For instance, using the following Docker Compose file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# your others services
|
|
||||||
|
|
||||||
gotenberg:
|
|
||||||
image: thecodingmachine/gotenberg:3
|
|
||||||
```
|
|
||||||
|
|
||||||
You may now launch your services using:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ docker-compose up --scale gotenberg=your_number_of_instances
|
|
||||||
```
|
|
||||||
|
|
||||||
When requesting the Gotenberg service with your client(s), Docker will automatically
|
|
||||||
redirect a request to a Gotenberg container according to the round-robin strategy.
|
|
||||||
@@ -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).
|
|
||||||
29
build/fonts.conf
Normal file
29
build/fonts.conf
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version='1.0'?>
|
||||||
|
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
|
||||||
|
<fontconfig>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="rgba">
|
||||||
|
<const>rgb</const>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="hinting">
|
||||||
|
<bool>true</bool>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="hintstyle">
|
||||||
|
<const>hintslight</const>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="antialias">
|
||||||
|
<bool>true</bool>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="lcdfilter">
|
||||||
|
<const>lcddefault</const>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
</fontconfig>
|
||||||
15
build/install-chromium.sh
Executable file
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,37 +0,0 @@
|
|||||||
ARG GOLANG_VERSION
|
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | GolangCI-Lint
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs GolangCI-Lint, a linters Runner for Go. 5x faster
|
|
||||||
# | than gometalinter.
|
|
||||||
# |
|
|
||||||
|
|
||||||
ENV GOLANGCI_LINT_VERSION 1.12.3
|
|
||||||
|
|
||||||
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", "--disable=lll", "--disable=errcheck", "--disable=gosec", "--disable=gochecknoglobals", "--disable=gochecknoinits" ]
|
|
||||||
@@ -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
3
build/pdftk.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
exec java -jar /usr/bin/pdftk-all.jar "$@"
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
ARG GOLANG_VERSION
|
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
|
||||||
|
|
||||||
FROM thecodingmachine/gotenberg:base
|
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | 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,24 +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 TestChromeLaunch
|
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvLaunch
|
|
||||||
|
|
||||||
# Running others tests.
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/printer
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
|
||||||
|
|
||||||
# Finally testing processes shutdown.
|
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
|
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
|
||||||
|
|
||||||
# Testing Go client.
|
|
||||||
go build -o /usr/local/bin/gotenberg cmd/gotenberg/main.go
|
|
||||||
gotenberg &
|
|
||||||
sleep 10
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/pkg
|
|
||||||
119
cmd/gotenberg.go
Normal file
119
cmd/gotenberg.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package gotenbergcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Creates the roo` 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)
|
||||||
|
|
||||||
|
// Parses the flags...
|
||||||
|
err := fs.Parse(os.Args[1:])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and creates 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)
|
||||||
|
|
||||||
|
// Starts 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))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
|
||||||
|
|
||||||
|
for _, a := range apps {
|
||||||
|
id := a.(gotenberg.Module).Descriptor().ID
|
||||||
|
app := a.(gotenberg.App)
|
||||||
|
|
||||||
|
err = app.Stop(gracefulShutdownCtx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[ERROR] stopping %s: %s\n", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
@@ -1,21 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
// Gotenberg modules.
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set on build time.
|
|
||||||
var version = "snapshot"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
notify.Println(fmt.Sprintf("Gotenberg %s", version))
|
gotenbergcmd.Run()
|
||||||
if err := api.Start(); err != nil {
|
|
||||||
notify.ErrPrintln(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|||||||
742
docs/index.html
742
docs/index.html
@@ -1,742 +0,0 @@
|
|||||||
<!DOCTYPE html><html><head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
||||||
<title>Gotenberg · A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</title>
|
|
||||||
<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="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/49701110-4c8b9280-fbe8-11e8-895c-a4b9c7d2515b.png" alt="Gotenberg logo" width="250" height="250"/>
|
|
||||||
<span class="text">Gotenberg</span>
|
|
||||||
<span class="subtext">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Content-wrapper">
|
|
||||||
<div class="Sidebar">
|
|
||||||
<div class="Menu">
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#introduction">Introduction</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#install">Install</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#html">HTML</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#markdown">Markdown</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#office">Office</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#merge">Merge</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#webhook">Webhook</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#scalability">Scalability</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<a href="#links">Links</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Content">
|
|
||||||
|
|
||||||
<div class="Page" id="introduction">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="introduction" href="#introduction">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Introduction</h1>
|
|
||||||
<p><a href="https://github.com/thecodingmachine/gotenberg/">Gotenberg</a> is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>HTML and Markdown conversions using Google Chrome headless</li>
|
|
||||||
<li>Office conversions (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using <a href="https://github.com/dagwieers/unoconv">unoconv</a></li>
|
|
||||||
<li>Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2</li>
|
|
||||||
<li>Failure prevention: PM2 automatically restarts previous processes if they fail</li>
|
|
||||||
<li>Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!</li>
|
|
||||||
<li>Easily interact with the API using our <a href="https://github.com/thecodingmachine/gotenberg/pkg">Go</a> and <a href="https://github.com/thecodingmachine/gotenberg-php-client">PHP</a> libraries</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="install">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="install" href="#install">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Install</h1>
|
|
||||||
<p>Gotenberg is shipped within a Docker image.</p>
|
|
||||||
|
|
||||||
<p>You may start it with:</p>
|
|
||||||
|
|
||||||
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:3
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<blockquote>
|
|
||||||
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<p>Or add it in your Docker Compose stack:</p>
|
|
||||||
|
|
||||||
<pre class="chroma">version<span class="p">:</span><span class="w"> </span><span class="s1">'3'</span><span class="w">
|
|
||||||
</span><span class="w">
|
|
||||||
</span><span class="w"></span>services<span class="p">:</span><span class="w">
|
|
||||||
</span><span class="w">
|
|
||||||
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
|
|
||||||
</span><span class="w">
|
|
||||||
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
|
|
||||||
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">3</span><span class="w">
|
|
||||||
</span><span class="w"></span></pre>
|
|
||||||
|
|
||||||
<blockquote>
|
|
||||||
<p>The API will be available under <code>gotenberg:3000</code> in your Docker Compose network.</p>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<p>It may also be deployed with Kubernetes.</p>
|
|
||||||
|
|
||||||
<p>In the following examples, we will assume your
|
|
||||||
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.go_client" href="#install.go_client">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go client</h2>
|
|
||||||
|
|
||||||
<pre class="chroma">$ go get -u github.com/thecodingmachine/gotenberg
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.php_client" href="#install.php_client">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP client</h2>
|
|
||||||
|
|
||||||
<p>Unless your project already has a PSR7 <code>HttpClient</code>, install <code>php-http/guzzle6-adapter</code>:</p>
|
|
||||||
|
|
||||||
<pre class="chroma">$ composer require php-http/guzzle6-adapter
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<p>Then the PHP client:</p>
|
|
||||||
|
|
||||||
<pre class="chroma">$ composer require thecodingmachine/gotenberg-php-client
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="html">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="html" href="#html">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>HTML</h1>
|
|
||||||
<p>Gotenberg provides the endpoint <code>/convert/html</code> for HTML conversions.</p>
|
|
||||||
|
|
||||||
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic" href="#html.basic">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Basic</h2>
|
|
||||||
|
|
||||||
<p>The only requirement is to send a file named <code>index.html</code>: it is the file
|
|
||||||
which will be converted to PDF.</p>
|
|
||||||
|
|
||||||
<p>For instance:</p>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="cp"><!doctype html></span>
|
|
||||||
<span class="p"><</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">"en"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">head</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">title</span><span class="p"></span><span class="p">></span>My PDF<span class="p"><</span><span class="p">/</span><span class="nt">title</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">head</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">body</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">h1</span><span class="p"></span><span class="p">></span>Hello world!<span class="p"><</span><span class="p">/</span><span class="nt">h1</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">body</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">html</span><span class="p">></span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic.guzzle" href="#html.basic.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic.go" href="#html.basic.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">"index.html"</span><span class="p">,</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">c</span><span class="p">.</span><span class="nx">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic.php" href="#html.basic.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer" href="#html.header_and_footer">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Header and footer</h2>
|
|
||||||
|
|
||||||
<p>You may also add a header and/or a footer in the resulting PDF.
|
|
||||||
Respectively, a file named <code>header.html</code> and <code>footer.html</code>.</p>
|
|
||||||
|
|
||||||
<p>Each of them <strong>has to be a complete HTML document</strong>:</p>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="p"><</span><span class="nt">html</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">head</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">style</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="nt">body</span> <span class="p">{</span>
|
|
||||||
<span class="k">font-size</span><span class="p">:</span> <span class="mi">8</span><span class="kt">rem</span><span class="p">;</span>
|
|
||||||
<span class="k">margin</span><span class="p">:</span> <span class="mi">4</span><span class="kt">rem</span> <span class="kc">auto</span><span class="p">;</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">style</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">head</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">body</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">p</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">"pageNumber"</span><span class="p"></span><span class="p">><</span><span class="p">/</span><span class="nt">span</span><span class="p">></span> of <span class="p"><</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">"totalPages"</span><span class="p"></span><span class="p">><</span><span class="p">/</span><span class="nt">span</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">p</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">body</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">html</span><span class="p">></span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<p>The following classes allow you to inject printing values:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><code>date</code>: formatted print date</li>
|
|
||||||
<li><code>title</code>: document title</li>
|
|
||||||
<li><code>pageNumber</code>: current page number</li>
|
|
||||||
<li><code>totalPage</code>: total pages in the document</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<blockquote>
|
|
||||||
<p><strong>Attention:</strong> the CSS properties are independant of the ones used in the <code>index.html</code> file.
|
|
||||||
Also, <code>footer.html</code> CSS properties override the ones from <code>header.html</code>.</p>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer.guzzle" href="#html.header_and_footer.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@header.html
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@footer.html
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer.go" href="#html.header_and_footer.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">"index.html"</span><span class="p">,</span>
|
|
||||||
<span class="nx">Options</span><span class="p">:</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLOptions</span><span class="p">{</span>
|
|
||||||
<span class="nx">HeaderFilePath</span><span class="p">:</span> <span class="s">"header.html"</span><span class="p">,</span>
|
|
||||||
<span class="nx">FooterFilePath</span><span class="p">:</span> <span class="s">"footer.html"</span><span class="p">,</span>
|
|
||||||
<span class="p">},</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">c</span><span class="p">.</span><span class="nx">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer.php" href="#html.header_and_footer.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets" href="#html.assets">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Assets</h2>
|
|
||||||
|
|
||||||
<p>You may also send additional files. For instance, images, fonts, stylesheets and so on.</p>
|
|
||||||
|
|
||||||
<p>The only requirement is to make sure that their paths
|
|
||||||
are on the same level as the <code>index.html</code> file.</p>
|
|
||||||
|
|
||||||
<p>In others words, this will work:</p>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="cp"><!doctype html></span>
|
|
||||||
<span class="p"><</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">"en"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">head</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">title</span><span class="p"></span><span class="p">></span>My PDF<span class="p"><</span><span class="p">/</span><span class="nt">title</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">head</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">body</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">h1</span><span class="p"></span><span class="p">></span>Hello world!<span class="p"><</span><span class="p">/</span><span class="nt">h1</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"img.png"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">body</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">html</span><span class="p">></span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<p>But this won’t:</p>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="cp"><!doctype html></span>
|
|
||||||
<span class="p"><</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">"en"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">head</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">title</span><span class="p"></span><span class="p">></span>My PDF<span class="p"><</span><span class="p">/</span><span class="nt">title</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">head</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">body</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">h1</span><span class="p"></span><span class="p">></span>Hello world!<span class="p"><</span><span class="p">/</span><span class="nt">h1</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"/foo/img.png"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">body</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">html</span><span class="p">></span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<p>You may also use <em>remote</em> paths for Google fonts, images and so on.</p>
|
|
||||||
|
|
||||||
<blockquote>
|
|
||||||
<p>If you want to install fonts directly in the Gotenberg Docker image,
|
|
||||||
see to the <a href="#office.fonts">fonts section</a>.</p>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.guzzle" href="#html.assets.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@style.css
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@img.png
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@font.woff
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.go" href="#html.assets.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">"index.html"</span><span class="p">,</span>
|
|
||||||
<span class="nx">AssetFilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
|
|
||||||
<span class="s">"style.css"</span><span class="p">,</span>
|
|
||||||
<span class="s">"img.png"</span><span class="p">,</span>
|
|
||||||
<span class="s">"font.woff"</span><span class="p">,</span>
|
|
||||||
<span class="p">},</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">c</span><span class="p">.</span><span class="nx">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.php" href="#html.assets.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation" href="#html.paper_size_margins_orientation">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Paper size, margins, orientation</h2>
|
|
||||||
|
|
||||||
<p>You may also customize the resulting PDF paper.</p>
|
|
||||||
|
|
||||||
<p>By default, it will be rendered in <code>A4</code> size with <code>1 inch</code> margins and <code>portrait</code> orientation.</p>
|
|
||||||
|
|
||||||
<blockquote>
|
|
||||||
<p>Paper size and margins have to be provided in <code>inches</code>.
|
|
||||||
Also, you have to set both <code>paperWidth</code> and <code>paperHeight</code>. Same for margins.</p>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.guzzle" href="#html.paper_size_margins_orientation.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html
|
|
||||||
--form <span class="nv">paperWidth</span><span class="o">=</span><span class="m">8</span>.27
|
|
||||||
--form <span class="nv">paperHeight</span><span class="o">=</span><span class="m">11</span>.27
|
|
||||||
--form <span class="nv">marginTop</span><span class="o">=</span><span class="m">0</span>
|
|
||||||
--form <span class="nv">marginBottom</span><span class="o">=</span><span class="m">0</span>
|
|
||||||
--form <span class="nv">marginLeft</span><span class="o">=</span><span class="m">0</span>
|
|
||||||
--form <span class="nv">marginRight</span><span class="o">=</span><span class="m">0</span>
|
|
||||||
--form <span class="nv">landscape</span><span class="o">=</span><span class="nb">true</span>
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.go" href="#html.paper_size_margins_orientation.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">"index.html"</span><span class="p">,</span>
|
|
||||||
<span class="nx">Options</span><span class="p">:</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLOptions</span><span class="p">{</span>
|
|
||||||
<span class="nx">PaperSize</span><span class="p">:</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nx">A4</span><span class="p">,</span>
|
|
||||||
<span class="nx">PaperMargins</span><span class="p">:</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nx">NoMargins</span><span class="p">,</span>
|
|
||||||
<span class="nx">Landscape</span><span class="p">:</span> <span class="kc">true</span>
|
|
||||||
<span class="p">},</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">c</span><span class="p">.</span><span class="nx">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.php" href="#html.paper_size_margins_orientation.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="markdown">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown" href="#markdown">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Markdown</h1>
|
|
||||||
<p>Gotenberg provides the endpoint <code>/convert/markdown</code> for Markdown conversions.</p>
|
|
||||||
|
|
||||||
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic" href="#markdown.basic">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Basic</h2>
|
|
||||||
|
|
||||||
<p>Markdown conversions work the same as HTML conversions.</p>
|
|
||||||
|
|
||||||
<p>Only difference is that you have access to the Go template function <code>toHTML</code>
|
|
||||||
in the file <code>index.html</code> which will convert a given markdown file to HTML.</p>
|
|
||||||
|
|
||||||
<p>For instance:</p>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="cp"><!doctype html></span>
|
|
||||||
<span class="p"><</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">"en"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">head</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p"></span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">title</span><span class="p"></span><span class="p">></span>My PDF<span class="p"><</span><span class="p">/</span><span class="nt">title</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">head</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="nt">body</span><span class="p"></span><span class="p">></span>
|
|
||||||
{{ toHTML .DirPath "file.md" }}
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">body</span><span class="p">></span>
|
|
||||||
<span class="p"><</span><span class="p">/</span><span class="nt">html</span><span class="p">></span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic.guzzle" href="#markdown.basic.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/markdown <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@file.md
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic.go" href="#markdown.basic.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">MarkdownRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">"index.html"</span><span class="p">,</span>
|
|
||||||
<span class="nx">MarkdownFilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
|
|
||||||
<span class="s">"file.md"</span><span class="p">,</span>
|
|
||||||
<span class="p">},</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">c</span><span class="p">.</span><span class="nx">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic.php" href="#markdown.basic.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="office">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="office" href="#office">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Office</h1>
|
|
||||||
<p>Gotenberg provides the endpoint <code>/convert/office</code> for Office documents conversions.</p>
|
|
||||||
|
|
||||||
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic" href="#office.basic">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Basic</h2>
|
|
||||||
|
|
||||||
<p>You may send one or more Office documents. Following file extensions are accepted:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><code>.doc</code></li>
|
|
||||||
<li><code>.docx</code></li>
|
|
||||||
<li><code>.odt</code></li>
|
|
||||||
<li><code>.xls</code></li>
|
|
||||||
<li><code>.xlsx</code></li>
|
|
||||||
<li><code>.ods</code></li>
|
|
||||||
<li><code>.ppt</code></li>
|
|
||||||
<li><code>.pptx</code></li>
|
|
||||||
<li><code>.odp</code></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>All files will be merged into a single resulting PDF.</p>
|
|
||||||
|
|
||||||
<blockquote>
|
|
||||||
<p><strong>Attention:</strong> currently, <code>unoconv</code> cannot perform concurrent conversions.
|
|
||||||
That’s why for Office conversions, the API does only one conversion at a time.
|
|
||||||
See the <a href="#scalability">scalability section</a> to find how to mitigate this issue.</p>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic.guzzle" href="#office.basic.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/office <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@document.docx
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@document2.docx
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic.go" href="#office.basic.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">OfficeRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">FilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
|
|
||||||
<span class="s">"document.docx"</span><span class="p">,</span>
|
|
||||||
<span class="s">"document2.docx"</span><span class="p">,</span>
|
|
||||||
<span class="p">},</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">c</span><span class="p">.</span><span class="nx">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic.php" href="#office.basic.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="office.fonts" href="#office.fonts">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Fonts</h2>
|
|
||||||
|
|
||||||
<p>By default, only <code>ttf-mscorefonts</code> fonts are installed.</p>
|
|
||||||
|
|
||||||
<p>If you wish to use more fonts, you will have to create your own image:</p>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="k">FROM</span><span class="s"> thecodingmachine/gotenberg:3</span><span class="err">
|
|
||||||
</span><span class="err">
|
|
||||||
</span><span class="err"></span><span class="k">RUN</span> apt-get -y install yourfonts<span class="err">
|
|
||||||
</span><span class="err"></span></pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="merge">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="merge" href="#merge">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Merge</h1>
|
|
||||||
<p>Gotenberg provides the endpoint <code>/merge</code> for merging PDFs.</p>
|
|
||||||
|
|
||||||
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic" href="#merge.basic">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Basic</h2>
|
|
||||||
|
|
||||||
<p>Nothing special here: you may send one or more PDF files and the API
|
|
||||||
will merge them and return the resulting PDF file.</p>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic.guzzle" href="#merge.basic.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/merge <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@file.pdf
|
|
||||||
--form <span class="nv">files</span><span class="o">=</span>@file2.pdf
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic.go" href="#merge.basic.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">MergeRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">FilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
|
|
||||||
<span class="s">"file.pdf"</span><span class="p">,</span>
|
|
||||||
<span class="s">"file2.pdf"</span><span class="p">,</span>
|
|
||||||
<span class="p">},</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">c</span><span class="p">.</span><span class="nx">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic.php" href="#merge.basic.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="webhook">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook" href="#webhook">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Webhook</h1>
|
|
||||||
<p>All endpoints accept a form field named <code>webhookURL</code>.</p>
|
|
||||||
|
|
||||||
<p>If provided, the API will send the resulting PDF file in a <code>POST</code> request with the <code>application/pdf</code> Content-Type
|
|
||||||
to given URL.</p>
|
|
||||||
|
|
||||||
<p>By doing so, your requests to the API will be done before the conversions are actually done!</p>
|
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples" href="#webhook.examples">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Examples</h2>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples.guzzle" href="#webhook.examples.guzzle">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Guzzle</h3>
|
|
||||||
|
|
||||||
<pre class="chroma">$ curl --request POST <span class="se">\
|
|
||||||
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
|
|
||||||
</span><span class="se"></span> --header <span class="s1">'Content-Type: multipart/form-data'</span> <span class="se">\
|
|
||||||
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html
|
|
||||||
--form <span class="nv">webhookURL</span><span class="o">=</span><span class="s1">'http://myapp.com/webhook/'</span>
|
|
||||||
> result.pdf
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples.go" href="#webhook.examples.go">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Go</h3>
|
|
||||||
|
|
||||||
<pre class="chroma"><span class="kn">import</span> <span class="s">"github.com/thecodingmachine/gotenberg/pkg"</span>
|
|
||||||
|
|
||||||
<span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
|
|
||||||
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">"http://localhost:3000"</span><span class="p">}</span>
|
|
||||||
<span class="nx">req</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
|
|
||||||
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">"index.html"</span><span class="p">,</span>
|
|
||||||
<span class="nx">Options</span><span class="p">:</span> <span class="o">&</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLOptions</span><span class="p">{</span>
|
|
||||||
<span class="nx">WebHookURL</span><span class="p">:</span> <span class="s">"http://myapp.com/webhook/"</span><span class="p">,</span>
|
|
||||||
<span class="p">},</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">"result.pdf"</span>
|
|
||||||
<span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
|
|
||||||
<span class="p">}</span>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples.php" href="#webhook.examples.php">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>PHP</h3>
|
|
||||||
|
|
||||||
<p>TODO</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="scalability">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="scalability" href="#scalability">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Scalability</h1>
|
|
||||||
<p>The API being stateless, you may scale it as much as you want.</p>
|
|
||||||
|
|
||||||
<p>For instance, using the following Docker Compose file:</p>
|
|
||||||
|
|
||||||
<pre class="chroma">version<span class="p">:</span><span class="w"> </span><span class="s1">'3'</span><span class="w">
|
|
||||||
</span><span class="w">
|
|
||||||
</span><span class="w"></span>services<span class="p">:</span><span class="w">
|
|
||||||
</span><span class="w">
|
|
||||||
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
|
|
||||||
</span><span class="w">
|
|
||||||
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
|
|
||||||
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">3</span><span class="w">
|
|
||||||
</span><span class="w"></span></pre>
|
|
||||||
|
|
||||||
<p>You may now launch your services using:</p>
|
|
||||||
|
|
||||||
<pre class="chroma">$ docker-compose up --scale <span class="nv">gotenberg</span><span class="o">=</span>your_number_of_instances
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<p>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.</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Page" id="links">
|
|
||||||
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="links" href="#links">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
|
||||||
</a>Links</h1>
|
|
||||||
<ul>
|
|
||||||
<li>Follow the progress on the <a href="https://github.com/thecodingmachine/gotenberg">GitHub repository</a></li>
|
|
||||||
<li>Follow <a href="https://twitter.com/gulnap">@gulnap</a> on Twitter</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Psst: TheCodingMachine is always looking for <a href="https://coders.thecodingmachine.com">talented coders</a>.</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="Footer"></div>
|
|
||||||
<script src="theme/gotenberg/js/index.js"></script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div></body></html>
|
|
||||||
342
docs/theme/gotenberg/css/index.css
vendored
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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
docs/theme/gotenberg/img/favicon-16x16.png
vendored
BIN
docs/theme/gotenberg/img/favicon-16x16.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 923 B |
BIN
docs/theme/gotenberg/img/favicon-32x32.png
vendored
BIN
docs/theme/gotenberg/img/favicon-32x32.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
docs/theme/gotenberg/img/favicon.ico
vendored
BIN
docs/theme/gotenberg/img/favicon.ico
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB |
46
docs/theme/gotenberg/js/index.js
vendored
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())
|
|
||||||
47
docs/theme/gotenberg/views/index.html
vendored
47
docs/theme/gotenberg/views/index.html
vendored
@@ -1,47 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<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="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="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/49701110-4c8b9280-fbe8-11e8-895c-a4b9c7d2515b.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>
|
|
||||||
73
go.mod
73
go.mod
@@ -1,21 +1,58 @@
|
|||||||
module github.com/thecodingmachine/gotenberg
|
module github.com/gotenberg/gotenberg/v7
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/derekparker/delve v1.1.0
|
github.com/alexliesenfeld/health v0.6.0
|
||||||
github.com/google/go-cmp v0.2.0 // indirect
|
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||||
github.com/gorilla/websocket v1.4.0 // indirect
|
github.com/chromedp/cdproto v0.0.0-20210823203301-2c0adcc9edc4
|
||||||
github.com/hhrutter/pdfcpu v0.1.18
|
github.com/chromedp/chromedp v0.7.4
|
||||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/labstack/gommon v0.2.8
|
github.com/google/uuid v1.3.0
|
||||||
github.com/mafredri/cdp v0.20.0
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1
|
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||||
github.com/pkg/errors v0.8.0 // indirect
|
github.com/klauspost/compress v1.13.4 // 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.5.0
|
||||||
github.com/stretchr/objx v0.1.1 // indirect
|
github.com/labstack/gommon v0.3.0
|
||||||
github.com/stretchr/testify v1.2.2
|
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
|
github.com/mholt/archiver/v3 v3.5.0
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
github.com/microcosm-cc/bluemonday v1.0.15
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
github.com/nwaples/rardecode v1.1.2 // indirect
|
||||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
|
github.com/pdfcpu/pdfcpu v0.3.12
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.8 // indirect
|
||||||
|
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.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.7.0
|
||||||
|
go.uber.org/zap v1.19.0
|
||||||
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||||
|
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
|
golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 // indirect
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||||
|
golang.org/x/text v0.3.7
|
||||||
|
golang.org/x/tools v0.1.5 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
|
github.com/dsnet/compress v0.0.1 // 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/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.8 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // 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
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
237
go.sum
237
go.sum
@@ -1,53 +1,198 @@
|
|||||||
|
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.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
|
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
|
||||||
|
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
|
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/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20210713064928-7d28b402946a/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20210823203301-2c0adcc9edc4 h1:cD7F5LfNjC4dtb+BQe1KxxE8RsceYtS+Kyo+yEs4iUc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20210823203301-2c0adcc9edc4/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||||
|
github.com/chromedp/chromedp v0.7.4 h1:U+0d3WbB/Oj4mDuBOI0P7S3PJEued5UZIl5AJ3QulwU=
|
||||||
|
github.com/chromedp/chromedp v0.7.4/go.mod h1:dBj+SXuQHznp6ZPwZeDDEBZKwclUwDLbZ0hjMialMYs=
|
||||||
|
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
|
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||||
|
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/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
|
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||||
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
|
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||||
github.com/hhrutter/pdfcpu v0.1.18 h1:GX3d6tZxmQlmB5X9VUyOtx7+HZZpR+8ISLqBineP6dc=
|
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||||
github.com/hhrutter/pdfcpu v0.1.18/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73 h1:oQvA3Xy8274W5Ldgo6Nogy4lX8XO5AaIBsjF5Ggk0IU=
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73/go.mod h1:rS0D1UPvC8/3sXjhSwEq+K1olh7ipbDhjDWATN2KSgA=
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
github.com/mafredri/cdp v0.20.0 h1:dRq16FwXCAAbolR4W2iyRNsL14Bp6jFHLbRp2EkICVs=
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
github.com/mafredri/cdp v0.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/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/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
|
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
|
||||||
|
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
|
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
|
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||||
|
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
|
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.5.0 h1:JXk6H5PAw9I3GwizqUHhYyS4f45iyGebR/c1xNCeOCY=
|
||||||
|
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
||||||
|
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||||
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
|
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.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
|
||||||
|
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
|
||||||
|
github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.15 h1:J4uN+qPng9rvkBZBoBb8YGR+ijuklIMpSOZZLjYpbeY=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.15/go.mod h1:ZLvAzeakRwrGnzQEvstVzVt3ZpqOF2+sdFr0Om+ce30=
|
||||||
|
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
|
github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M=
|
||||||
|
github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
|
github.com/pdfcpu/pdfcpu v0.3.12 h1:B+MdKisilWNSk5OCO58Z9U6H93usH73xqk6hMOaZCls=
|
||||||
|
github.com/pdfcpu/pdfcpu v0.3.12/go.mod h1:8XVBtVxuuIuSZL4Ez15Q4QoC+H8zeAaGnuiOEwAk8jA=
|
||||||
|
github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||||
|
github.com/ulikunitz/xz v0.5.7/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.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
|
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||||
|
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||||
|
go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
|
||||||
|
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||||
|
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
|
||||||
|
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/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-20210820121016-41cdb8703e55 h1:rw6UNGRMfarCepjI8qOepea/SXwIBVfTKjztZ5gBbq4=
|
||||||
|
golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/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-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
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-20201208040808-7e3f01d25324/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/labstack/echo/middleware"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start starts the API server on port 3000.
|
|
||||||
func Start() error {
|
|
||||||
e := setup()
|
|
||||||
// start Chrome headless and
|
|
||||||
// unoconv listener with PM2.
|
|
||||||
chrome := &pm2.Chrome{}
|
|
||||||
unoconv := &pm2.Unoconv{}
|
|
||||||
if err := chrome.Launch(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := unoconv.Launch(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// run our API in a goroutine so that it doesn't block.
|
|
||||||
go func() {
|
|
||||||
notify.Println("http server started on port 3000")
|
|
||||||
if err := e.Start(":3000"); err != nil {
|
|
||||||
e.Logger.Fatalf("%v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
|
||||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
|
||||||
signal.Notify(quit, os.Interrupt)
|
|
||||||
// block until we receive our signal.
|
|
||||||
<-quit
|
|
||||||
// create a deadline to wait for.
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
// doesn't block if no connections, but will otherwise wait
|
|
||||||
// until the timeout deadline.
|
|
||||||
notify.Println("shutting down http server... (Ctrl+C to force)")
|
|
||||||
return e.Shutdown(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() *echo.Echo {
|
|
||||||
e := echo.New()
|
|
||||||
e.HideBanner = true
|
|
||||||
e.HidePort = true
|
|
||||||
e.Use(middleware.Logger())
|
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
if err := next(c); err != nil {
|
|
||||||
// TODO should return a better HTTP status code
|
|
||||||
// than 500 for some cases.
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%v", err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
e.POST("/merge", merge)
|
|
||||||
g := e.Group("/convert")
|
|
||||||
g.POST("/html", convertHTML)
|
|
||||||
g.POST("/markdown", convertMarkdown)
|
|
||||||
g.POST("/office", convertOffice)
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func newContext(r *resource) (context.Context, context.CancelFunc) {
|
|
||||||
webhookURL := r.webhookURL()
|
|
||||||
if webhookURL == "" {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
||||||
return ctx, cancel
|
|
||||||
}
|
|
||||||
return context.Background(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func print(c echo.Context, p printer.Printer, r *resource) error {
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting result file name: %v", err)
|
|
||||||
}
|
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
if r.webhookURL() == "" {
|
|
||||||
// if no webhook URL given, run conversion
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or and error.
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.Attachment(fpath, filename)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/*
|
|
||||||
Package api starts a HTTP server on port 3000.
|
|
||||||
|
|
||||||
It accepts POST requests with a multipart/form-data Content-Type
|
|
||||||
for converting HTML, Markdown and Office documents to PDF.
|
|
||||||
|
|
||||||
It is also able to merge a list of PDF files.
|
|
||||||
*/
|
|
||||||
package api
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertHTML(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
p := &printer.HTML{Context: ctx}
|
|
||||||
indexPath, err := r.filePath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.WithLocalURL(indexPath)
|
|
||||||
headerPath, _ := r.filePath("header.html")
|
|
||||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
footerPath, _ := r.filePath("footer.html")
|
|
||||||
if err := p.WithFooterFile(footerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
paperSize, err := r.paperSize()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.PaperWidth = paperSize[0]
|
|
||||||
p.PaperHeight = paperSize[1]
|
|
||||||
paperMargins, err := r.paperMargins()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.MarginTop = paperMargins[0]
|
|
||||||
p.MarginBottom = paperMargins[1]
|
|
||||||
p.MarginLeft = paperMargins[2]
|
|
||||||
p.MarginRight = paperMargins[3]
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
body, contentType := test.HTMLMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, convertHTML(c))
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertMarkdown(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
indexPath, err := r.filePath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
|
|
||||||
headerPath, _ := r.filePath("header.html")
|
|
||||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
footerPath, _ := r.filePath("footer.html")
|
|
||||||
if err := p.WithFooterFile(footerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
paperSize, err := r.paperSize()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.PaperWidth = paperSize[0]
|
|
||||||
p.PaperHeight = paperSize[1]
|
|
||||||
paperMargins, err := r.paperMargins()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.MarginTop = paperMargins[0]
|
|
||||||
p.MarginBottom = paperMargins[1]
|
|
||||||
p.MarginLeft = paperMargins[2]
|
|
||||||
p.MarginRight = paperMargins[3]
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
body, contentType := test.MarkdownMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, convertMarkdown(c))
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
func merge(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
fpaths, err := r.filePaths([]string{".pdf"})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting result file name: %v", err)
|
|
||||||
}
|
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
|
||||||
dest := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
if r.webhookURL() == "" {
|
|
||||||
// if no webhook URL given, run merge
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or and error.
|
|
||||||
return printer.Merge(fpaths, dest)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
if err := printer.Merge(fpaths, dest); err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(dest)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
body, contentType := test.PDFMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, merge(c))
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var officeExts = []string{
|
|
||||||
".doc",
|
|
||||||
".docx",
|
|
||||||
".odt",
|
|
||||||
".xls",
|
|
||||||
".xlsx",
|
|
||||||
".ods",
|
|
||||||
".ppt",
|
|
||||||
".pptx",
|
|
||||||
".odp",
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertOffice(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
fpaths, err := r.filePaths(officeExts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(fpaths) == 0 {
|
|
||||||
return errors.New("no suitable office documents to convert")
|
|
||||||
}
|
|
||||||
p := &printer.Office{Context: ctx, FilePaths: fpaths}
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
body, contentType := test.OfficeMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, convertOffice(c))
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
webhookURL string = "webhookURL"
|
|
||||||
paperWidth string = "paperWidth"
|
|
||||||
paperHeight string = "paperHeight"
|
|
||||||
marginTop string = "marginTop"
|
|
||||||
marginBottom string = "marginBottom"
|
|
||||||
marginLeft string = "marginLeft"
|
|
||||||
marginRight string = "marginRight"
|
|
||||||
landscape string = "landscape"
|
|
||||||
)
|
|
||||||
|
|
||||||
// resource facilitates storing and accessing
|
|
||||||
// data from a multipart/form-data request.
|
|
||||||
type resource struct {
|
|
||||||
values map[string]string
|
|
||||||
dirPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newResource(c echo.Context) (*resource, error) {
|
|
||||||
v := make(map[string]string)
|
|
||||||
v[webhookURL] = c.FormValue(webhookURL)
|
|
||||||
v[paperWidth] = c.FormValue(paperWidth)
|
|
||||||
v[paperHeight] = c.FormValue(paperHeight)
|
|
||||||
v[marginTop] = c.FormValue(marginTop)
|
|
||||||
v[marginBottom] = c.FormValue(marginBottom)
|
|
||||||
v[marginLeft] = c.FormValue(marginLeft)
|
|
||||||
v[marginRight] = c.FormValue(marginRight)
|
|
||||||
v[landscape] = c.FormValue(landscape)
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
|
||||||
}
|
|
||||||
r := &resource{values: v, dirPath: dirPath}
|
|
||||||
form, err := c.MultipartForm()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting multipart form: %v", err)
|
|
||||||
}
|
|
||||||
for _, files := range form.File {
|
|
||||||
for _, fh := range files {
|
|
||||||
in, err := fh.Open()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
if err := r.writeFile(fh.Filename, in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) writeFile(filename string, in io.Reader) error {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
if err := out.Chmod(0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := out.Seek(0, 0); err != nil {
|
|
||||||
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) filePath(filename string) (string, error) {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
_, err := os.Stat(fpath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return "", fmt.Errorf("%s: file does not exist", filename)
|
|
||||||
}
|
|
||||||
absPath, err := filepath.Abs(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: getting absolute path: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return absPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) filePaths(exts []string) ([]string, error) {
|
|
||||||
var fpaths []string
|
|
||||||
err := filepath.Walk(r.dirPath, func(path string, info os.FileInfo, _ error) error {
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fpath, err := r.filePath(info.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, ext := range exts {
|
|
||||||
if filepath.Ext(fpath) == ext {
|
|
||||||
fpaths = append(fpaths, fpath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return fpaths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) paperSize() ([2]float64, error) {
|
|
||||||
defaultSize := [2]float64{8.27, 11.7}
|
|
||||||
widthStr := r.values[paperWidth]
|
|
||||||
heightStr := r.values[paperHeight]
|
|
||||||
if widthStr == "" || heightStr == "" {
|
|
||||||
return defaultSize, nil
|
|
||||||
}
|
|
||||||
width, err := strconv.ParseFloat(widthStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultSize, fmt.Errorf("paper width: %v", err)
|
|
||||||
}
|
|
||||||
height, err := strconv.ParseFloat(heightStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultSize, fmt.Errorf("paper height: %v", err)
|
|
||||||
}
|
|
||||||
return [2]float64{width, height}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) paperMargins() ([4]float64, error) {
|
|
||||||
defaultMargins := [4]float64{1, 1, 1, 1}
|
|
||||||
topStr := r.values[marginTop]
|
|
||||||
bottomStr := r.values[marginBottom]
|
|
||||||
leftStr := r.values[marginLeft]
|
|
||||||
rightStr := r.values[marginRight]
|
|
||||||
if topStr == "" || bottomStr == "" || leftStr == "" || rightStr == "" {
|
|
||||||
return defaultMargins, nil
|
|
||||||
}
|
|
||||||
top, err := strconv.ParseFloat(topStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin top: %v", err)
|
|
||||||
}
|
|
||||||
bottom, err := strconv.ParseFloat(bottomStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin bottom: %v", err)
|
|
||||||
}
|
|
||||||
left, err := strconv.ParseFloat(leftStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin left: %v", err)
|
|
||||||
}
|
|
||||||
right, err := strconv.ParseFloat(rightStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin right: %v", err)
|
|
||||||
}
|
|
||||||
return [4]float64{top, bottom, left, right}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) landscape() (bool, error) {
|
|
||||||
landscapeStr := r.values[landscape]
|
|
||||||
if landscapeStr == "" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
landscape, err := strconv.ParseBool(landscapeStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("landscape: %v", err)
|
|
||||||
}
|
|
||||||
return landscape, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) webhookURL() string { return r.values[webhookURL] }
|
|
||||||
func (r *resource) removeAll() error { return os.RemoveAll(r.dirPath) }
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/*
|
|
||||||
Package notify is used across the application
|
|
||||||
to display nice outputs to the user.
|
|
||||||
*/
|
|
||||||
package notify
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package notify
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/gommon/color"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
stdout *color.Color
|
|
||||||
stderr *color.Color
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
stdout = color.New()
|
|
||||||
stdout.SetOutput(os.Stdout)
|
|
||||||
stderr = color.New()
|
|
||||||
stderr.SetOutput(os.Stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Println prints a message to stdout.
|
|
||||||
func Println(message string) {
|
|
||||||
stdout.Printf("⇨ %s\n", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrPrintln prints an error to stderr.
|
|
||||||
func ErrPrintln(err error) {
|
|
||||||
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Chrome facilitates starting or shutting down
|
|
||||||
// Chrome headless with PM2.
|
|
||||||
type Chrome struct{}
|
|
||||||
|
|
||||||
// Launch starts Chrome headless with PM2.
|
|
||||||
func (c *Chrome) Launch() error {
|
|
||||||
return launch(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown stops Chrome headless and
|
|
||||||
// removes it from the list of PM2
|
|
||||||
// processes.
|
|
||||||
func (c *Chrome) Shutdown() error {
|
|
||||||
return shutdown(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) getArgs() []string {
|
|
||||||
return []string{
|
|
||||||
"--no-sandbox",
|
|
||||||
"--headless",
|
|
||||||
"--remote-debugging-port=9222",
|
|
||||||
"--disable-gpu",
|
|
||||||
"--disable-translate",
|
|
||||||
"--disable-extensions",
|
|
||||||
"--disable-background-networking",
|
|
||||||
"--safebrowsing-disable-auto-update",
|
|
||||||
"--disable-sync",
|
|
||||||
"--disable-default-apps",
|
|
||||||
"--hide-scrollbars",
|
|
||||||
"--metrics-recording-only",
|
|
||||||
"--mute-audio",
|
|
||||||
"--no-first-run",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) getName() string {
|
|
||||||
return "google-chrome-stable"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) getFullname() string {
|
|
||||||
return "Chrome headless"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) isViable() bool {
|
|
||||||
// check if Chrome is correctly running.
|
|
||||||
devt := devtool.New("http://127.0.0.1:9222")
|
|
||||||
_, err := devt.Create(context.TODO())
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) warmup() {
|
|
||||||
notify.Println(fmt.Sprintf("warming-up %s", c.getFullname()))
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Process(new(Chrome))
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChromeLaunch(t *testing.T) {
|
|
||||||
p := &Chrome{}
|
|
||||||
err := p.Launch()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChromeShutdown(t *testing.T) {
|
|
||||||
p := &Chrome{}
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
Package pm2 facilitates starting external
|
|
||||||
processes on which our API depends.
|
|
||||||
|
|
||||||
For instance, it starts 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,71 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Process is a type that can launch or
|
|
||||||
// shutdown a process with PM2.
|
|
||||||
type Process interface {
|
|
||||||
Launch() error
|
|
||||||
Shutdown() error
|
|
||||||
getArgs() []string
|
|
||||||
getName() string
|
|
||||||
getFullname() string
|
|
||||||
isViable() bool
|
|
||||||
warmup()
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxRestartAttempts int = 5
|
|
||||||
|
|
||||||
var humanNames = map[string]string{
|
|
||||||
"start": "started",
|
|
||||||
"restart": "restarted",
|
|
||||||
"stop": "stopped",
|
|
||||||
}
|
|
||||||
|
|
||||||
func launch(p Process) error {
|
|
||||||
if err := run(p, "start"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.warmup()
|
|
||||||
if !p.isViable() {
|
|
||||||
attempts := 0
|
|
||||||
for attempts < maxRestartAttempts && !p.isViable() {
|
|
||||||
run(p, "restart")
|
|
||||||
p.warmup()
|
|
||||||
attempts++
|
|
||||||
}
|
|
||||||
if !p.isViable() {
|
|
||||||
return fmt.Errorf("failed to launch %s", p.getFullname())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shutdown(p Process) error {
|
|
||||||
return run(p, "stop")
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(p Process, cmdName string) error {
|
|
||||||
cmdArgs := []string{
|
|
||||||
cmdName,
|
|
||||||
p.getName(),
|
|
||||||
}
|
|
||||||
if cmdName == "start" {
|
|
||||||
cmdArgs = append(cmdArgs, "--interpreter none", "--")
|
|
||||||
cmdArgs = append(cmdArgs, p.getArgs()...)
|
|
||||||
}
|
|
||||||
cmd := exec.Command(
|
|
||||||
"pm2",
|
|
||||||
cmdArgs...,
|
|
||||||
)
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.getFullname(), err)
|
|
||||||
}
|
|
||||||
notify.Println(fmt.Sprintf("%s %s with PM2", p.getFullname(), humanNames[cmdName]))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
// Unoconv facilitates starting or shutting down
|
|
||||||
// unoconv listener with PM2.
|
|
||||||
type Unoconv struct{}
|
|
||||||
|
|
||||||
// Launch starts unoconv listener with PM2.
|
|
||||||
func (u *Unoconv) Launch() error {
|
|
||||||
return launch(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown stops unoconv listener and
|
|
||||||
// removes it from the list of PM2
|
|
||||||
// processes.
|
|
||||||
func (u *Unoconv) Shutdown() error {
|
|
||||||
return shutdown(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unoconv) getArgs() []string {
|
|
||||||
return []string{
|
|
||||||
"--listener",
|
|
||||||
"--verbose",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unoconv) getName() string {
|
|
||||||
return "unoconv"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unoconv) getFullname() string {
|
|
||||||
return "unoconv listener"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unoconv) isViable() bool {
|
|
||||||
// TODO find a way to check if
|
|
||||||
// unoconv is correctly started?
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *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 TestUnoconvLaunch(t *testing.T) {
|
|
||||||
p := &Unoconv{}
|
|
||||||
err := p.Launch()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconvShutdown(t *testing.T) {
|
|
||||||
p := &Unoconv{}
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
Package printer contains structs which convert
|
|
||||||
a specific file type to PDF:
|
|
||||||
|
|
||||||
// converting HTML to PDF.
|
|
||||||
p := &printer.HTML{
|
|
||||||
Context: context.Background(),
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
Landscape: false,
|
|
||||||
}
|
|
||||||
p.WithLocalURL("index.html")
|
|
||||||
if err := p.Print("result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// converting Markdown to PDF:
|
|
||||||
// it assumes here that our template "index.html"
|
|
||||||
// will call toHTML method to convert
|
|
||||||
// markdown files to HTML.
|
|
||||||
p := &printer.Markdown{
|
|
||||||
Context: context.Background(),
|
|
||||||
TemplatePath: "index.html",
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
Landscape: false,
|
|
||||||
}
|
|
||||||
if err := p.Print("result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// converting Office documents to PDF:
|
|
||||||
// it converts each files independently and
|
|
||||||
// then merge them.
|
|
||||||
//
|
|
||||||
// Also, as unoconv cannot perform
|
|
||||||
// concurrent conversions, a lock is applied.
|
|
||||||
p := &printer.Office{
|
|
||||||
Context: ctx,
|
|
||||||
FilePaths: []string{"document.docx", "presentation.pptx"}
|
|
||||||
}
|
|
||||||
if err := p.Print("result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
It is also able to merge a list of PDF files:
|
|
||||||
|
|
||||||
if err := printer.Merge([]string{"foo.pdf", "bar.pdf"}, "result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
package printer
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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/runtime"
|
|
||||||
"github.com/mafredri/cdp/protocol/target"
|
|
||||||
"github.com/mafredri/cdp/rpcc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTML facilitates HTML to PDF conversion.
|
|
||||||
type HTML struct {
|
|
||||||
Context context.Context
|
|
||||||
URL string
|
|
||||||
HeaderHTML string
|
|
||||||
FooterHTML string
|
|
||||||
PaperWidth float64
|
|
||||||
PaperHeight float64
|
|
||||||
MarginTop float64
|
|
||||||
MarginBottom float64
|
|
||||||
MarginLeft float64
|
|
||||||
MarginRight float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
|
||||||
|
|
||||||
// Print converts HTML to PDF.
|
|
||||||
// Credits: https://medium.com/compass-true-north/go-service-to-convert-web-pages-to-pdf-using-headless-chrome-5fd9ffbae1af
|
|
||||||
func (html *HTML) Print(destination string) error {
|
|
||||||
// use the DevTools HTTP/JSON API to manage targets (e.g. pages, webworkers).
|
|
||||||
devt := devtool.New("http://127.0.0.1:9222")
|
|
||||||
pt, err := devt.Create(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating DevTools target: %v", err)
|
|
||||||
}
|
|
||||||
// open a new RPC connection to the Chrome Debugging Protocol target.
|
|
||||||
conn, err := rpcc.DialContext(html.Context, pt.WebSocketDebuggerURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating RPC connection: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
// create new browser context.
|
|
||||||
baseBrowser := cdp.NewClient(conn)
|
|
||||||
newContextTarget, err := baseBrowser.Target.CreateBrowserContext(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new browser context: %v", err)
|
|
||||||
}
|
|
||||||
// create a new blank target with the new browser context.
|
|
||||||
newTargetArgs := target.NewCreateTargetArgs("about:blank").
|
|
||||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
|
||||||
newTarget, err := baseBrowser.Target.CreateTarget(html.Context, newTargetArgs)
|
|
||||||
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(html.Context, newTargetWsURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connecting client to blank target: %v", err)
|
|
||||||
}
|
|
||||||
defer newContextConn.Close()
|
|
||||||
// close the target when done.
|
|
||||||
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
|
||||||
defer baseBrowser.Target.CloseTarget(html.Context, closeTargetArgs)
|
|
||||||
c := cdp.NewClient(newContextConn)
|
|
||||||
// enable the runtime.
|
|
||||||
if err := c.Runtime.Enable(html.Context); err != nil {
|
|
||||||
return fmt.Errorf("enabling runtime: %v", err)
|
|
||||||
}
|
|
||||||
// enable the network.
|
|
||||||
if err := c.Network.Enable(html.Context, network.NewEnableArgs()); err != nil {
|
|
||||||
return fmt.Errorf("enabling network: %v", err)
|
|
||||||
}
|
|
||||||
// enable events on the page domain.
|
|
||||||
if err := c.Page.Enable(html.Context); err != nil {
|
|
||||||
return fmt.Errorf("enabling events on page domain: %v", err)
|
|
||||||
}
|
|
||||||
// create a client to listen for the load event to be fired.
|
|
||||||
loadEventFiredClient, err := c.Page.LoadEventFired(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating client listening for load event: %v", err)
|
|
||||||
}
|
|
||||||
defer loadEventFiredClient.Close()
|
|
||||||
// tell the page to navigate to the URL.
|
|
||||||
navArgs := page.NewNavigateArgs(html.URL)
|
|
||||||
_, err = c.Page.Navigate(html.Context, navArgs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: navigating to URL: %v", html.URL, err)
|
|
||||||
}
|
|
||||||
// wait for the page to finish loading.
|
|
||||||
_, err = loadEventFiredClient.Recv()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("waiting for page loading: %v", err)
|
|
||||||
}
|
|
||||||
// inject a script to make sure web fonts are loaded.
|
|
||||||
script := `new Promise((resolve, reject) => {
|
|
||||||
document.fonts.ready.then(function () {
|
|
||||||
resolve('fonts loaded');
|
|
||||||
});
|
|
||||||
setTimeout(resolve.bind(resolve, 'timeout'), %.0f);
|
|
||||||
});`
|
|
||||||
scriptArg := runtime.NewEvaluateArgs(script).SetAwaitPromise(true)
|
|
||||||
returnObj, _ := c.Runtime.Evaluate(html.Context, scriptArg)
|
|
||||||
loadFontsResult := string(returnObj.Result.Value)
|
|
||||||
if strings.Contains(loadFontsResult, "timeout") {
|
|
||||||
return errors.New("timed out loading fonts")
|
|
||||||
}
|
|
||||||
// if no header or footer, use the default template
|
|
||||||
// for avoiding displaying default Chrome templates.
|
|
||||||
if html.HeaderHTML == "" {
|
|
||||||
html.HeaderHTML = defaultHeaderFooterHTML
|
|
||||||
}
|
|
||||||
if html.FooterHTML == "" {
|
|
||||||
html.FooterHTML = defaultHeaderFooterHTML
|
|
||||||
}
|
|
||||||
print, err := c.Page.PrintToPDF(
|
|
||||||
html.Context,
|
|
||||||
page.NewPrintToPDFArgs().
|
|
||||||
SetPaperWidth(html.PaperWidth).
|
|
||||||
SetPaperHeight(html.PaperHeight).
|
|
||||||
SetMarginTop(html.MarginTop).
|
|
||||||
SetMarginBottom(html.MarginBottom).
|
|
||||||
SetMarginLeft(html.MarginLeft).
|
|
||||||
SetMarginRight(html.MarginRight).
|
|
||||||
SetLandscape(html.Landscape).
|
|
||||||
SetDisplayHeaderFooter(true).
|
|
||||||
SetHeaderTemplate(html.HeaderHTML).
|
|
||||||
SetFooterTemplate(html.FooterHTML).
|
|
||||||
SetPrintBackground(true),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("printing page to PDF: %v", err)
|
|
||||||
}
|
|
||||||
return writeBytesToFile(destination, print.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLocalURL sets a local URL from a file path.
|
|
||||||
func (html *HTML) WithLocalURL(fpath string) {
|
|
||||||
html.URL = fmt.Sprintf("file://%s", fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHeaderFile sets header content from a file.
|
|
||||||
func (html *HTML) WithHeaderFile(fpath string) error {
|
|
||||||
if fpath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
contentHTML, err := fileContentToString(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
html.HeaderHTML = contentHTML
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFooterFile sets footer content from a file.
|
|
||||||
func (html *HTML) WithFooterFile(fpath string) error {
|
|
||||||
if fpath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
contentHTML, err := fileContentToString(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
html.FooterHTML = contentHTML
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileContentToString(fpath string) (string, error) {
|
|
||||||
b, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(HTML))
|
|
||||||
)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
dirPath := test.HTMLTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
html := &HTML{
|
|
||||||
Context: ctx,
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
MarginTop: 1,
|
|
||||||
MarginBottom: 1,
|
|
||||||
MarginLeft: 1,
|
|
||||||
MarginRight: 1,
|
|
||||||
}
|
|
||||||
html.WithLocalURL(fmt.Sprintf("%s/%s", dirPath, "index.html"))
|
|
||||||
err := html.WithHeaderFile(fmt.Sprintf("%s/%s", dirPath, "header.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
err = html.WithFooterFile(fmt.Sprintf("%s/%s", dirPath, "footer.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err = html.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
|
||||||
"github.com/russross/blackfriday/v2"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Markdown facilitates Markdown to PDF conversion.
|
|
||||||
type Markdown struct {
|
|
||||||
Context context.Context
|
|
||||||
TemplatePath string
|
|
||||||
HeaderHTML string
|
|
||||||
FooterHTML string
|
|
||||||
PaperWidth float64
|
|
||||||
PaperHeight float64
|
|
||||||
MarginTop float64
|
|
||||||
MarginBottom float64
|
|
||||||
MarginLeft float64
|
|
||||||
MarginRight float64
|
|
||||||
Landscape bool
|
|
||||||
|
|
||||||
html *HTML
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateData struct {
|
|
||||||
DirPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print converts markdown to PDF.
|
|
||||||
func (md *Markdown) Print(destination string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
if md.HeaderHTML != "" {
|
|
||||||
md.html.HeaderHTML = md.HeaderHTML
|
|
||||||
}
|
|
||||||
if md.FooterHTML != "" {
|
|
||||||
md.html.FooterHTML = md.FooterHTML
|
|
||||||
}
|
|
||||||
md.html.PaperWidth = md.PaperWidth
|
|
||||||
md.html.PaperHeight = md.PaperHeight
|
|
||||||
md.html.MarginTop = md.MarginTop
|
|
||||||
md.html.MarginBottom = md.MarginBottom
|
|
||||||
md.html.MarginLeft = md.MarginLeft
|
|
||||||
md.html.MarginRight = md.MarginRight
|
|
||||||
md.html.Landscape = md.Landscape
|
|
||||||
tmpl, err := template.
|
|
||||||
New(filepath.Base(md.TemplatePath)).
|
|
||||||
Funcs(template.FuncMap{"toHTML": toHTML}).
|
|
||||||
ParseFiles(md.TemplatePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: parsing template: %v", md.TemplatePath, err)
|
|
||||||
}
|
|
||||||
dirPath := filepath.Dir(md.TemplatePath)
|
|
||||||
data := &templateData{DirPath: dirPath}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
|
||||||
return fmt.Errorf("%s: executing template: %v", md.TemplatePath, err)
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
|
||||||
if err := writeBytesToFile(dst, buffer.Bytes()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
md.html.WithLocalURL(dst)
|
|
||||||
return md.html.Print(destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toHTML(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)
|
|
||||||
contentHTML := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
|
||||||
return template.HTML(contentHTML), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHeaderFile sets header content from a file.
|
|
||||||
func (md *Markdown) WithHeaderFile(fpath string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
return md.html.WithHeaderFile(fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFooterFile sets footer content from a file.
|
|
||||||
func (md *Markdown) WithFooterFile(fpath string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
return md.html.WithFooterFile(fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(Markdown))
|
|
||||||
)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
dirPath := test.MarkdownTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
markdown := &Markdown{
|
|
||||||
Context: ctx,
|
|
||||||
TemplatePath: fmt.Sprintf("%s/%s", dirPath, "index.html"),
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
MarginTop: 1,
|
|
||||||
MarginBottom: 1,
|
|
||||||
MarginLeft: 1,
|
|
||||||
MarginRight: 1,
|
|
||||||
}
|
|
||||||
err := markdown.WithHeaderFile(fmt.Sprintf("%s/%s", dirPath, "header.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
err = markdown.WithFooterFile(fmt.Sprintf("%s/%s", dirPath, "footer.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err = markdown.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
// Office facilitates Office documents to PDF conversion.
|
|
||||||
type Office struct {
|
|
||||||
Context context.Context
|
|
||||||
FilePaths []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print converts Office documents to PDF.
|
|
||||||
func (o *Office) Print(destination string) error {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
fpaths := make([]string, len(o.FilePaths))
|
|
||||||
dirPath := filepath.Dir(destination)
|
|
||||||
for i, fpath := range o.FilePaths {
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmpDest := fmt.Sprintf("%s/%s.pdf", dirPath, baseFilename)
|
|
||||||
cmd := exec.CommandContext(
|
|
||||||
o.Context,
|
|
||||||
"unoconv",
|
|
||||||
"--format",
|
|
||||||
"pdf",
|
|
||||||
"--output",
|
|
||||||
tmpDest,
|
|
||||||
fpath,
|
|
||||||
)
|
|
||||||
_, err = cmd.Output()
|
|
||||||
if o.Context.Err() == context.DeadlineExceeded {
|
|
||||||
return errors.New("unoconv: command timed out")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unoconv: non-zero exit code: %v", err)
|
|
||||||
}
|
|
||||||
fpaths[i] = tmpDest
|
|
||||||
}
|
|
||||||
if len(fpaths) == 1 {
|
|
||||||
return os.Rename(fpaths[0], destination)
|
|
||||||
}
|
|
||||||
return Merge(fpaths, destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(Office))
|
|
||||||
)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
dirPath := test.OfficeTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
office := &Office{
|
|
||||||
Context: ctx,
|
|
||||||
FilePaths: []string{
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "document.docx"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err := office.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
pdfcpuAPI "github.com/hhrutter/pdfcpu/pkg/api"
|
|
||||||
pdfcpuConfig "github.com/hhrutter/pdfcpu/pkg/pdfcpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge merges PDF files.
|
|
||||||
func Merge(fpaths []string, destination string) error {
|
|
||||||
cmd := pdfcpuAPI.MergeCommand(fpaths, destination, pdfcpuConfig.NewDefaultConfiguration())
|
|
||||||
_, err := pdfcpuAPI.Merge(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeBytesToFile(dst string, b []byte) error {
|
|
||||||
if err := ioutil.WriteFile(dst, b, 0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: writting file: %v", dst, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
dirPath := test.PDFTestDirPath(t)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err := Merge(
|
|
||||||
[]string{
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "gotenberg.pdf"),
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "gotenberg.pdf"),
|
|
||||||
},
|
|
||||||
dst,
|
|
||||||
)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Gotenberg Go client
|
|
||||||
|
|
||||||
A simple Go client for interacting with a Gotenberg API.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go get -u github.com/thecodingmachine/gotenberg
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// HTML conversion example.
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
"style.css",
|
|
||||||
"img.png",
|
|
||||||
},
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
HeaderFilePath: "header.html",
|
|
||||||
FooterFilePath: "footer.html",
|
|
||||||
PaperSize: gotenberg.A4,
|
|
||||||
PaperMargins: gotenberg.NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "foo.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For more complete usages, head to the [documentation](https://thecodingmachine.github.io/gotenberg).
|
|
||||||
155
pkg/client.go
155
pkg/client.go
@@ -1,155 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
webhookURL string = "webhookURL"
|
|
||||||
paperWidth string = "paperWidth"
|
|
||||||
paperHeight string = "paperHeight"
|
|
||||||
marginTop string = "marginTop"
|
|
||||||
marginBottom string = "marginBottom"
|
|
||||||
marginLeft string = "marginLeft"
|
|
||||||
marginRight string = "marginRight"
|
|
||||||
landscape string = "landscape"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// A3 paper size.
|
|
||||||
A3 = [2]float64{11.7, 16.5}
|
|
||||||
// A4 paper size.
|
|
||||||
A4 = [2]float64{8.27, 11.7}
|
|
||||||
// A5 paper size.
|
|
||||||
A5 = [2]float64{5.8, 8.3}
|
|
||||||
// A6 paper size.
|
|
||||||
A6 = [2]float64{4.1, 5.8}
|
|
||||||
// Letter paper size.
|
|
||||||
Letter = [2]float64{8.5, 11}
|
|
||||||
// Legal paper size.
|
|
||||||
Legal = [2]float64{8.5, 14}
|
|
||||||
// Tabloid paper size.
|
|
||||||
Tabloid = [2]float64{11, 17}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// NoMargins removes margins.
|
|
||||||
NoMargins = [4]float64{0, 0, 0, 0}
|
|
||||||
// NormalMargins uses 1 inche margins.
|
|
||||||
NormalMargins = [4]float64{1, 1, 1, 1}
|
|
||||||
// LargeMargins uses 2 inche margins.
|
|
||||||
LargeMargins = [4]float64{2, 2, 2, 2}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client facilitates interacting with
|
|
||||||
// the Gotenberg API.
|
|
||||||
type Client struct {
|
|
||||||
Hostname string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request is a type for sending
|
|
||||||
// form values and form files to
|
|
||||||
// the Gotenberg API.
|
|
||||||
type Request interface {
|
|
||||||
validate() error
|
|
||||||
getPostURL() string
|
|
||||||
getFormValues() map[string]string
|
|
||||||
getFormFiles() map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post sends a request to the Gotenberg API
|
|
||||||
// and returns the response.
|
|
||||||
func (c *Client) Post(req Request) (*http.Response, error) {
|
|
||||||
if err := req.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
body, contentType, err := multipartForm(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL := fmt.Sprintf("%s%s", c.Hostname, req.getPostURL())
|
|
||||||
resp, err := http.Post(URL, contentType, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store creates the resulting PDF to given destination.
|
|
||||||
func (c *Client) Store(req Request, dest string) error {
|
|
||||||
if hasWebhook(req) {
|
|
||||||
return errors.New("cannot use Store method with a webhook")
|
|
||||||
}
|
|
||||||
resp, err := c.Post(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return writeNewFile(dest, resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasWebhook(req Request) bool {
|
|
||||||
webhookURL, ok := req.getFormValues()[webhookURL]
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return webhookURL != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeNewFile(fpath string, in io.Reader) error {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil {
|
|
||||||
return fmt.Errorf("%s: making directory for file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
err = out.Chmod(0644)
|
|
||||||
if err != nil && runtime.GOOS != "windows" {
|
|
||||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(out, in)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileExists(name string) bool {
|
|
||||||
_, err := os.Stat(name)
|
|
||||||
return !os.IsNotExist(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func multipartForm(req Request) (*bytes.Buffer, string, error) {
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
defer writer.Close()
|
|
||||||
for filename, fpath := range req.getFormFiles() {
|
|
||||||
in, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: opening file: %v", filename, err)
|
|
||||||
}
|
|
||||||
part, err := writer.CreateFormFile("files", filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: creating form file: %v", filename, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(part, in)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: copying file: %v", filename, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, value := range req.getFormValues() {
|
|
||||||
if err := writer.WriteField(name, value); err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: writting form field: %v", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return body, writer.FormDataContentType(), nil
|
|
||||||
}
|
|
||||||
31
pkg/doc.go
31
pkg/doc.go
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
Package gotenberg is a Go client for
|
|
||||||
interacting with a Gotenberg API.
|
|
||||||
|
|
||||||
For instance, if you want to convert HTML:
|
|
||||||
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// HTML conversion example.
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
"style.css",
|
|
||||||
"img.png",
|
|
||||||
},
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
HeaderFilePath: "header.html",
|
|
||||||
FooterFilePath: "footer.html",
|
|
||||||
PaperSize: gotenberg.A4,
|
|
||||||
PaperMargins: gotenberg.NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "foo.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
For more complete usages, head to the https://thecodingmachine.gotenberg.github.io.
|
|
||||||
*/
|
|
||||||
package gotenberg
|
|
||||||
187
pkg/gotenberg/cmd.go
Normal file
187
pkg/gotenberg/cmd.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
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("cmd"),
|
||||||
|
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("cmd"),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() error {
|
||||||
|
if cmd.ctx == nil {
|
||||||
|
return errors.New("nil context")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errChan <- cmd.process.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-errChan:
|
||||||
|
errProc := cmd.Kill()
|
||||||
|
if errProc != nil {
|
||||||
|
cmd.logger.Error(errProc.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unix process error: %w", err)
|
||||||
|
case <-cmd.ctx.Done():
|
||||||
|
errProc := cmd.Kill()
|
||||||
|
if errProc != nil {
|
||||||
|
cmd.logger.Error(errProc.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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)
|
||||||
|
}
|
||||||
220
pkg/gotenberg/cmd_test.go
Normal file
220
pkg/gotenberg/cmd_test.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
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 Setpgid to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandContext(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
ctx context.Context
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ctx: nil,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.TODO(),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
|
||||||
|
|
||||||
|
if err == nil && !cmd.process.SysProcAttr.Setpgid {
|
||||||
|
t.Fatalf("test %d: expected Setpgid to be true", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestCmd_Start(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
cmd Cmd
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewNop(), "foo"),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
err := tc.cmd.Start()
|
||||||
|
|
||||||
|
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 TestCmd_Exec(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
cmd Cmd
|
||||||
|
timeout time.Duration
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewNop(), "foo"),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewNop(), "foo"),
|
||||||
|
timeout: time.Duration(5) * time.Second,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
timeout: time.Duration(5) * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewNop(), "sleep", "3"),
|
||||||
|
timeout: time.Duration(2) * time.Second,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if tc.timeout > 0 {
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tc.cmd.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tc.cmd.Exec()
|
||||||
|
|
||||||
|
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 TestCmd_pipeOutput(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
cmd Cmd
|
||||||
|
run bool
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
|
_, err := cmd.process.StdoutPipe()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
|
_, err := cmd.process.StderrPipe()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
|
||||||
|
run: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
err := tc.cmd.pipeOutput()
|
||||||
|
|
||||||
|
if tc.run {
|
||||||
|
errStart := tc.cmd.process.Start()
|
||||||
|
|
||||||
|
if errStart != nil {
|
||||||
|
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestCmd_Kill(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
cmd Cmd
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: Cmd{logger: zap.NewNop()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||||
|
err := cmd.process.Start()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
err := cmd.process.Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
err := tc.cmd.Kill()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
pkg/gotenberg/context.go
Normal file
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
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
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
|
||||||
109
pkg/gotenberg/flags.go
Normal file
109
pkg/gotenberg/flags.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/gommon/bytes"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParsedFlags wraps a flag.FlagSet so that retrieving the typed values is
|
||||||
|
// easier.
|
||||||
|
type ParsedFlags struct {
|
||||||
|
*flag.FlagSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustString returns the string value of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustString(name string) string {
|
||||||
|
val, err := f.GetString(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustStringSlice returns the string slice value of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustStringSlice(name string) []string {
|
||||||
|
val, err := f.GetStringSlice(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBool returns the boolean value of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustBool(name string) bool {
|
||||||
|
val, err := f.GetBool(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustInt returns the int value of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustInt(name string) int {
|
||||||
|
val, err := f.GetInt(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustFloat64 returns the float value of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustFloat64(name string) float64 {
|
||||||
|
val, err := f.GetFloat64(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustDuration returns the time.Duration value of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustDuration(name string) time.Duration {
|
||||||
|
val, err := f.GetDuration(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustHumanReadableBytesString returns the human-readable bytes string of a
|
||||||
|
// flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustHumanReadableBytesString(name string) string {
|
||||||
|
val, err := f.GetString(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = bytes.Parse(val)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRegexp returns the regular expression of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustRegexp(name string) *regexp.Regexp {
|
||||||
|
val, err := f.GetString(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return regexp.MustCompile(val)
|
||||||
|
}
|
||||||
378
pkg/gotenberg/flags_test.go
Normal file
378
pkg/gotenberg/flags_test.go
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsedFlags_MustString(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.String("foo", "", "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=foo"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustString(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustStringSlice(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.StringSlice("foo", make([]string, 0), "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=foo", "--foo=bar", "--foo=baz"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustStringSlice(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustBool(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.Bool("foo", false, "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=true"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustBool(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustInt(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.Int("foo", 0, "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustInt(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustFloat64(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.Float64("foo", 1.0, "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=2.0"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustFloat64(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustDuration(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.Duration("foo", time.Duration(1)*time.Second, "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=2m"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustDuration(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustHumanReadableBytesString(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.String("foo", "1MB", "")
|
||||||
|
fs.String("bar", "1MB", "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=1GB", "--bar=foo"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "baz",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustHumanReadableBytesString(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustRegexp(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.String("foo", "", "")
|
||||||
|
fs.String("bar", "", "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=", "--bar=*"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "baz",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustRegexp(tc.name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
33
pkg/gotenberg/fs.go
Normal file
33
pkg/gotenberg/fs.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TmpPath returns the default directory to use for temporary files and
|
||||||
|
// directories. Most if not all files and directories created by the
|
||||||
|
// application and its dependencies must be based on this default directory.
|
||||||
|
func TmpPath() string {
|
||||||
|
return os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDirPath returns a random absolute path based on the temporary path.
|
||||||
|
func NewDirPath() string {
|
||||||
|
return fmt.Sprintf("%s/%s", TmpPath(), uuid.New())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MkdirAll creates a random directory based on the temporary path and
|
||||||
|
// returns its absolute path.
|
||||||
|
func MkdirAll() (string, error) {
|
||||||
|
path := NewDirPath()
|
||||||
|
|
||||||
|
err := os.MkdirAll(path, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create directory %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
59
pkg/gotenberg/fs_test.go
Normal file
59
pkg/gotenberg/fs_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTmpPath(t *testing.T) {
|
||||||
|
osTempDir := os.TempDir()
|
||||||
|
tmpPath := TmpPath()
|
||||||
|
|
||||||
|
if tmpPath != osTempDir {
|
||||||
|
t.Errorf("expected path '%s' but got '%s'", osTempDir, tmpPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDirPath(t *testing.T) {
|
||||||
|
newDirPath := NewDirPath()
|
||||||
|
tmpPath := TmpPath()
|
||||||
|
|
||||||
|
if !strings.HasPrefix(newDirPath, tmpPath) {
|
||||||
|
t.Fatalf("expected path '%s' to start with '%s'", newDirPath, tmpPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
newDirPaths := make([]string, 1000)
|
||||||
|
for i := range newDirPaths {
|
||||||
|
newDirPaths[i] = NewDirPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, newDirPath := range newDirPaths {
|
||||||
|
for j, comparison := range newDirPaths {
|
||||||
|
if i == j {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if newDirPath == comparison {
|
||||||
|
t.Fatalf("expected path '%s' (index %d) to be unique, but found an identical path on index %d", newDirPath, i, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMkdirAll(t *testing.T) {
|
||||||
|
path, err := MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpPath := TmpPath()
|
||||||
|
if !strings.HasPrefix(path, tmpPath) {
|
||||||
|
t.Fatalf("expected path '%s' to start with '%s'", path, tmpPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected path '%s' to exist but got: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
pkg/gotenberg/logging.go
Normal file
14
pkg/gotenberg/logging.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import "go.uber.org/zap"
|
||||||
|
|
||||||
|
// LoggerProvider is a module interface which exposes a method for creating a
|
||||||
|
// zap.Logger for other modules.
|
||||||
|
//
|
||||||
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
|
// provider, _ := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
|
// logger, _ := provider.(gotenberg.LoggerProvider).Logger(m)
|
||||||
|
// }
|
||||||
|
type LoggerProvider interface {
|
||||||
|
Logger(mod Module) (*zap.Logger, error)
|
||||||
|
}
|
||||||
135
pkg/gotenberg/modules.go
Normal file
135
pkg/gotenberg/modules.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Module is a sort of plugin which adds new functionalities to the application
|
||||||
|
// or other modules.
|
||||||
|
//
|
||||||
|
// type YourModule struct {
|
||||||
|
// property string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (YourModule) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
// return gotenberg.ModuleDescriptor{
|
||||||
|
// ID: "your_module",
|
||||||
|
// FlagSet: func() *flag.FlagSet {
|
||||||
|
// fs := flag.NewFlagSet("your_module", flag.ExitOnError)
|
||||||
|
// fs.String("your_module-property", "default value", "flag description")
|
||||||
|
//
|
||||||
|
// return fs
|
||||||
|
// }(),
|
||||||
|
// New: func() gotenberg.Module { return new(YourModule) },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
type Module interface {
|
||||||
|
Descriptor() ModuleDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleDescriptor describes your module for the application.
|
||||||
|
type ModuleDescriptor struct {
|
||||||
|
// ID is the unique name (snake case) of the module.
|
||||||
|
// Required.
|
||||||
|
ID string
|
||||||
|
|
||||||
|
// FlagSet is the definition of the flags of the module.
|
||||||
|
// Optional.
|
||||||
|
FlagSet *flag.FlagSet
|
||||||
|
|
||||||
|
// New returns a new and empty instance of the module's type.
|
||||||
|
// Required.
|
||||||
|
New func() Module
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provisioner is a module interface for modules which have to be initialized
|
||||||
|
// according to flags, environment variables, the context, etc.
|
||||||
|
type Provisioner interface {
|
||||||
|
Provision(*Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator is a module interface for modules which have to be validated after
|
||||||
|
// provisioning.
|
||||||
|
type Validator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// App is a module interface for modules which can be started or stopped by the
|
||||||
|
// application.
|
||||||
|
type App interface {
|
||||||
|
Start() error
|
||||||
|
// StartupMessage returns a custom message to display on startup. If it
|
||||||
|
// returns an empty string, a default startup message is used instead.
|
||||||
|
StartupMessage() string
|
||||||
|
Stop(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRegisterModule registers a module.
|
||||||
|
//
|
||||||
|
// To register a module, create an init() method in the module main go file:
|
||||||
|
//
|
||||||
|
// func init() {
|
||||||
|
// gotenberg.MustRegisterModule(YourModule{})
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Then, in the main command (github.com/gotenberg/gotenberg/v7/cmd/gotenberg),
|
||||||
|
// import the module:
|
||||||
|
//
|
||||||
|
// imports (
|
||||||
|
// // Gotenberg modules.
|
||||||
|
// _ "your_module_path"
|
||||||
|
// )
|
||||||
|
func MustRegisterModule(mod Module) {
|
||||||
|
desc := mod.Descriptor()
|
||||||
|
|
||||||
|
if desc.ID == "" {
|
||||||
|
panic("module with an empty ID cannot be registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc.New == nil {
|
||||||
|
panic("module New function cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if val := desc.New(); val == nil {
|
||||||
|
panic("module New function cannot return a nil instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptorsMu.Lock()
|
||||||
|
defer descriptorsMu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := descriptors[desc.ID]; ok {
|
||||||
|
panic(fmt.Sprintf("module %s is already registered", desc.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptors[desc.ID] = desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModuleDescriptors returns the descriptors of all registered modules.
|
||||||
|
func GetModuleDescriptors() []ModuleDescriptor {
|
||||||
|
descriptorsMu.RLock()
|
||||||
|
defer descriptorsMu.RUnlock()
|
||||||
|
|
||||||
|
mods := make([]ModuleDescriptor, len(descriptors))
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for _, desc := range descriptors {
|
||||||
|
mods[i] = desc
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(mods, func(i, j int) bool {
|
||||||
|
return mods[i].ID < mods[j].ID
|
||||||
|
})
|
||||||
|
|
||||||
|
return mods
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
descriptors = make(map[string]ModuleDescriptor)
|
||||||
|
descriptorsMu sync.RWMutex
|
||||||
|
)
|
||||||
135
pkg/gotenberg/modules_test.go
Normal file
135
pkg/gotenberg/modules_test.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtoModule struct {
|
||||||
|
descriptor func() ModuleDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoModule) Descriptor() ModuleDescriptor {
|
||||||
|
return mod.descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtoProvisioner struct {
|
||||||
|
ProtoModule
|
||||||
|
provision func(ctx *Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoProvisioner) Provision(ctx *Context) error {
|
||||||
|
return mod.provision(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtoValidator struct {
|
||||||
|
ProtoModule
|
||||||
|
validate func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoValidator) Validate() error {
|
||||||
|
return mod.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustRegisterModule(t *testing.T) {
|
||||||
|
descriptorsMu.RLock()
|
||||||
|
descriptors = map[string]ModuleDescriptor{
|
||||||
|
"a": {ID: "a"},
|
||||||
|
}
|
||||||
|
descriptorsMu.RUnlock()
|
||||||
|
|
||||||
|
for i, tc := range []struct {
|
||||||
|
ID string
|
||||||
|
New func() Module
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ID: "",
|
||||||
|
New: func() Module { return new(ProtoModule) },
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
New: nil,
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
New: func() Module { return nil },
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
New: func() Module { return new(ProtoModule) },
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
New: func() Module { return new(ProtoModule) },
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
mod := struct{ ProtoModule }{}
|
||||||
|
mod.descriptor = func() ModuleDescriptor { return ModuleDescriptor{ID: tc.ID, New: tc.New} }
|
||||||
|
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("test %d: expected panic but got none", i)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
MustRegisterModule(mod)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptorsMu.RLock()
|
||||||
|
descriptors = make(map[string]ModuleDescriptor)
|
||||||
|
descriptorsMu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetModuleDescriptors(t *testing.T) {
|
||||||
|
descriptorsMu.RLock()
|
||||||
|
descriptors = map[string]ModuleDescriptor{
|
||||||
|
"d": {ID: "d"},
|
||||||
|
"c": {ID: "c"},
|
||||||
|
"b": {ID: "b"},
|
||||||
|
"a": {ID: "a"},
|
||||||
|
}
|
||||||
|
descriptorsMu.RUnlock()
|
||||||
|
|
||||||
|
expect := []ModuleDescriptor{
|
||||||
|
{ID: "a"},
|
||||||
|
{ID: "b"},
|
||||||
|
{ID: "c"},
|
||||||
|
{ID: "d"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := GetModuleDescriptors()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actual, expect) {
|
||||||
|
t.Errorf("expected %v but got %v", expect, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptorsMu.RLock()
|
||||||
|
descriptors = make(map[string]ModuleDescriptor)
|
||||||
|
descriptorsMu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ Module = (*ProtoModule)(nil)
|
||||||
|
_ Provisioner = (*ProtoProvisioner)(nil)
|
||||||
|
_ Module = (*ProtoProvisioner)(nil)
|
||||||
|
_ Validator = (*ProtoValidator)(nil)
|
||||||
|
_ Module = (*ProtoValidator)(nil)
|
||||||
|
)
|
||||||
52
pkg/gotenberg/pdfengine.go
Normal file
52
pkg/gotenberg/pdfengine.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrPDFEngineMethodNotAvailable happens if a PDFEngine method is not
|
||||||
|
// available in the implementation.
|
||||||
|
ErrPDFEngineMethodNotAvailable = errors.New("method not available")
|
||||||
|
|
||||||
|
// ErrPDFFormatNotAvailable happens if a PDFEngine Convert's method does
|
||||||
|
// not handle a specific format.
|
||||||
|
ErrPDFFormatNotAvailable = errors.New("PDF format not available")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatPDFA1a string = "PDF/A-1a"
|
||||||
|
FormatPDFA1b string = "PDF/A-1b"
|
||||||
|
FormatPDFA2a string = "PDF/A-2a"
|
||||||
|
FormatPDFA2b string = "PDF/A-2b"
|
||||||
|
FormatPDFA2u string = "PDF/A-2u"
|
||||||
|
FormatPDFA3a string = "PDF/A-3a"
|
||||||
|
FormatPDFA3b string = "PDF/A-3b"
|
||||||
|
FormatPDFA3u string = "PDF/A-3u"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PDFEngine is a module interface which exposes methods for manipulating one
|
||||||
|
// or more PDFs. Implementations may abstract powerful tools like PDFtk, or
|
||||||
|
// fulfill those methods contracts in Golang directly.
|
||||||
|
type PDFEngine interface {
|
||||||
|
// Merge merges the given PDFs into a unique PDF. The pages' order reflects
|
||||||
|
// order of the given files.
|
||||||
|
Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||||
|
|
||||||
|
// Convert converts the given PDF to a specific PDF format.
|
||||||
|
Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDFEngineProvider is a module interface which exposes a method for creating a
|
||||||
|
// PDFEngine for other modules.
|
||||||
|
//
|
||||||
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
|
// provider, _ := ctx.Module(new(gotenberg.PDFEngineProvider))
|
||||||
|
// pdfengines, _ := provider.(gotenberg.PDFEngineProvider).PDFEngine()
|
||||||
|
// }
|
||||||
|
type PDFEngineProvider interface {
|
||||||
|
PDFEngine() (PDFEngine, error)
|
||||||
|
}
|
||||||
78
pkg/html.go
78
pkg/html.go
@@ -1,78 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTMLRequest facilitates HTML conversion
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type HTMLRequest struct {
|
|
||||||
IndexFilePath string
|
|
||||||
AssetFilePaths []string
|
|
||||||
Options *HTMLOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTMLOptions gathers all options
|
|
||||||
// for HTML conversion with the Gotenberg API.
|
|
||||||
type HTMLOptions struct {
|
|
||||||
WebHookURL string
|
|
||||||
HeaderFilePath string
|
|
||||||
FooterFilePath string
|
|
||||||
PaperSize [2]float64
|
|
||||||
PaperMargins [4]float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) validate() error {
|
|
||||||
if !fileExists(html.IndexFilePath) {
|
|
||||||
return fmt.Errorf("%s: index file does not exist", html.IndexFilePath)
|
|
||||||
}
|
|
||||||
if html.Options.HeaderFilePath != "" && !fileExists(html.Options.HeaderFilePath) {
|
|
||||||
return fmt.Errorf("%s: header file does not exist", html.Options.HeaderFilePath)
|
|
||||||
}
|
|
||||||
if html.Options.FooterFilePath != "" && !fileExists(html.Options.FooterFilePath) {
|
|
||||||
return fmt.Errorf("%s: footer file does not exist", html.Options.FooterFilePath)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) getPostURL() string {
|
|
||||||
return "/convert/html"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) getFormValues() map[string]string {
|
|
||||||
if html.Options == nil {
|
|
||||||
html.Options = &HTMLOptions{}
|
|
||||||
}
|
|
||||||
values := make(map[string]string)
|
|
||||||
values[webhookURL] = html.Options.WebHookURL
|
|
||||||
values[paperWidth] = fmt.Sprintf("%f", html.Options.PaperSize[0])
|
|
||||||
values[paperHeight] = fmt.Sprintf("%f", html.Options.PaperSize[1])
|
|
||||||
values[marginTop] = fmt.Sprintf("%f", html.Options.PaperMargins[0])
|
|
||||||
values[marginBottom] = fmt.Sprintf("%f", html.Options.PaperMargins[1])
|
|
||||||
values[marginLeft] = fmt.Sprintf("%f", html.Options.PaperMargins[2])
|
|
||||||
values[marginRight] = fmt.Sprintf("%f", html.Options.PaperMargins[3])
|
|
||||||
values[landscape] = strconv.FormatBool(html.Options.Landscape)
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) getFormFiles() map[string]string {
|
|
||||||
if html.Options == nil {
|
|
||||||
html.Options = &HTMLOptions{}
|
|
||||||
}
|
|
||||||
files := make(map[string]string)
|
|
||||||
files["index.html"] = html.IndexFilePath
|
|
||||||
files["header.html"] = html.Options.HeaderFilePath
|
|
||||||
files["footer.html"] = html.Options.FooterFilePath
|
|
||||||
for _, fpath := range html.AssetFilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Request(new(HTMLRequest))
|
|
||||||
)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
c := &Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &HTMLRequest{
|
|
||||||
IndexFilePath: test.HTMLTestFilePath(t, "index.html"),
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
test.HTMLTestFilePath(t, "font.woff"),
|
|
||||||
test.HTMLTestFilePath(t, "img.gif"),
|
|
||||||
test.HTMLTestFilePath(t, "style.css"),
|
|
||||||
},
|
|
||||||
Options: &HTMLOptions{
|
|
||||||
HeaderFilePath: test.HTMLTestFilePath(t, "header.html"),
|
|
||||||
FooterFilePath: test.HTMLTestFilePath(t, "footer.html"),
|
|
||||||
PaperSize: A4,
|
|
||||||
PaperMargins: NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
|
|
||||||
err = c.Store(req, dest)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.FileExists(t, dest)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MarkdownRequest facilitates Markdown conversion
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type MarkdownRequest struct {
|
|
||||||
IndexFilePath string
|
|
||||||
MarkdownFilePaths []string
|
|
||||||
AssetFilePaths []string
|
|
||||||
Options *MarkdownOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkdownOptions gathers all options
|
|
||||||
// for Markdown conversion with the Gotenberg API.
|
|
||||||
type MarkdownOptions struct {
|
|
||||||
WebHookURL string
|
|
||||||
HeaderFilePath string
|
|
||||||
FooterFilePath string
|
|
||||||
PaperSize [2]float64
|
|
||||||
PaperMargins [4]float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) validate() error {
|
|
||||||
if !fileExists(markdown.IndexFilePath) {
|
|
||||||
return fmt.Errorf("%s: index file does not exist", markdown.IndexFilePath)
|
|
||||||
}
|
|
||||||
if markdown.Options.HeaderFilePath != "" && !fileExists(markdown.Options.HeaderFilePath) {
|
|
||||||
return fmt.Errorf("%s: header file does not exist", markdown.Options.HeaderFilePath)
|
|
||||||
}
|
|
||||||
if markdown.Options.FooterFilePath != "" && !fileExists(markdown.Options.FooterFilePath) {
|
|
||||||
return fmt.Errorf("%s: footer file does not exist", markdown.Options.FooterFilePath)
|
|
||||||
}
|
|
||||||
for _, fpath := range markdown.MarkdownFilePaths {
|
|
||||||
if !fileExists(fpath) {
|
|
||||||
return fmt.Errorf("%s: markdown file does not exist", fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) getPostURL() string {
|
|
||||||
return "/convert/markdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) getFormValues() map[string]string {
|
|
||||||
if markdown.Options == nil {
|
|
||||||
markdown.Options = &MarkdownOptions{}
|
|
||||||
}
|
|
||||||
values := make(map[string]string)
|
|
||||||
values[webhookURL] = markdown.Options.WebHookURL
|
|
||||||
values[paperWidth] = fmt.Sprintf("%f", markdown.Options.PaperSize[0])
|
|
||||||
values[paperHeight] = fmt.Sprintf("%f", markdown.Options.PaperSize[1])
|
|
||||||
values[marginTop] = fmt.Sprintf("%f", markdown.Options.PaperMargins[0])
|
|
||||||
values[marginBottom] = fmt.Sprintf("%f", markdown.Options.PaperMargins[1])
|
|
||||||
values[marginLeft] = fmt.Sprintf("%f", markdown.Options.PaperMargins[2])
|
|
||||||
values[marginRight] = fmt.Sprintf("%f", markdown.Options.PaperMargins[3])
|
|
||||||
values[landscape] = strconv.FormatBool(markdown.Options.Landscape)
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) getFormFiles() map[string]string {
|
|
||||||
if markdown.Options == nil {
|
|
||||||
markdown.Options = &MarkdownOptions{}
|
|
||||||
}
|
|
||||||
files := make(map[string]string)
|
|
||||||
files["index.html"] = markdown.IndexFilePath
|
|
||||||
files["header.html"] = markdown.Options.HeaderFilePath
|
|
||||||
files["footer.html"] = markdown.Options.FooterFilePath
|
|
||||||
for _, fpath := range markdown.MarkdownFilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
for _, fpath := range markdown.AssetFilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Request(new(MarkdownRequest))
|
|
||||||
)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
c := &Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &MarkdownRequest{
|
|
||||||
IndexFilePath: test.MarkdownTestFilePath(t, "index.html"),
|
|
||||||
MarkdownFilePaths: []string{
|
|
||||||
test.MarkdownTestFilePath(t, "paragraph1.md"),
|
|
||||||
test.MarkdownTestFilePath(t, "paragraph2.md"),
|
|
||||||
test.MarkdownTestFilePath(t, "paragraph3.md"),
|
|
||||||
},
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
test.HTMLTestFilePath(t, "font.woff"),
|
|
||||||
test.HTMLTestFilePath(t, "img.gif"),
|
|
||||||
test.HTMLTestFilePath(t, "style.css"),
|
|
||||||
},
|
|
||||||
Options: &MarkdownOptions{
|
|
||||||
HeaderFilePath: test.MarkdownTestFilePath(t, "header.html"),
|
|
||||||
FooterFilePath: test.MarkdownTestFilePath(t, "footer.html"),
|
|
||||||
PaperSize: A4,
|
|
||||||
PaperMargins: NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
|
|
||||||
err = c.Store(req, dest)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.FileExists(t, dest)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
55
pkg/merge.go
55
pkg/merge.go
@@ -1,55 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MergeRequest facilitates merging PDF
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type MergeRequest struct {
|
|
||||||
FilePaths []string
|
|
||||||
Options *MergeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeOptions gathers all options
|
|
||||||
// for merging PDF
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type MergeOptions struct {
|
|
||||||
WebHookURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) validate() error {
|
|
||||||
for _, fpath := range merge.FilePaths {
|
|
||||||
if !fileExists(fpath) {
|
|
||||||
return fmt.Errorf("%s: PDF file does not exist", fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) getPostURL() string {
|
|
||||||
return "/merge"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) getFormValues() map[string]string {
|
|
||||||
if merge.Options == nil {
|
|
||||||
merge.Options = &MergeOptions{}
|
|
||||||
}
|
|
||||||
values := make(map[string]string)
|
|
||||||
values[webhookURL] = merge.Options.WebHookURL
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) getFormFiles() map[string]string {
|
|
||||||
files := make(map[string]string)
|
|
||||||
for _, fpath := range merge.FilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Request(new(MergeRequest))
|
|
||||||
)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
c := &Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &MergeRequest{
|
|
||||||
FilePaths: []string{
|
|
||||||
test.PDFTestFilePath(t, "gotenberg.pdf"),
|
|
||||||
test.PDFTestFilePath(t, "gotenberg.pdf"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
|
|
||||||
err = c.Store(req, dest)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.FileExists(t, dest)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
490
pkg/modules/api/api.go
Normal file
490
pkg/modules/api/api.go
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gotenberg.MustRegisterModule(API{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// API is a module which provides an HTTP server. Other modules may add
|
||||||
|
// "multipart/form-data" routes, middlewares or health checks.
|
||||||
|
type API struct {
|
||||||
|
port int
|
||||||
|
readTimeout time.Duration
|
||||||
|
processTimeout time.Duration
|
||||||
|
writeTimeout time.Duration
|
||||||
|
rootPath string
|
||||||
|
traceHeader string
|
||||||
|
disableHealthCheckLogging bool
|
||||||
|
webhookAllowList *regexp.Regexp
|
||||||
|
webhookDenyList *regexp.Regexp
|
||||||
|
webhookErrorAllowList *regexp.Regexp
|
||||||
|
webhookErrorDenyList *regexp.Regexp
|
||||||
|
webhookMaxRetry int
|
||||||
|
webhookRetryMinWait time.Duration
|
||||||
|
webhookRetryMaxWait time.Duration
|
||||||
|
disableWebhook bool
|
||||||
|
|
||||||
|
multipartFormDataRoutes []MultipartFormDataRoute
|
||||||
|
externalMiddlewares []Middleware
|
||||||
|
healthChecks []health.CheckerOption
|
||||||
|
logger *zap.Logger
|
||||||
|
srv *echo.Echo
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultipartFormDataRouter is a module interface which adds
|
||||||
|
// "multipart/form-data" routes to the API.
|
||||||
|
type MultipartFormDataRouter interface {
|
||||||
|
Routes() ([]MultipartFormDataRoute, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultipartFormDataRoute represents a "multipart/form-data" route. All routes
|
||||||
|
// uses the HTTP POST method.
|
||||||
|
type MultipartFormDataRoute struct {
|
||||||
|
// Path is the sub path of the route. Must start with a slash.
|
||||||
|
// Required.
|
||||||
|
Path string
|
||||||
|
|
||||||
|
// Handler is the function which handles the request.
|
||||||
|
// Required.
|
||||||
|
Handler func(ctx *Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareProvider is a module interface which adds middlewares to the API.
|
||||||
|
type MiddlewareProvider interface {
|
||||||
|
Middlewares() ([]Middleware, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewarePriority is a type which helps to determine the execution order of
|
||||||
|
// middlewares provided by the MiddlewareProvider modules.
|
||||||
|
type MiddlewarePriority uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
VeryLowPriority MiddlewarePriority = iota
|
||||||
|
LowPriority
|
||||||
|
MediumPriority
|
||||||
|
HighPriority
|
||||||
|
VeryHighPriority
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware is a middleware which can be added to the API's middlewares
|
||||||
|
// chain.
|
||||||
|
//
|
||||||
|
// middleware := Middleware{
|
||||||
|
// Handler: func() echo.MiddlewareFunc {
|
||||||
|
// return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
// return func(c echo.Context) error {
|
||||||
|
// rootPath := c.Get("rootPath").(string)
|
||||||
|
// healthURI := fmt.Sprintf("%shealth", rootPath)
|
||||||
|
//
|
||||||
|
// // Skip the middleware if health check URI.
|
||||||
|
// if c.Request().RequestURI == healthURI {
|
||||||
|
// // Call the next middleware in the chain.
|
||||||
|
// return next(c)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Your middleware process.
|
||||||
|
// // ...
|
||||||
|
//
|
||||||
|
// // Call the next middleware in the chain.
|
||||||
|
// return next(c)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }(),
|
||||||
|
// }
|
||||||
|
type Middleware struct {
|
||||||
|
// RunBeforeRouter tells if the middleware should run before the router
|
||||||
|
// process an HTTP request.
|
||||||
|
// Optional.
|
||||||
|
RunBeforeRouter bool
|
||||||
|
|
||||||
|
// Priority tells if the middleware should be positioned high or not in
|
||||||
|
// the middlewares chain.
|
||||||
|
// Default to VeryLowPriority.
|
||||||
|
// Optional.
|
||||||
|
Priority MiddlewarePriority
|
||||||
|
|
||||||
|
// Handler is the function of the middleware.
|
||||||
|
// Required.
|
||||||
|
Handler echo.MiddlewareFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthChecker is a module interface which allows adding health checks to the
|
||||||
|
// API.
|
||||||
|
//
|
||||||
|
// See https://github.com/alexliesenfeld/health for more details.
|
||||||
|
type HealthChecker interface {
|
||||||
|
Checks() ([]health.CheckerOption, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor returns an API's module descriptor.
|
||||||
|
func (API) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{
|
||||||
|
ID: "api",
|
||||||
|
FlagSet: func() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
||||||
|
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
||||||
|
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
||||||
|
fs.Duration("api-read-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to read a complete request, including the body")
|
||||||
|
fs.Duration("api-process-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to process a request")
|
||||||
|
fs.Duration("api-write-timeout", time.Duration(30)*time.Second, "Set the maximum duration before timing out writes of the response")
|
||||||
|
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
||||||
|
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
||||||
|
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
||||||
|
fs.String("api-webhook-allow-list", "", "Set the allowed URLs for the webhook feature using a regular expression")
|
||||||
|
fs.String("api-webhook-deny-list", "", "Set the denied URLs for the webhook feature using a regular expression")
|
||||||
|
fs.String("api-webhook-error-allow-list", "", "Set the allowed URLs in case of an error for the webhook feature using a regular expression")
|
||||||
|
fs.String("api-webhook-error-deny-list", "", "Set the denied URLs in case of an error for the webhook feature using a regular expression")
|
||||||
|
fs.Int("api-webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
|
||||||
|
fs.Duration("api-webhook-retry-min-wait", time.Duration(1)*time.Second, "Set the minimum duration to wait before trying to call the webhook again")
|
||||||
|
fs.Duration("api-webhook-retry-max-wait", time.Duration(30)*time.Second, "Set the maximum duration to wait before trying to call the webhook again")
|
||||||
|
fs.Bool("api-disable-webhook", false, "Disable the webhook feature")
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
New: func() gotenberg.Module { return new(API) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets the module properties.
|
||||||
|
func (a *API) Provision(ctx *gotenberg.Context) error {
|
||||||
|
flags := ctx.ParsedFlags()
|
||||||
|
a.port = flags.MustInt("api-port")
|
||||||
|
a.readTimeout = flags.MustDuration("api-read-timeout")
|
||||||
|
a.processTimeout = flags.MustDuration("api-process-timeout")
|
||||||
|
a.writeTimeout = flags.MustDuration("api-write-timeout")
|
||||||
|
a.rootPath = flags.MustString("api-root-path")
|
||||||
|
a.traceHeader = flags.MustString("api-trace-header")
|
||||||
|
a.disableHealthCheckLogging = flags.MustBool("api-disable-health-check-logging")
|
||||||
|
a.webhookAllowList = flags.MustRegexp("api-webhook-allow-list")
|
||||||
|
a.webhookDenyList = flags.MustRegexp("api-webhook-deny-list")
|
||||||
|
a.webhookErrorAllowList = flags.MustRegexp("api-webhook-error-allow-list")
|
||||||
|
a.webhookErrorDenyList = flags.MustRegexp("api-webhook-error-deny-list")
|
||||||
|
a.webhookMaxRetry = flags.MustInt("api-webhook-max-retry")
|
||||||
|
a.webhookRetryMinWait = flags.MustDuration("api-webhook-retry-min-wait")
|
||||||
|
a.webhookRetryMaxWait = flags.MustDuration("api-webhook-retry-max-wait")
|
||||||
|
a.disableWebhook = flags.MustBool("api-disable-webhook")
|
||||||
|
|
||||||
|
// Port from env?
|
||||||
|
portEnvVar := flags.MustString("api-port-from-env")
|
||||||
|
if portEnvVar != "" {
|
||||||
|
val, ok := os.LookupEnv(portEnvVar)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("environment variable '%s' does not exist", portEnvVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val == "" {
|
||||||
|
return fmt.Errorf("environment variable '%s' is empty", portEnvVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get int value of environment variable '%s': %w", portEnvVar, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.port = port
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get routes from modules.
|
||||||
|
mods, err := ctx.Modules(new(MultipartFormDataRouter))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get multipart/form-data routers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
routers := make([]MultipartFormDataRouter, len(mods))
|
||||||
|
for i, router := range mods {
|
||||||
|
routers[i] = router.(MultipartFormDataRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, router := range routers {
|
||||||
|
routes, err := router.Routes()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get routes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.multipartFormDataRoutes = append(a.multipartFormDataRoutes, routes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get middlewares from modules.
|
||||||
|
mods, err = ctx.Modules(new(MiddlewareProvider))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get middleware providers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middlewareProviders := make([]MiddlewareProvider, len(mods))
|
||||||
|
for i, middlewareProvider := range mods {
|
||||||
|
middlewareProviders[i] = middlewareProvider.(MiddlewareProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, middlewareProvider := range middlewareProviders {
|
||||||
|
middlewares, err := middlewareProvider.Middlewares()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get middlewares: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.externalMiddlewares = append(a.externalMiddlewares, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort middlewares by priority.
|
||||||
|
sort.Slice(a.externalMiddlewares, func(i, j int) bool {
|
||||||
|
return a.externalMiddlewares[i].Priority > a.externalMiddlewares[j].Priority
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get health checks from modules.
|
||||||
|
mods, err = ctx.Modules(new(HealthChecker))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get health checkers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
healthCheckers := make([]HealthChecker, len(mods))
|
||||||
|
for i, healthChecker := range mods {
|
||||||
|
healthCheckers[i] = healthChecker.(HealthChecker)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, healthChecker := range healthCheckers {
|
||||||
|
checks, err := healthChecker.Checks()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get health checks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.healthChecks = append(a.healthChecks, checks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(a)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger = logger
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the module properties.
|
||||||
|
func (a API) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if a.port < 1 || a.port > 65535 {
|
||||||
|
err = multierr.Append(err,
|
||||||
|
errors.New("port must be more than 1 and less than 65535"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(a.rootPath, "/") {
|
||||||
|
err = multierr.Append(err,
|
||||||
|
errors.New("root path must start with /"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(a.rootPath, "/") {
|
||||||
|
err = multierr.Append(err,
|
||||||
|
errors.New("root path must end with /"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(strings.TrimSpace(a.traceHeader)) == 0 {
|
||||||
|
err = multierr.Append(err,
|
||||||
|
errors.New("trace header must not be empty"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
routesMap := make(map[string]MultipartFormDataRoute, len(a.multipartFormDataRoutes))
|
||||||
|
|
||||||
|
for _, route := range a.multipartFormDataRoutes {
|
||||||
|
if route.Path == "" {
|
||||||
|
return errors.New("route with empty path cannot be registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(route.Path, "/") {
|
||||||
|
return fmt.Errorf("route %s does not start with /", route.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if route.Handler == nil {
|
||||||
|
return fmt.Errorf("route %s has a nil handler", route.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := routesMap[route.Path]; ok {
|
||||||
|
return fmt.Errorf("route %s is already registered", route.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
routesMap[route.Path] = route
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, middleware := range a.externalMiddlewares {
|
||||||
|
if middleware.Handler == nil {
|
||||||
|
return errors.New("a middleware has a nil handler")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the HTTP server.
|
||||||
|
func (a *API) Start() error {
|
||||||
|
a.srv = echo.New()
|
||||||
|
a.srv.HideBanner = true
|
||||||
|
a.srv.HidePort = true
|
||||||
|
a.srv.Server.ReadTimeout = a.readTimeout
|
||||||
|
a.srv.Server.WriteTimeout = a.writeTimeout
|
||||||
|
a.srv.HTTPErrorHandler = httpErrorHandler(a.traceHeader)
|
||||||
|
|
||||||
|
a.srv.Pre(
|
||||||
|
latencyMiddleware(),
|
||||||
|
rootPathMiddleware(a.rootPath),
|
||||||
|
traceMiddleware(a.traceHeader),
|
||||||
|
loggerMiddleware(a.logger, a.disableHealthCheckLogging),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, externalMiddleware := range a.externalMiddlewares {
|
||||||
|
if externalMiddleware.RunBeforeRouter {
|
||||||
|
a.srv.Pre(externalMiddleware.Handler)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
a.srv.Use(externalMiddleware.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
hardTimeout := a.processTimeout + (time.Duration(5) * time.Second)
|
||||||
|
|
||||||
|
a.srv.GET(
|
||||||
|
fmt.Sprintf("%shealth", a.rootPath),
|
||||||
|
func() echo.HandlerFunc {
|
||||||
|
checks := append(a.healthChecks, health.WithTimeout(a.processTimeout))
|
||||||
|
checker := health.NewChecker(checks...)
|
||||||
|
|
||||||
|
return echo.WrapHandler(health.NewHandler(checker))
|
||||||
|
}(),
|
||||||
|
timeoutMiddleware(hardTimeout),
|
||||||
|
)
|
||||||
|
|
||||||
|
formsGroup := a.srv.Group(
|
||||||
|
fmt.Sprintf("%sforms", a.rootPath),
|
||||||
|
contextMiddleware(
|
||||||
|
contextMiddlewareConfig{
|
||||||
|
traceHeader: a.traceHeader,
|
||||||
|
timeout: struct {
|
||||||
|
process time.Duration
|
||||||
|
write time.Duration
|
||||||
|
}{
|
||||||
|
process: a.processTimeout,
|
||||||
|
write: a.writeTimeout,
|
||||||
|
},
|
||||||
|
webhook: struct {
|
||||||
|
allowList *regexp.Regexp
|
||||||
|
denyList *regexp.Regexp
|
||||||
|
errorAllowList *regexp.Regexp
|
||||||
|
errorDenyList *regexp.Regexp
|
||||||
|
maxRetry int
|
||||||
|
retryMinWait time.Duration
|
||||||
|
retryMaxWait time.Duration
|
||||||
|
disable bool
|
||||||
|
}{
|
||||||
|
allowList: a.webhookAllowList,
|
||||||
|
denyList: a.webhookDenyList,
|
||||||
|
errorAllowList: a.webhookErrorAllowList,
|
||||||
|
errorDenyList: a.webhookErrorDenyList,
|
||||||
|
maxRetry: a.webhookMaxRetry,
|
||||||
|
retryMinWait: a.webhookRetryMinWait,
|
||||||
|
retryMaxWait: a.webhookRetryMaxWait,
|
||||||
|
disable: a.disableWebhook,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
timeoutMiddleware(hardTimeout),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add routes from other modules.
|
||||||
|
for _, route := range a.multipartFormDataRoutes {
|
||||||
|
formsGroup.POST(
|
||||||
|
route.Path,
|
||||||
|
func(route MultipartFormDataRoute) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*Context)
|
||||||
|
|
||||||
|
err := route.Handler(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("handle request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}(route),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As the listen method is blocking, run it in a goroutine.
|
||||||
|
go func() {
|
||||||
|
err := a.srv.Start(fmt.Sprintf(":%d", a.port))
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
a.logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartupMessage returns a custom startup message.
|
||||||
|
func (a API) StartupMessage() string {
|
||||||
|
return fmt.Sprintf("server listening on port %d", a.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the HTTP server.
|
||||||
|
func (a API) Stop(ctx context.Context) error {
|
||||||
|
return a.srv.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraceDuration updates the expiration time of files and directories parsed by
|
||||||
|
// the gc.GarbageCollector.
|
||||||
|
func (a API) GraceDuration() time.Duration {
|
||||||
|
duration := a.readTimeout + a.processTimeout + a.writeTimeout
|
||||||
|
|
||||||
|
if a.disableWebhook {
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < a.webhookMaxRetry; i++ {
|
||||||
|
// Yep... Golang does not allow int * time.Duration.
|
||||||
|
duration += a.webhookRetryMaxWait
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*API)(nil)
|
||||||
|
_ gotenberg.Provisioner = (*API)(nil)
|
||||||
|
_ gotenberg.Validator = (*API)(nil)
|
||||||
|
_ gotenberg.App = (*API)(nil)
|
||||||
|
_ gc.GarbageCollectorGraceDurationModifier = (*API)(nil)
|
||||||
|
)
|
||||||
793
pkg/modules/api/api_test.go
Normal file
793
pkg/modules/api/api_test.go
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtoModule struct {
|
||||||
|
descriptor func() gotenberg.ModuleDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return mod.descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtoValidator struct {
|
||||||
|
ProtoModule
|
||||||
|
validate func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoValidator) Validate() error {
|
||||||
|
return mod.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtoMultipartFormDataRouter struct {
|
||||||
|
ProtoValidator
|
||||||
|
routes func() ([]MultipartFormDataRoute, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoMultipartFormDataRouter) Routes() ([]MultipartFormDataRoute, error) {
|
||||||
|
return mod.routes()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtoMiddlewareProvider struct {
|
||||||
|
ProtoValidator
|
||||||
|
middlewares func() ([]Middleware, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoMiddlewareProvider) Middlewares() ([]Middleware, error) {
|
||||||
|
return mod.middlewares()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtoHealthChecker struct {
|
||||||
|
ProtoValidator
|
||||||
|
checks func() ([]health.CheckerOption, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ProtoHealthChecker) Checks() ([]health.CheckerOption, error) {
|
||||||
|
return mod.checks()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtoLoggerProvider struct {
|
||||||
|
ProtoModule
|
||||||
|
logger func(mod gotenberg.Module) (*zap.Logger, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (factory ProtoLoggerProvider) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return factory.logger(mod)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPI_Descriptor(t *testing.T) {
|
||||||
|
descriptor := API{}.Descriptor()
|
||||||
|
|
||||||
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
|
expect := reflect.TypeOf(new(API))
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPI_Provision(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
ctx *gotenberg.Context
|
||||||
|
setEnv func(i int)
|
||||||
|
expectPort int
|
||||||
|
expectMiddlewares []Middleware
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
fs := new(API).Descriptor().FlagSet
|
||||||
|
err := fs.Parse([]string{"--api-port-from-env=FOO"})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: fs,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
fs := new(API).Descriptor().FlagSet
|
||||||
|
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: fs,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
setEnv: func(i int) {
|
||||||
|
err := os.Setenv("PORT", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
fs := new(API).Descriptor().FlagSet
|
||||||
|
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: fs,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
setEnv: func(i int) {
|
||||||
|
err := os.Setenv("PORT", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
fs := new(API).Descriptor().FlagSet
|
||||||
|
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: fs,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
setEnv: func(i int) {
|
||||||
|
err := os.Setenv("PORT", "1337")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectPort: 1337,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := struct{ ProtoMultipartFormDataRouter }{}
|
||||||
|
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}
|
||||||
|
mod.routes = func() ([]MultipartFormDataRoute, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := struct{ ProtoMiddlewareProvider }{}
|
||||||
|
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}
|
||||||
|
mod.middlewares = func() ([]Middleware, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := struct{ ProtoMiddlewareProvider }{}
|
||||||
|
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mod.middlewares = func() ([]Middleware, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := struct{ ProtoMultipartFormDataRouter }{}
|
||||||
|
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mod.routes = func() ([]MultipartFormDataRoute, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := struct{ ProtoHealthChecker }{}
|
||||||
|
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}
|
||||||
|
mod.checks = func() ([]health.CheckerOption, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := struct{ ProtoHealthChecker }{}
|
||||||
|
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.validate = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mod.checks = func() ([]health.CheckerOption, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := struct{ ProtoLoggerProvider }{}
|
||||||
|
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.logger = func(_ gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod1 := struct{ ProtoMultipartFormDataRouter }{}
|
||||||
|
mod1.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
|
||||||
|
}
|
||||||
|
mod1.validate = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mod1.routes = func() ([]MultipartFormDataRoute, error) {
|
||||||
|
return []MultipartFormDataRoute{{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mod2 := struct{ ProtoMiddlewareProvider }{}
|
||||||
|
mod2.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
|
||||||
|
}
|
||||||
|
mod2.validate = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mod2.middlewares = func() ([]Middleware, error) {
|
||||||
|
return []Middleware{
|
||||||
|
{
|
||||||
|
Priority: VeryLowPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: LowPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: MediumPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: HighPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: VeryHighPriority,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mod3 := struct{ ProtoHealthChecker }{}
|
||||||
|
mod3.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }}
|
||||||
|
}
|
||||||
|
mod3.validate = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mod3.checks = func() ([]health.CheckerOption, error) {
|
||||||
|
return []health.CheckerOption{health.WithDisabledAutostart()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mod4 := struct{ ProtoLoggerProvider }{}
|
||||||
|
mod4.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "qux", New: func() gotenberg.Module { return mod4 }}
|
||||||
|
}
|
||||||
|
mod4.logger = func(_ gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(API).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod1.Descriptor(),
|
||||||
|
mod2.Descriptor(),
|
||||||
|
mod3.Descriptor(),
|
||||||
|
mod4.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectMiddlewares: []Middleware{
|
||||||
|
{
|
||||||
|
Priority: VeryHighPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: HighPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: MediumPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: LowPriority,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Priority: VeryLowPriority,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if tc.setEnv != nil {
|
||||||
|
tc.setEnv(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod := new(API)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
|
if tc.expectPort != 0 && mod.port != tc.expectPort {
|
||||||
|
t.Errorf("expected port %d but got %d", tc.expectPort, mod.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(mod.externalMiddlewares, tc.expectMiddlewares) {
|
||||||
|
t.Errorf("expected %+v, but got: %+v", tc.expectMiddlewares, mod.externalMiddlewares)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestAPI_Validate(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
port int
|
||||||
|
rootPath string
|
||||||
|
traceHeader string
|
||||||
|
routes []MultipartFormDataRoute
|
||||||
|
middlewares []Middleware
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
port: 0,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: 65536,
|
||||||
|
rootPath: "foo",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: 10,
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: []MultipartFormDataRoute{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: 10,
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: []MultipartFormDataRoute{
|
||||||
|
{
|
||||||
|
Path: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: 10,
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: []MultipartFormDataRoute{
|
||||||
|
{
|
||||||
|
Path: "/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: 10,
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: []MultipartFormDataRoute{
|
||||||
|
{
|
||||||
|
Path: "/foo",
|
||||||
|
Handler: func(_ *Context) error { return nil },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/foo",
|
||||||
|
Handler: func(_ *Context) error { return nil },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: 10,
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
middlewares: []Middleware{
|
||||||
|
{
|
||||||
|
Priority: HighPriority,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: 10,
|
||||||
|
rootPath: "/foo/",
|
||||||
|
traceHeader: "foo",
|
||||||
|
routes: []MultipartFormDataRoute{
|
||||||
|
{
|
||||||
|
Path: "/foo",
|
||||||
|
Handler: func(_ *Context) error { return nil },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
middlewares: []Middleware{
|
||||||
|
{
|
||||||
|
Priority: HighPriority,
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
mod := API{
|
||||||
|
port: tc.port,
|
||||||
|
rootPath: tc.rootPath,
|
||||||
|
traceHeader: tc.traceHeader,
|
||||||
|
multipartFormDataRoutes: tc.routes,
|
||||||
|
externalMiddlewares: tc.middlewares,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mod.Validate()
|
||||||
|
|
||||||
|
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 TestAPI_Start(t *testing.T) {
|
||||||
|
mod := new(API)
|
||||||
|
mod.port = 3000
|
||||||
|
mod.rootPath = "/"
|
||||||
|
mod.multipartFormDataRoutes = []MultipartFormDataRoute{
|
||||||
|
{
|
||||||
|
Path: "/foo",
|
||||||
|
Handler: func(ctx *Context) error {
|
||||||
|
ctx.outputPaths = []string{
|
||||||
|
"/tests/test/testdata/api/sample1.txt",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/bar",
|
||||||
|
Handler: func(_ *Context) error { return errors.New("foo") },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mod.externalMiddlewares = []Middleware{
|
||||||
|
{
|
||||||
|
RunBeforeRouter: true,
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mod.logger = zap.NewNop()
|
||||||
|
|
||||||
|
err := mod.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// health request.
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
|
|
||||||
|
mod.srv.ServeHTTP(recorder, healthRequest)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "multipart/form-data" request.
|
||||||
|
multipartRequest := func(URL string) *http.Request {
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := writer.WriteField("foo", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = part.Write([]byte("foo"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, URL, body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
mod.srv.ServeHTTP(recorder, multipartRequest("/forms/foo"))
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
mod.srv.ServeHTTP(recorder, multipartRequest("/forms/bar"))
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("expected %d status code but got %d", http.StatusInternalServerError, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mod.srv.Shutdown(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPI_StartupMessage(t *testing.T) {
|
||||||
|
mod := API{
|
||||||
|
port: 3000,
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := mod.StartupMessage()
|
||||||
|
expect := "server listening on port 3000"
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPI_Stop(t *testing.T) {
|
||||||
|
mod := API{
|
||||||
|
port: 3000,
|
||||||
|
multipartFormDataRoutes: []MultipartFormDataRoute{
|
||||||
|
{
|
||||||
|
Path: "/foo",
|
||||||
|
Handler: func(_ *Context) error { return nil },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mod.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mod.Stop(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPI_GraceDuration(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
mod API
|
||||||
|
expect time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
mod: API{
|
||||||
|
readTimeout: time.Duration(1) * time.Second,
|
||||||
|
processTimeout: time.Duration(1) * time.Second,
|
||||||
|
writeTimeout: time.Duration(1) * time.Second,
|
||||||
|
disableWebhook: true,
|
||||||
|
},
|
||||||
|
expect: time.Duration(3) * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mod: API{
|
||||||
|
readTimeout: time.Duration(1) * time.Second,
|
||||||
|
processTimeout: time.Duration(1) * time.Second,
|
||||||
|
writeTimeout: time.Duration(1) * time.Second,
|
||||||
|
webhookMaxRetry: 5,
|
||||||
|
webhookRetryMaxWait: time.Duration(5) * time.Second,
|
||||||
|
},
|
||||||
|
expect: time.Duration(28) * time.Second,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
actual := tc.mod.GraceDuration()
|
||||||
|
|
||||||
|
if actual != tc.expect {
|
||||||
|
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*ProtoModule)(nil)
|
||||||
|
_ gotenberg.Validator = (*ProtoValidator)(nil)
|
||||||
|
_ gotenberg.Module = (*ProtoValidator)(nil)
|
||||||
|
_ MultipartFormDataRouter = (*ProtoMultipartFormDataRouter)(nil)
|
||||||
|
_ gotenberg.Module = (*ProtoMultipartFormDataRouter)(nil)
|
||||||
|
_ gotenberg.Validator = (*ProtoMultipartFormDataRouter)(nil)
|
||||||
|
_ MiddlewareProvider = (*ProtoMiddlewareProvider)(nil)
|
||||||
|
_ gotenberg.Module = (*ProtoMiddlewareProvider)(nil)
|
||||||
|
_ gotenberg.Validator = (*ProtoMiddlewareProvider)(nil)
|
||||||
|
_ HealthChecker = (*ProtoHealthChecker)(nil)
|
||||||
|
_ gotenberg.Module = (*ProtoHealthChecker)(nil)
|
||||||
|
_ gotenberg.Validator = (*ProtoHealthChecker)(nil)
|
||||||
|
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
|
||||||
|
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
|
||||||
|
)
|
||||||
319
pkg/modules/api/context.go
Normal file
319
pkg/modules/api/context.go
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/flate"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mholt/archiver/v3"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/text/runes"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrContextAlreadyClosed happens when the context has been canceled.
|
||||||
|
ErrContextAlreadyClosed = errors.New("context already closed")
|
||||||
|
|
||||||
|
// ErrOutOfBoundsOutputPath happens when an output path is not within
|
||||||
|
// context's working directory. It enforces having all the files in the
|
||||||
|
// same directory.
|
||||||
|
ErrOutOfBoundsOutputPath = errors.New("output path is not within context's working directory")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is the request context for a "multipart/form-data" requests.
|
||||||
|
type Context struct {
|
||||||
|
dirPath string
|
||||||
|
values map[string][]string
|
||||||
|
files map[string]string
|
||||||
|
|
||||||
|
outputPaths []string
|
||||||
|
|
||||||
|
cancelled bool
|
||||||
|
logger *zap.Logger
|
||||||
|
echoCtx echo.Context
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// newContext returns a Context by parsing a "multipart/form-data" request.
|
||||||
|
func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration) (*Context, context.CancelFunc, error) {
|
||||||
|
processCtx, processCancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
|
||||||
|
ctx := &Context{
|
||||||
|
outputPaths: make([]string, 0),
|
||||||
|
cancelled: false,
|
||||||
|
logger: logger,
|
||||||
|
echoCtx: echoCtx,
|
||||||
|
Context: processCtx,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A custom cancel function which removes the context's working directory
|
||||||
|
// when called.
|
||||||
|
cancel := func() context.CancelFunc {
|
||||||
|
return func() {
|
||||||
|
if ctx.cancelled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
processCancel()
|
||||||
|
|
||||||
|
if ctx.dirPath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.RemoveAll(ctx.dirPath)
|
||||||
|
if err != nil {
|
||||||
|
ctx.logger.Error(fmt.Sprintf("remove context's working directory: %s", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.logger.Debug(fmt.Sprintf("'%s' removed", ctx.dirPath))
|
||||||
|
ctx.cancelled = true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
form, err := echoCtx.MultipartForm()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
if errors.Is(err, http.ErrNotMultipart) {
|
||||||
|
return nil, cancel, WrapError(
|
||||||
|
fmt.Errorf("get multipart form: %w", err),
|
||||||
|
NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: want 'multipart/form-data'"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, http.ErrMissingBoundary) {
|
||||||
|
return nil, cancel, WrapError(
|
||||||
|
fmt.Errorf("get multipart form: %w", err),
|
||||||
|
NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: no boundary"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), io.EOF.Error()) {
|
||||||
|
return nil, cancel, WrapError(
|
||||||
|
fmt.Errorf("get multipart form: %w", err),
|
||||||
|
NewSentinelHTTPError(http.StatusBadRequest, "Malformed body: it does not match the 'Content-Type' header boundaries"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, cancel, fmt.Errorf("get multipart form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath, err := gotenberg.MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, cancel, fmt.Errorf("create working directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.dirPath = dirPath
|
||||||
|
ctx.values = form.Value
|
||||||
|
ctx.files = make(map[string]string)
|
||||||
|
|
||||||
|
copyToDisk := func(fh *multipart.FileHeader) error {
|
||||||
|
// Avoid directory traversal and normalize filename.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/104.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/228.
|
||||||
|
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||||
|
|
||||||
|
filename, _, err := transform.String(t, strings.ToLower(filepath.Base(fh.Filename)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("transform filename: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := fh.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open multipart file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := in.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close file header: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
|
||||||
|
|
||||||
|
out, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create local file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := out.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close local file: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copy multipart file to local file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.files[filename] = path
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, files := range form.File {
|
||||||
|
for _, fh := range files {
|
||||||
|
err = copyToDisk(fh)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Log().Debug(fmt.Sprintf("form data values: %+v", ctx.values))
|
||||||
|
ctx.Log().Debug(fmt.Sprintf("form data files: %+v", ctx.files))
|
||||||
|
|
||||||
|
return ctx, cancel, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request returns the http.Request.
|
||||||
|
func (ctx Context) Request() *http.Request {
|
||||||
|
return ctx.echoCtx.Request()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormData return a FormData.
|
||||||
|
func (ctx Context) FormData() *FormData {
|
||||||
|
return &FormData{
|
||||||
|
values: ctx.values,
|
||||||
|
files: ctx.files,
|
||||||
|
errors: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePath generates a path within the context's working directory. It
|
||||||
|
// does not create a file.
|
||||||
|
func (ctx Context) GeneratePath(extension string) string {
|
||||||
|
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOutputPaths adds the given paths. Those paths will be used later to build
|
||||||
|
// the output file.
|
||||||
|
func (ctx *Context) AddOutputPaths(paths ...string) error {
|
||||||
|
if ctx.cancelled {
|
||||||
|
return ErrContextAlreadyClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
if !strings.HasPrefix(path, ctx.dirPath) {
|
||||||
|
return ErrOutOfBoundsOutputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.outputPaths = append(ctx.outputPaths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log returns the context zap.Logger.
|
||||||
|
func (ctx Context) Log() *zap.Logger {
|
||||||
|
return ctx.logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOutputFile builds the output file according to the output paths
|
||||||
|
// registered in the context. If many output paths, an archive is created.
|
||||||
|
func (ctx Context) buildOutputFile() (string, error) {
|
||||||
|
if ctx.cancelled {
|
||||||
|
return "", ErrContextAlreadyClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ctx.outputPaths) == 0 {
|
||||||
|
return "", errors.New("no output path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ctx.outputPaths) == 1 {
|
||||||
|
ctx.logger.Debug(fmt.Sprintf("only one output file '%s', skip archive creation", ctx.outputPaths[0]))
|
||||||
|
|
||||||
|
return ctx.outputPaths[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
z := archiver.Zip{
|
||||||
|
CompressionLevel: flate.DefaultCompression,
|
||||||
|
MkdirAll: true,
|
||||||
|
SelectiveCompression: true,
|
||||||
|
ContinueOnError: false,
|
||||||
|
OverwriteExisting: false,
|
||||||
|
ImplicitTopLevelFolder: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
archivePath := ctx.GeneratePath(".zip")
|
||||||
|
|
||||||
|
err := z.Archive(ctx.outputPaths, archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("archive output files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.logger.Debug(fmt.Sprintf("archive '%s' created", archivePath))
|
||||||
|
|
||||||
|
return archivePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockContext is a helper for tests.
|
||||||
|
//
|
||||||
|
// ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
type MockContext struct {
|
||||||
|
*Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDirPath sets the context's working directory path.
|
||||||
|
//
|
||||||
|
// ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
// ctx.SetDirPath("/foo")
|
||||||
|
func (ctx *MockContext) SetDirPath(path string) {
|
||||||
|
ctx.dirPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValues sets the values.
|
||||||
|
//
|
||||||
|
// ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
// ctx.SetValues(map[string][]string{
|
||||||
|
// "url": {
|
||||||
|
// "foo",
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
func (ctx *MockContext) SetValues(values map[string][]string) {
|
||||||
|
ctx.values = values
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFiles sets the files.
|
||||||
|
//
|
||||||
|
// ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
// ctx.SetFiles(map[string]string{
|
||||||
|
// "foo": "/foo",
|
||||||
|
// })
|
||||||
|
func (ctx *MockContext) SetFiles(files map[string]string) {
|
||||||
|
ctx.files = files
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCancelled sets if the context is cancelled or not.
|
||||||
|
//
|
||||||
|
// ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
// ctx.SetCancelled(true)
|
||||||
|
func (ctx *MockContext) SetCancelled(cancelled bool) {
|
||||||
|
ctx.cancelled = cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputPaths returns the registered output paths.
|
||||||
|
// ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
// outputPaths := ctx.OutputPaths()
|
||||||
|
func (ctx MockContext) OutputPaths() []string {
|
||||||
|
return ctx.outputPaths
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user