Compare commits
276 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
2
.github/CODE_OF_CONDUCT.md
vendored
@@ -43,4 +43,4 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
|
||||
9
.github/CONTRIBUTING.md
vendored
@@ -1,6 +1,6 @@
|
||||
# Contributing
|
||||
|
||||
Hi! Thank your for considering contributing to Gotenberg. You'll
|
||||
Hi! Thank you for considering contributing to Gotenberg. You'll
|
||||
find below useful information about how to contribute to the Gotenberg project.
|
||||
|
||||
## Contributing code
|
||||
@@ -8,8 +8,9 @@ find below useful information about how to contribute to the Gotenberg project.
|
||||
### Install from sources
|
||||
|
||||
1. Install and run the latest version of Docker
|
||||
2. Fork this repository
|
||||
3. Clone it to your local Go environment (requires *Go* >= 1.11)
|
||||
2. Verify your Go version (>= 1.14)
|
||||
3. Fork this repository
|
||||
4. Clone it outside of your `GOPATH` (we're using Go modules)
|
||||
|
||||
### Working with git
|
||||
|
||||
@@ -35,4 +36,4 @@ add a new one!
|
||||
|
||||
* [Code of conduct](CODE_OF_CONDUCT.md)
|
||||
* [Issue template](ISSUE_TEMPLATE.md)
|
||||
* [Pull request template](PULL_REQUEST_TEMPLATE.md)
|
||||
* [Pull request template](PULL_REQUEST_TEMPLATE.md)
|
||||
|
||||
5
.github/ISSUE_TEMPLATE.md
vendored
@@ -21,6 +21,9 @@ Please search on the [issue tracker](../../../issues) before creating one.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Logs (LOG_LEVEL="DEBUG")
|
||||
<!--- Include here a relevant snippet of the log entries of your Gotenberg instance -->
|
||||
|
||||
## Context
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
@@ -29,4 +32,4 @@ Please search on the [issue tracker](../../../issues) before creating one.
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
* Version used:
|
||||
* Operating System and version:
|
||||
* Link to your project:
|
||||
* Link to your project:
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -40,6 +40,6 @@ Fixes #
|
||||
- [ ] 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`)?
|
||||
- [ ] Have you updated the documentation (Markdown files under `build > docs > content` and then `make doc`)?
|
||||
- [ ] I have squashed any insignificant commits
|
||||
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code
|
||||
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code
|
||||
|
||||
26
.github/workflows/tests_maybe_release.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: tests_maybe_release
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
tests_maybe_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v1
|
||||
- name: Lint code
|
||||
run: make lint
|
||||
- name: Run tests
|
||||
run: |
|
||||
make tests CODE_COVERAGE=1
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
- name: Release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
run: make publish VERSION=${{ github.event.release.tag_name }} DOCKER_USER=${{ secrets.DOCKER_USER }} DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}
|
||||
2
.gitignore
vendored
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
coverage.txt
|
||||
25
.travis.yml
@@ -1,25 +0,0 @@
|
||||
sudo: required
|
||||
|
||||
language: generic
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: d6pliM7iR7KJ3TQkr1C0tHi0HgU6Vdmc/ycw11XrXs794/dgj6g9UDrhgcvT2mTzOVIUNouHx7gyEWAlwJv7p8PPyguHJfIsyqMWfaXNPoytTx1ETUiygl1G/UsvXVdry6I5tZF/U+/PJ3hBXoAvo8EOFe0/MxxvP3ghCeeCpeEwEdoYP2jbADGffywVUNI7OEdmS29A28GusXXp2cWCIZYSq5Gn5iu1ZMPaJA6SkRTXpS+iVVLUvlbz5HW09ETFfurTvsdz5Ze4JICiuQt///CR1uwdxD8K7Ils/mkenaMtja9NHA9RdQBa51Azn8iCLsObCTYc52Zb6atbCi/X4ae9M8GA8qaaLigNz5G4v1BHneJylYfWKj5GwqUA3VmRu1mlEw7d2sN2uFRmgIl5kgsYYyOVOLBOKaJWhyBhNnR4U8KeqwUkOnlSdmNZnjfexXXBEcKHen1SozpXwMnzZh8LZuN6BNYd6TVpKmjulGI3vqNMM0c4JJZ9pSm4q5FjbNGQArwCsujDDwY4bCGZSJJTPMXjYOrx4yJ1RY+0TpyV9oDU3ogEqaWnhrkgrphfiYBlDY2bEvrxMTMukLCxJb0Nru6LGCpg4X1P9hu9r0gceqUSnBgdJI4rYTNddenxlEd4j/Thqv835stJWz6YoV+rfvERuSUpntl8xvrk3U8=
|
||||
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
|
||||
|
||||
stages:
|
||||
- tests
|
||||
- publish
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: tests
|
||||
script: make lint
|
||||
- stage: tests
|
||||
script: make tests
|
||||
- stage: publish
|
||||
if: tag IS present
|
||||
script: ./scripts/publish.sh $TRAVIS_TAG $DOCKER_USER $DOCKER_PASS
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 TheCodingMachine
|
||||
Copyright (c) 2019 TheCodingMachine
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
62
Makefile
@@ -1,30 +1,66 @@
|
||||
GOLANG_VERSION=1.14
|
||||
VERSION=snapshot
|
||||
GOLANG_VERSION=1.11.2
|
||||
DOCKER_USER=
|
||||
DOCKER_PASSWORD=
|
||||
DOCKER_REGISTRY=thecodingmachine
|
||||
GOTENBERG_USER_GID=1001
|
||||
GOTENBERG_USER_UID=1001
|
||||
GOLANGCI_LINT_VERSION=1.27.0
|
||||
CODE_COVERAGE=0
|
||||
TINI_VERSION=0.19.0
|
||||
MAXIMUM_WAIT_TIMEOUT=30.0
|
||||
MAXIMUM_WAIT_DELAY=10.0
|
||||
MAXIMUM_WEBHOOK_URL_TIMEOUT=30.0
|
||||
DEFAULT_WAIT_TIMEOUT=10.0
|
||||
DEFAULT_WEBHOOK_URL_TIMEOUT=10.0
|
||||
DEFAULT_LISTEN_PORT=3000
|
||||
DISABLE_GOOGLE_CHROME=0
|
||||
DISABLE_UNOCONV=0
|
||||
LOG_LEVEL=INFO
|
||||
ROOT_PATH=/
|
||||
DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576
|
||||
GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=0
|
||||
|
||||
# generate documentation.
|
||||
doc:
|
||||
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."
|
||||
# build the base Docker image.
|
||||
base:
|
||||
docker build --build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) --build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) -t $(DOCKER_REGISTRY)/gotenberg:base -f build/base/Dockerfile .
|
||||
|
||||
# build the workspace Docker image.
|
||||
workspace:
|
||||
make base
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:workspace -f build/workspace/Dockerfile .
|
||||
|
||||
# gofmt and goimports all go files.
|
||||
fmt:
|
||||
go fmt ./...
|
||||
go mod tidy
|
||||
|
||||
# run all linters.
|
||||
lint:
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg:lint
|
||||
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm $(DOCKER_REGISTRY)/gotenberg:lint
|
||||
|
||||
# run all tests.
|
||||
tests:
|
||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:tests -f build/tests/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg:tests
|
||||
make workspace
|
||||
./scripts/tests.sh $(DOCKER_REGISTRY) $(CODE_COVERAGE)
|
||||
|
||||
# build Docker image.
|
||||
# generate documentation.
|
||||
doc:
|
||||
make workspace
|
||||
docker build -t $(DOCKER_REGISTRY)/gotenberg:docs -f build/docs/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REGISTRY)/gotenberg:docs
|
||||
|
||||
# build Gotenberg Docker image.
|
||||
image:
|
||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
make workspace
|
||||
docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
|
||||
# start the API using previously built Docker image.
|
||||
gotenberg:
|
||||
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
|
||||
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e ROOT_PATH=$(ROOT_PATH) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -e GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=$(GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REGISTRY)/gotenberg:$(VERSION)
|
||||
|
||||
# publish Gotenberg images according to version.
|
||||
publish:
|
||||
make workspace
|
||||
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
||||
21
README.md
@@ -1,8 +1,9 @@
|
||||
<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/69229423-ac731300-0b85-11ea-8c2e-2cc00ecdb269.PNG" alt="Gotenberg logo" width="250" height="250" />
|
||||
</p>
|
||||
<h3 align="center">Gotenberg</h3>
|
||||
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
|
||||
<p align="center"><a href="https://thecodingmachine.github.io/gotenberg">Documentation</a> · <a href="/.github/CONTRIBUTING.md">Contributing</a></p>
|
||||
|
||||
---
|
||||
|
||||
@@ -12,17 +13,15 @@ At TheCodingMachine, we build a lot of web applications (intranets, extranets an
|
||||
|
||||
* HTML and Markdown conversions using Google Chrome headless
|
||||
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
||||
* Performance :zap:: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
||||
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
|
||||
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
||||
* Easily interact with the API using our [Go](/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beautiful PDFs!
|
||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||
|
||||
## Quick start
|
||||
|
||||
Open a terminal and run the following command:
|
||||
|
||||
```bash
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:3
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
|
||||
```
|
||||
|
||||
The API is now available on your host at `http://localhost:3000`.
|
||||
@@ -32,11 +31,15 @@ to learn how to interact with it!
|
||||
|
||||
## Badges
|
||||
|
||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:3)
|
||||
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
||||
[](https://hub.docker.com/r/thecodingmachine/gotenberg)
|
||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:6)
|
||||
[](https://github.com/thecodingmachine/gotenberg/workflows/tests_maybe_release)
|
||||
[](https://godoc.org/github.com/thecodingmachine/gotenberg)
|
||||
[](https://codecov.io/gh/thecodingmachine/gotenberg)
|
||||
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
||||
|
||||
---
|
||||
|
||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
|
||||
</p>
|
||||
|
||||
BIN
assets/logo.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
assets/logo2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@@ -1,6 +1,4 @@
|
||||
FROM debian:stretch-slim
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
FROM debian:buster-slim
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Common libraries
|
||||
@@ -8,41 +6,121 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
# |
|
||||
# | Libraries used in the build process of this image.
|
||||
# |
|
||||
|
||||
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
|
||||
apt-get update &&\
|
||||
apt-get install -y curl wget python3-pip
|
||||
RUN apt-get update &&\
|
||||
apt-get --no-install-recommends install -y ca-certificates curl gnupg procps &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | PM2
|
||||
# | Microsoft font installer
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | 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 -
|
||||
RUN apt-get update &&\
|
||||
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 &&\
|
||||
apt --no-install-recommends install -y ./ttf-mscorefonts-installer_3.8_all.deb && rm ./ttf-mscorefonts-installer_3.8_all.deb &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Chrome
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs Chrome for HTML and Markdown conversions.
|
||||
# | Installs Chrome.
|
||||
# |
|
||||
|
||||
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||
RUN 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 &&\
|
||||
apt-get -y install google-chrome-stable
|
||||
apt-get install --no-install-recommends -y --allow-unauthenticated google-chrome-stable &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | LibreOffice
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs LibreOffice.
|
||||
# |
|
||||
|
||||
# https://github.com/nextcloud/docker/issues/380
|
||||
RUN mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
||||
echo "deb http://httpredir.debian.org/debian/ buster-backports main contrib non-free" >> /etc/apt/sources.list &&\
|
||||
apt-get update &&\
|
||||
apt-get --no-install-recommends -t buster-backports -y install libreoffice &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Unoconv
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs unoconv and LibreOffice for Office documents conversions.
|
||||
# | Installs unoconv.
|
||||
# |
|
||||
|
||||
RUN pip3 install unoconv &&\
|
||||
# https://github.com/nextcloud/docker/issues/380
|
||||
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
||||
apt-get -y install libreoffice
|
||||
ENV UNO_URL=https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv
|
||||
|
||||
RUN curl -Ls $UNO_URL -o /usr/bin/unoconv &&\
|
||||
chmod +x /usr/bin/unoconv &&\
|
||||
ln -s /usr/bin/python3 /usr/bin/python &&\
|
||||
unoconv --version
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | PDFtk
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs PDFtk as an alternative to pdfcpu for merging PDFs.
|
||||
# | https://github.com/thecodingmachine/gotenberg/issues/29
|
||||
# |
|
||||
|
||||
ARG PDFTK_VERSION=924565150
|
||||
|
||||
RUN curl -o /usr/bin/pdftk "https://gitlab.com/pdftk-java/pdftk/-/jobs/${PDFTK_VERSION}/artifacts/raw/build/native-image/pdftk" \
|
||||
&& chmod a+x /usr/bin/pdftk
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Fonts
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs a handful of fonts.
|
||||
# | Note: ttf-mscorefonts-installer are installed on top of this Dockerfile.
|
||||
# |
|
||||
|
||||
# Credits:
|
||||
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile
|
||||
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html
|
||||
RUN apt-get update &&\
|
||||
apt-get install --no-install-recommends -y \
|
||||
culmus \
|
||||
fonts-beng \
|
||||
fonts-hosny-amiri \
|
||||
fonts-lklug-sinhala \
|
||||
fonts-lohit-guru \
|
||||
fonts-lohit-knda \
|
||||
fonts-samyak-gujr \
|
||||
fonts-samyak-mlym \
|
||||
fonts-samyak-taml \
|
||||
fonts-sarai \
|
||||
fonts-sil-abyssinica \
|
||||
fonts-sil-padauk \
|
||||
fonts-telu \
|
||||
fonts-thai-tlwg \
|
||||
fonts-liberation \
|
||||
ttf-wqy-zenhei \
|
||||
fonts-arphic-uming \
|
||||
fonts-ipafont-mincho \
|
||||
fonts-ipafont-gothic \
|
||||
fonts-unfonts-core &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY build/base/fonts/* /usr/local/share/fonts/
|
||||
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Default user
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | All processes in the Docker container will run as a dedicated
|
||||
# | non-root user.
|
||||
# |
|
||||
|
||||
ARG GOTENBERG_USER_GID=1001
|
||||
ARG GOTENBERG_USER_UID=1001
|
||||
|
||||
RUN groupadd --gid ${GOTENBERG_USER_GID} gotenberg \
|
||||
&& useradd --uid ${GOTENBERG_USER_UID} --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \
|
||||
&& mkdir /gotenberg \
|
||||
&& chown gotenberg: /gotenberg
|
||||
29
build/base/fonts.conf
Normal file
@@ -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>
|
||||
BIN
build/base/fonts/NotoColorEmoji.ttf
Normal file
@@ -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,299 +0,0 @@
|
||||
---
|
||||
title: HTML
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
The only requirement is to send a file named `index.html`: it is the file
|
||||
which will be converted to PDF.
|
||||
|
||||
For instance:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Header and footer
|
||||
|
||||
You may also add a header and/or a footer in the resulting PDF.
|
||||
Respectively, a file named `header.html` and `footer.html`.
|
||||
|
||||
Each of them **has to be a complete HTML document**:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 8rem;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The following classes allow you to inject printing values:
|
||||
|
||||
* `date`: formatted print date
|
||||
* `title`: document title
|
||||
* `pageNumber`: current page number
|
||||
* `totalPage`: total pages in the document
|
||||
|
||||
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
||||
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@header.html \
|
||||
--form files=@footer.html \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetHeader("header.html")
|
||||
req.SetFooter("footer.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$header = DocumentFactory::makeFromPath('header.html', 'header.html');
|
||||
$footer = DocumentFactory::makeFromPath('footer.html', 'footer.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setHeader($header);
|
||||
$request->setFooter($footer);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Assets
|
||||
|
||||
You may also send additional files. For instance: images, fonts, stylesheets and so on.
|
||||
|
||||
The only requirement is to make sure that their paths
|
||||
are on the same level as the `index.html` file.
|
||||
|
||||
In others words, this will work:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
But this won't:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="/foo/img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
You may also use *remote* paths for Google fonts, images and so on.
|
||||
|
||||
> If you want to install fonts directly in the Gotenberg Docker image,
|
||||
> see to the [fonts section](#office.fonts).
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@style.css \
|
||||
--form files=@img.png \
|
||||
--form files=@font.woff \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetAssets([]string{
|
||||
"font.woff",
|
||||
"img.gif",
|
||||
"style.css",
|
||||
})
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$assets = [
|
||||
DocumentFactory::makeFromPath('style.css', 'style.css'),
|
||||
DocumentFactory::makeFromPath('img.png', 'img.png'),
|
||||
DocumentFactory::makeFromPath('font.woff', 'font.woff'),
|
||||
];
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setAssets($assets);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Paper size, margins, orientation
|
||||
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
|
||||
|
||||
> Paper size and margins have to be provided in `inches`.
|
||||
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form paperWidth=8.27 \
|
||||
--form paperHeight=11.27 \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
--form landscape=true \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetPaperSize(gotenberg.A4)
|
||||
req.SetMargins(gotenberg.NoMargins)
|
||||
req.SetLandscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$request->setLandscape(true);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
@@ -1,137 +0,0 @@
|
||||
---
|
||||
title: Office
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/office` for Office document conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
You may send one or more Office documents. Following file extensions are accepted:
|
||||
|
||||
* `.txt`
|
||||
* `.rtf`
|
||||
* `.doc`
|
||||
* `.docx`
|
||||
* `.odt`
|
||||
* `.xls`
|
||||
* `.xlsx`
|
||||
* `.ods`
|
||||
* `.ppt`
|
||||
* `.pptx`
|
||||
* `.odp`
|
||||
|
||||
All files will be merged into a single resulting PDF.
|
||||
|
||||
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
|
||||
> That's why for Office conversions, the API does only one conversion at a time.
|
||||
> See the [scalability section](#scalability) to find how to mitigate this issue.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form files=@document2.docx \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx", "document2.docx"})
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Paper size and orientation
|
||||
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `A4` size and `portrait` orientation.
|
||||
|
||||
> Paper size has to be provided in `inches`.
|
||||
> Also, you have to set both `paperWidth` and `paperHeight`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form paperWidth=8.27 \
|
||||
--form paperHeight=11.27 \
|
||||
--form landscape=true \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx"})
|
||||
req.SetPaperSize(gotenberg.A4)
|
||||
req.SetLandscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setLandscape(true);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Fonts
|
||||
|
||||
By default, only `ttf-mscorefonts` fonts are installed.
|
||||
|
||||
If you wish to use more fonts, you will have to create your own image:
|
||||
|
||||
```Dockerfile
|
||||
FROM thecodingmachine/gotenberg:3
|
||||
|
||||
RUN apt-get -y install yourfonts
|
||||
```
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `webhookURL`.
|
||||
|
||||
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
||||
to given URL.
|
||||
|
||||
By doing so, your requests to the API will be over before the conversions are actually done!
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetWebhookURL("http://myapp.com/webhook/")
|
||||
dest := "result.pdf"
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
@@ -1,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).
|
||||
22
build/docs/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM thecodingmachine/gotenberg:workspace
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | static
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs the static site anti-framework – general-purpose library,
|
||||
# | purpose-built commands for various domains.
|
||||
# |
|
||||
|
||||
RUN go get github.com/apex/static/cmd/static-docs
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Final touch
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
|
||||
WORKDIR /gotenberg/docs
|
||||
|
||||
CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ]
|
||||
@@ -6,7 +6,5 @@ title: Introduction
|
||||
|
||||
* HTML and Markdown conversions using Google Chrome headless
|
||||
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
||||
* Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
||||
* Failure prevention: PM2 automatically restarts previous processes if they fail
|
||||
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||
73
build/docs/content/01-install.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Install
|
||||
---
|
||||
|
||||
Gotenberg is shipped within a Docker image.
|
||||
|
||||
**[Gotenberg should ONLY be used in a trusted network by trusted applications. Do NOT expose Gotenberg to the external world.](https://blazej-adamczyk.medium.com/0-day-bug-breaks-multi-million-dollar-system-38c9e31b27e9)**
|
||||
|
||||
You may start it with:
|
||||
|
||||
```bash
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
|
||||
```
|
||||
|
||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
The image uses a dedicated non-root user called `gotenberg` with uid and gid `1001`.
|
||||
|
||||
If you wish to change those uid and gid, you will have to:
|
||||
|
||||
* clone the project
|
||||
* re-build the image
|
||||
* publish the image in your own Docker registry
|
||||
|
||||
For instance:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/thecodingmachine/gotenberg.git
|
||||
$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=version
|
||||
```
|
||||
|
||||
> `master` branch is always up-to-date with the latest version of the API.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
You may also add it in your Docker Compose stack:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
# your other services
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg:6
|
||||
```
|
||||
|
||||
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
||||
|
||||
## Kubernetes
|
||||
|
||||
It may also be deployed with Kubernetes.
|
||||
|
||||
Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0.2` CPU).
|
||||
|
||||
> The more resources are granted, the quicker will be the conversions.
|
||||
|
||||
In the deployment specification of the pod, also specify the uid of the user `gotenberg`:
|
||||
|
||||
```
|
||||
securityContext:
|
||||
privileged: false
|
||||
runAsUser: 1001
|
||||
```
|
||||
|
||||
## Cloud Run (Google Cloud)
|
||||
|
||||
If you're looking for cost savings, you might be interested by [Cloud Run](https://cloud.google.com/run).
|
||||
However, according to some users, doing asynchronous conversion (with a webhook) might not working.
|
||||
|
||||
In the following examples, we will assume your
|
||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||
34
build/docs/content/02-clients.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Clients
|
||||
---
|
||||
|
||||
We provide clients in various languages for easing the interactions with the API.
|
||||
|
||||
## Go client
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7
|
||||
```
|
||||
|
||||
See also the example from the [README](https://github.com/thecodingmachine/gotenberg-go-client/blob/master/README.md).
|
||||
|
||||
## PHP client
|
||||
|
||||
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
||||
|
||||
```bash
|
||||
$ composer require php-http/guzzle6-adapter
|
||||
```
|
||||
|
||||
Then the [PHP client](https://github.com/thecodingmachine/gotenberg-php-client):
|
||||
|
||||
```bash
|
||||
$ composer require thecodingmachine/gotenberg-php-client
|
||||
```
|
||||
|
||||
See also the example from the [README](https://github.com/thecodingmachine/gotenberg-php-client/blob/master/README.md).
|
||||
|
||||
## Community clients
|
||||
|
||||
* [JavaScript/TypeScript client](https://github.com/yumauri/gotenberg-js-client) by [yumauri](https://github.com/yumauri)
|
||||
* [C# client](https://github.com/ChangemakerStudios/GotenbergSharpApiClient) by [ChangemakerStudios](https://github.com/ChangemakerStudios)
|
||||
125
build/docs/content/03-environment-variables.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: Environment variables
|
||||
---
|
||||
|
||||
You may customize the API behaviour thanks to environment variables.
|
||||
|
||||
## Log level
|
||||
|
||||
The API provides structured logging allowing you to have relevant information
|
||||
about what's going on.
|
||||
|
||||
> If a TTY is attached, the log entries are displayed in text format with colors, otherwise in JSON format.
|
||||
|
||||
You may customize the severity of the log entries thanks to the environment variable `LOG_LEVEL`.
|
||||
|
||||
It accepts one of the following severities: `"DEBUG"`, `"INFO"` (default) and `"ERROR"`.
|
||||
|
||||
## Default listen port
|
||||
|
||||
By default, the API will listen on port `3000`.
|
||||
|
||||
You may customize this value with the environment variable `DEFAULT_LISTEN_PORT`.
|
||||
|
||||
This environment variable accepts any string that can be turned into a port number.
|
||||
|
||||
## Root path
|
||||
|
||||
By default, the API root path is `/`.
|
||||
|
||||
You may customize this value with the environment variable `ROOT_PATH`.
|
||||
|
||||
This environment variable accepts a string starting and ending with `/`.
|
||||
|
||||
For instance, `/gotenberg/` is a valid value while `gotenberg` is not.
|
||||
|
||||
> This is useful if you wish to do service discovery via URL paths.
|
||||
|
||||
## Disable Google Chrome
|
||||
|
||||
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`
|
||||
for disabling Google Chrome.
|
||||
|
||||
It takes the strings `"0"` or `"1"` as value where `1` means `true`
|
||||
|
||||
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
|
||||
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
||||
|
||||
## Default Google Chrome rpcc buffer size
|
||||
|
||||
When performing a [HTML](#html), [URL](#url) or [Markdown](#markdown) conversion, the API might return
|
||||
a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
|
||||
|
||||
If so, you may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`.
|
||||
|
||||
It takes a string representation of an int as value (e.g. `"1048576"` for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.
|
||||
|
||||
> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`.
|
||||
> See the [rpcc buffer size section](#html.rpcc_buffer_size).
|
||||
|
||||
## Google Chrome ignore certificate errors
|
||||
|
||||
When performing a [URL](#url) conversion, Google Chrome will not accept certificate errors.
|
||||
|
||||
You may allow insecure connections by setting the `GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS` environment variable to `"1"`.
|
||||
|
||||
**You should be careful with this feature and only enable it in your development environment.**
|
||||
|
||||
## Disable LibreOffice (unoconv)
|
||||
|
||||
You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`.
|
||||
|
||||
> If LibreOffice (unoconv) is disabled, the following conversion will **not** be available anymore:
|
||||
> [Office](#office)
|
||||
|
||||
## Default wait timeout
|
||||
|
||||
By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful.
|
||||
If unsucessful, it returns a `504` HTTP code.
|
||||
|
||||
You may customize this timeout thanks to the environment variable `DEFAULT_WAIT_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
> The default timeout may also be overridden per request thanks to the form field `waitTimeout`.
|
||||
> See the [timeout section](#timeout).
|
||||
|
||||
## Maximum wait timeout
|
||||
|
||||
By default, the value of the form field `waitTimeout` cannot be more than 30 seconds.
|
||||
|
||||
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
## Default webhook URL timeout
|
||||
|
||||
By default, the API will wait 10 seconds before it considers the sending of the resulting PDF to be unsuccessful.
|
||||
|
||||
> See the [webhook section](#webhook).
|
||||
|
||||
You may customize this timeout thanks to the environment variable `DEFAULT_WEBHOOK_URL_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
> The default timeout may also be overridden per request thanks to the form field `webhookURLTimeout`.
|
||||
> See the [webhook timeout section](#webhook.timeout).
|
||||
|
||||
## Maximum webhook URL timeout
|
||||
|
||||
By default, the value of the form field `webhookURLTimeout` cannot be more than 30 seconds.
|
||||
|
||||
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WEBHOOK_URL_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
## Maximum wait delay
|
||||
|
||||
By default, the value of the form field `waitDelay` cannot be more than 10 seconds.
|
||||
|
||||
> See the [wait delay section](#html.wait_delay).
|
||||
|
||||
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_DELAY`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
451
build/docs/content/04-html.md
Normal file
@@ -0,0 +1,451 @@
|
||||
---
|
||||
title: HTML
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
The only requirement is to send a file named `index.html`: it is the file
|
||||
which will be converted to PDF.
|
||||
|
||||
For instance:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Header and footer
|
||||
|
||||
You may also add a header and/or a footer in the resulting PDF.
|
||||
Respectively, a file named `header.html` and `footer.html`.
|
||||
|
||||
Each of them **has to be a complete HTML document**:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 8rem;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The following classes allow you to inject printing values:
|
||||
|
||||
* `date`: formatted print date
|
||||
* `title`: document title
|
||||
* `pageNumber`: current page number
|
||||
* `totalPage`: total pages in the document
|
||||
|
||||
There are some limitations:
|
||||
|
||||
* JavaScript is not executed
|
||||
* external resources are not loaded
|
||||
* the CSS properties are independant of the ones used in the `index.html` file
|
||||
* `footer.html` CSS properties override the ones from `header.html`
|
||||
* only fonts installed in the Docker image are loaded (see the [fonts section](#fonts))
|
||||
* images only work using a `base64` encoded source (`<img src="data:image/png;base64, iVBORw0K... />`)
|
||||
* `background-color` and `color` CSS properties require an additional `-webkit-print-color-adjust: exact` CSS property in order to work
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@header.html \
|
||||
--form files=@footer.html \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
header, _ := gotenberg.NewDocumentFromPath("header.html", "/path/to/file")
|
||||
footer, _ := gotenberg.NewDocumentFromPath("footer.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.Header(header)
|
||||
req.Footer(footer)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$header = DocumentFactory::makeFromPath('header.html', '/path/to/file');
|
||||
$footer = DocumentFactory::makeFromPath('footer.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setHeader($header);
|
||||
$request->setFooter($footer);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Assets
|
||||
|
||||
You may also send additional files. For instance: images, fonts, stylesheets and so on.
|
||||
|
||||
The only requirement is to make sure that their paths
|
||||
are on the same level as the `index.html` file.
|
||||
|
||||
In other words, this will work:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
But this won't:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="/foo/img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
You may also use *remote* paths for Google fonts, images and so on.
|
||||
|
||||
> If you want to install fonts directly in the Gotenberg Docker image,
|
||||
> see to the [fonts section](#fonts).
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@style.css \
|
||||
--form files=@img.png \
|
||||
--form files=@font.woff \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file")
|
||||
img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file")
|
||||
font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.Assets(style, img, font)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$assets = [
|
||||
DocumentFactory::makeFromPath('style.css', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('img.png', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('font.woff', '/path/to/file'),
|
||||
];
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setAssets($assets);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Paper size, margins, orientation, scaling
|
||||
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation and 100% (`1.0`) page scale.
|
||||
|
||||
> Paper size and margins have to be provided in `inches`. Same for margins.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form paperWidth=8.27 \
|
||||
--form paperHeight=11.69 \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
--form landscape=true \
|
||||
--form scale=0.75 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.PaperSize(gotenberg.A4)
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
req.Landscape(true)
|
||||
req.Scale(0.75)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$request->setLandscape(true);
|
||||
$request->setScale(0.75);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Page ranges
|
||||
|
||||
You may specify the page ranges to convert.
|
||||
|
||||
The format is the same as the one from the print options
|
||||
of Google Chrome, e.g. `1-5,8,11-13`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form pageRanges='1-3,5' \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.PageRanges("1-3,5")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setPageRanges('1-3,5');
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Wait delay
|
||||
|
||||
In some cases, you may want to wait a certain amount of time to make sure the
|
||||
page you're trying to generate is fully rendered. For instance, if your page relies
|
||||
a lot on JavaScript for rendering.
|
||||
|
||||
> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form waitDelay=5.5 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WaitDelay(5.5)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitDelay(5.5);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Rpcc buffer size
|
||||
|
||||
The API might return a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
|
||||
|
||||
If so, you may increase this buffer size with a form field named `googleChromeRpccBufferSize`.
|
||||
|
||||
It takes an int as value (e.g. `1048576` for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_google_chrome_rpcc_buffer_size) section.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form googleChromeRpccBufferSize=1048576 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.GoogleChromeRpccBufferSize(1048576)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setGoogleChromeRpccBufferSize(1048576);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
106
build/docs/content/05-url.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: URL
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/url` for remote URL conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
This endpoint does not accept an `index.html` file nor assets files but a form field
|
||||
named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions.
|
||||
|
||||
> **Attention:** when converting a website to PDF, you should remove all margins.
|
||||
> If not, some of the content of the page might be hidden.
|
||||
|
||||
> **Attention:** if you try to convert a URL from a Docker Compose service named `app` (i.e. `removeURL` = `http://app/an/entrypoint`),
|
||||
> the resulting PDF will be blank. Make sure to rename your service to avoid this issue.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/url \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form remoteURL=https://google.com \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := gotenberg.NewURLRequest("https://google.com")
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\URLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$request = new URLRequest('https://google.com');
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Custom HTTP headers
|
||||
|
||||
You may send your own HTTP headers to the `remoteURL`.
|
||||
|
||||
For instance, by adding the HTTP header `Gotenberg-Remoteurl-Your-Header` to your request,
|
||||
the API will send a request to the `remoteURL` with the HTTP header `Your-Header`.
|
||||
|
||||
> **Attention:** the API uses a canonical format for the HTTP headers:
|
||||
> it transforms the first
|
||||
> letter and any letter following a hyphen to upper case;
|
||||
> the rest are converted to lowercase. For example, the
|
||||
> canonical key for `accept-encoding` is `Accept-Encoding`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/url \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--header 'Gotenberg-Remoteurl-Your-Header: Foo' \
|
||||
--form remoteURL=https://google.com \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := gotenberg.NewURLRequest("https://google.com")
|
||||
req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\URLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$request = new URLRequest('https://google.com');
|
||||
$request->addRemoteURLHTTPHeader('Your-Header', 'Foo')
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
@@ -36,20 +36,20 @@ $ curl --request POST \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@file.md \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewMarkdownRequest("index.html", []string{"file.md"})
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file")
|
||||
req := gotenberg.NewMarkdownRequest(index, markdown)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -60,11 +60,11 @@ use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\MarkdownRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$markdowns = [
|
||||
DocumentFactory::makeFromPath('file.md', 'file.md'),
|
||||
DocumentFactory::makeFromPath('file.md', '/path/to/file'),
|
||||
];
|
||||
$request = new MarkdownRequest($index, $markdowns);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
167
build/docs/content/07-office.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
title: Office
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/office` for Office document conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
You may send one or more Office documents. Following file extensions are accepted:
|
||||
|
||||
* `.txt`
|
||||
* `.rtf`
|
||||
* `.fodt`
|
||||
* `.doc`
|
||||
* `.docx`
|
||||
* `.odt`
|
||||
* `.xls`
|
||||
* `.xlsx`
|
||||
* `.ods`
|
||||
* `.ppt`
|
||||
* `.pptx`
|
||||
* `.odp`
|
||||
|
||||
All files will be merged into a single resulting PDF.
|
||||
|
||||
> **Attention:** Gotenberg merges the PDF files alphabetically.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form files=@document2.docx \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
|
||||
doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file")
|
||||
req := gotenberg.NewOfficeRequest(doc, doc2)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('document2.docx', '/path/to/file'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Orientation
|
||||
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `portrait` orientation.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form landscape=true \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
|
||||
req := gotenberg.NewOfficeRequest(doc)
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$request->setLandscape(true);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Page ranges
|
||||
|
||||
You may specify the page ranges to convert.
|
||||
|
||||
The format is the same as the one from the print options
|
||||
of LibreOffice, e.g. `1-1` or `1-4`.
|
||||
|
||||
> **Attention:** if more than one document, the page ranges will be
|
||||
> applied for each document.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form pageRanges='1-3' \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
|
||||
req := gotenberg.NewOfficeRequest(doc)
|
||||
req.PageRanges("1-3")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$request->setPageRanges('1-3');
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
@@ -8,9 +8,11 @@ It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
Nothing special here: you may send one or more PDF files and the API
|
||||
Nothing fancy here: you may send one or more PDF files and the API
|
||||
will merge them and return the resulting PDF file.
|
||||
|
||||
> **Attention:** Gotenberg merges the PDF files alphabetically.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
@@ -19,20 +21,20 @@ $ curl --request POST \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@file.pdf \
|
||||
--form files=@file2.pdf \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewMergeRequest([]string{"file.pdf", "file2.pdf"})
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
pdf, _ := gotenberg.NewDocumentFromPath("file.pdf", "/path/to/file")
|
||||
pdf2, _ := gotenberg.NewDocumentFromPath("file2.pdf", "/path/to/file")
|
||||
req := gotenberg.NewMergeRequest(pdf, pdf2)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
```
|
||||
|
||||
### PHP
|
||||
@@ -44,10 +46,10 @@ use TheCodingMachine\Gotenberg\MergeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('file.pdf', 'file.pdf'),
|
||||
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
|
||||
DocumentFactory::makeFromPath('file.pdf', '/path/to/file'),
|
||||
DocumentFactory::makeFromPath('file2.pdf', '/path/to/file'),
|
||||
];
|
||||
$request = new MergeRequest($files);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
52
build/docs/content/09-timeout.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Timeout
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `waitTimeout`.
|
||||
|
||||
The API will wait the given **seconds** before it considers the conversion to be unsucessful.
|
||||
If unsucessful, it returns a `504` HTTP code.
|
||||
|
||||
It takes a float as value (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section.
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form waitTimeout=2.5
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WaitTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitTimeout(2.5);
|
||||
$dest = 'result.pdf';
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
149
build/docs/content/10-webhook.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `webhookURL`.
|
||||
|
||||
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
||||
to given URL.
|
||||
|
||||
By doing so, your requests to the API will be over before the conversions are actually done!
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
|
||||
## Timeout
|
||||
|
||||
If a `webhookURL` is provided, you may also send a form field named `webhookURLTimeout`.
|
||||
|
||||
The API will wait the given **seconds** before it considers the sending of the resulting PDF to be unsucessful.
|
||||
|
||||
It takes a float as value (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_webhook_url_timeout) section.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/' \
|
||||
--form webhookURLTimeout=2.5
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
req.WebhookURLTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$request->setWebhookURLTimeout(2.5);
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
|
||||
## Custom HTTP headers
|
||||
|
||||
You may send your own HTTP headers to the `webhookURL`.
|
||||
|
||||
For instance, by adding the HTTP header `Gotenberg-Webhookurl-Your-Header` to your request,
|
||||
the API will send a request to the `webhookURL` with the HTTP header `Your-Header`.
|
||||
|
||||
> **Attention:** the API uses a canonical format for the HTTP headers:
|
||||
> it transforms the first
|
||||
> letter and any letter following a hyphen to upper case;
|
||||
> the rest are converted to lowercase. For example, the
|
||||
> canonical key for `accept-encoding` is `Accept-Encoding`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--header 'Gotenberg-Webhookurl-Your-Header: Foo' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$request->addWebhookURLHTTPHeader('Your-Header', 'Foo');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
49
build/docs/content/11-result-filename.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Result filename
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `resultFilename`.
|
||||
|
||||
If provided, the API will return the resulting PDF file with the given filename.
|
||||
Otherwise a random filename is used.
|
||||
|
||||
> **Attention:** this feature does not work if the form field `webhookURL` is given.
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form resultFilename='foo.pdf'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v7"
|
||||
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
|
||||
req := gotenberg.NewHTMLRequest(index)
|
||||
req.ResultFilename("foo.pdf")
|
||||
resp, _ := c.Post(req)
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setResultFilename('foo.pdf');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
53
build/docs/content/12-scalability.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Scalability
|
||||
---
|
||||
|
||||
The API uses under the hood intricate programs.
|
||||
|
||||
Gotenberg tries to abstract as much complexity as possible but it can
|
||||
only do it to a certain extent.
|
||||
|
||||
For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk
|
||||
instances as there are requests. The limitation here is the available memory and CPU usage.
|
||||
|
||||
On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel.
|
||||
Indeed, Google Chrome misbehaves if there are too many concurrent conversions.
|
||||
|
||||
**The more concurrent requests, the more `504` HTTP codes the API will return.**
|
||||
|
||||
> See our [load testing use case](https://github.com/thecodingmachine/gotenberg/tree/master/loadtesting) for more details about the API behaviour under heavy load.
|
||||
|
||||
## Strategies
|
||||
|
||||
### Increase timeout
|
||||
|
||||
You may increase the conversion timeout. In other words, you accept that a conversion takes more time
|
||||
if the API is under heavy load.
|
||||
|
||||
> See [timeout section](#timeout).
|
||||
|
||||
### Scaling
|
||||
|
||||
The API being stateless, you may scale it as much as you want.
|
||||
|
||||
For instance, using the following Docker Compose file:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
# your other services
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg:6
|
||||
```
|
||||
|
||||
You may now launch your services using:
|
||||
|
||||
```bash
|
||||
$ docker-compose up --scale gotenberg=your_number_of_instances
|
||||
```
|
||||
|
||||
When requesting the Gotenberg service with your client(s), Docker will automatically
|
||||
redirect a request to a Gotenberg container according to the round-robin strategy.
|
||||
12
build/docs/content/13-ping.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Ping
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/ping` for checking the API availability with
|
||||
a simple `GET` request.
|
||||
|
||||
Currently this endpoint does nothing special. A better way to monitor
|
||||
Gotenberg would be by checking the memory usage.
|
||||
|
||||
Also, as the API uses under the hood intricate programs, you should
|
||||
restart your Gotenberg instances from time to time to ensure a nominal behaviour.
|
||||
17
build/docs/content/14-fonts.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Fonts
|
||||
---
|
||||
|
||||
By default, a handful of fonts are installed. Asian characters are also supported out of the box.
|
||||
|
||||
If you wish to use more fonts, you will have to create your own image:
|
||||
|
||||
```Dockerfile
|
||||
FROM thecodingmachine/gotenberg:6
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get -y install yourfonts
|
||||
|
||||
USER gotenberg
|
||||
```
|
||||
12
build/docs/content/15-links.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Links
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="150" height="150" />
|
||||
</p>
|
||||
|
||||
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
|
||||
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
|
||||
* Thanks to [@mafredri](https://github.com/mafredri) for his help and his wonderful [cdp](https://github.com/mafredri/cdp) library
|
||||
* OpenAPI documentation for the API can be found [on GitHub](https://github.com/thecodingmachine/gotenberg/blob/master/docs/openapi.yaml)
|
||||
@@ -1,21 +1,6 @@
|
||||
ARG GOLANG_VERSION
|
||||
ARG GOLANGCI_LINT_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
|
||||
FROM golangci/golangci-lint:v$GOLANGCI_LINT_VERSION
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Final touch
|
||||
@@ -24,14 +9,14 @@ RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.s
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /lint
|
||||
# Define our working directory outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /gotenberg/lint
|
||||
|
||||
# Copy our module dependencies definitions.
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
# Copy our code source.
|
||||
COPY . .
|
||||
|
||||
# Install module dependencies.
|
||||
RUN go mod download
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=lll", "--disable=errcheck", "--disable=gosec", "--disable=gochecknoglobals", "--disable=gochecknoinits" ]
|
||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit", "--disable=gomnd", "--disable=goimports" ]
|
||||
@@ -1,28 +1,50 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Binary
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Buils Gotenberg binary.
|
||||
# | Builds Gotenberg binary.
|
||||
# |
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
||||
FROM thecodingmachine/gotenberg:workspace AS workspace
|
||||
|
||||
ARG VERSION
|
||||
|
||||
ENV GOOS=linux \
|
||||
GOARCH=amd64 \
|
||||
GOARCH=amd64 \
|
||||
CGO_ENABLED=0
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /gotenberg
|
||||
WORKDIR /gotenberg/package
|
||||
|
||||
# Install module dependencies.
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
# Copy our source code.
|
||||
COPY . .
|
||||
COPY internal ./internal
|
||||
COPY cmd ./cmd
|
||||
|
||||
# Build our binary.
|
||||
RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
|
||||
RUN go build -o gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
|
||||
|
||||
FROM thecodingmachine/gotenberg:base
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Tini
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | An helper for reaping zombie processes.
|
||||
# |
|
||||
|
||||
ARG TINI_VERSION
|
||||
|
||||
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static /tini
|
||||
RUN chmod +x /tini
|
||||
ENTRYPOINT [ "/tini", "--" ]
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Final touch
|
||||
@@ -31,13 +53,10 @@ RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/g
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
|
||||
FROM thecodingmachine/gotenberg:base
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
|
||||
COPY --from=workspace /gotenberg/package/gotenberg /usr/local/bin/
|
||||
|
||||
USER gotenberg
|
||||
WORKDIR /gotenberg
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "gotenberg" ]
|
||||
CMD [ "gotenberg" ]
|
||||
|
||||
@@ -1,50 +1,16 @@
|
||||
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.
|
||||
# |
|
||||
FROM thecodingmachine/gotenberg:workspace
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /tests
|
||||
|
||||
# Copy our module dependencies definitions.
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
USER gotenberg
|
||||
WORKDIR /gotenberg/tests
|
||||
|
||||
# Install module dependencies.
|
||||
RUN go mod download
|
||||
COPY --chown=gotenberg:gotenberg go.mod go.sum ./
|
||||
|
||||
ENTRYPOINT ["build/tests/docker-entrypoint.sh"]
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
# Copy our code source.
|
||||
COPY --chown=gotenberg:gotenberg . .
|
||||
|
||||
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
||||
@@ -2,24 +2,19 @@
|
||||
|
||||
set -xe
|
||||
|
||||
# Testing PM2 processes launch separatly for avoiding
|
||||
# spending to much time on each tests depending on
|
||||
# them.
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeLaunch
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvLaunch
|
||||
# Make sure the user running the
|
||||
# tests is the Gotenberg user.
|
||||
CURRENT_USER=$(whoami)
|
||||
if [ "$CURRENT_USER" != "gotenberg" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Running others tests.
|
||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/printer
|
||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
||||
# Start Google Chrome headless.
|
||||
go run test/cmd/chrome.go
|
||||
|
||||
# Finally testing processes shutdown.
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
||||
|
||||
# Testing Go client.
|
||||
go build -o /usr/local/bin/gotenberg cmd/gotenberg/main.go
|
||||
gotenberg &
|
||||
sleep 10
|
||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/pkg
|
||||
sleep 5 # allows Gotenberg to remove generated files (concurrent requests).
|
||||
# Run our tests.
|
||||
if [ "$CODE_COVERAGE" = "1" ]; then
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
else
|
||||
go test -race -cover ./...
|
||||
fi
|
||||
52
build/workspace/Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch as golang
|
||||
|
||||
FROM thecodingmachine/gotenberg:base
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Common libraries
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Libraries used in the build process of this image.
|
||||
# |
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
g++ \
|
||||
gcc \
|
||||
libc6-dev \
|
||||
make \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Golang
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs Golang.
|
||||
# |
|
||||
|
||||
COPY --from=golang /usr/local/go /usr/local/go
|
||||
|
||||
ENV GOPATH /gotenberg/go
|
||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
|
||||
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" &&\
|
||||
chmod -R 777 "$GOPATH"
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Final touch
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
|
||||
# Make sure the Gotenber user is able to
|
||||
# call the Go binary.
|
||||
USER gotenberg
|
||||
|
||||
RUN go version &&\
|
||||
go env
|
||||
|
||||
USER root
|
||||
@@ -1,21 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/chrome"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// version will be set on build time.
|
||||
// nolint: gochecknoglobals
|
||||
var version = "snapshot"
|
||||
|
||||
func main() {
|
||||
notify.Println(fmt.Sprintf("Gotenberg %s", version))
|
||||
if err := api.Start(); err != nil {
|
||||
notify.ErrPrintln(err)
|
||||
os.Exit(1)
|
||||
const op string = "main"
|
||||
config, err := conf.FromEnv()
|
||||
systemLogger := xlog.New(config.LogLevel(), "system")
|
||||
if err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
systemLogger.InfoOpf(op, "Gotenberg %s", version)
|
||||
systemLogger.DebugOpf(op, "configuration: %+v", config)
|
||||
if !config.DisableGoogleChrome() {
|
||||
// start Google Chrome headless.
|
||||
if err := chrome.Start(systemLogger, config.GoogleChromeIgnoreCertificateErrors()); err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
}
|
||||
// create our API.
|
||||
srv := xhttp.New(config)
|
||||
// run our API in a goroutine so that it doesn't block.
|
||||
go func() {
|
||||
systemLogger.InfoOpf(op, "http server started on port '%d'", config.DefaultListenPort())
|
||||
if err := srv.Start(fmt.Sprintf(":%d", config.DefaultListenPort())); err != nil {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
quit := make(chan os.Signal, 1)
|
||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
// block until we receive our signal.
|
||||
<-quit
|
||||
// create a deadline to wait for.
|
||||
ctx, cancel := xcontext.WithTimeout(systemLogger, 120)
|
||||
defer cancel()
|
||||
// doesn't block if no connections, but will otherwise wait
|
||||
// until the timeout deadline.
|
||||
systemLogger.InfoOp(op, "shutting down http server...")
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
systemLogger.InfoOp(op, "bye!")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
1564
docs/index.html
558
docs/openapi.yaml
Normal file
@@ -0,0 +1,558 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Gotenberg
|
||||
version: 6.3.1
|
||||
license:
|
||||
name: MIT
|
||||
url: 'https://github.com/thecodingmachine/gotenberg/blob/master/LICENSE'
|
||||
contact:
|
||||
url: 'https://github.com/thecodingmachine/gotenberg'
|
||||
description: >-
|
||||
A Docker-powered stateless API for converting HTML, Markdown and Office
|
||||
documents to PDF.
|
||||
servers:
|
||||
- url: 'http://localhost:3000'
|
||||
description: Local server with the default Docker image and port
|
||||
paths:
|
||||
/convert/html:
|
||||
post:
|
||||
summary: Convert a given HTML file to PDF
|
||||
description: >-
|
||||
Send an HTML file called `index.html` as a multipart form request, and
|
||||
get the resulting PDF file. You can optionally include `header.html` and
|
||||
`footer.html` files as part of the request as well.
|
||||
|
||||
|
||||
An example `index.html` file can be as follows:
|
||||
|
||||
```html
|
||||
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
|
||||
You may also add a header and/or a footer in the resulting PDF by
|
||||
sending a file named `header.html` and `footer.html` respectively. Both
|
||||
the header and the footer files has to be a complete HTML document. An
|
||||
example `footer.html` can be as follows:
|
||||
|
||||
```html
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 8rem;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
|
||||
The following classes will allow you to inject printing values in your
|
||||
document:
|
||||
|
||||
- `date`: Formatted print date.
|
||||
|
||||
- `title`: Document title.
|
||||
|
||||
- `pageNumber`: Current page number.
|
||||
|
||||
- `totalPage`: Total pages in the document.
|
||||
|
||||
|
||||
There are some limitations with header and footer files:
|
||||
|
||||
- JavaScript is not executed.
|
||||
|
||||
- External resources are not loaded.
|
||||
|
||||
- The CSS properties are independant of the ones used in the
|
||||
`index.html` file.
|
||||
|
||||
- `footer.html` CSS properties override the ones from `header.html`.
|
||||
|
||||
- Only fonts installed in the Docker image are loaded (see the fonts
|
||||
section)
|
||||
|
||||
- Images only work using a `base64` encoded source, e.g. `<img
|
||||
src="data:image/png;base64, iVBORw0K... />`
|
||||
|
||||
- `background-color` and `color` CSS properties require an additional
|
||||
`-webkit-print-color-adjust: exact` CSS property in order to work.
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTMLConvertRequestBody'
|
||||
description: >-
|
||||
The request body must have an `index.html` file in the `files` array,
|
||||
as well as all the referred resources on the same level as the
|
||||
`index.html` file. The request can also include `header.html` and
|
||||
`footer.html`, given the limitations in the API description above.
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/SuccessfulPDF'
|
||||
/convert/url:
|
||||
post:
|
||||
summary: Convert the contents of a given URL to PDF
|
||||
description: >-
|
||||
Send a remote URL in your API request via the `remoteURL` parameter, and
|
||||
get the resulting PDF file. The API will fetch the given URL and render
|
||||
the page to PDF using the underlying headless Chrome instance.
|
||||
requestBody:
|
||||
required: true
|
||||
description: >-
|
||||
The URL conversion request has to be a `multipart/form-data` request
|
||||
that includes a `remoteURL`. The API uses a headless Chrome instance
|
||||
to do the conversion, therefore print parameter such as margins and
|
||||
paper size are also accepted as optional parameters.
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/URLConvertRequestBody'
|
||||
examples: {}
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/SuccessfulPDF'
|
||||
/convert/markdown:
|
||||
post:
|
||||
summary: Convert a Markdown file to PDF
|
||||
description: >-
|
||||
Send an HTML file called `index.html` as a multipart form request and
|
||||
embed a markdown file into the HTML file using the Golang template
|
||||
function `toHTML`, the API will convert the markdown and embed it into
|
||||
the HTML this way and render the resulting page. Markdown conversion
|
||||
works almost exactly the same way as HTML conversion, therefore refer to
|
||||
the HTML conversion page for all the options you can use when converting
|
||||
Markdown documents as well. You can optionally include `header.html` and
|
||||
`footer.html` files as part of the request as well.
|
||||
|
||||
|
||||
An example `index.html` file can be as follows:
|
||||
|
||||
```html
|
||||
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
{{ toHTML .DirPath "file.md" }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
|
||||
Whereas your `file.md` file in the same level would be like:
|
||||
|
||||
```md
|
||||
|
||||
# Title
|
||||
|
||||
Content
|
||||
|
||||
```
|
||||
|
||||
|
||||
The API will convert the markdown to HTML and embed it into your
|
||||
`index.html` file, then render the resulting page. You can include your
|
||||
own styling and more in your HTML file.
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MarkdownConvertRequestBody'
|
||||
description: >-
|
||||
The request body must have an `index.html` file in the `files` array,
|
||||
as well as all the referred markdown resources on the same level as
|
||||
the `index.html` file.
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/SuccessfulPDF'
|
||||
/convert/office:
|
||||
post:
|
||||
summary: Convert an Office document to PDF
|
||||
description: >-
|
||||
Send one or more Office documents and get the resulting PDF file by
|
||||
merging all the files. The following file extensions are accepted:
|
||||
|
||||
- `.txt`
|
||||
|
||||
- `.rtf`
|
||||
|
||||
- `.fodt`
|
||||
|
||||
- `.doc`
|
||||
|
||||
- `.docx`
|
||||
|
||||
- `.odt`
|
||||
|
||||
- `.xls`
|
||||
|
||||
- `.xlsx`
|
||||
|
||||
- `.ods`
|
||||
|
||||
- `.ppt`
|
||||
|
||||
- `.pptx`
|
||||
|
||||
- `.odp`
|
||||
|
||||
|
||||
All files will be merged into a single PDF.
|
||||
|
||||
> **Attention:** The files will be merged alphabetically for the
|
||||
resulting PDF.
|
||||
|
||||
|
||||
You may also specify the page ranges to convert from the incoming Office
|
||||
documents. The expected format is the same as the one from the print
|
||||
options of LibreOffice, e.g. `1-1` or `1-4`.
|
||||
|
||||
|
||||
> **Attention:** if more than one document, the page ranges will be
|
||||
applied for each document.
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OfficeConvertRequestBody'
|
||||
description: >-
|
||||
The request body must have an `index.html` file in the `files` array,
|
||||
as well as all the referred markdown resources on the same level as
|
||||
the `index.html` file.
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/SuccessfulPDF'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
/convert/merge:
|
||||
post:
|
||||
summary: Merge multiple PDFs into a single PDF
|
||||
description: >-
|
||||
You can send multiple PDF files to this endpoint, the API will merge
|
||||
them into a single PDF and return the resulting PDF file.
|
||||
|
||||
|
||||
> **Attention:** The PDF files will be merged alphabetically.
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
files:
|
||||
type: array
|
||||
description: >-
|
||||
List of HTML files to be converted to PDF. An `index.html`
|
||||
file is required, and any other resources that are
|
||||
referenced through the HTML file must be included as well.
|
||||
All the referenced files must be on the same level as the
|
||||
`index.html` file.
|
||||
items:
|
||||
type: string
|
||||
format: binary
|
||||
resultFilename:
|
||||
type: string
|
||||
example: output.pdf
|
||||
description: >-
|
||||
If provided, the API will return the resulting PDF file with
|
||||
the given filename. Otherwise a random filename is used.
|
||||
required:
|
||||
- files
|
||||
description: >-
|
||||
The request body must have an `index.html` file in the `files` array,
|
||||
as well as all the referred markdown resources on the same level as
|
||||
the `index.html` file.
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/SuccessfulPDF'
|
||||
/ping:
|
||||
get:
|
||||
summary: Your GET endpoint
|
||||
tags: []
|
||||
responses:
|
||||
'200':
|
||||
description: The API is working fine.
|
||||
operationId: ''
|
||||
description: >-
|
||||
A simple endpoint to use as a healthcheck for the API availability with
|
||||
a simple request.
|
||||
components:
|
||||
schemas:
|
||||
HTMLConvertRequestBody:
|
||||
title: HTML Conversion Request Body
|
||||
type: object
|
||||
properties:
|
||||
files:
|
||||
type: array
|
||||
description: >-
|
||||
List of HTML files to be converted to PDF. An `index.html` file is
|
||||
required, and any other resources that are referenced through the
|
||||
HTML file must be included as well. All the referenced files must be
|
||||
on the same level as the `index.html` file.
|
||||
items:
|
||||
type: string
|
||||
format: binary
|
||||
marginTop:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Top margin for the page in inches.
|
||||
marginBottom:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Bottom margin for the page in inches.
|
||||
marginLeft:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Left margin for the page in inches.
|
||||
marginRight:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Right margin for the page in inches.
|
||||
paperWidth:
|
||||
type: number
|
||||
example: 8.27
|
||||
description: >-
|
||||
Paper width to be used while rendering the PDF. The default page
|
||||
size is A4.
|
||||
paperHeight:
|
||||
type: number
|
||||
example: 11.69
|
||||
description: >-
|
||||
Paper height to be used while rendering the PDF. The default page
|
||||
size is A4.
|
||||
landscape:
|
||||
type: boolean
|
||||
example: true
|
||||
default: false
|
||||
description: >-
|
||||
The default orientation for rendering the page is "portrait" mode.
|
||||
By sending "landscape" parameter, you can ask the output to be
|
||||
landscape.
|
||||
resultFilename:
|
||||
type: string
|
||||
example: output.pdf
|
||||
description: >-
|
||||
If provided, the API will return the resulting PDF file with the
|
||||
given filename. Otherwise a random filename is used.
|
||||
required:
|
||||
- files
|
||||
MarkdownConvertRequestBody:
|
||||
title: Markdown Conversion Request Body
|
||||
type: object
|
||||
properties:
|
||||
files:
|
||||
type: array
|
||||
description: >-
|
||||
List of HTML files to be converted to PDF. An `index.html` file is
|
||||
required, and any other resources that are referenced through the
|
||||
HTML file must be included as well. All the referenced files must be
|
||||
on the same level as the `index.html` file.
|
||||
items:
|
||||
type: string
|
||||
format: binary
|
||||
marginTop:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Top margin for the page in inches.
|
||||
marginBottom:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Bottom margin for the page in inches.
|
||||
marginLeft:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Left margin for the page in inches.
|
||||
marginRight:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Right margin for the page in inches.
|
||||
paperWidth:
|
||||
type: number
|
||||
example: 8.27
|
||||
description: >-
|
||||
Paper width to be used while rendering the PDF. The default page
|
||||
size is A4.
|
||||
paperHeight:
|
||||
type: number
|
||||
example: 11.69
|
||||
description: >-
|
||||
Paper height to be used while rendering the PDF. The default page
|
||||
size is A4.
|
||||
landscape:
|
||||
type: boolean
|
||||
example: true
|
||||
default: false
|
||||
description: >-
|
||||
The default orientation for rendering the page is "portrait" mode.
|
||||
By sending "landscape" parameter, you can ask the output to be
|
||||
landscape.
|
||||
resultFilename:
|
||||
type: string
|
||||
example: output.pdf
|
||||
description: >-
|
||||
If provided, the API will return the resulting PDF file with the
|
||||
given filename. Otherwise a random filename is used.
|
||||
required:
|
||||
- files
|
||||
URLConvertRequestBody:
|
||||
title: URL Conversion Request Body
|
||||
type: object
|
||||
properties:
|
||||
remoteURL:
|
||||
type: string
|
||||
example: 'https://google.com'
|
||||
marginTop:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Top margin for the page in inches.
|
||||
marginBottom:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Bottom margin for the page in inches.
|
||||
marginLeft:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Left margin for the page in inches.
|
||||
marginRight:
|
||||
type: number
|
||||
example: 0
|
||||
default: 1
|
||||
description: Right margin for the page in inches.
|
||||
paperWidth:
|
||||
type: number
|
||||
example: 8.27
|
||||
description: >-
|
||||
Paper width to be used while rendering the PDF. The default page
|
||||
size is A4.
|
||||
paperHeight:
|
||||
type: number
|
||||
example: 11.69
|
||||
description: >-
|
||||
Paper height to be used while rendering the PDF. The default page
|
||||
size is A4.
|
||||
landscape:
|
||||
type: boolean
|
||||
example: true
|
||||
default: false
|
||||
description: >-
|
||||
The default orientation for rendering the page is "portrait" mode.
|
||||
By sending "landscape" parameter, you can ask the output to be
|
||||
landscape.
|
||||
resultFilename:
|
||||
type: string
|
||||
example: output.pdf
|
||||
description: >-
|
||||
If provided, the API will return the resulting PDF file with the
|
||||
given filename. Otherwise a random filename is used.
|
||||
required:
|
||||
- remoteURL
|
||||
OfficeConvertRequestBody:
|
||||
title: Office Conversion Request Body
|
||||
type: object
|
||||
properties:
|
||||
files:
|
||||
type: array
|
||||
description: >-
|
||||
List of HTML files to be converted to PDF. An `index.html` file is
|
||||
required, and any other resources that are referenced through the
|
||||
HTML file must be included as well. All the referenced files must be
|
||||
on the same level as the `index.html` file.
|
||||
items:
|
||||
type: string
|
||||
format: binary
|
||||
pageRanges:
|
||||
type: string
|
||||
example: 1-3
|
||||
description: >-
|
||||
The page ranges to be converted to PDF for the incoming Office
|
||||
documents. **If there are multiple files sent to the API, this page
|
||||
range will apply to all of the documents**.
|
||||
landscape:
|
||||
type: boolean
|
||||
example: true
|
||||
default: false
|
||||
description: >-
|
||||
The default orientation for rendering the page is "portrait" mode.
|
||||
By sending "landscape" parameter, you can ask the output to be
|
||||
landscape.
|
||||
resultFilename:
|
||||
type: string
|
||||
example: output.pdf
|
||||
description: >-
|
||||
If provided, the API will return the resulting PDF file with the
|
||||
given filename. Otherwise a random filename is used.
|
||||
required:
|
||||
- files
|
||||
MergeFilesRequestBody:
|
||||
title: Merge Files Request Body
|
||||
type: object
|
||||
properties:
|
||||
files:
|
||||
type: array
|
||||
description: >-
|
||||
List of HTML files to be converted to PDF. An `index.html` file is
|
||||
required, and any other resources that are referenced through the
|
||||
HTML file must be included as well. All the referenced files must be
|
||||
on the same level as the `index.html` file.
|
||||
items:
|
||||
type: string
|
||||
format: binary
|
||||
resultFilename:
|
||||
type: string
|
||||
example: output.pdf
|
||||
description: >-
|
||||
If provided, the API will return the resulting PDF file with the
|
||||
given filename. Otherwise a random filename is used.
|
||||
required:
|
||||
- files
|
||||
securitySchemes: {}
|
||||
responses:
|
||||
SuccessfulPDF:
|
||||
description: Resulting PDF file from the conversion.
|
||||
content:
|
||||
application/pdf:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
BIN
docs/theme/gotenberg/img/apple-touch-icon-114x114.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-120x120.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-144x144.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-152x152.png
vendored
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-57x57.png
vendored
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-60x60.png
vendored
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-72x72.png
vendored
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-76x76.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/theme/gotenberg/img/favicon-128.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/theme/gotenberg/img/favicon-16x16.png
vendored
|
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 917 B |
BIN
docs/theme/gotenberg/img/favicon-196x196.png
vendored
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/theme/gotenberg/img/favicon-32x32.png
vendored
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
docs/theme/gotenberg/img/favicon-96x96.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/theme/gotenberg/img/favicon.ico
vendored
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 34 KiB |
BIN
docs/theme/gotenberg/img/mstile-144x144.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/theme/gotenberg/img/mstile-150x150.png
vendored
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x150.png
vendored
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x310.png
vendored
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
docs/theme/gotenberg/img/mstile-70x70.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
27
docs/theme/gotenberg/views/index.html
vendored
@@ -1,12 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-10196481-9"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-10196481-9');
|
||||
</script>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{.Title}} · {{.Subtitle}}</title>
|
||||
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="theme/gotenberg/img/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="theme/gotenberg/img/apple-touch-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="theme/gotenberg/img/apple-touch-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="theme/gotenberg/img/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="theme/gotenberg/img/apple-touch-icon-60x60.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="theme/gotenberg/img/apple-touch-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="theme/gotenberg/img/apple-touch-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="theme/gotenberg/img/apple-touch-icon-152x152.png" />
|
||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-196x196.png" sizes="196x196" />
|
||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-16x16.png" sizes="16x16" />
|
||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-16x16.png" sizes="16x16" />
|
||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-128.png" sizes="128x128" />
|
||||
<link rel="stylesheet" href="theme/gotenberg/css/index.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -14,7 +34,7 @@
|
||||
<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" />
|
||||
<img src="https://user-images.githubusercontent.com/8983173/69229423-ac731300-0b85-11ea-8c2e-2cc00ecdb269.PNG" alt="Gotenberg logo" width="250" height="250" />
|
||||
<span class="text">{{.Title}}</span>
|
||||
<span class="subtext">{{.Subtitle}}</span>
|
||||
</div>
|
||||
@@ -38,7 +58,8 @@
|
||||
{{.Content}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="Footer"></div>
|
||||
<div class="Footer">
|
||||
</div>
|
||||
<script src="theme/gotenberg/js/index.js"></script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
37
go.mod
@@ -1,21 +1,26 @@
|
||||
module github.com/thecodingmachine/gotenberg
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/derekparker/delve v1.1.0
|
||||
github.com/google/go-cmp v0.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.0 // indirect
|
||||
github.com/hhrutter/pdfcpu v0.1.19
|
||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73
|
||||
github.com/labstack/gommon v0.2.8
|
||||
github.com/mafredri/cdp v0.20.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.1
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.1.16
|
||||
github.com/labstack/gommon v0.3.0
|
||||
github.com/mafredri/cdp v0.28.0
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/microcosm-cc/bluemonday v1.0.2
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
|
||||
github.com/russross/blackfriday/v2 v2.0.1
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
||||
github.com/stretchr/objx v0.1.1 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
|
||||
)
|
||||
|
||||
137
go.sum
@@ -1,55 +1,102 @@
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
|
||||
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hhrutter/pdfcpu v0.1.18 h1:GX3d6tZxmQlmB5X9VUyOtx7+HZZpR+8ISLqBineP6dc=
|
||||
github.com/hhrutter/pdfcpu v0.1.18/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
||||
github.com/hhrutter/pdfcpu v0.1.19 h1:J6GjNFDPjurQWallPW9gmz3CcNqBfvgB7Dad9T3e8Hc=
|
||||
github.com/hhrutter/pdfcpu v0.1.19/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73 h1:oQvA3Xy8274W5Ldgo6Nogy4lX8XO5AaIBsjF5Ggk0IU=
|
||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73/go.mod h1:rS0D1UPvC8/3sXjhSwEq+K1olh7ipbDhjDWATN2KSgA=
|
||||
github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=
|
||||
github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/mafredri/cdp v0.20.0 h1:dRq16FwXCAAbolR4W2iyRNsL14Bp6jFHLbRp2EkICVs=
|
||||
github.com/mafredri/cdp v0.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
|
||||
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
|
||||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/mafredri/cdp v0.28.0 h1:v/LWC3GpactA1EaS737bl+eNUhvOaDh+FzvyoDxNM5o=
|
||||
github.com/mafredri/cdp v0.28.0/go.mod h1:11586MgpyJuQR4qiXQ/7HBame2FtGIBzymaUHZcQ0PY=
|
||||
github.com/mafredri/go-lint v0.0.0-20180911205320-920981dfc79e/go.mod h1:k/zdyxI3q6dup24o8xpYjJKTCf2F7rfxLp6w/efTiWs=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
|
||||
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200330183114-f8bfb4ee3038 h1:FUufmZ17Dp41BwLDPii6R8vp0QtIquTiDIxcbFlY1ek=
|
||||
golang.org/x/tools v0.0.0-20200330183114-f8bfb4ee3038/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||
)
|
||||
|
||||
// Start starts the API server on port 3000.
|
||||
func Start() error {
|
||||
e := setup()
|
||||
// start Chrome headless and
|
||||
// unoconv listener with PM2.
|
||||
chrome := &pm2.Chrome{}
|
||||
unoconv := &pm2.Unoconv{}
|
||||
if err := chrome.Launch(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unoconv.Launch(); err != nil {
|
||||
return err
|
||||
}
|
||||
// run our API in a goroutine so that it doesn't block.
|
||||
go func() {
|
||||
notify.Println("http server started on port 3000")
|
||||
if err := e.Start(":3000"); err != nil {
|
||||
e.Logger.Fatalf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
quit := make(chan os.Signal, 1)
|
||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
// block until we receive our signal.
|
||||
<-quit
|
||||
// create a deadline to wait for.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
// doesn't block if no connections, but will otherwise wait
|
||||
// until the timeout deadline.
|
||||
notify.Println("shutting down http server... (Ctrl+C to force)")
|
||||
return e.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func setup() *echo.Echo {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if err := next(c); err != nil {
|
||||
// TODO should return a better HTTP status code
|
||||
// than 500 for some cases.
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
})
|
||||
e.POST("/merge", merge)
|
||||
g := e.Group("/convert")
|
||||
g.POST("/html", convertHTML)
|
||||
g.POST("/markdown", convertMarkdown)
|
||||
g.POST("/office", convertOffice)
|
||||
return e
|
||||
}
|
||||
|
||||
func newContext(r *resource) (context.Context, context.CancelFunc) {
|
||||
webhookURL := r.webhookURL()
|
||||
if webhookURL == "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
return ctx, cancel
|
||||
}
|
||||
return context.Background(), nil
|
||||
}
|
||||
|
||||
func print(c echo.Context, p printer.Printer, r *resource) error {
|
||||
baseFilename, err := rand.Get()
|
||||
if err != nil {
|
||||
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
|
||||
}
|
||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
if r.webhookURL() == "" {
|
||||
defer r.removeAll()
|
||||
// if no webhook URL given, run conversion
|
||||
// and directly return the resulting PDF file
|
||||
// or an error.
|
||||
if err := p.Print(fpath); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Attachment(fpath, filename)
|
||||
}
|
||||
// as a webhook URL has been given, we
|
||||
// run the following lines in a goroutine so that
|
||||
// it doesn't block.
|
||||
go func() {
|
||||
defer r.removeAll()
|
||||
if err := p.Print(fpath); err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func hijackErr(err error, r *resource) error {
|
||||
if r != nil {
|
||||
defer r.removeAll()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -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,51 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
func convertHTML(c echo.Context) error {
|
||||
r, err := newResource(c)
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
ctx, cancel := newContext(r)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
p := &printer.HTML{Context: ctx}
|
||||
indexPath, err := r.filePath("index.html")
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.WithLocalURL(indexPath)
|
||||
headerPath, _ := r.filePath("header.html")
|
||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
footerPath, _ := r.filePath("footer.html")
|
||||
if err := p.WithFooterFile(footerPath); err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
paperSize, err := r.paperSize()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.PaperWidth = paperSize[0]
|
||||
p.PaperHeight = paperSize[1]
|
||||
paperMargins, err := r.paperMargins()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.MarginTop = paperMargins[0]
|
||||
p.MarginBottom = paperMargins[1]
|
||||
p.MarginLeft = paperMargins[2]
|
||||
p.MarginRight = paperMargins[3]
|
||||
landscape, err := r.landscape()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.Landscape = landscape
|
||||
return print(c, p, r)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestHTML(t *testing.T) {
|
||||
body, contentType := test.HTMLTestMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
if assert.NoError(t, convertHTML(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
func convertMarkdown(c echo.Context) error {
|
||||
r, err := newResource(c)
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
ctx, cancel := newContext(r)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
indexPath, err := r.filePath("index.html")
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
|
||||
headerPath, _ := r.filePath("header.html")
|
||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
footerPath, _ := r.filePath("footer.html")
|
||||
if err := p.WithFooterFile(footerPath); err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
paperSize, err := r.paperSize()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.PaperWidth = paperSize[0]
|
||||
p.PaperHeight = paperSize[1]
|
||||
paperMargins, err := r.paperMargins()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.MarginTop = paperMargins[0]
|
||||
p.MarginBottom = paperMargins[1]
|
||||
p.MarginLeft = paperMargins[2]
|
||||
p.MarginRight = paperMargins[3]
|
||||
landscape, err := r.landscape()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.Landscape = landscape
|
||||
return print(c, p, r)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
body, contentType := test.MarkdownTestMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
if assert.NoError(t, convertMarkdown(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||
)
|
||||
|
||||
func merge(c echo.Context) error {
|
||||
r, err := newResource(c)
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
fpaths, err := r.filePaths([]string{".pdf"})
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return hijackErr(errors.New("no suitable PDF files to merge"), r)
|
||||
}
|
||||
baseFilename, err := rand.Get()
|
||||
if err != nil {
|
||||
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
|
||||
}
|
||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
if r.webhookURL() == "" {
|
||||
defer r.removeAll()
|
||||
// if no webhook URL given, run merge
|
||||
// and directly return the resulting PDF file
|
||||
// or an error.
|
||||
if err := printer.Merge(fpaths, fpath); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Attachment(fpath, filename)
|
||||
}
|
||||
// as a webhook URL has been given, we
|
||||
// run the following lines in a goroutine so that
|
||||
// it doesn't block.
|
||||
go func() {
|
||||
defer r.removeAll()
|
||||
if err := printer.Merge(fpaths, fpath); err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
body, contentType := test.PDFTestMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
if assert.NoError(t, merge(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
var officeExts = []string{
|
||||
".txt",
|
||||
".rtf",
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ods",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odp",
|
||||
}
|
||||
|
||||
func convertOffice(c echo.Context) error {
|
||||
r, err := newResource(c)
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
ctx, cancel := newContext(r)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
fpaths, err := r.filePaths(officeExts)
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return hijackErr(errors.New("no suitable office documents to convert"), r)
|
||||
}
|
||||
p := &printer.Office{Context: ctx, FilePaths: fpaths}
|
||||
paperSize, err := r.paperSize()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.PaperWidth = paperSize[0]
|
||||
p.PaperHeight = paperSize[1]
|
||||
landscape, err := r.landscape()
|
||||
if err != nil {
|
||||
return hijackErr(err, r)
|
||||
}
|
||||
p.Landscape = landscape
|
||||
return print(c, p, r)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestOffice(t *testing.T) {
|
||||
body, contentType := test.OfficeTestMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
if assert.NoError(t, convertOffice(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
webhookURL string = "webhookURL"
|
||||
paperWidth string = "paperWidth"
|
||||
paperHeight string = "paperHeight"
|
||||
marginTop string = "marginTop"
|
||||
marginBottom string = "marginBottom"
|
||||
marginLeft string = "marginLeft"
|
||||
marginRight string = "marginRight"
|
||||
landscape string = "landscape"
|
||||
)
|
||||
|
||||
// resource facilitates storing and accessing
|
||||
// data from a multipart/form-data request.
|
||||
type resource struct {
|
||||
values map[string]string
|
||||
dirPath string
|
||||
}
|
||||
|
||||
func newResource(c echo.Context) (*resource, error) {
|
||||
v := make(map[string]string)
|
||||
v[webhookURL] = c.FormValue(webhookURL)
|
||||
v[paperWidth] = c.FormValue(paperWidth)
|
||||
v[paperHeight] = c.FormValue(paperHeight)
|
||||
v[marginTop] = c.FormValue(marginTop)
|
||||
v[marginBottom] = c.FormValue(marginBottom)
|
||||
v[marginLeft] = c.FormValue(marginLeft)
|
||||
v[marginRight] = c.FormValue(marginRight)
|
||||
v[landscape] = c.FormValue(landscape)
|
||||
dirPath, err := rand.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
||||
}
|
||||
r := &resource{values: v, dirPath: dirPath}
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return r, fmt.Errorf("getting multipart form: %v", err)
|
||||
}
|
||||
for _, files := range form.File {
|
||||
for _, fh := range files {
|
||||
in, err := fh.Open()
|
||||
if err != nil {
|
||||
return r, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
||||
}
|
||||
defer in.Close()
|
||||
if err := r.writeFile(fh.Filename, in); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *resource) writeFile(filename string, in io.Reader) error {
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
out, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
||||
}
|
||||
defer out.Close()
|
||||
if err := out.Chmod(0644); err != nil {
|
||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
||||
}
|
||||
if _, err := out.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resource) filePath(filename string) (string, error) {
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
_, err := os.Stat(fpath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("%s: file does not exist", filename)
|
||||
}
|
||||
absPath, err := filepath.Abs(fpath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: getting absolute path: %v", fpath, err)
|
||||
}
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
func (r *resource) filePaths(exts []string) ([]string, error) {
|
||||
var fpaths []string
|
||||
err := filepath.Walk(r.dirPath, func(path string, info os.FileInfo, _ error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fpath, err := r.filePath(info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ext := range exts {
|
||||
if filepath.Ext(fpath) == ext {
|
||||
fpaths = append(fpaths, fpath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fpaths, nil
|
||||
}
|
||||
|
||||
func (r *resource) paperSize() ([2]float64, error) {
|
||||
defaultSize := [2]float64{8.27, 11.7}
|
||||
widthStr := r.values[paperWidth]
|
||||
heightStr := r.values[paperHeight]
|
||||
if widthStr == "" || heightStr == "" {
|
||||
return defaultSize, nil
|
||||
}
|
||||
width, err := strconv.ParseFloat(widthStr, 64)
|
||||
if err != nil {
|
||||
return defaultSize, fmt.Errorf("paper width: %v", err)
|
||||
}
|
||||
height, err := strconv.ParseFloat(heightStr, 64)
|
||||
if err != nil {
|
||||
return defaultSize, fmt.Errorf("paper height: %v", err)
|
||||
}
|
||||
return [2]float64{width, height}, nil
|
||||
}
|
||||
|
||||
func (r *resource) paperMargins() ([4]float64, error) {
|
||||
defaultMargins := [4]float64{1, 1, 1, 1}
|
||||
topStr := r.values[marginTop]
|
||||
bottomStr := r.values[marginBottom]
|
||||
leftStr := r.values[marginLeft]
|
||||
rightStr := r.values[marginRight]
|
||||
if topStr == "" || bottomStr == "" || leftStr == "" || rightStr == "" {
|
||||
return defaultMargins, nil
|
||||
}
|
||||
top, err := strconv.ParseFloat(topStr, 64)
|
||||
if err != nil {
|
||||
return defaultMargins, fmt.Errorf("margin top: %v", err)
|
||||
}
|
||||
bottom, err := strconv.ParseFloat(bottomStr, 64)
|
||||
if err != nil {
|
||||
return defaultMargins, fmt.Errorf("margin bottom: %v", err)
|
||||
}
|
||||
left, err := strconv.ParseFloat(leftStr, 64)
|
||||
if err != nil {
|
||||
return defaultMargins, fmt.Errorf("margin left: %v", err)
|
||||
}
|
||||
right, err := strconv.ParseFloat(rightStr, 64)
|
||||
if err != nil {
|
||||
return defaultMargins, fmt.Errorf("margin right: %v", err)
|
||||
}
|
||||
return [4]float64{top, bottom, left, right}, nil
|
||||
}
|
||||
|
||||
func (r *resource) landscape() (bool, error) {
|
||||
landscapeStr := r.values[landscape]
|
||||
if landscapeStr == "" {
|
||||
return false, nil
|
||||
}
|
||||
landscape, err := strconv.ParseBool(landscapeStr)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("landscape: %v", err)
|
||||
}
|
||||
return landscape, nil
|
||||
}
|
||||
|
||||
func (r *resource) webhookURL() string { return r.values[webhookURL] }
|
||||
func (r *resource) removeAll() error { return os.RemoveAll(r.dirPath) }
|
||||
3
internal/app/xhttp/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package xhttp defines our own implementation
|
||||
// of echo.Echo.
|
||||
package xhttp
|
||||
383
internal/app/xhttp/handler.go
Normal file
@@ -0,0 +1,383 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/chrome"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
)
|
||||
|
||||
func pingEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "ping")
|
||||
}
|
||||
|
||||
func mergeEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "merge")
|
||||
}
|
||||
|
||||
func htmlEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/html")
|
||||
}
|
||||
|
||||
func urlEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/url")
|
||||
}
|
||||
|
||||
func markdownEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/markdown")
|
||||
}
|
||||
|
||||
func officeEndpoint(config conf.Config) string {
|
||||
return fmt.Sprintf("%s%s", config.RootPath(), "convert/office")
|
||||
}
|
||||
|
||||
func isMultipartFormDataEndpoint(config conf.Config, path string) bool {
|
||||
var multipartFormDataEndpoints []string
|
||||
multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint(config))
|
||||
if !config.DisableGoogleChrome() {
|
||||
multipartFormDataEndpoints = append(
|
||||
multipartFormDataEndpoints,
|
||||
htmlEndpoint(config),
|
||||
urlEndpoint(config),
|
||||
markdownEndpoint(config),
|
||||
)
|
||||
}
|
||||
if !config.DisableUnoconv() {
|
||||
multipartFormDataEndpoints = append(
|
||||
multipartFormDataEndpoints,
|
||||
officeEndpoint(config),
|
||||
)
|
||||
}
|
||||
for _, endpoint := range multipartFormDataEndpoints {
|
||||
if endpoint == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pingHandler is the handler for healthcheck.
|
||||
func pingHandler(c echo.Context) error {
|
||||
const op string = "xhttp.pingHandler"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
config := ctx.Config()
|
||||
|
||||
logger.DebugOp(op, "handling ping request...")
|
||||
|
||||
if !config.DisableGoogleChrome() {
|
||||
_, err := chrome.IsViable(logger)
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeHandler is the handler for merging
|
||||
// PDF files.
|
||||
func mergeHandler(c echo.Context) error {
|
||||
const op string = "xhttp.mergeHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling merge request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := mergePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
fpaths, err := r.Fpaths(".pdf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewMergePrinter(logger, fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// htmlHandler is the handler for converting
|
||||
// HTML to PDF.
|
||||
func htmlHandler(c echo.Context) error {
|
||||
const op string = "xhttp.htmlHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling HTML request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewHTMLPrinter(logger, fpath, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// urlHandler is the handler for converting
|
||||
// a URL to PDF.
|
||||
func urlHandler(c echo.Context) error {
|
||||
const op string = "xhttp.urlHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling URL request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.CustomHTTPHeaders = resource.RemoteURLCustomHTTPHeaders(r)
|
||||
if !r.HasArg(resource.RemoteURLArgKey) {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' not found or empty", resource.RemoteURLArgKey),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
remoteURL, err := r.StringArg(resource.RemoteURLArgKey, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewURLPrinter(logger, remoteURL, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// markdownHandler is the handler for converting
|
||||
// Markdown to PDF.
|
||||
func markdownHandler(c echo.Context) error {
|
||||
const op string = "xhttp.markdownHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling Markdown request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := printer.NewMarkdownPrinter(logger, fpath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// officeHandler is the handler for converting
|
||||
// Office documents to PDF.
|
||||
func officeHandler(c echo.Context) error {
|
||||
const op string = "xhttp.officeHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling Office request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := officePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpaths, err := r.Fpaths(
|
||||
".txt",
|
||||
".rtf",
|
||||
".fodt",
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ods",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odp",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewOfficePrinter(logger, fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert(ctx context.Context, p printer.Printer) error {
|
||||
const op string = "xhttp.convert"
|
||||
resolver := func() error {
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
baseFilename := xrand.Get()
|
||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||
fpath := fmt.Sprintf("%s/%s", r.DirPath(), filename)
|
||||
// if no webhook URL given, run conversion
|
||||
// and directly return the resulting PDF file
|
||||
// or an error.
|
||||
if !r.HasArg(resource.WebhookURLArgKey) {
|
||||
logger.DebugOpf(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
|
||||
return convertSync(ctx, p, filename, fpath)
|
||||
}
|
||||
// as a webhook URL has been given, we
|
||||
// run the following lines in a goroutine so that
|
||||
// it doesn't block.
|
||||
logger.DebugOpf(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
|
||||
return convertAsync(ctx, p, filename, fpath)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertSync(ctx context.Context, p printer.Printer, filename, fpath string) error {
|
||||
const op = "xhttp.convertSync"
|
||||
resolver := func() error {
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
|
||||
if err := p.Print(fpath); err != nil {
|
||||
return err
|
||||
}
|
||||
if !r.HasArg(resource.ResultFilenameArgKey) {
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"no '%s' found, using generated filename '%s'",
|
||||
resource.RemoteURLArgKey,
|
||||
filename,
|
||||
)
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"'%s' found, so not using generated filename",
|
||||
resource.ResultFilenameArgKey,
|
||||
)
|
||||
filename, err := r.StringArg(resource.ResultFilenameArgKey, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string) error {
|
||||
const op = "xhttp.convertAsync"
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
webhookURL, err := r.StringArg(resource.WebhookURLArgKey, "")
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
webhookURLTimeout, err := resource.WebhookURLTimeoutArg(r, ctx.Config())
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
go func() {
|
||||
defer r.Close()
|
||||
if err := p.Print(fpath); err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"preparing to send result file '%s' to '%s'...",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
httpClient := &http.Client{
|
||||
Timeout: xtime.Duration(webhookURLTimeout),
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, webhookURL, f)
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
req.Header.Set(echo.HeaderContentType, "application/pdf")
|
||||
// set custom headers (if any).
|
||||
customHTTPHeaders := resource.WebhookURLCustomHTTPHeaders(r)
|
||||
if len(customHTTPHeaders) > 0 {
|
||||
for key, value := range customHTTPHeaders {
|
||||
req.Header.Set(key, value)
|
||||
logger.DebugOpf(op, "set '%s' to custom HTTP header '%s'", value, key)
|
||||
}
|
||||
} else {
|
||||
logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...")
|
||||
}
|
||||
// send the result file.
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"sending result file '%s' to '%s'...",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
resp, err := httpClient.Do(req) /* #nosec */
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"result file '%s' sent to '%s'",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
635
internal/app/xhttp/handler_test.go
Normal file
@@ -0,0 +1,635 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestPingHandler(t *testing.T) {
|
||||
// should return 200.
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := pingEndpoint(config)
|
||||
req := httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
}
|
||||
|
||||
func TestMergeHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := mergeEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
}
|
||||
|
||||
func TestHTMLHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := htmlEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.HTMLMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is is > config.MaximumWaitDelay().
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestURLHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := urlEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.URLMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is is > config.MaximumWaitDelay().
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestMarkdownHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := markdownEndpoint(config)
|
||||
// should return 200.
|
||||
body, contentType := test.MarkdownMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is is > config.MaximumWaitDelay().
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestOfficeHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := officeEndpoint(config)
|
||||
// should return 200.
|
||||
// note: increased the wait timeout as it happens to timeout from time to time.
|
||||
body, contentType := test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "30"})
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 405 as Method is wrong.
|
||||
req = httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req)
|
||||
// should return 415 as Content-Type is wrong.
|
||||
body, _ = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
customHeaderRealKey := http.CanonicalHeaderKey("MyCustomHeader")
|
||||
customHeaderKey := fmt.Sprintf("%s%s", resource.WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderRealKey)
|
||||
customHeaderValue := "foo"
|
||||
status := make(chan error, 2)
|
||||
rcv := echo.New()
|
||||
rcv.POST("/foo", func(c echo.Context) error {
|
||||
if c.Request().Header.Get(echo.HeaderContentType) != "application/pdf" {
|
||||
status <- fmt.Errorf("wrong Content-type: got '%s' want '%s'", c.Request().Header.Get(echo.HeaderContentType), "application/pdf")
|
||||
return nil
|
||||
}
|
||||
if c.Request().Header.Get(customHeaderRealKey) != customHeaderValue {
|
||||
status <- fmt.Errorf("wrong '%s': got '%s' want '%s'", customHeaderRealKey, c.Request().Header.Get(customHeaderRealKey), customHeaderValue)
|
||||
return nil
|
||||
}
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
status <- err
|
||||
return nil
|
||||
}
|
||||
if body == nil || len(body) == 0 {
|
||||
status <- errors.New("empty body")
|
||||
return nil
|
||||
}
|
||||
status <- nil
|
||||
return nil
|
||||
})
|
||||
go func() {
|
||||
rcv.Start(":3001")
|
||||
}()
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
// our custom server should receive the PDF.
|
||||
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"})
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
req.Header.Set(customHeaderKey, customHeaderValue)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
err := <-status
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestResultFilename(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"})
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get(echo.HeaderContentDisposition))
|
||||
}
|
||||
153
internal/app/xhttp/middleware.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
)
|
||||
|
||||
// contextMiddleware extends the default echo.Context with
|
||||
// our custom context.Context.
|
||||
func contextMiddleware(config conf.Config) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// generate a unique identifier for the request.
|
||||
trace := xrand.Get()
|
||||
// create the logger for this request using
|
||||
// the previous identifier as trace.
|
||||
logger := xlog.New(config.LogLevel(), trace)
|
||||
// extend the current echo context with our custom
|
||||
// context.
|
||||
ctx := context.New(c, logger, config)
|
||||
// if it's not a multipart/form-data request,
|
||||
// there is no need to create a Resource.
|
||||
if !isMultipartFormDataEndpoint(config, ctx.Path()) {
|
||||
// validate method for healthcheck endpoint.
|
||||
if ctx.Path() == pingEndpoint(config) && ctx.Request().Method != http.MethodGet {
|
||||
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
// validate method.
|
||||
if ctx.Request().Method != http.MethodPost {
|
||||
err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed))
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
// validate Content-Type.
|
||||
contentType := ctx.Request().Header.Get("Content-Type")
|
||||
if !strings.Contains(contentType, "multipart/form-data") {
|
||||
err := doErr(ctx, echo.NewHTTPError(http.StatusUnsupportedMediaType))
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
// it's a multipart/form-data request, create a
|
||||
// Resource.
|
||||
if err := ctx.WithResource(trace); err != nil {
|
||||
err = doCleanup(ctx, err)
|
||||
err = doErr(ctx, err)
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loggerMiddleware logs the result of a request.
|
||||
func loggerMiddleware(config conf.Config) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
err := next(ctx)
|
||||
// we do not want to log healthcheck requests if
|
||||
// log level is not set to DEBUG.
|
||||
isDebug := ctx.Path() == pingEndpoint(config)
|
||||
return ctx.LogRequestResult(err, isDebug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupMiddleware removes a resource.Resource
|
||||
// at the end of a request.
|
||||
func cleanupMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
return doCleanup(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errorMiddleware handles errors (if any).
|
||||
func errorMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
err := next(ctx)
|
||||
if err == nil {
|
||||
// so far so good!
|
||||
return nil
|
||||
}
|
||||
return doErr(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doCleanup(ctx context.Context, err error) error {
|
||||
const op string = "xhttp.cleanup"
|
||||
if !ctx.HasResource() {
|
||||
// nothing to remove.
|
||||
return err
|
||||
}
|
||||
r := ctx.MustResource()
|
||||
// if a webhook URL has been given,
|
||||
// do not remove the resource.Resource here because
|
||||
// we don't know if the result file has been
|
||||
// generated or sent.
|
||||
if r.HasArg(resource.WebhookURLArgKey) {
|
||||
return err
|
||||
}
|
||||
// a resource.Resource is associated with our custom context.
|
||||
if resourceErr := r.Close(); resourceErr != nil {
|
||||
xerr := xerror.New(op, resourceErr)
|
||||
ctx.XLogger().ErrorOp(xerror.Op(xerr), xerr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func doErr(ctx context.Context, err error) error {
|
||||
// if it's an error from echo
|
||||
// like 404 not found and so on.
|
||||
if echoHTTPErr, ok := err.(*echo.HTTPError); ok {
|
||||
// required to have a correct status code.
|
||||
ctx.Error(echoHTTPErr)
|
||||
return echoHTTPErr
|
||||
}
|
||||
// we log the initial error before returning
|
||||
// the HTTP error.
|
||||
errOp := xerror.Op(err)
|
||||
logger := ctx.XLogger()
|
||||
logger.ErrorOp(errOp, err)
|
||||
// handle our custom HTTP error.
|
||||
var httpErr error
|
||||
errCode := xerror.Code(err)
|
||||
errMessage := xerror.Message(err)
|
||||
switch errCode {
|
||||
case xerror.InvalidCode:
|
||||
httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage)
|
||||
case xerror.TimeoutCode:
|
||||
httpErr = echo.NewHTTPError(http.StatusGatewayTimeout, errMessage)
|
||||
default:
|
||||
httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage)
|
||||
}
|
||||
// required to have a correct status code.
|
||||
ctx.Error(httpErr)
|
||||
return httpErr
|
||||
}
|
||||
114
internal/app/xhttp/option.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
func mergePrinterOptions(r resource.Resource, config conf.Config) (printer.MergePrinterOptions, error) {
|
||||
const op string = "xhttp.mergePrinterOptions"
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.MergePrinterOptions{}, xerror.New(op, err)
|
||||
}
|
||||
return printer.MergePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.ChromePrinterOptions, error) {
|
||||
const op string = "xhttp.chromePrinterOptions"
|
||||
resolver := func() (printer.ChromePrinterOptions, error) {
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
waitDelay, err := resource.WaitDelayArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
headerHTML, footerHTML,
|
||||
err := resource.HeaderFooterContents(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
paperWidth, paperHeight,
|
||||
err := resource.PaperSizeArgs(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
marginTop, marginBottom, marginLeft, marginRight,
|
||||
err := resource.MarginArgs(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
pageRanges, err := r.StringArg(resource.PageRangesArgKey, "")
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
scale, err := resource.ScaleArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
return printer.ChromePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
PageRanges: pageRanges,
|
||||
RpccBufferSize: googleChromeRpccBufferSize,
|
||||
CustomHTTPHeaders: make(map[string]string),
|
||||
Scale: scale,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
if err != nil {
|
||||
return opts, xerror.New(op, err)
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func officePrinterOptions(r resource.Resource, config conf.Config) (printer.OfficePrinterOptions, error) {
|
||||
const op string = "xhttp.officePrinterOptions"
|
||||
resolver := func() (printer.OfficePrinterOptions, error) {
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
pageRanges, err := r.StringArg(resource.PageRangesArgKey, "")
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
return printer.OfficePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
Landscape: landscape,
|
||||
PageRanges: pageRanges,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
if err != nil {
|
||||
return opts, xerror.New(op, err)
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
210
internal/app/xhttp/pkg/context/context.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// Context extends the default echo.Context.
|
||||
type Context struct {
|
||||
echo.Context
|
||||
logger xlog.Logger
|
||||
config conf.Config
|
||||
resource resource.Resource
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// New creates a new Context.
|
||||
func New(c echo.Context, logger xlog.Logger, config conf.Config) Context {
|
||||
return Context{
|
||||
c,
|
||||
logger,
|
||||
config,
|
||||
resource.Resource{},
|
||||
time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
MustCastFromEchoContext cast an echo.Context
|
||||
to our custom Context.
|
||||
|
||||
It panics if casting goes wrong.
|
||||
*/
|
||||
func MustCastFromEchoContext(c echo.Context) Context {
|
||||
const op string = "context.MustCastFromEchoContext"
|
||||
ctx, ok := c.(Context)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("%s: unable to cast an echo.Context to our custom context.Context", op))
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
/*
|
||||
XLogger returns the xlog.Logger associated
|
||||
with the Context.
|
||||
|
||||
This method should be used instead of the
|
||||
default Logger() method coming from
|
||||
the echo.Context.
|
||||
*/
|
||||
func (ctx Context) XLogger() xlog.Logger {
|
||||
return ctx.logger
|
||||
}
|
||||
|
||||
// Config returns the conf.Config associated
|
||||
// with the Context.
|
||||
func (ctx Context) Config() conf.Config {
|
||||
return ctx.config
|
||||
}
|
||||
|
||||
// WithResource creates a resource.Resource and
|
||||
// adds it to the Context.
|
||||
func (ctx *Context) WithResource(directoryName string) error {
|
||||
const op string = "context.Context.WithResource"
|
||||
resolver := func() (resource.Resource, error) {
|
||||
r, err := resource.New(ctx.logger, directoryName)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
// retrieve custom headers from request.
|
||||
for key, value := range ctx.Request().Header {
|
||||
r.WithCustomHTTPHeader(key, value[0])
|
||||
}
|
||||
// retrieve form values from request.
|
||||
for _, key := range resource.ArgKeys() {
|
||||
r.WithArg(key, ctx.FormValue(string(key)))
|
||||
}
|
||||
// write form files from request.
|
||||
form, err := ctx.MultipartForm()
|
||||
if err != nil {
|
||||
/*
|
||||
(very) special case: one and
|
||||
only one file has been sent
|
||||
and it is empty.
|
||||
*/
|
||||
if strings.Contains(err.Error(), io.EOF.Error()) {
|
||||
return r, xerror.Invalid(op, "one file has been sent but it is empty: does it exist?", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
for _, files := range form.File {
|
||||
for _, fh := range files {
|
||||
in, err := fh.Open()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
defer in.Close()
|
||||
// avoid directory traversal.
|
||||
filename := filepath.Base(fh.Filename)
|
||||
if err := r.WithFile(filename, in); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
resource, err := resolver()
|
||||
ctx.resource = resource
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasResource returns true if the Context
|
||||
// has a resource.Resource.
|
||||
func (ctx Context) HasResource() bool {
|
||||
return !reflect.DeepEqual(ctx.resource, resource.Resource{})
|
||||
}
|
||||
|
||||
/*
|
||||
MustResource returns the resource.Resource
|
||||
associated with the Context.
|
||||
|
||||
It panics if no resource.Resource.
|
||||
*/
|
||||
func (ctx Context) MustResource() resource.Resource {
|
||||
const op string = "context.Context.MustResource"
|
||||
if !ctx.HasResource() {
|
||||
panic(fmt.Sprintf("%s: unable to retrieve the resource.Resource from our custom context.Context", op))
|
||||
}
|
||||
return ctx.resource
|
||||
}
|
||||
|
||||
/*
|
||||
LogRequestResult logs the result of a request.
|
||||
This method should only be used by a middleware!
|
||||
|
||||
If an error is given, returns the exact same error.
|
||||
*/
|
||||
func (ctx Context) LogRequestResult(err error, isDebug bool) error {
|
||||
const op string = "context.Context.LogRequestResult"
|
||||
req := ctx.Request()
|
||||
resp := ctx.Response()
|
||||
stopTime := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"remote_ip": ctx.RealIP(),
|
||||
"host": req.Host,
|
||||
"uri": req.RequestURI,
|
||||
"method": req.Method,
|
||||
"path": path(req),
|
||||
"referer": req.Referer(),
|
||||
"user_agent": req.UserAgent(),
|
||||
"status": resp.Status,
|
||||
"latency": lantency(ctx.startTime, stopTime),
|
||||
"latency_human": latencyHuman(ctx.startTime, stopTime),
|
||||
"bytes_in": bytesIn(req),
|
||||
"bytes_out": bytesOut(resp),
|
||||
}
|
||||
if err != nil {
|
||||
ctx.logger.WithFields(fields).ErrorOpf(op, "request failed")
|
||||
return err
|
||||
}
|
||||
if isDebug {
|
||||
ctx.logger.WithFields(fields).DebugOpf(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
ctx.logger.WithFields(fields).InfoOpf(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func path(r *http.Request) string {
|
||||
path := r.URL.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func lantency(startTime time.Time, stopTime time.Time) string {
|
||||
return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10)
|
||||
}
|
||||
|
||||
func latencyHuman(startTime time.Time, stopTime time.Time) string {
|
||||
return stopTime.Sub(startTime).String()
|
||||
}
|
||||
|
||||
func bytesIn(r *http.Request) string {
|
||||
bytesIn := r.Header.Get(echo.HeaderContentLength)
|
||||
if bytesIn == "" {
|
||||
bytesIn = "0"
|
||||
}
|
||||
return bytesIn
|
||||
}
|
||||
|
||||
func bytesOut(r *echo.Response) string {
|
||||
return strconv.FormatInt(r.Size, 10)
|
||||
}
|
||||
78
internal/app/xhttp/pkg/context/context_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestMustCastFromEchoContext(t *testing.T) {
|
||||
// should be OK.
|
||||
ctx := New(
|
||||
test.DummyEchoContext(),
|
||||
test.DebugLogger(),
|
||||
conf.DefaultConfig(),
|
||||
)
|
||||
assert.NotPanics(t, func() {
|
||||
result := MustCastFromEchoContext(ctx)
|
||||
assert.Equal(t, ctx, result)
|
||||
})
|
||||
// should not be OK.
|
||||
assert.Panics(t, func() {
|
||||
MustCastFromEchoContext(test.DummyEchoContext())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogRequestResult(t *testing.T) {
|
||||
ctx := New(
|
||||
test.DummyEchoContext(),
|
||||
test.DebugLogger(),
|
||||
conf.DefaultConfig(),
|
||||
)
|
||||
// Info log.
|
||||
err := ctx.LogRequestResult(nil, false)
|
||||
assert.Nil(t, err)
|
||||
// Debug log.
|
||||
err = ctx.LogRequestResult(nil, true)
|
||||
assert.Nil(t, err)
|
||||
// Error log.
|
||||
err = ctx.LogRequestResult(errors.New("foo"), true)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestGetters(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
ctx := New(
|
||||
test.DummyEchoContext(),
|
||||
logger,
|
||||
config,
|
||||
)
|
||||
// Logger.
|
||||
assert.Equal(t, logger, ctx.XLogger())
|
||||
// Config.
|
||||
assert.Equal(t, config, ctx.Config())
|
||||
// Context should not have a resource.Resource.
|
||||
assert.Equal(t, false, ctx.HasResource())
|
||||
assert.Panics(t, func() {
|
||||
ctx.MustResource()
|
||||
})
|
||||
// Context should have a resource.Resource.
|
||||
ctx = New(
|
||||
test.EchoContextMultipart(t),
|
||||
logger,
|
||||
config,
|
||||
)
|
||||
err := ctx.WithResource(resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, true, ctx.HasResource())
|
||||
assert.NotPanics(t, func() {
|
||||
r := ctx.MustResource()
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
7
internal/app/xhttp/pkg/context/doc.go
Normal file
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
Package context extends the default echo.Context.
|
||||
|
||||
All functions return our standard xerror.Error
|
||||
in case of error.
|
||||
*/
|
||||
package context
|
||||
319
internal/app/xhttp/pkg/resource/arg.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// ArgKey is a type for
|
||||
// arguments' keys.
|
||||
type ArgKey string
|
||||
|
||||
const (
|
||||
// ResultFilenameArgKey is the key
|
||||
// of the argument "resultFilename".
|
||||
ResultFilenameArgKey ArgKey = "resultFilename"
|
||||
// WaitTimeoutArgKey is the key
|
||||
// of the argument "waitTimeout".
|
||||
WaitTimeoutArgKey ArgKey = "waitTimeout"
|
||||
// WebhookURLArgKey is the key
|
||||
// of the argument "webhookURL".
|
||||
WebhookURLArgKey ArgKey = "webhookURL"
|
||||
// WebhookURLTimeoutArgKey is the key
|
||||
// of the argument "webhookURLTimeout".
|
||||
WebhookURLTimeoutArgKey ArgKey = "webhookURLTimeout"
|
||||
// RemoteURLArgKey is the key
|
||||
// of the argument "remoteURL".
|
||||
RemoteURLArgKey ArgKey = "remoteURL"
|
||||
// WaitDelayArgKey is the key
|
||||
// of the argument "waitDelay".
|
||||
WaitDelayArgKey ArgKey = "waitDelay"
|
||||
// PaperWidthArgKey is the key
|
||||
// of the argument "paperWidth".
|
||||
PaperWidthArgKey ArgKey = "paperWidth"
|
||||
// PaperHeightArgKey is the key
|
||||
// of the argument "paperHeight".
|
||||
PaperHeightArgKey ArgKey = "paperHeight"
|
||||
// MarginTopArgKey is the key
|
||||
// of the argument "marginTop".
|
||||
MarginTopArgKey ArgKey = "marginTop"
|
||||
// MarginBottomArgKey is the key
|
||||
// of the argument "marginBottom".
|
||||
MarginBottomArgKey ArgKey = "marginBottom"
|
||||
// MarginLeftArgKey is the key
|
||||
// of the argument "marginLeft".
|
||||
MarginLeftArgKey ArgKey = "marginLeft"
|
||||
// MarginRightArgKey is the key
|
||||
// of the argument "marginRight".
|
||||
MarginRightArgKey ArgKey = "marginRight"
|
||||
// LandscapeArgKey is the key
|
||||
// of the argument "landscape".
|
||||
LandscapeArgKey ArgKey = "landscape"
|
||||
// PageRangesArgKey is the key
|
||||
// of the argument "pageRanges".
|
||||
PageRangesArgKey ArgKey = "pageRanges"
|
||||
// GoogleChromeRpccBufferSizeArgKey is the key
|
||||
// of the argument "googleChromeRpccBufferSize".
|
||||
GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize"
|
||||
// ScaleArgKey is the key
|
||||
// of the argument "scale".
|
||||
ScaleArgKey ArgKey = "scale"
|
||||
)
|
||||
|
||||
/*
|
||||
ArgKeys returns a slice
|
||||
containing all available
|
||||
arguments' keys.
|
||||
*/
|
||||
func ArgKeys() []ArgKey {
|
||||
return []ArgKey{
|
||||
ResultFilenameArgKey,
|
||||
WaitTimeoutArgKey,
|
||||
WebhookURLArgKey,
|
||||
WebhookURLTimeoutArgKey,
|
||||
RemoteURLArgKey,
|
||||
WaitDelayArgKey,
|
||||
PaperWidthArgKey,
|
||||
PaperHeightArgKey,
|
||||
MarginTopArgKey,
|
||||
MarginBottomArgKey,
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
PageRangesArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
ScaleArgKey,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
WaitTimeoutArg is a helper for retrieving
|
||||
the "waitTimeout" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WaitTimeoutArg(r Resource, config conf.Config) (float64, error) {
|
||||
const op string = "resource.WaitTimeoutArg"
|
||||
result, err := r.Float64Arg(
|
||||
WaitTimeoutArgKey,
|
||||
config.DefaultWaitTimeout(),
|
||||
xassert.Float64NotInferiorTo(0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWaitTimeout()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
WaitDelayArg is a helper for retrieving
|
||||
the "waitDelay" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WaitDelayArg(r Resource, config conf.Config) (float64, error) {
|
||||
const (
|
||||
op string = "resource.WaitDelayArg"
|
||||
defaultWaitDelay float64 = 0.0
|
||||
)
|
||||
result, err := r.Float64Arg(
|
||||
WaitDelayArgKey,
|
||||
defaultWaitDelay,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWaitDelay()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
WebhookURLTimeoutArg is a helper for retrieving
|
||||
the "webhookURLTimeout" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WebhookURLTimeoutArg(r Resource, config conf.Config) (float64, error) {
|
||||
const op string = "resource.WebhookURLTimeoutArg"
|
||||
result, err := r.Float64Arg(
|
||||
WebhookURLTimeoutArgKey,
|
||||
config.DefaultWebhookURLTimeout(),
|
||||
xassert.Float64NotInferiorTo(0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWebhookURLTimeout()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
PaperSizeArgs is a helper for retrieving
|
||||
the "paperWidth" and "paperHeight" arguments
|
||||
as float64.
|
||||
*/
|
||||
func PaperSizeArgs(r Resource, config conf.Config) (float64, float64, error) {
|
||||
const op string = "resource.PaperSizeArgs"
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
resolver := func() (float64, float64, error) {
|
||||
paperWidth, err := r.Float64Arg(
|
||||
PaperWidthArgKey,
|
||||
opts.PaperWidth,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.PaperWidth,
|
||||
opts.PaperHeight,
|
||||
err
|
||||
}
|
||||
paperHeight, err := r.Float64Arg(
|
||||
PaperHeightArgKey,
|
||||
opts.PaperHeight,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.PaperWidth,
|
||||
opts.PaperHeight,
|
||||
err
|
||||
}
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
nil
|
||||
}
|
||||
paperWidth, paperHeight,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
nil
|
||||
}
|
||||
|
||||
/*
|
||||
MarginArgs is a helper for retrieving
|
||||
the "marginTop", "marginBottom", "marginLeft"
|
||||
and "marginRight" arguments as float64.
|
||||
*/
|
||||
func MarginArgs(r Resource, config conf.Config) (float64, float64, float64, float64, error) {
|
||||
const op string = "resource.MarginArgs"
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
resolver := func() (float64, float64, float64, float64, error) {
|
||||
marginTop, err := r.Float64Arg(
|
||||
MarginTopArgKey,
|
||||
opts.MarginTop,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
marginBottom, err := r.Float64Arg(
|
||||
MarginBottomArgKey,
|
||||
opts.MarginBottom,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
marginLeft, err := r.Float64Arg(
|
||||
MarginLeftArgKey,
|
||||
opts.MarginLeft,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
marginRight, err := r.Float64Arg(
|
||||
MarginRightArgKey,
|
||||
opts.MarginRight,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
nil
|
||||
}
|
||||
marginTop, marginBottom, marginLeft, marginRight,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
nil
|
||||
}
|
||||
|
||||
/*
|
||||
GoogleChromeRpccBufferSizeArg is a helper for retrieving
|
||||
the "googleChromeRpccBufferSize" argument as int64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func GoogleChromeRpccBufferSizeArg(r Resource, config conf.Config) (int64, error) {
|
||||
const op string = "resource.GoogleChromeRpccBufferSizeArg"
|
||||
result, err := r.Int64Arg(
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
config.DefaultGoogleChromeRpccBufferSize(),
|
||||
xassert.Int64NotInferiorTo(0.0),
|
||||
xassert.Int64NotSuperiorTo(config.MaximumGoogleChromeRpccBufferSize()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
ScaleArg is a helper for retrieving
|
||||
the "scale" argument as float64.
|
||||
*/
|
||||
func ScaleArg(r Resource, config conf.Config) (float64, error) {
|
||||
const op string = "resource.ScaleArg"
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
result, err := r.Float64Arg(
|
||||
ScaleArgKey,
|
||||
opts.Scale,
|
||||
xassert.Float64NotInferiorTo(0.1),
|
||||
xassert.Float64NotSuperiorTo(2.0),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
396
internal/app/xhttp/pkg/resource/arg_test.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestArgKeys(t *testing.T) {
|
||||
expected := []ArgKey{
|
||||
ResultFilenameArgKey,
|
||||
WaitTimeoutArgKey,
|
||||
WebhookURLArgKey,
|
||||
WebhookURLTimeoutArgKey,
|
||||
RemoteURLArgKey,
|
||||
WaitDelayArgKey,
|
||||
PaperWidthArgKey,
|
||||
PaperHeightArgKey,
|
||||
MarginTopArgKey,
|
||||
MarginBottomArgKey,
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
PageRangesArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
ScaleArgKey,
|
||||
}
|
||||
assert.Equal(t, expected, ArgKeys())
|
||||
}
|
||||
|
||||
func TestWaitTimeoutArg(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = config.DefaultWaitTimeout()
|
||||
v, err := WaitTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 5.0
|
||||
r.WithArg(WaitTimeoutArgKey, "5.0")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = config.DefaultWaitTimeout()
|
||||
r.WithArg(WaitTimeoutArgKey, "-1.0")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumWaitTimeout().
|
||||
expected = config.DefaultWaitTimeout()
|
||||
r.WithArg(WaitTimeoutArgKey, "31.0")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = config.DefaultWaitTimeout()
|
||||
r.WithArg(WaitTimeoutArgKey, "foo")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestWaitDelayArg(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue float64 = 0.0
|
||||
)
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = defaultValue
|
||||
v, err := WaitDelayArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 5.0
|
||||
r.WithArg(WaitDelayArgKey, "5.0")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = defaultValue
|
||||
r.WithArg(WaitDelayArgKey, "-1.0")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumWaitDelay().
|
||||
expected = defaultValue
|
||||
r.WithArg(WaitDelayArgKey, "31.0")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = defaultValue
|
||||
r.WithArg(WaitDelayArgKey, "foo")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestWebhookURLTimeoutArg(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
v, err := WebhookURLTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 5.0
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "5.0")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "-1.0")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumWebhookURLTimeout().
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "31.0")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "foo")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestPaperSizeArgs(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// arguments do not exist.
|
||||
width, height, err := PaperSizeArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, opts.PaperWidth, width)
|
||||
assert.Equal(t, opts.PaperHeight, height)
|
||||
// arguments exist.
|
||||
expected = 5.0
|
||||
r.WithArg(PaperWidthArgKey, "5.0")
|
||||
r.WithArg(PaperHeightArgKey, "5.0")
|
||||
width, height, err = PaperSizeArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, width)
|
||||
assert.Equal(t, expected, height)
|
||||
// should not be OK as arguments
|
||||
// value are < 0.
|
||||
expected = opts.PaperWidth
|
||||
r.WithArg(PaperWidthArgKey, "-1.0")
|
||||
width, _, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, width)
|
||||
r.WithArg(PaperWidthArgKey, "5.0")
|
||||
expected = opts.PaperHeight
|
||||
r.WithArg(PaperHeightArgKey, "-1.0")
|
||||
_, height, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, height)
|
||||
r.WithArg(PaperHeightArgKey, "5.0")
|
||||
// should not be OK as
|
||||
// arguments value are invalids.
|
||||
expected = opts.PaperWidth
|
||||
r.WithArg(PaperWidthArgKey, "foo")
|
||||
width, _, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, width)
|
||||
r.WithArg(PaperWidthArgKey, "5.0")
|
||||
expected = opts.PaperHeight
|
||||
r.WithArg(PaperHeightArgKey, "foo")
|
||||
_, height, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, height)
|
||||
r.WithArg(PaperHeightArgKey, "5.0")
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestMarginArgs(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// arguments do not exist.
|
||||
top, bottom, left, right, err := MarginArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, opts.MarginTop, top)
|
||||
assert.Equal(t, opts.MarginBottom, bottom)
|
||||
assert.Equal(t, opts.MarginLeft, left)
|
||||
assert.Equal(t, opts.MarginRight, right)
|
||||
// arguments exist.
|
||||
expected = 5.0
|
||||
r.WithArg(MarginTopArgKey, "5.0")
|
||||
r.WithArg(MarginBottomArgKey, "5.0")
|
||||
r.WithArg(MarginLeftArgKey, "5.0")
|
||||
r.WithArg(MarginRightArgKey, "5.0")
|
||||
top, bottom, left, right, err = MarginArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, top)
|
||||
assert.Equal(t, expected, bottom)
|
||||
assert.Equal(t, expected, left)
|
||||
assert.Equal(t, expected, right)
|
||||
// should not be OK as arguments
|
||||
// value are < 0.
|
||||
expected = opts.MarginTop
|
||||
r.WithArg(MarginTopArgKey, "-1.0")
|
||||
top, _, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, top)
|
||||
r.WithArg(MarginTopArgKey, "5.0")
|
||||
expected = opts.MarginBottom
|
||||
r.WithArg(MarginBottomArgKey, "-1.0")
|
||||
_, bottom, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, bottom)
|
||||
r.WithArg(MarginBottomArgKey, "5.0")
|
||||
expected = opts.MarginLeft
|
||||
r.WithArg(MarginLeftArgKey, "-1.0")
|
||||
_, _, left, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, left)
|
||||
r.WithArg(MarginLeftArgKey, "5.0")
|
||||
expected = opts.MarginRight
|
||||
r.WithArg(MarginRightArgKey, "-1.0")
|
||||
_, _, _, right, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, right)
|
||||
r.WithArg(MarginRightArgKey, "5.0")
|
||||
// should not be OK as
|
||||
// arguments value are invalids.
|
||||
expected = opts.MarginTop
|
||||
r.WithArg(MarginTopArgKey, "foo")
|
||||
top, _, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, top)
|
||||
r.WithArg(MarginTopArgKey, "5.0")
|
||||
expected = opts.MarginBottom
|
||||
r.WithArg(MarginBottomArgKey, "foo")
|
||||
_, bottom, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, bottom)
|
||||
r.WithArg(MarginBottomArgKey, "5.0")
|
||||
expected = opts.MarginLeft
|
||||
r.WithArg(MarginLeftArgKey, "foo")
|
||||
_, _, left, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, left)
|
||||
r.WithArg(MarginLeftArgKey, "5.0")
|
||||
expected = opts.MarginRight
|
||||
r.WithArg(MarginRightArgKey, "foo")
|
||||
_, _, _, right, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, right)
|
||||
r.WithArg(MarginRightArgKey, "5.0")
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleChromeRpccBufferSizeArg(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected int64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
v, err := GoogleChromeRpccBufferSizeArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 10
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "10")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "-1")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "104857601")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "foo")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestScaleArg(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue float64 = 1.0
|
||||
)
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = defaultValue
|
||||
v, err := ScaleArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 0.5
|
||||
r.WithArg(ScaleArgKey, "0.5")
|
||||
v, err = ScaleArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.1.
|
||||
expected = defaultValue
|
||||
r.WithArg(ScaleArgKey, "-1.0")
|
||||
v, err = ScaleArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > 2.0.
|
||||
expected = defaultValue
|
||||
r.WithArg(ScaleArgKey, "2.1")
|
||||
v, err = ScaleArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = defaultValue
|
||||
r.WithArg(ScaleArgKey, "foo")
|
||||
v, err = ScaleArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
8
internal/app/xhttp/pkg/resource/doc.go
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package resource helps managing
|
||||
arguments and files for a conversion.
|
||||
|
||||
All functions return our standard xerror.Error
|
||||
in case of error.
|
||||
*/
|
||||
package resource
|
||||
91
internal/app/xhttp/pkg/resource/file.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// file represents a file within the resource.
|
||||
type file struct {
|
||||
fpath string
|
||||
}
|
||||
|
||||
// write writes given content to the
|
||||
// resourceFile location.
|
||||
func (f file) write(in io.Reader) error {
|
||||
const op string = "resource.file.write"
|
||||
resolver := func() error {
|
||||
out, err := os.Create(f.fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
if err := out.Chmod(0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := out.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// content returns the string content of
|
||||
// the file.
|
||||
func (f file) content() (string, error) {
|
||||
const op string = "resource.file.content"
|
||||
b, err := ioutil.ReadFile(f.fpath)
|
||||
if err != nil {
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
/*
|
||||
HeaderFooterContents is a helper for retrieving
|
||||
the content of the files "header.html"
|
||||
and "footer.html".
|
||||
*/
|
||||
func HeaderFooterContents(r Resource, config conf.Config) (string, string, error) {
|
||||
const op string = "resource.HeaderFooterContents"
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
resolver := func() (string, string, error) {
|
||||
headerHTML, err := r.Fcontent("header.html", opts.HeaderHTML)
|
||||
if err != nil {
|
||||
return opts.HeaderHTML,
|
||||
opts.FooterHTML,
|
||||
err
|
||||
}
|
||||
footerHTML, err := r.Fcontent("footer.html", opts.FooterHTML)
|
||||
if err != nil {
|
||||
return opts.HeaderHTML,
|
||||
opts.FooterHTML,
|
||||
err
|
||||
}
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
nil
|
||||
}
|
||||
headerHTML, footerHTML,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
nil
|
||||
}
|
||||
46
internal/app/xhttp/pkg/resource/file_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestHeaderFooterContents(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected string
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// arguments do not exist.
|
||||
header, footer, err := HeaderFooterContents(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, opts.HeaderHTML, header)
|
||||
assert.Equal(t, opts.FooterHTML, footer)
|
||||
// arguments exist.
|
||||
expected = "Gutenberg"
|
||||
fpath := test.OfficeFpaths(t)[2]
|
||||
f1, err := os.Open(fpath)
|
||||
assert.Nil(t, err)
|
||||
defer f1.Close() // nolint: errcheck
|
||||
err = r.WithFile("header.html", f1)
|
||||
assert.Nil(t, err)
|
||||
f2, err := os.Open(fpath)
|
||||
assert.Nil(t, err)
|
||||
defer f2.Close() // nolint: errcheck
|
||||
err = r.WithFile("footer.html", f2)
|
||||
assert.Nil(t, err)
|
||||
header, footer, err = HeaderFooterContents(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, header, expected)
|
||||
assert.Contains(t, footer, expected)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
37
internal/app/xhttp/pkg/resource/header.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// RemoteURLCustomHTTPHeaderCanonicalBaseKey is the base key
|
||||
// of custom headers send to the remote URL.
|
||||
RemoteURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Remoteurl-"
|
||||
// WebhookURLCustomHTTPHeaderCanonicalBaseKey is the base key
|
||||
// of custom headers send to the webhook URL.
|
||||
WebhookURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Webhookurl-"
|
||||
)
|
||||
|
||||
func fetchCustomHTTPHeaders(r Resource, baseKey string) map[string]string {
|
||||
customHeaders := make(map[string]string)
|
||||
for key, value := range r.customHeaders {
|
||||
if strings.Contains(key, baseKey) {
|
||||
realKey := strings.Replace(key, baseKey, "", 1)
|
||||
customHeaders[realKey] = value
|
||||
}
|
||||
}
|
||||
return customHeaders
|
||||
}
|
||||
|
||||
// RemoteURLCustomHTTPHeaders is a helper for retrieving
|
||||
// the custom headers for the URL conversion.
|
||||
func RemoteURLCustomHTTPHeaders(r Resource) map[string]string {
|
||||
return fetchCustomHTTPHeaders(r, RemoteURLCustomHTTPHeaderCanonicalBaseKey)
|
||||
}
|
||||
|
||||
// WebhookURLCustomHTTPHeaders is a helper for retrieving
|
||||
// the custom headers for the webhook URL.
|
||||
func WebhookURLCustomHTTPHeaders(r Resource) map[string]string {
|
||||
return fetchCustomHTTPHeaders(r, WebhookURLCustomHTTPHeaderCanonicalBaseKey)
|
||||
}
|
||||
54
internal/app/xhttp/pkg/resource/header_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestRemoteURLCustomHeaders(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// should find the custom header.
|
||||
customHeaderValue := "bar"
|
||||
customHeaderCanonicalRealKey := "Foo"
|
||||
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", RemoteURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
|
||||
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
|
||||
r.WithCustomHTTPHeader("Bar", "Bar")
|
||||
expected := map[string]string{
|
||||
customHeaderCanonicalRealKey: customHeaderValue,
|
||||
}
|
||||
notExpected := map[string]string{
|
||||
customHeaderCanonicalKey: customHeaderValue,
|
||||
}
|
||||
v := RemoteURLCustomHTTPHeaders(r)
|
||||
assert.Equal(t, expected, v)
|
||||
assert.NotEqual(t, notExpected, v)
|
||||
}
|
||||
|
||||
func TestWebhookURLCustomHeaders(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// should find the custom header.
|
||||
customHeaderValue := "bar"
|
||||
customHeaderCanonicalRealKey := "Foo"
|
||||
customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey))
|
||||
r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue)
|
||||
r.WithCustomHTTPHeader("Bar", "Bar")
|
||||
expected := map[string]string{
|
||||
customHeaderCanonicalRealKey: customHeaderValue,
|
||||
}
|
||||
notExpected := map[string]string{
|
||||
customHeaderCanonicalKey: customHeaderValue,
|
||||
}
|
||||
v := WebhookURLCustomHTTPHeaders(r)
|
||||
assert.Equal(t, expected, v)
|
||||
assert.NotEqual(t, notExpected, v)
|
||||
}
|
||||
258
internal/app/xhttp/pkg/resource/resource.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
/*
|
||||
TemporaryDirectory is the directory
|
||||
where all the resources directory
|
||||
are located.
|
||||
*/
|
||||
const TemporaryDirectory string = "tmp"
|
||||
|
||||
// Resource helps managing
|
||||
// arguments and files for a conversion.
|
||||
type Resource struct {
|
||||
logger xlog.Logger
|
||||
dirPath string
|
||||
customHeaders map[string]string
|
||||
args map[ArgKey]string
|
||||
files map[string]file
|
||||
}
|
||||
|
||||
// New creates a Resource where its files will
|
||||
// be located in the given directory name.
|
||||
func New(logger xlog.Logger, directoryName string) (Resource, error) {
|
||||
const op string = "resource.New"
|
||||
resolver := func() (string, error) {
|
||||
dirPath := fmt.Sprintf("%s/%s", TemporaryDirectory, directoryName)
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
absDirPath, err := filepath.Abs(dirPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return absDirPath, nil
|
||||
}
|
||||
dirPath, err := resolver()
|
||||
if err != nil {
|
||||
return Resource{}, xerror.New(op, err)
|
||||
}
|
||||
logger.DebugOpf(op, "resource directory '%s' created", directoryName)
|
||||
return Resource{
|
||||
logger: logger,
|
||||
dirPath: dirPath,
|
||||
customHeaders: make(map[string]string),
|
||||
args: make(map[ArgKey]string),
|
||||
files: make(map[string]file),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close removes the working directory of the
|
||||
// Resource if it exists.
|
||||
func (r Resource) Close() error {
|
||||
const op string = "resource.Resource.Close"
|
||||
if _, err := os.Stat(r.dirPath); os.IsNotExist(err) {
|
||||
r.logger.DebugOpf(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
|
||||
return nil
|
||||
}
|
||||
if err := os.RemoveAll(r.dirPath); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
r.logger.DebugOpf(op, "resource directory '%s' removed", r.dirPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithCustomHTTPHeader add a new custom header to the Resource.
|
||||
// Given key should be in canonical format.
|
||||
func (r *Resource) WithCustomHTTPHeader(key string, value string) {
|
||||
const op string = "resource.Resource.WithCustomHTTPHeader"
|
||||
// should already be in canonical format.
|
||||
canonicalKey := http.CanonicalHeaderKey(key)
|
||||
if strings.Contains(canonicalKey, RemoteURLCustomHTTPHeaderCanonicalBaseKey) ||
|
||||
strings.Contains(canonicalKey, WebhookURLCustomHTTPHeaderCanonicalBaseKey) {
|
||||
r.customHeaders[canonicalKey] = value
|
||||
r.logger.DebugOpf(op, "added '%s' with value '%s' to resource custom HTTP headers", canonicalKey, value)
|
||||
return
|
||||
}
|
||||
r.logger.DebugOpf(op, "skipping '%s' as it is not a custom HTTP header...", canonicalKey)
|
||||
}
|
||||
|
||||
// WithArg add a new argument to the Resource.
|
||||
func (r *Resource) WithArg(key ArgKey, value string) {
|
||||
const op string = "resource.Resource.WithArg"
|
||||
r.args[key] = value
|
||||
r.logger.DebugOpf(op, "added '%s' with value '%s' to resource args", key, value)
|
||||
}
|
||||
|
||||
// WithFile add a new file to the Resource.
|
||||
func (r *Resource) WithFile(filename string, in io.Reader) error {
|
||||
const op string = "resource.Resource.WithFile"
|
||||
resolver := func() error {
|
||||
// see https://github.com/thecodingmachine/gotenberg/issues/104.
|
||||
normalized, err := normalize.String(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, normalized)
|
||||
file := file{fpath: fpath}
|
||||
if err := file.write(in); err != nil {
|
||||
return err
|
||||
}
|
||||
r.files[filename] = file
|
||||
r.logger.DebugOpf(op, "resource file '%s' created", filename)
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DirPath returns the directory path
|
||||
// of the Resource.
|
||||
func (r Resource) DirPath() string {
|
||||
return r.dirPath
|
||||
}
|
||||
|
||||
// HasArg returns true if given key exists
|
||||
// among the Resource and its value is not empty.
|
||||
func (r Resource) HasArg(key ArgKey) bool {
|
||||
if v, ok := r.args[key]; ok {
|
||||
return v != ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
StringArg returns the value of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.String.
|
||||
*/
|
||||
func (r Resource) StringArg(key ArgKey, defaultValue string, rules ...xassert.RuleString) (string, error) {
|
||||
const op string = "resource.Resource.StringArg"
|
||||
result, err := xassert.String(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Int64Arg returns the int64 representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Int64.
|
||||
*/
|
||||
func (r Resource) Int64Arg(key ArgKey, defaultValue int64, rules ...xassert.RuleInt64) (int64, error) {
|
||||
const op string = "resource.Resource.Int64Arg"
|
||||
result, err := xassert.Int64(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Float64Arg returns the float64 representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Float64.
|
||||
*/
|
||||
func (r Resource) Float64Arg(key ArgKey, defaultValue float64, rules ...xassert.RuleFloat64) (float64, error) {
|
||||
const op string = "resource.Resource.Float64Arg"
|
||||
result, err := xassert.Float64(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
BoolArg returns the boolean representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Bool.
|
||||
*/
|
||||
func (r Resource) BoolArg(key ArgKey, defaultValue bool) (bool, error) {
|
||||
const op string = "resource.Resource.BoolArg"
|
||||
result, err := xassert.Bool(string(key), r.args[key], defaultValue)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Fpath returns the path of the given filename.
|
||||
// This filename should exist whithin the Resource.
|
||||
func (r Resource) Fpath(filename string) (string, error) {
|
||||
const op string = "resource.Resource.Fpath"
|
||||
file, ok := r.files[filename]
|
||||
if !ok {
|
||||
return "", xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("resource file '%s' does not exist", filename),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return file.fpath, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Fpaths returns the paths of the files
|
||||
having one of the given file extensions.
|
||||
|
||||
It should found at least one path.
|
||||
*/
|
||||
func (r Resource) Fpaths(exts ...string) ([]string, error) {
|
||||
const op string = "resource.Resource.Fpaths"
|
||||
var fpaths []string
|
||||
for filename, file := range r.files {
|
||||
for _, ext := range exts {
|
||||
if filepath.Ext(filename) == ext {
|
||||
fpaths = append(fpaths, file.fpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return nil, xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("no resource file found for extensions '%v'", exts),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return fpaths, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Fcontent returns the string content of the
|
||||
given filename.
|
||||
|
||||
If filename does not exist within the Resource,
|
||||
returns the default value.
|
||||
*/
|
||||
func (r Resource) Fcontent(filename, defaultValue string) (string, error) {
|
||||
const op string = "resource.Resource.Fcontent"
|
||||
file, ok := r.files[filename]
|
||||
if !ok {
|
||||
return defaultValue, nil
|
||||
}
|
||||
content, err := file.content()
|
||||
if err != nil {
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
287
internal/app/xhttp/pkg/resource/resource_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestStringArg(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue string = "FOO"
|
||||
)
|
||||
var expected string
|
||||
rule := xassert.StringOneOf([]string{"FOO", "BAR"})
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
r.WithArg(ResultFilenameArgKey, "")
|
||||
v, err := r.StringArg(ResultFilenameArgKey, defaultValue)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, defaultValue, v)
|
||||
// result should be equal to given value
|
||||
// as it is one of "FOO" and "BAR".
|
||||
expected = "BAR"
|
||||
r.WithArg(ResultFilenameArgKey, expected)
|
||||
v, err = r.StringArg(ResultFilenameArgKey, defaultValue, rule)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as given value is not
|
||||
// one of "FOO" and "BAR".
|
||||
expected = defaultValue
|
||||
r.WithArg(ResultFilenameArgKey, "BAZ")
|
||||
v, err = r.StringArg(ResultFilenameArgKey, defaultValue, rule)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestInt64Arg(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue int64 = 10
|
||||
)
|
||||
var expected int64
|
||||
rule := xassert.Int64NotInferiorTo(6)
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
r.WithArg(WaitTimeoutArgKey, "")
|
||||
v, err := r.Int64Arg(WaitTimeoutArgKey, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to given value
|
||||
// but as integer.
|
||||
r.WithArg(WaitTimeoutArgKey, "5")
|
||||
v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue)
|
||||
expected = 5
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as given value is not
|
||||
// a string representation of an integer.
|
||||
r.WithArg(WaitTimeoutArgKey, "foo")
|
||||
v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
test.AssertError(t, err)
|
||||
// should not be OK as given value does not
|
||||
// validate the rule x >= 6.
|
||||
r.WithArg(WaitTimeoutArgKey, "5")
|
||||
v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
test.AssertError(t, err)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestFloat64Arg(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue float64 = 10.0
|
||||
)
|
||||
var expected float64
|
||||
rule := xassert.Float64NotInferiorTo(6.0)
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
r.WithArg(WaitTimeoutArgKey, "")
|
||||
v, err := r.Float64Arg(WaitTimeoutArgKey, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to given value
|
||||
// but as float.
|
||||
r.WithArg(WaitTimeoutArgKey, "5.5")
|
||||
v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue)
|
||||
expected = 5.5
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as given value is not
|
||||
// a string representation of a float.
|
||||
r.WithArg(WaitTimeoutArgKey, "foo")
|
||||
v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
test.AssertError(t, err)
|
||||
// should not be OK as given value does not
|
||||
// validate the rule x >= 6.
|
||||
r.WithArg(WaitTimeoutArgKey, "5.0")
|
||||
v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
test.AssertError(t, err)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestBoolArg(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue bool = true
|
||||
)
|
||||
var expected bool
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
r.WithArg(LandscapeArgKey, "")
|
||||
v, err := r.BoolArg(LandscapeArgKey, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to given value
|
||||
// but as boolean.
|
||||
r.WithArg(LandscapeArgKey, "1")
|
||||
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
|
||||
expected = true
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
r.WithArg(LandscapeArgKey, "true")
|
||||
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
|
||||
expected = true
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
r.WithArg(LandscapeArgKey, "0")
|
||||
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
|
||||
expected = false
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
r.WithArg(LandscapeArgKey, "false")
|
||||
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
|
||||
expected = false
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as given value is not
|
||||
// a string representation of a boolean.
|
||||
r.WithArg(LandscapeArgKey, "foo")
|
||||
v, err = r.BoolArg(LandscapeArgKey, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
test.AssertError(t, err)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestFpath(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// file exists.
|
||||
fpath := test.MergeFpaths(t)[0]
|
||||
f, err := os.Open(fpath)
|
||||
assert.Nil(t, err)
|
||||
filename := "foo.pdf"
|
||||
err = r.WithFile(filename, f)
|
||||
assert.Nil(t, err)
|
||||
absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName))
|
||||
assert.Nil(t, err)
|
||||
expected := fmt.Sprintf("%s/%s", absDirPath, filename)
|
||||
v, err := r.Fpath(filename)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as file does
|
||||
// not exist.
|
||||
_, err = r.Fpath("bar.pdf")
|
||||
test.AssertError(t, err)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestFpaths(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
fpath := test.MergeFpaths(t)[0]
|
||||
f, err := os.Open(fpath)
|
||||
assert.Nil(t, err)
|
||||
defer f.Close() // nolint: errcheck
|
||||
filename := "foo.pdf"
|
||||
err = r.WithFile(filename, f)
|
||||
assert.Nil(t, err)
|
||||
// file extension exists.
|
||||
absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName))
|
||||
assert.Nil(t, err)
|
||||
expected := []string{
|
||||
fmt.Sprintf("%s/%s", absDirPath, filename),
|
||||
}
|
||||
fpaths, err := r.Fpaths(".pdf")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, fpaths)
|
||||
// should not be OK as file extension
|
||||
// does not exist.
|
||||
_, err = r.Fpaths(".html")
|
||||
test.AssertError(t, err)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestFcontent(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue string = "Gutenberg"
|
||||
)
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
filename := "foo.txt"
|
||||
// file does not exist, expecting
|
||||
// value.
|
||||
v, err := r.Fcontent(filename, defaultValue)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, defaultValue, v)
|
||||
// file exists.
|
||||
fpath := test.OfficeFpaths(t)[2]
|
||||
f, err := os.Open(fpath)
|
||||
assert.Nil(t, err)
|
||||
defer f.Close() // nolint: errcheck
|
||||
err = r.WithFile(filename, f)
|
||||
assert.Nil(t, err)
|
||||
v, err = r.Fcontent(filename, defaultValue)
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, v, defaultValue)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGetters(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
logger := test.DebugLogger()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// has arg.
|
||||
assert.Equal(t, false, r.HasArg(LandscapeArgKey))
|
||||
r.WithArg(LandscapeArgKey, "foo")
|
||||
assert.Equal(t, true, r.HasArg(LandscapeArgKey))
|
||||
// directory path.
|
||||
absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName))
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, absDirPath, r.DirPath())
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
31
internal/app/xhttp/xhttp.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
)
|
||||
|
||||
// New returns a custom echo.Echo.
|
||||
func New(config conf.Config) *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
srv.Use(contextMiddleware(config))
|
||||
srv.Use(loggerMiddleware(config))
|
||||
srv.Use(cleanupMiddleware())
|
||||
srv.Use(errorMiddleware())
|
||||
srv.GET(pingEndpoint(config), pingHandler)
|
||||
srv.POST(mergeEndpoint(config), mergeHandler)
|
||||
if config.DisableGoogleChrome() && config.DisableUnoconv() {
|
||||
return srv
|
||||
}
|
||||
if !config.DisableGoogleChrome() {
|
||||
srv.POST(htmlEndpoint(config), htmlHandler)
|
||||
srv.POST(urlEndpoint(config), urlHandler)
|
||||
srv.POST(markdownEndpoint(config), markdownHandler)
|
||||
}
|
||||
if !config.DisableUnoconv() {
|
||||
srv.POST(officeEndpoint(config), officeHandler)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
171
internal/app/xhttp/xhttp_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestNonExistingEndpoint(t *testing.T) {
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// "/" endpoint should return 404.
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
}
|
||||
|
||||
func TestDisableChromeEndpoints(t *testing.T) {
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 404.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL endpoint should return 404.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office endpoint should return 200.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
|
||||
}
|
||||
|
||||
func TestDisableUnoconvEndpoints(t *testing.T) {
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "1")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 200.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// URL endpoint should return 200.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Markdown endpoint should return 200.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Office endpoint should return 404.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "0")
|
||||
}
|
||||
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "1")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 404.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL endpoint should return 404.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office endpoint should return 404.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "0")
|
||||
}
|
||||
|
||||
func TestCustomRootPath(t *testing.T) {
|
||||
os.Setenv(conf.RootPathEnvVar, "/foo/")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 200.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// URL endpoint should return 200.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Markdown endpoint should return 200.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Office endpoint should return 200.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.RootPathEnvVar, "/")
|
||||
}
|
||||
186
internal/pkg/chrome/chrome.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package chrome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mafredri/cdp/devtool"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
)
|
||||
|
||||
// Start starts Google Chrome headless in background.
|
||||
func Start(logger xlog.Logger, ignoreCertificateErrors bool) error {
|
||||
const op string = "chrome.Start"
|
||||
logger.DebugOp(op, "starting new Google Chrome headless process on port 9222...")
|
||||
resolver := func() error {
|
||||
cmd, err := cmd(logger, ignoreCertificateErrors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we try to start the process.
|
||||
xexec.LogBeforeExecute(logger, cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
// if the process failed to start correctly,
|
||||
// we have to restart it.
|
||||
isViable, _ := IsViable(logger)
|
||||
if !isViable {
|
||||
return restart(logger, cmd.Process, ignoreCertificateErrors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmd(logger xlog.Logger, ignoreCertificateErrors bool) (*exec.Cmd, error) {
|
||||
const op string = "chrome.cmd"
|
||||
binary := "google-chrome-stable"
|
||||
args := []string{
|
||||
"--no-sandbox",
|
||||
"--headless",
|
||||
// see https://github.com/thecodingmachine/gotenberg/issues/157.
|
||||
"--disable-dev-shm-usage",
|
||||
// See https://github.com/puppeteer/puppeteer/issues/661
|
||||
// and https://github.com/puppeteer/puppeteer/issues/2410.
|
||||
"--font-render-hinting=none",
|
||||
"--remote-debugging-port=9222",
|
||||
"--disable-gpu",
|
||||
"--disable-translate",
|
||||
"--disable-extensions",
|
||||
"--disable-background-networking",
|
||||
"--safebrowsing-disable-auto-update",
|
||||
"--disable-sync",
|
||||
"--disable-default-apps",
|
||||
"--hide-scrollbars",
|
||||
"--metrics-recording-only",
|
||||
"--mute-audio",
|
||||
"--no-first-run",
|
||||
}
|
||||
|
||||
if ignoreCertificateErrors {
|
||||
args = append(args, "--ignore-certificate-errors")
|
||||
}
|
||||
|
||||
cmd, err := xexec.Command(logger, binary, args...)
|
||||
if err != nil {
|
||||
return nil, xerror.New(op, err)
|
||||
}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func kill(logger xlog.Logger, proc *os.Process) error {
|
||||
const op string = "chrome.kill"
|
||||
logger.DebugOp(op, "killing Google Chrome headless process using port 9222...")
|
||||
resolver := func() error {
|
||||
err := syscall.Kill(-proc.Pid, syscall.SIGKILL)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(err.Error(), "no such process") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restart(logger xlog.Logger, proc *os.Process, ignoreCertificateErrors bool) error {
|
||||
const op string = "chrome.restart"
|
||||
logger.DebugOp(op, "restarting Google Chrome headless process using port 9222...")
|
||||
resolver := func() error {
|
||||
// kill the existing process first.
|
||||
if err := kill(logger, proc); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd, err := cmd(logger, ignoreCertificateErrors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we try to restart the process.
|
||||
xexec.LogBeforeExecute(logger, cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
// if the process failed to restart correctly,
|
||||
// we have to restart it again.
|
||||
isViable, _ := IsViable(logger)
|
||||
if !isViable {
|
||||
return restart(logger, cmd.Process, ignoreCertificateErrors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsViable checks if Google Chrome is healthy.
|
||||
func IsViable(logger xlog.Logger) (bool, error) {
|
||||
const (
|
||||
op string = "chrome.IsViable"
|
||||
maxViabilityTests int = 20
|
||||
)
|
||||
viable := func() (bool, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
endpoint := "http://localhost:9222"
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"checking Google Chrome headless process viability via endpoint '%s/json/version'",
|
||||
endpoint,
|
||||
)
|
||||
v, err := devtool.New(endpoint).Version(ctx)
|
||||
if err != nil {
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"Google Chrome headless is not viable as endpoint returned '%v'",
|
||||
err.Error(),
|
||||
)
|
||||
return false, err
|
||||
}
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"Google Chrome headless is viable as endpoint returned '%v'",
|
||||
v,
|
||||
)
|
||||
return true, nil
|
||||
}
|
||||
result := false
|
||||
var err error
|
||||
|
||||
for i := 0; i < maxViabilityTests && !result; i++ {
|
||||
warmup(logger)
|
||||
result, err = viable()
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func warmup(logger xlog.Logger) {
|
||||
const (
|
||||
op string = "chrome.warmup"
|
||||
seconds float64 = 0.5
|
||||
)
|
||||
warmupTime := xtime.Duration(seconds)
|
||||
logger.DebugOpf(
|
||||
op,
|
||||
"waiting '%v' for allowing Google Chrome to warmup",
|
||||
warmupTime,
|
||||
)
|
||||
time.Sleep(warmupTime)
|
||||
}
|
||||
3
internal/pkg/chrome/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package chrome helps starting
|
||||
// Google Chrome headless in background.
|
||||
package chrome
|
||||