Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd8d2b5e95 | ||
|
|
8d89b2e80a | ||
|
|
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.13)
|
||||
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:
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -42,4 +42,4 @@ Fixes #
|
||||
- [ ] Have you successfully ran tests with your changes locally (`make tests`)?
|
||||
- [ ] Have you updated the documentation (`make doc`)?
|
||||
- [ ] I have squashed any insignificant commits
|
||||
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code
|
||||
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code
|
||||
|
||||
14
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- releases/*
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish to Docker Hub
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- run: echo $GITHUB_REF
|
||||
#- run: make publish VERSION=$GITHUB_REF DOCKER_USER=${{ secrets.DOCKER_USER }} DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}
|
||||
12
.github/workflows/push.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Push
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Lint, tests and code coverage
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
#- run: make lint
|
||||
#- run: make tests CODE_COVERAGE=1
|
||||
#- run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
|
||||
2
.gitignore
vendored
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
coverage.txt
|
||||
@@ -17,9 +17,10 @@ stages:
|
||||
jobs:
|
||||
include:
|
||||
- stage: tests
|
||||
script: make lint
|
||||
- stage: tests
|
||||
script: make tests
|
||||
script:
|
||||
- make lint
|
||||
- make tests CODE_COVERAGE=1
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
- stage: publish
|
||||
if: tag IS present
|
||||
script: ./scripts/publish.sh $TRAVIS_TAG $DOCKER_USER $DOCKER_PASS
|
||||
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$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
|
||||
|
||||
59
Makefile
@@ -1,30 +1,63 @@
|
||||
GOLANG_VERSION=1.13
|
||||
VERSION=snapshot
|
||||
GOLANG_VERSION=1.11.2
|
||||
DOCKER_USER=
|
||||
DOCKER_PASSWORD=
|
||||
DOCKER_REPOSITORY=thecodingmachine
|
||||
GOLANGCI_LINT_VERSION=1.19.1
|
||||
CODE_COVERAGE=0
|
||||
TINI_VERSION=0.18.0
|
||||
MAXIMUM_WAIT_TIMEOUT=30.0
|
||||
MAXIMUM_WAIT_DELAY=10.0
|
||||
MAXIMUM_WEBHOOK_URL_TIMEOUT=30.0
|
||||
DEFAULT_WAIT_TIMEOUT=10.0
|
||||
DEFAULT_WEBHOOK_URL_TIMEOUT=10.0
|
||||
DEFAULT_LISTEN_PORT=3000
|
||||
DISABLE_GOOGLE_CHROME=0
|
||||
DISABLE_UNOCONV=0
|
||||
LOG_LEVEL=INFO
|
||||
DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576
|
||||
|
||||
# generate documentation.
|
||||
doc:
|
||||
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 -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
|
||||
|
||||
# build the workspace Docker image.
|
||||
workspace:
|
||||
make base
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:workspace -f build/workspace/Dockerfile .
|
||||
|
||||
# gofmt and goimports all go files.
|
||||
fmt:
|
||||
go fmt ./...
|
||||
go mod tidy
|
||||
|
||||
# run all linters.
|
||||
lint:
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg:lint
|
||||
make workspace
|
||||
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm $(DOCKER_REPOSITORY)/gotenberg:lint
|
||||
|
||||
# run all tests.
|
||||
tests:
|
||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:tests -f build/tests/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg:tests
|
||||
make workspace
|
||||
./scripts/tests.sh $(DOCKER_REPOSITORY) $(CODE_COVERAGE)
|
||||
|
||||
# build Docker image.
|
||||
# generate documentation.
|
||||
doc:
|
||||
make workspace
|
||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REPOSITORY)/gotenberg:docs
|
||||
|
||||
# build Gotenberg Docker image.
|
||||
image:
|
||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
make workspace
|
||||
docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
|
||||
# start the API using previously built Docker image.
|
||||
gotenberg:
|
||||
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
|
||||
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||
|
||||
# publish Gotenberg images according to version.
|
||||
publish:
|
||||
make workspace
|
||||
./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REPOSITORY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
||||
12
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/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.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
|
||||
* 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,9 +31,10 @@ to learn how to interact with it!
|
||||
|
||||
## Badges
|
||||
|
||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:3)
|
||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:6)
|
||||
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
||||
[](https://godoc.org/github.com/thecodingmachine/gotenberg)
|
||||
[](https://codecov.io/gh/thecodingmachine/gotenberg)
|
||||
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
FROM debian:stretch-slim
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
FROM debian:buster-slim
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Common libraries
|
||||
@@ -9,40 +7,104 @@ 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 &&\
|
||||
RUN echo "deb http://httpredir.debian.org/debian/ buster main contrib non-free" > /etc/apt/sources.list &&\
|
||||
apt-get update &&\
|
||||
apt-get install -y curl wget python3-pip
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | PM2
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs PM2 for launching programs in background and with failure
|
||||
# | recovering. In our case: Chrome (headless) and Office (headless).
|
||||
# |
|
||||
|
||||
RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
|
||||
apt-get install -y curl wget gnupg ttf-mscorefonts-installer procps
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Chrome
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs Chrome for HTML and Markdown conversions.
|
||||
# | Installs Chrome.
|
||||
# |
|
||||
|
||||
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
||||
apt-get update &&\
|
||||
apt-get -y install google-chrome-stable
|
||||
apt-get -y --allow-unauthenticated install google-chrome-stable
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | LibreOffice
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs LibreOffice.
|
||||
# |
|
||||
|
||||
# https://github.com/nextcloud/docker/issues/380
|
||||
RUN mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
||||
echo "deb http://httpredir.debian.org/debian/ buster-backports main contrib non-free" >> /etc/apt/sources.list &&\
|
||||
apt-get update &&\
|
||||
apt-get -t buster-backports -y install libreoffice
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Unoconv
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs unoconv and LibreOffice for Office documents conversions.
|
||||
# | Installs unoconv.
|
||||
# |
|
||||
|
||||
RUN pip3 install unoconv &&\
|
||||
# https://github.com/nextcloud/docker/issues/380
|
||||
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
||||
apt-get -y install libreoffice
|
||||
ENV UNO_URL=https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv
|
||||
|
||||
RUN curl -Ls $UNO_URL -o /usr/bin/unoconv &&\
|
||||
chmod +x /usr/bin/unoconv &&\
|
||||
ln -s /usr/bin/python3 /usr/bin/python &&\
|
||||
unoconv --version
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | PDFtk
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs PDFtk as an alternative to pdfcpu for merging PDFs.
|
||||
# | https://github.com/thecodingmachine/gotenberg/issues/29
|
||||
# |
|
||||
|
||||
RUN apt-get -y install pdftk
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Fonts
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs a handful of fonts.
|
||||
# | Note: ttf-mscorefonts-installer are installed on top of this Dockerfile.
|
||||
# |
|
||||
|
||||
# Credits:
|
||||
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile
|
||||
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html
|
||||
RUN apt-get install -y \
|
||||
culmus \
|
||||
fonts-beng \
|
||||
fonts-hosny-amiri \
|
||||
fonts-lklug-sinhala \
|
||||
fonts-lohit-guru \
|
||||
fonts-lohit-knda \
|
||||
fonts-samyak-gujr \
|
||||
fonts-samyak-mlym \
|
||||
fonts-samyak-taml \
|
||||
fonts-sarai \
|
||||
fonts-sil-abyssinica \
|
||||
fonts-sil-padauk \
|
||||
fonts-telu \
|
||||
fonts-thai-tlwg \
|
||||
fonts-liberation \
|
||||
ttf-wqy-zenhei \
|
||||
fonts-arphic-uming \
|
||||
fonts-ipafont-mincho \
|
||||
fonts-ipafont-gothic \
|
||||
fonts-unfonts-core
|
||||
|
||||
COPY build/base/fonts/* /usr/local/share/fonts/
|
||||
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Default user
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | All processes in the Docker container will run as a dedicated
|
||||
# | non-root user.
|
||||
# |
|
||||
|
||||
RUN groupadd --gid 1001 gotenberg \
|
||||
&& useradd --uid 1001 --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \
|
||||
&& mkdir /gotenberg \
|
||||
&& chown gotenberg: /gotenberg
|
||||
29
build/base/fonts.conf
Normal file
@@ -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,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.
|
||||
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
|
||||
51
build/docs/content/01-install.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Install
|
||||
---
|
||||
|
||||
Gotenberg is shipped within a Docker image.
|
||||
|
||||
> It uses a dedicated non-root user called `gotenberg` with uid and gid `1001`.
|
||||
|
||||
You may start it with:
|
||||
|
||||
```bash
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
|
||||
```
|
||||
|
||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Docker Compose
|
||||
|
||||
You may also add it in your Docker Compose stack:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
# your others services
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg:6
|
||||
```
|
||||
|
||||
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
||||
|
||||
## Kubernetes
|
||||
|
||||
It may also be deployed with Kubernetes.
|
||||
|
||||
Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0.2` CPU).
|
||||
|
||||
> The more resources are granted, the quicker will be the conversions.
|
||||
|
||||
In the deployment specification of the pod, also specify the uid `1001` of the user `gotenberg`:
|
||||
|
||||
```
|
||||
securityContext:
|
||||
privileged: false
|
||||
runAsUser: 1001
|
||||
```
|
||||
|
||||
In the following examples, we will assume your
|
||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||
25
build/docs/content/02-clients.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Clients
|
||||
---
|
||||
|
||||
We provide clients in various languages for easing the interactions with the API.
|
||||
|
||||
## Go client
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v6
|
||||
```
|
||||
|
||||
## PHP client
|
||||
|
||||
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
||||
|
||||
```bash
|
||||
$ composer require php-http/guzzle6-adapter
|
||||
```
|
||||
|
||||
Then the PHP client:
|
||||
|
||||
```bash
|
||||
$ composer require thecodingmachine/gotenberg-php-client
|
||||
```
|
||||
105
build/docs/content/03-environment-variables.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Environment variables
|
||||
---
|
||||
|
||||
You may customize the API behaviour thanks to environment variables.
|
||||
|
||||
## Log level
|
||||
|
||||
The API provides structured logging allowing you to have relevant information
|
||||
about what's going on.
|
||||
|
||||
> If a TTY is attached, the log entries are displayed in text format with colors, otherwise in JSON format.
|
||||
|
||||
You may customize the severity of the log entries thanks to the environment variable `LOG_LEVEL`.
|
||||
|
||||
It accepts one of the following severities: `"DEBUG"`, `"INFO"` (default) and `"ERROR"`.
|
||||
|
||||
## Default listen port
|
||||
|
||||
By default, the API will listen on port `3000`.
|
||||
|
||||
You may customize this value with the environment variable `DEFAULT_LISTEN_PORT`.
|
||||
|
||||
This environment variable accepts any string that can be turned into a port number.
|
||||
|
||||
## Disable Google Chrome
|
||||
|
||||
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`
|
||||
for disabling Google Chrome.
|
||||
|
||||
It takes the strings `"0"` or `"1"` as value where `1` means `true`
|
||||
|
||||
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
|
||||
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
||||
|
||||
## Default Google Chrome rpcc buffer size
|
||||
|
||||
When performing a [HTML](#html), [URL](#url) or [Markdown](#markdown) conversion, the API might return
|
||||
a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
|
||||
|
||||
If so, you may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`.
|
||||
|
||||
It takes a string representation of an int as value (e.g. `"1048576"` for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.
|
||||
|
||||
> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`.
|
||||
> See the [rpcc buffer size section](#html.rpcc_buffer_size).
|
||||
|
||||
## Disable LibreOffice (unoconv)
|
||||
|
||||
You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`.
|
||||
|
||||
> If LibreOffice (unoconv) is disabled, the following conversion will **not** be available anymore:
|
||||
> [Office](#office)
|
||||
|
||||
## Default wait timeout
|
||||
|
||||
By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful.
|
||||
If unsucessful, it returns a `504` HTTP code.
|
||||
|
||||
You may customize this timeout thanks to the environment variable `DEFAULT_WAIT_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
> The default timeout may also be overridden per request thanks to the form field `waitTimeout`.
|
||||
> See the [timeout section](#timeout).
|
||||
|
||||
## Maximum wait timeout
|
||||
|
||||
By default, the value of the form field `waitTimeout` cannot be more than 30 seconds.
|
||||
|
||||
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
## Default webhook URL timeout
|
||||
|
||||
By default, the API will wait 10 seconds before it considers the sending of the resulting PDF to be unsuccessful.
|
||||
|
||||
> See the [webhook section](#webhook).
|
||||
|
||||
You may customize this timeout thanks to the environment variable `DEFAULT_WEBHOOK_URL_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
> The default timeout may also be overridden per request thanks to the form field `webhookURLTimeout`.
|
||||
> See the [webhook timeout section](#webhook.timeout).
|
||||
|
||||
## Maximum webhook URL timeout
|
||||
|
||||
By default, the value of the form field `webhookURLTimeout` cannot be more than 30 seconds.
|
||||
|
||||
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WEBHOOK_URL_TIMEOUT`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
|
||||
## Maximum wait delay
|
||||
|
||||
By default, the value of the form field `waitDelay` cannot be more than 10 seconds.
|
||||
|
||||
> See the [wait delay section](#html.wait_delay).
|
||||
|
||||
You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_DELAY`.
|
||||
|
||||
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||
@@ -33,13 +33,13 @@ $ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
@@ -95,8 +95,14 @@ The following classes allow you to inject printing values:
|
||||
* `pageNumber`: current page number
|
||||
* `totalPage`: total pages in the document
|
||||
|
||||
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
||||
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
||||
There are some limitations:
|
||||
|
||||
* JavaScript is not executed
|
||||
* external resources are not loaded
|
||||
* the CSS properties are independant of the ones used in the `index.html` file
|
||||
* `footer.html` CSS properties override the ones from `header.html`
|
||||
* only fonts installed in the Docker image are loaded (see the [fonts section](#fonts))
|
||||
* images only work using a `base64` encoded source (`<img src="data:image/png;base64, iVBORw0K... />`)
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -107,19 +113,19 @@ $ curl --request POST \
|
||||
--form files=@index.html \
|
||||
--form files=@header.html \
|
||||
--form files=@footer.html \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetHeader("header.html")
|
||||
req.SetFooter("footer.html")
|
||||
req.Header("header.html")
|
||||
req.Footer("footer.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -185,7 +191,7 @@ But this won't:
|
||||
You may also use *remote* paths for Google fonts, images and so on.
|
||||
|
||||
> If you want to install fonts directly in the Gotenberg Docker image,
|
||||
> see to the [fonts section](#office.fonts).
|
||||
> see to the [fonts section](#fonts).
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -197,22 +203,18 @@ $ curl --request POST \
|
||||
--form files=@style.css \
|
||||
--form files=@img.png \
|
||||
--form files=@font.woff \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetAssets([]string{
|
||||
"font.woff",
|
||||
"img.gif",
|
||||
"style.css",
|
||||
})
|
||||
req.Assets("font.woff", "img.gif", "style.css")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -234,8 +236,8 @@ $assets = [
|
||||
];
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setAssets($assets);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Paper size, margins, orientation
|
||||
@@ -244,8 +246,7 @@ You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
|
||||
|
||||
> Paper size and margins have to be provided in `inches`.
|
||||
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
|
||||
> Paper size and margins have to be provided in `inches`. Same for margins.
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -255,26 +256,26 @@ $ curl --request POST \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form paperWidth=8.27 \
|
||||
--form paperHeight=11.27 \
|
||||
--form paperHeight=11.69 \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
--form landscape=true \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetPaperSize(gotenberg.A4)
|
||||
req.SetMargins(gotenberg.NoMargins)
|
||||
req.SetLandscape(true)
|
||||
req.PaperSize(gotenberg.A4)
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -294,6 +295,107 @@ $request = new HTMLRequest($index);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$request->setLandscape(true);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Wait delay
|
||||
|
||||
In some cases, you may want to wait a certain amount of time to make sure the
|
||||
page you're trying to generate is fully rendered. For instance, if your page relies
|
||||
a lot on JavaScript for rendering.
|
||||
|
||||
> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form waitDelay=5.5 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WaitDelay(5.5)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitDelay(5.5);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Rpcc buffer size
|
||||
|
||||
The API might return a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`.
|
||||
|
||||
If so, you may increase this buffer size with a form field named `googleChromeRpccBufferSize`.
|
||||
|
||||
It takes an int as value (e.g. `1048576` for 1 MB).
|
||||
The hard limit is 100 MB and is defined by Google Chrome itself.
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_google_chrome_rpcc_buffer_size) section.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form googleChromeRpccBufferSize=1048576 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.GoogleChromeRpccBufferSize(1048576)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setGoogleChromeRpccBufferSize(1048576);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
56
build/docs/content/05-url.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: URL
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/url` for remote URL conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
This endpoint does not accept an `index.html` file nor assets files but a form field
|
||||
named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions.
|
||||
|
||||
> **Attention:** when converting a website to PDF, you should remove all margins.
|
||||
> If not, some of the content of the page might be hidden.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/url \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form remoteURL=https://google.com \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := gotenberg.NewURLRequest("https://google.com")
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\URLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$request = new URLRequest('https://google.com');
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
@@ -36,17 +36,17 @@ $ 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/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewMarkdownRequest("index.html", []string{"file.md"})
|
||||
req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -65,6 +65,6 @@ $markdowns = [
|
||||
DocumentFactory::makeFromPath('file.md', 'file.md'),
|
||||
];
|
||||
$request = new MarkdownRequest($index, $markdowns);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
@@ -12,6 +12,7 @@ You may send one or more Office documents. Following file extensions are accepte
|
||||
|
||||
* `.txt`
|
||||
* `.rtf`
|
||||
* `.fodt`
|
||||
* `.doc`
|
||||
* `.docx`
|
||||
* `.odt`
|
||||
@@ -24,9 +25,7 @@ You may send one or more Office documents. Following file extensions are accepte
|
||||
|
||||
All files will be merged into a single resulting PDF.
|
||||
|
||||
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
|
||||
> That's why for Office conversions, the API does only one conversion at a time.
|
||||
> See the [scalability section](#scalability) to find how to mitigate this issue.
|
||||
> **Attention:** Gotenberg merges the PDF files alphabetically.
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -36,17 +35,17 @@ $ curl --request POST \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form files=@document2.docx \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx", "document2.docx"})
|
||||
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -65,18 +64,15 @@ $files = [
|
||||
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Paper size and orientation
|
||||
## Orientation
|
||||
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `A4` size and `portrait` orientation.
|
||||
|
||||
> Paper size has to be provided in `inches`.
|
||||
> Also, you have to set both `paperWidth` and `paperHeight`.
|
||||
By default, it will be rendered with `portrait` orientation.
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -85,22 +81,19 @@ $ 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
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx"})
|
||||
req.SetPaperSize(gotenberg.A4)
|
||||
req.SetLandscape(true)
|
||||
req, _ := gotenberg.NewOfficeRequest("document.docx")
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -118,20 +111,7 @@ $files = [
|
||||
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setLandscape(true);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Fonts
|
||||
|
||||
By default, only `ttf-mscorefonts` fonts are installed.
|
||||
|
||||
If you wish to use more fonts, you will have to create your own image:
|
||||
|
||||
```Dockerfile
|
||||
FROM thecodingmachine/gotenberg:3
|
||||
|
||||
RUN apt-get -y install yourfonts
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
@@ -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,17 +21,17 @@ $ 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/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewMergeRequest([]string{"file.pdf", "file2.pdf"})
|
||||
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -48,6 +50,6 @@ $files = [
|
||||
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
|
||||
];
|
||||
$request = new MergeRequest($files);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
53
build/docs/content/09-timeout.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Timeout
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `waitTimeout`.
|
||||
|
||||
The API will wait the given **seconds** before it considers the conversion to be unsucessful.
|
||||
If unsucessful, it returns a `504` HTTP code.
|
||||
|
||||
It takes a float as value (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section.
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form waitTimeout=2.5
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WaitTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitTimeout(2.5);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
101
build/docs/content/10-webhook.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `webhookURL`.
|
||||
|
||||
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
||||
to given URL.
|
||||
|
||||
By doing so, your requests to the API will be over before the conversions are actually done!
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
|
||||
## Timeout
|
||||
|
||||
If a `webhookURL` is provided, you may also send a form field named `webhookURLTimeout`.
|
||||
|
||||
The API will wait the given **seconds** before it considers the sending of the resulting PDF to be unsucessful.
|
||||
|
||||
It takes a float as value (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_webhook_url_timeout) section.
|
||||
|
||||
### Examples
|
||||
|
||||
#### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/' \
|
||||
--form webhookURLTimeout=2.5
|
||||
```
|
||||
|
||||
#### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
req.WebhookURLTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
#### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$request->setWebhookURLTimeout(2.5);
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: Webhook
|
||||
title: Result filename
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `webhookURL`.
|
||||
All endpoints accept a form field named `resultFilename`.
|
||||
|
||||
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
||||
to given URL.
|
||||
If provided, the API will return the resulting PDF file with the given filename.
|
||||
Otherwise a random filename is used.
|
||||
|
||||
By doing so, your requests to the API will be over before the conversions are actually done!
|
||||
> **Attention:** this feature does not work if the form field `webhookURL` is given.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -18,19 +18,18 @@ $ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
--form resultFilename='foo.pdf'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v6"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.SetWebhookURL("http://myapp.com/webhook/")
|
||||
dest := "result.pdf"
|
||||
req.ResultFilename("foo.pdf")
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
@@ -41,10 +40,11 @@ func main() {
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$request->setResultFilename('foo.pdf');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
```
|
||||
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 extend.
|
||||
|
||||
For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk
|
||||
instances are there are requests. The limitation here is the available memory and CPU usage.
|
||||
|
||||
On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel.
|
||||
Indeed, Google Chrome misbehaves if there are too many concurrent conversions.
|
||||
|
||||
**The more concurrent requests, the more `504` HTTP codes the API will return.**
|
||||
|
||||
> See our [load testing use case](https://github.com/thecodingmachine/gotenberg/tree/master/loadtesting) for more details about the API behaviour under heavy load.
|
||||
|
||||
## Strategies
|
||||
|
||||
### Increase timeout
|
||||
|
||||
You may increase the conversion timeout. In other words, you accept that a conversion takes more time
|
||||
if the API is under heavy load.
|
||||
|
||||
> See [timeout section](#timeout).
|
||||
|
||||
### Scaling
|
||||
|
||||
The API being stateless, you may scale it as much as you want.
|
||||
|
||||
For instance, using the following Docker Compose file:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
# your others services
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg:6
|
||||
```
|
||||
|
||||
You may now launch your services using:
|
||||
|
||||
```bash
|
||||
$ docker-compose up --scale gotenberg=your_number_of_instances
|
||||
```
|
||||
|
||||
When requesting the Gotenberg service with your client(s), Docker will automatically
|
||||
redirect a request to a Gotenberg container according to the round-robin strategy.
|
||||
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
|
||||
```
|
||||
@@ -4,5 +4,6 @@ title: Links
|
||||
|
||||
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
|
||||
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
|
||||
* Thanks to [@mafredri](https://github.com/mafredri) for its help and its wonderful [cdp](https://github.com/mafredri/cdp) library
|
||||
|
||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
||||
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).
|
||||
@@ -1,8 +1,4 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
FROM thecodingmachine/gotenberg:workspace
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | GolangCI-Lint
|
||||
@@ -12,7 +8,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
# | than gometalinter.
|
||||
# |
|
||||
|
||||
ENV GOLANGCI_LINT_VERSION 1.12.3
|
||||
ARG GOLANGCI_LINT_VERSION
|
||||
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
||||
golangci-lint --version
|
||||
@@ -24,14 +20,17 @@ RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.s
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /lint
|
||||
|
||||
# Copy our module dependencies definitions.
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
# Define our working directory outside of $GOPATH (we're using go modules).
|
||||
USER gotenberg
|
||||
WORKDIR /gotenberg/lint
|
||||
|
||||
# Install module dependencies.
|
||||
RUN go mod download
|
||||
COPY --chown=gotenberg:gotenberg go.mod go.sum ./
|
||||
|
||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=lll", "--disable=errcheck", "--disable=gosec", "--disable=gochecknoglobals", "--disable=gochecknoinits" ]
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
# Copy our code source.
|
||||
COPY --chown=gotenberg:gotenberg . .
|
||||
|
||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen" ]
|
||||
@@ -1,5 +1,3 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Binary
|
||||
# |--------------------------------------------------------------------------
|
||||
@@ -7,7 +5,7 @@ ARG GOLANG_VERSION
|
||||
# | Buils Gotenberg binary.
|
||||
# |
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
||||
FROM thecodingmachine/gotenberg:workspace AS workspace
|
||||
|
||||
ARG VERSION
|
||||
|
||||
@@ -16,13 +14,37 @@ ENV GOOS=linux \
|
||||
CGO_ENABLED=0
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /gotenberg
|
||||
WORKDIR /gotenberg/package
|
||||
|
||||
# Install module dependencies.
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
# Copy our source code.
|
||||
COPY . .
|
||||
COPY internal ./internal
|
||||
COPY cmd ./cmd
|
||||
|
||||
# Build our binary.
|
||||
RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
|
||||
RUN go build -o gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
|
||||
|
||||
FROM thecodingmachine/gotenberg:base
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Tini
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | An helper for reaping zombie processes.
|
||||
# |
|
||||
|
||||
ARG TINI_VERSION
|
||||
|
||||
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
|
||||
RUN chmod +x /tini
|
||||
ENTRYPOINT [ "/tini", "--" ]
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Final touch
|
||||
@@ -31,12 +53,9 @@ RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/g
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
|
||||
FROM thecodingmachine/gotenberg:base
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
|
||||
COPY --from=workspace /gotenberg/package/gotenberg /usr/local/bin/
|
||||
|
||||
USER gotenberg
|
||||
WORKDIR /gotenberg
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -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
|
||||
@@ -2,20 +2,62 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/chrome"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// version will be set on build time.
|
||||
// nolint: gochecknoglobals
|
||||
var version = "snapshot"
|
||||
|
||||
func main() {
|
||||
notify.Println(fmt.Sprintf("Gotenberg %s", version))
|
||||
if err := api.Start(); err != nil {
|
||||
notify.ErrPrintln(err)
|
||||
os.Exit(1)
|
||||
const op string = "main"
|
||||
config, err := conf.FromEnv()
|
||||
systemLogger := xlog.New(config.LogLevel(), "system")
|
||||
if err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
systemLogger.InfofOp(op, "Gotenberg %s", version)
|
||||
systemLogger.DebugfOp(op, "configuration: %+v", config)
|
||||
if !config.DisableGoogleChrome() {
|
||||
// start Google Chrome headless.
|
||||
if err := chrome.Start(systemLogger); err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
}
|
||||
// create our API.
|
||||
srv := xhttp.New(config)
|
||||
// run our API in a goroutine so that it doesn't block.
|
||||
go func() {
|
||||
systemLogger.InfofOp(op, "http server started on port '%d'", config.DefaultListenPort())
|
||||
if err := srv.Start(fmt.Sprintf(":%d", config.DefaultListenPort())); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
quit := make(chan os.Signal, 1)
|
||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
// block until we receive our signal.
|
||||
<-quit
|
||||
// create a deadline to wait for.
|
||||
ctx, cancel := xcontext.WithTimeout(systemLogger, 120)
|
||||
defer cancel()
|
||||
// doesn't block if no connections, but will otherwise wait
|
||||
// until the timeout deadline.
|
||||
systemLogger.InfoOp(op, "shutting down http server...")
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
systemLogger.InfoOp(op, "bye!")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
1190
docs/index.html
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 |
24
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/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
|
||||
<span class="text">{{.Title}}</span>
|
||||
<span class="subtext">{{.Subtitle}}</span>
|
||||
</div>
|
||||
|
||||
38
go.mod
@@ -1,21 +1,27 @@
|
||||
module github.com/thecodingmachine/gotenberg
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/derekparker/delve v1.1.0
|
||||
github.com/google/go-cmp v0.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.0 // indirect
|
||||
github.com/hhrutter/pdfcpu v0.1.19
|
||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73
|
||||
github.com/labstack/gommon v0.2.8
|
||||
github.com/mafredri/cdp v0.20.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.1
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/gorilla/websocket v1.4.1 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.1.10
|
||||
github.com/labstack/gommon v0.3.0
|
||||
github.com/mafredri/cdp v0.24.2
|
||||
github.com/mattn/go-isatty v0.0.9
|
||||
github.com/microcosm-cc/bluemonday v1.0.2
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
|
||||
github.com/russross/blackfriday/v2 v2.0.1
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
||||
github.com/stretchr/objx v0.1.1 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad // indirect
|
||||
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 // indirect
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
)
|
||||
|
||||
112
go.sum
@@ -1,55 +1,81 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
|
||||
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hhrutter/pdfcpu v0.1.18 h1:GX3d6tZxmQlmB5X9VUyOtx7+HZZpR+8ISLqBineP6dc=
|
||||
github.com/hhrutter/pdfcpu v0.1.18/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
||||
github.com/hhrutter/pdfcpu v0.1.19 h1:J6GjNFDPjurQWallPW9gmz3CcNqBfvgB7Dad9T3e8Hc=
|
||||
github.com/hhrutter/pdfcpu v0.1.19/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73 h1:oQvA3Xy8274W5Ldgo6Nogy4lX8XO5AaIBsjF5Ggk0IU=
|
||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73/go.mod h1:rS0D1UPvC8/3sXjhSwEq+K1olh7ipbDhjDWATN2KSgA=
|
||||
github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=
|
||||
github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/mafredri/cdp v0.20.0 h1:dRq16FwXCAAbolR4W2iyRNsL14Bp6jFHLbRp2EkICVs=
|
||||
github.com/mafredri/cdp v0.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo/v4 v4.1.10 h1:/yhIpO50CBInUbE/nHJtGIyhBv0dJe2cDAYxc3V3uMo=
|
||||
github.com/labstack/echo/v4 v4.1.10/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/mafredri/cdp v0.24.2 h1:Rzhj/EQw9opbiwUpNML7P+4Hvf0/nSYPaDbiCEpILOM=
|
||||
github.com/mafredri/cdp v0.24.2/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ=
|
||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA=
|
||||
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -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
|
||||
301
internal/app/xhttp/handler.go
Normal file
@@ -0,0 +1,301 @@
|
||||
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/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
)
|
||||
|
||||
const (
|
||||
pingEndpoint string = "/ping"
|
||||
mergeEndpoint string = "/merge"
|
||||
convertGroupEndpoint string = "/convert"
|
||||
htmlEndpoint string = "/html"
|
||||
urlEndpoint string = "/url"
|
||||
markdownEndpoint string = "/markdown"
|
||||
officeEndpoint string = "/office"
|
||||
)
|
||||
|
||||
// pingHandler is the handler for healthcheck.
|
||||
func pingHandler(c echo.Context) error {
|
||||
const op string = "xhttp.pingHandler"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling ping request...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeHandler is the handler for merging
|
||||
// PDF files.
|
||||
func mergeHandler(c echo.Context) error {
|
||||
const op string = "xhttp.mergeHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling merge request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := mergePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
fpaths, err := r.Fpaths(".pdf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewMergePrinter(logger, fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// htmlHandler is the handler for converting
|
||||
// HTML to PDF.
|
||||
func htmlHandler(c echo.Context) error {
|
||||
const op string = "xhttp.htmlHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling HTML request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewHTMLPrinter(logger, fpath, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// urlHandler is the handler for converting
|
||||
// a URL to PDF.
|
||||
func urlHandler(c echo.Context) error {
|
||||
const op string = "xhttp.urlHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling URL request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !r.HasArg(resource.RemoteURLArgKey) {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' not found or empty", resource.RemoteURLArgKey),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
remoteURL, err := r.StringArg(resource.RemoteURLArgKey, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewURLPrinter(logger, remoteURL, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// markdownHandler is the handler for converting
|
||||
// Markdown to PDF.
|
||||
func markdownHandler(c echo.Context) error {
|
||||
const op string = "xhttp.markdownHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling Markdown request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := printer.NewMarkdownPrinter(logger, fpath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// officeHandler is the handler for converting
|
||||
// Office documents to PDF.
|
||||
func officeHandler(c echo.Context) error {
|
||||
const op string = "xhttp.officeHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling Office request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := officePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpaths, err := r.Fpaths(
|
||||
".txt",
|
||||
".rtf",
|
||||
".fodt",
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ods",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odp",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewOfficePrinter(logger, fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert(ctx context.Context, p printer.Printer) error {
|
||||
const op string = "xhttp.convert"
|
||||
resolver := func() error {
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
baseFilename := xrand.Get()
|
||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||
fpath := fmt.Sprintf("%s/%s", r.DirPath(), filename)
|
||||
// if no webhook URL given, run conversion
|
||||
// and directly return the resulting PDF file
|
||||
// or an error.
|
||||
if !r.HasArg(resource.WebhookURLArgKey) {
|
||||
logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
|
||||
return convertSync(ctx, p, filename, fpath)
|
||||
}
|
||||
// as a webhook URL has been given, we
|
||||
// run the following lines in a goroutine so that
|
||||
// it doesn't block.
|
||||
logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
|
||||
return convertAsync(ctx, p, filename, fpath)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertSync(ctx context.Context, p printer.Printer, filename, fpath string) error {
|
||||
const op = "xhttp.convertSync"
|
||||
resolver := func() error {
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
|
||||
if err := p.Print(fpath); err != nil {
|
||||
return err
|
||||
}
|
||||
if !r.HasArg(resource.ResultFilenameArgKey) {
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"no '%s' found, using generated filename '%s'",
|
||||
resource.RemoteURLArgKey,
|
||||
filename,
|
||||
)
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"'%s' found, so not using generated filename",
|
||||
resource.ResultFilenameArgKey,
|
||||
)
|
||||
filename, err := r.StringArg(resource.ResultFilenameArgKey, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string) error {
|
||||
const op = "xhttp.convertAsync"
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
webhookURL, err := r.StringArg(resource.WebhookURLArgKey, "")
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
webhookURLTimeout, err := resource.WebhookURLTimeoutArg(r, ctx.Config())
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
go func() {
|
||||
defer r.Close() // nolint: errcheck
|
||||
if err := p.Print(fpath); err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
defer f.Close() // nolint: errcheck
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"sending result file '%s' to '%s'",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
httpClient := &http.Client{
|
||||
Timeout: xtime.Duration(webhookURLTimeout),
|
||||
}
|
||||
resp, err := httpClient.Post(webhookURL, "application/pdf", f) /* #nosec */
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
586
internal/app/xhttp/handler_test.go
Normal file
@@ -0,0 +1,586 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestPingHandler(t *testing.T) {
|
||||
// should return 200.
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
}
|
||||
|
||||
func TestMergeHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
// should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
}
|
||||
|
||||
func TestHTMLHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint)
|
||||
// should return 200.
|
||||
body, contentType := test.HTMLMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is is > config.MaximumWaitDelay().
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestURLHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint)
|
||||
// should return 200.
|
||||
body, contentType := test.URLMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is is > config.MaximumWaitDelay().
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestMarkdownHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint)
|
||||
// should return 200.
|
||||
body, contentType := test.MarkdownMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is is > config.MaximumWaitDelay().
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitDelay" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperWidth" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "paperHeight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginTop" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginBottom" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginLeft" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "marginRight" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "googleChromeRpccBufferSize" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestOfficeHandler(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint)
|
||||
// should return 200.
|
||||
body, contentType := test.OfficeMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is < 0.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is is > config.MaximumWaitTimeout().
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 400 as "waitTimeout" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// should return 504.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req)
|
||||
// should return 400 as "landscape" form field
|
||||
// value is invalid.
|
||||
body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
}
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
status := make(chan error, 2)
|
||||
rcv := echo.New()
|
||||
rcv.POST("/foo", func(c echo.Context) error {
|
||||
if c.Request().Header.Get("Content-type") != "application/pdf" {
|
||||
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
|
||||
return nil
|
||||
}
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
status <- err
|
||||
return nil
|
||||
}
|
||||
if body == nil || len(body) == 0 {
|
||||
status <- errors.New("empty body")
|
||||
return nil
|
||||
}
|
||||
status <- nil
|
||||
return nil
|
||||
})
|
||||
go func() {
|
||||
rcv.Start(":3001")
|
||||
}()
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
// our custom server should receive the PDF.
|
||||
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"})
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
err := <-status
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestResultFilename(t *testing.T) {
|
||||
config := conf.DefaultConfig()
|
||||
srv := New(config)
|
||||
body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"})
|
||||
req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
|
||||
}
|
||||
136
internal/app/xhttp/middleware.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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 its an healthcheck request, there
|
||||
// is no need to create a Resource.
|
||||
if ctx.Path() == pingEndpoint {
|
||||
return next(ctx)
|
||||
}
|
||||
// if the endpoint is not for healthcheck, create a
|
||||
// Resource.
|
||||
if err := ctx.WithResource(trace); err != nil {
|
||||
err = doCleanup(ctx, err)
|
||||
err = doErr(ctx, err)
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loggerMiddleware logs the result of a request.
|
||||
func loggerMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
err := next(ctx)
|
||||
// we do not want to log healthcheck requests if
|
||||
// log level is not set to DEBUG.
|
||||
isDebug := ctx.Path() == pingEndpoint
|
||||
return ctx.LogRequestResult(err, isDebug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupMiddleware removes a resource.Resource
|
||||
// at the end of a request.
|
||||
func cleanupMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
return doCleanup(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errorMiddleware handles errors (if any).
|
||||
func errorMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
err := next(ctx)
|
||||
if err == nil {
|
||||
// so far so good!
|
||||
return nil
|
||||
}
|
||||
return doErr(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doCleanup(ctx context.Context, err error) error {
|
||||
const op string = "xhttp.cleanup"
|
||||
if !ctx.HasResource() {
|
||||
// nothing to remove.
|
||||
return err
|
||||
}
|
||||
r := ctx.MustResource()
|
||||
// if a webhook URL has been given,
|
||||
// do not remove the resource.Resource here because
|
||||
// we don't know if the result file has been
|
||||
// generated or sent.
|
||||
if r.HasArg(resource.WebhookURLArgKey) {
|
||||
return err
|
||||
}
|
||||
// a resource.Resource is associated with our custom context.
|
||||
if resourceErr := r.Close(); resourceErr != nil {
|
||||
xerr := xerror.New(op, resourceErr)
|
||||
ctx.XLogger().ErrorOp(xerror.Op(xerr), xerr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func doErr(ctx context.Context, err error) error {
|
||||
// if it's an error from echo
|
||||
// like 404 not found and so on.
|
||||
if echoHTTPErr, ok := err.(*echo.HTTPError); ok {
|
||||
// required to have a correct status code.
|
||||
ctx.Error(echoHTTPErr)
|
||||
return echoHTTPErr
|
||||
}
|
||||
// we log the initial error before returning
|
||||
// the HTTP error.
|
||||
errOp := xerror.Op(err)
|
||||
logger := ctx.XLogger()
|
||||
logger.ErrorOp(errOp, err)
|
||||
// handle our custom HTTP error.
|
||||
var httpErr error
|
||||
errCode := xerror.Code(err)
|
||||
errMessage := xerror.Message(err)
|
||||
switch errCode {
|
||||
case xerror.InvalidCode:
|
||||
httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage)
|
||||
case xerror.TimeoutCode:
|
||||
httpErr = echo.NewHTTPError(http.StatusGatewayTimeout, errMessage)
|
||||
default:
|
||||
httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage)
|
||||
}
|
||||
// required to have a correct status code.
|
||||
ctx.Error(httpErr)
|
||||
return httpErr
|
||||
}
|
||||
98
internal/app/xhttp/option.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
func mergePrinterOptions(r resource.Resource, config conf.Config) (printer.MergePrinterOptions, error) {
|
||||
const op string = "xhttp.mergePrinterOptions"
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.MergePrinterOptions{}, xerror.New(op, err)
|
||||
}
|
||||
return printer.MergePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.ChromePrinterOptions, error) {
|
||||
const op string = "xhttp.chromePrinterOptions"
|
||||
resolver := func() (printer.ChromePrinterOptions, error) {
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
waitDelay, err := resource.WaitDelayArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
headerHTML, footerHTML,
|
||||
err := resource.HeaderFooterContents(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
paperWidth, paperHeight,
|
||||
err := resource.PaperSizeArgs(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
marginTop, marginBottom, marginLeft, marginRight,
|
||||
err := resource.MarginArgs(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
return printer.ChromePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
RpccBufferSize: googleChromeRpccBufferSize,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
if err != nil {
|
||||
return opts, xerror.New(op, err)
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func officePrinterOptions(r resource.Resource, config conf.Config) (printer.OfficePrinterOptions, error) {
|
||||
const op string = "xhttp.officePrinterOptions"
|
||||
resolver := func() (printer.OfficePrinterOptions, error) {
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
return printer.OfficePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
Landscape: landscape,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
if err != nil {
|
||||
return opts, xerror.New(op, err)
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
208
internal/app/xhttp/pkg/context/context.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// Context extends the default echo.Context.
|
||||
type Context struct {
|
||||
echo.Context
|
||||
logger xlog.Logger
|
||||
config conf.Config
|
||||
resource resource.Resource
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// New creates a new Context.
|
||||
func New(c echo.Context, logger xlog.Logger, config conf.Config) Context {
|
||||
return Context{
|
||||
c,
|
||||
logger,
|
||||
config,
|
||||
resource.Resource{},
|
||||
time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
MustCastFromEchoContext cast an echo.Context
|
||||
to our custom Context.
|
||||
|
||||
It panics if casting goes wrong.
|
||||
*/
|
||||
func MustCastFromEchoContext(c echo.Context) Context {
|
||||
const op string = "context.MustCastFromEchoContext"
|
||||
ctx, ok := c.(Context)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("%s: unable to cast an echo.Context to our custom context.Context", op))
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
/*
|
||||
XLogger returns the xlog.Logger associated
|
||||
with the Context.
|
||||
|
||||
This method should be used instead of the
|
||||
default Logger() method coming from
|
||||
the echo.Context.
|
||||
*/
|
||||
func (ctx Context) XLogger() xlog.Logger {
|
||||
return ctx.logger
|
||||
}
|
||||
|
||||
// Config returns the conf.Config associated
|
||||
// with the Context.
|
||||
func (ctx Context) Config() conf.Config {
|
||||
return ctx.config
|
||||
}
|
||||
|
||||
// WithResource creates a resource.Resource and
|
||||
// adds it to the Context.
|
||||
func (ctx *Context) WithResource(directoryName string) error {
|
||||
const op string = "context.Context.WithResource"
|
||||
resolver := func() (resource.Resource, error) {
|
||||
r, err := resource.New(ctx.logger, directoryName)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
// retrieve form values from request.
|
||||
for _, key := range resource.ArgKeys() {
|
||||
r.WithArg(key, ctx.FormValue(string(key)))
|
||||
}
|
||||
// write form files from request.
|
||||
form, err := ctx.MultipartForm()
|
||||
if err != nil {
|
||||
/*
|
||||
(very) special case: one and
|
||||
only one file has been sent
|
||||
and it is empty.
|
||||
*/
|
||||
if strings.Contains(err.Error(), io.EOF.Error()) {
|
||||
return r, xerror.Invalid(op, "one file has been sent but it is empty: does it exist?", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
for _, files := range form.File {
|
||||
for _, fh := range files {
|
||||
in, err := fh.Open()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
defer in.Close() // nolint: errcheck
|
||||
filename, err := normalize.String(fh.Filename)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if err := r.WithFile(filename, in); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
resource, err := resolver()
|
||||
ctx.resource = resource
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasResource returns true if the Context
|
||||
// has a resource.Resource.
|
||||
func (ctx Context) HasResource() bool {
|
||||
return !reflect.DeepEqual(ctx.resource, resource.Resource{})
|
||||
}
|
||||
|
||||
/*
|
||||
MustResource returns the resource.Resource
|
||||
associated with the Context.
|
||||
|
||||
It panics if no resource.Resource.
|
||||
*/
|
||||
func (ctx Context) MustResource() resource.Resource {
|
||||
const op string = "context.Context.MustResource"
|
||||
if !ctx.HasResource() {
|
||||
panic(fmt.Sprintf("%s: unable to retrieve the resource.Resource from our custom context.Context", op))
|
||||
}
|
||||
return ctx.resource
|
||||
}
|
||||
|
||||
/*
|
||||
LogRequestResult logs the result of a request.
|
||||
This method should only be used by a middleware!
|
||||
|
||||
If an error is given, returns the exact same error.
|
||||
*/
|
||||
func (ctx Context) LogRequestResult(err error, isDebug bool) error {
|
||||
const op string = "context.Context.LogRequestResult"
|
||||
req := ctx.Request()
|
||||
resp := ctx.Response()
|
||||
stopTime := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"remote_ip": ctx.RealIP(),
|
||||
"host": req.Host,
|
||||
"uri": req.RequestURI,
|
||||
"method": req.Method,
|
||||
"path": path(req),
|
||||
"referer": req.Referer(),
|
||||
"user_agent": req.UserAgent(),
|
||||
"status": resp.Status,
|
||||
"latency": lantency(ctx.startTime, stopTime),
|
||||
"latency_human": latencyHuman(ctx.startTime, stopTime),
|
||||
"bytes_in": bytesIn(req),
|
||||
"bytes_out": bytesOut(resp),
|
||||
}
|
||||
if err != nil {
|
||||
ctx.logger.WithFields(fields).ErrorfOp(op, "request failed")
|
||||
return err
|
||||
}
|
||||
if isDebug {
|
||||
ctx.logger.WithFields(fields).DebugfOp(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
ctx.logger.WithFields(fields).InfofOp(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func path(r *http.Request) string {
|
||||
path := r.URL.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func lantency(startTime time.Time, stopTime time.Time) string {
|
||||
return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10)
|
||||
}
|
||||
|
||||
func latencyHuman(startTime time.Time, stopTime time.Time) string {
|
||||
return stopTime.Sub(startTime).String()
|
||||
}
|
||||
|
||||
func bytesIn(r *http.Request) string {
|
||||
bytesIn := r.Header.Get(echo.HeaderContentLength)
|
||||
if bytesIn == "" {
|
||||
bytesIn = "0"
|
||||
}
|
||||
return bytesIn
|
||||
}
|
||||
|
||||
func bytesOut(r *echo.Response) string {
|
||||
return strconv.FormatInt(r.Size, 10)
|
||||
}
|
||||
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
|
||||
292
internal/app/xhttp/pkg/resource/arg.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// ArgKey is a type for
|
||||
// arguments' keys.
|
||||
type ArgKey string
|
||||
|
||||
const (
|
||||
// ResultFilenameArgKey is the key
|
||||
// of the argument "resultFilename".
|
||||
ResultFilenameArgKey ArgKey = "resultFilename"
|
||||
// WaitTimeoutArgKey is the key
|
||||
// of the argument "waitTimeout".
|
||||
WaitTimeoutArgKey ArgKey = "waitTimeout"
|
||||
// WebhookURLArgKey is the key
|
||||
// of the argument "webhookURL".
|
||||
WebhookURLArgKey ArgKey = "webhookURL"
|
||||
// WebhookURLTimeoutArgKey is the key
|
||||
// of the argument "webhookURLTimeout".
|
||||
WebhookURLTimeoutArgKey ArgKey = "webhookURLTimeout"
|
||||
// RemoteURLArgKey is the key
|
||||
// of the argument "remoteURL".
|
||||
RemoteURLArgKey ArgKey = "remoteURL"
|
||||
// WaitDelayArgKey is the key
|
||||
// of the argument "waitDelay".
|
||||
WaitDelayArgKey ArgKey = "waitDelay"
|
||||
// PaperWidthArgKey is the key
|
||||
// of the argument "paperWidth".
|
||||
PaperWidthArgKey ArgKey = "paperWidth"
|
||||
// PaperHeightArgKey is the key
|
||||
// of the argument "paperHeight".
|
||||
PaperHeightArgKey ArgKey = "paperHeight"
|
||||
// MarginTopArgKey is the key
|
||||
// of the argument "marginTop".
|
||||
MarginTopArgKey ArgKey = "marginTop"
|
||||
// MarginBottomArgKey is the key
|
||||
// of the argument "marginBottom".
|
||||
MarginBottomArgKey ArgKey = "marginBottom"
|
||||
// MarginLeftArgKey is the key
|
||||
// of the argument "marginLeft".
|
||||
MarginLeftArgKey ArgKey = "marginLeft"
|
||||
// MarginRightArgKey is the key
|
||||
// of the argument "marginRight".
|
||||
MarginRightArgKey ArgKey = "marginRight"
|
||||
// LandscapeArgKey is the key
|
||||
// of the argument "landscape".
|
||||
LandscapeArgKey ArgKey = "landscape"
|
||||
// GoogleChromeRpccBufferSizeArgKey is the key
|
||||
// of the argument "googleChromeRpccBufferSize".
|
||||
GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize"
|
||||
)
|
||||
|
||||
/*
|
||||
ArgKeys returns a slice
|
||||
containing all available
|
||||
arguments' keys.
|
||||
*/
|
||||
func ArgKeys() []ArgKey {
|
||||
return []ArgKey{
|
||||
ResultFilenameArgKey,
|
||||
WaitTimeoutArgKey,
|
||||
WebhookURLArgKey,
|
||||
WebhookURLTimeoutArgKey,
|
||||
RemoteURLArgKey,
|
||||
WaitDelayArgKey,
|
||||
PaperWidthArgKey,
|
||||
PaperHeightArgKey,
|
||||
MarginTopArgKey,
|
||||
MarginBottomArgKey,
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
WaitTimeoutArg is a helper for retrieving
|
||||
the "waitTimeout" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WaitTimeoutArg(r Resource, config conf.Config) (float64, error) {
|
||||
const op string = "resource.WaitTimeoutArg"
|
||||
result, err := r.Float64Arg(
|
||||
WaitTimeoutArgKey,
|
||||
config.DefaultWaitTimeout(),
|
||||
xassert.Float64NotInferiorTo(0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWaitTimeout()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
WaitDelayArg is a helper for retrieving
|
||||
the "waitDelay" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WaitDelayArg(r Resource, config conf.Config) (float64, error) {
|
||||
const (
|
||||
op string = "resource.WaitDelayArg"
|
||||
defaultWaitDelay float64 = 0.0
|
||||
)
|
||||
result, err := r.Float64Arg(
|
||||
WaitDelayArgKey,
|
||||
defaultWaitDelay,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWaitDelay()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
WebhookURLTimeoutArg is a helper for retrieving
|
||||
the "webhookURLTimeout" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WebhookURLTimeoutArg(r Resource, config conf.Config) (float64, error) {
|
||||
const op string = "resource.WebhookURLTimeoutArg"
|
||||
result, err := r.Float64Arg(
|
||||
WebhookURLTimeoutArgKey,
|
||||
config.DefaultWebhookURLTimeout(),
|
||||
xassert.Float64NotInferiorTo(0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWebhookURLTimeout()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
PaperSizeArgs is a helper for retrieving
|
||||
the "paperWidth" and "paperHeight" arguments
|
||||
as float64.
|
||||
*/
|
||||
func PaperSizeArgs(r Resource, config conf.Config) (float64, float64, error) {
|
||||
const op string = "resource.PaperSizeArgs"
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
resolver := func() (float64, float64, error) {
|
||||
paperWidth, err := r.Float64Arg(
|
||||
PaperWidthArgKey,
|
||||
opts.PaperWidth,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.PaperWidth,
|
||||
opts.PaperHeight,
|
||||
err
|
||||
}
|
||||
paperHeight, err := r.Float64Arg(
|
||||
PaperHeightArgKey,
|
||||
opts.PaperHeight,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.PaperWidth,
|
||||
opts.PaperHeight,
|
||||
err
|
||||
}
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
nil
|
||||
}
|
||||
paperWidth, paperHeight,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
nil
|
||||
}
|
||||
|
||||
/*
|
||||
MarginArgs is a helper for retrieving
|
||||
the "marginTop", "marginBottom", "marginLeft"
|
||||
and "marginRight" arguments as float64.
|
||||
*/
|
||||
func MarginArgs(r Resource, config conf.Config) (float64, float64, float64, float64, error) {
|
||||
const op string = "resource.MarginArgs"
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
resolver := func() (float64, float64, float64, float64, error) {
|
||||
marginTop, err := r.Float64Arg(
|
||||
MarginTopArgKey,
|
||||
opts.MarginTop,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
marginBottom, err := r.Float64Arg(
|
||||
MarginBottomArgKey,
|
||||
opts.MarginBottom,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
marginLeft, err := r.Float64Arg(
|
||||
MarginLeftArgKey,
|
||||
opts.MarginLeft,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
marginRight, err := r.Float64Arg(
|
||||
MarginRightArgKey,
|
||||
opts.MarginRight,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return opts.MarginTop,
|
||||
opts.MarginBottom,
|
||||
opts.MarginLeft,
|
||||
opts.MarginRight,
|
||||
err
|
||||
}
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
nil
|
||||
}
|
||||
marginTop, marginBottom, marginLeft, marginRight,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
nil
|
||||
}
|
||||
|
||||
/*
|
||||
GoogleChromeRpccBufferSizeArg is a helper for retrieving
|
||||
the "googleChromeRpccBufferSize" argument as int64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func GoogleChromeRpccBufferSizeArg(r Resource, config conf.Config) (int64, error) {
|
||||
const op string = "resource.GoogleChromeRpccBufferSizeArg"
|
||||
result, err := r.Int64Arg(
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
config.DefaultGoogleChromeRpccBufferSize(),
|
||||
xassert.Int64NotInferiorTo(0.0),
|
||||
xassert.Int64NotSuperiorTo(config.MaximumGoogleChromeRpccBufferSize()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
347
internal/app/xhttp/pkg/resource/arg_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestArgKeys(t *testing.T) {
|
||||
expected := []ArgKey{
|
||||
ResultFilenameArgKey,
|
||||
WaitTimeoutArgKey,
|
||||
WebhookURLArgKey,
|
||||
WebhookURLTimeoutArgKey,
|
||||
RemoteURLArgKey,
|
||||
WaitDelayArgKey,
|
||||
PaperWidthArgKey,
|
||||
PaperHeightArgKey,
|
||||
MarginTopArgKey,
|
||||
MarginBottomArgKey,
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
GoogleChromeRpccBufferSizeArgKey,
|
||||
}
|
||||
assert.Equal(t, expected, ArgKeys())
|
||||
}
|
||||
|
||||
func TestWaitTimeoutArg(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = config.DefaultWaitTimeout()
|
||||
v, err := WaitTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 5.0
|
||||
r.WithArg(WaitTimeoutArgKey, "5.0")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = config.DefaultWaitTimeout()
|
||||
r.WithArg(WaitTimeoutArgKey, "-1.0")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumWaitTimeout().
|
||||
expected = config.DefaultWaitTimeout()
|
||||
r.WithArg(WaitTimeoutArgKey, "31.0")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = config.DefaultWaitTimeout()
|
||||
r.WithArg(WaitTimeoutArgKey, "foo")
|
||||
v, err = WaitTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestWaitDelayArg(t *testing.T) {
|
||||
const (
|
||||
resourceDirectoryName string = "foo"
|
||||
defaultValue float64 = 0.0
|
||||
)
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = defaultValue
|
||||
v, err := WaitDelayArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 5.0
|
||||
r.WithArg(WaitDelayArgKey, "5.0")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = defaultValue
|
||||
r.WithArg(WaitDelayArgKey, "-1.0")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumWaitDelay().
|
||||
expected = defaultValue
|
||||
r.WithArg(WaitDelayArgKey, "31.0")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = defaultValue
|
||||
r.WithArg(WaitDelayArgKey, "foo")
|
||||
v, err = WaitDelayArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestWebhookURLTimeoutArg(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
v, err := WebhookURLTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 5.0
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "5.0")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "-1.0")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumWebhookURLTimeout().
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "31.0")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = config.DefaultWebhookURLTimeout()
|
||||
r.WithArg(WebhookURLTimeoutArgKey, "foo")
|
||||
v, err = WebhookURLTimeoutArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestPaperSizeArgs(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// arguments do not exist.
|
||||
width, height, err := PaperSizeArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, opts.PaperWidth, width)
|
||||
assert.Equal(t, opts.PaperHeight, height)
|
||||
// arguments exist.
|
||||
expected = 5.0
|
||||
r.WithArg(PaperWidthArgKey, "5.0")
|
||||
r.WithArg(PaperHeightArgKey, "5.0")
|
||||
width, height, err = PaperSizeArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, width)
|
||||
assert.Equal(t, expected, height)
|
||||
// should not be OK as arguments
|
||||
// value are < 0.
|
||||
expected = opts.PaperWidth
|
||||
r.WithArg(PaperWidthArgKey, "-1.0")
|
||||
width, _, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, width)
|
||||
r.WithArg(PaperWidthArgKey, "5.0")
|
||||
expected = opts.PaperHeight
|
||||
r.WithArg(PaperHeightArgKey, "-1.0")
|
||||
_, height, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, height)
|
||||
r.WithArg(PaperHeightArgKey, "5.0")
|
||||
// should not be OK as
|
||||
// arguments value are invalids.
|
||||
expected = opts.PaperWidth
|
||||
r.WithArg(PaperWidthArgKey, "foo")
|
||||
width, _, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, width)
|
||||
r.WithArg(PaperWidthArgKey, "5.0")
|
||||
expected = opts.PaperHeight
|
||||
r.WithArg(PaperHeightArgKey, "foo")
|
||||
_, height, err = PaperSizeArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, height)
|
||||
r.WithArg(PaperHeightArgKey, "5.0")
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestMarginArgs(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected float64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// arguments do not exist.
|
||||
top, bottom, left, right, err := MarginArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, opts.MarginTop, top)
|
||||
assert.Equal(t, opts.MarginBottom, bottom)
|
||||
assert.Equal(t, opts.MarginLeft, left)
|
||||
assert.Equal(t, opts.MarginRight, right)
|
||||
// arguments exist.
|
||||
expected = 5.0
|
||||
r.WithArg(MarginTopArgKey, "5.0")
|
||||
r.WithArg(MarginBottomArgKey, "5.0")
|
||||
r.WithArg(MarginLeftArgKey, "5.0")
|
||||
r.WithArg(MarginRightArgKey, "5.0")
|
||||
top, bottom, left, right, err = MarginArgs(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, top)
|
||||
assert.Equal(t, expected, bottom)
|
||||
assert.Equal(t, expected, left)
|
||||
assert.Equal(t, expected, right)
|
||||
// should not be OK as arguments
|
||||
// value are < 0.
|
||||
expected = opts.MarginTop
|
||||
r.WithArg(MarginTopArgKey, "-1.0")
|
||||
top, _, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, top)
|
||||
r.WithArg(MarginTopArgKey, "5.0")
|
||||
expected = opts.MarginBottom
|
||||
r.WithArg(MarginBottomArgKey, "-1.0")
|
||||
_, bottom, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, bottom)
|
||||
r.WithArg(MarginBottomArgKey, "5.0")
|
||||
expected = opts.MarginLeft
|
||||
r.WithArg(MarginLeftArgKey, "-1.0")
|
||||
_, _, left, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, left)
|
||||
r.WithArg(MarginLeftArgKey, "5.0")
|
||||
expected = opts.MarginRight
|
||||
r.WithArg(MarginRightArgKey, "-1.0")
|
||||
_, _, _, right, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, right)
|
||||
r.WithArg(MarginRightArgKey, "5.0")
|
||||
// should not be OK as
|
||||
// arguments value are invalids.
|
||||
expected = opts.MarginTop
|
||||
r.WithArg(MarginTopArgKey, "foo")
|
||||
top, _, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, top)
|
||||
r.WithArg(MarginTopArgKey, "5.0")
|
||||
expected = opts.MarginBottom
|
||||
r.WithArg(MarginBottomArgKey, "foo")
|
||||
_, bottom, _, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, bottom)
|
||||
r.WithArg(MarginBottomArgKey, "5.0")
|
||||
expected = opts.MarginLeft
|
||||
r.WithArg(MarginLeftArgKey, "foo")
|
||||
_, _, left, _, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, left)
|
||||
r.WithArg(MarginLeftArgKey, "5.0")
|
||||
expected = opts.MarginRight
|
||||
r.WithArg(MarginRightArgKey, "foo")
|
||||
_, _, _, right, err = MarginArgs(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, right)
|
||||
r.WithArg(MarginRightArgKey, "5.0")
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleChromeRpccBufferSizeArg(t *testing.T) {
|
||||
const resourceDirectoryName string = "foo"
|
||||
var expected int64
|
||||
logger := test.DebugLogger()
|
||||
config := conf.DefaultConfig()
|
||||
r, err := New(logger, resourceDirectoryName)
|
||||
assert.Nil(t, err)
|
||||
// argument does not exist.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
v, err := GoogleChromeRpccBufferSizeArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// argument exist.
|
||||
expected = 10
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "10")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is < 0.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "-1")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as argument
|
||||
// value is > config.MaximumGoogleChromeRpccBufferSize().
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "104857601")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// should not be OK as
|
||||
// argument value is invalid.
|
||||
expected = config.DefaultGoogleChromeRpccBufferSize()
|
||||
r.WithArg(GoogleChromeRpccBufferSizeArgKey, "foo")
|
||||
v, err = GoogleChromeRpccBufferSizeArg(r, config)
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
// finally...
|
||||
err = r.Close()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
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() // nolint: errcheck
|
||||
if err := out.Chmod(0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := out.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// content returns the string content of
|
||||
// the file.
|
||||
func (f file) content() (string, error) {
|
||||
const op string = "resource.file.content"
|
||||
b, err := ioutil.ReadFile(f.fpath)
|
||||
if err != nil {
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
/*
|
||||
HeaderFooterContents is a helper for retrieving
|
||||
the content of the files "header.html"
|
||||
and "footer.html".
|
||||
*/
|
||||
func HeaderFooterContents(r Resource, config conf.Config) (string, string, error) {
|
||||
const op string = "resource.HeaderFooterContents"
|
||||
opts := printer.DefaultChromePrinterOptions(config)
|
||||
resolver := func() (string, string, error) {
|
||||
headerHTML, err := r.Fcontent("header.html", opts.HeaderHTML)
|
||||
if err != nil {
|
||||
return opts.HeaderHTML,
|
||||
opts.FooterHTML,
|
||||
err
|
||||
}
|
||||
footerHTML, err := r.Fcontent("footer.html", opts.FooterHTML)
|
||||
if err != nil {
|
||||
return opts.HeaderHTML,
|
||||
opts.FooterHTML,
|
||||
err
|
||||
}
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
nil
|
||||
}
|
||||
headerHTML, footerHTML,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
227
internal/app/xhttp/pkg/resource/resource.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
/*
|
||||
TemporaryDirectory is the directory
|
||||
where all the resources directory
|
||||
are located.
|
||||
*/
|
||||
const TemporaryDirectory string = "tmp"
|
||||
|
||||
// Resource helps managing
|
||||
// arguments and files for a conversion.
|
||||
type Resource struct {
|
||||
logger xlog.Logger
|
||||
dirPath string
|
||||
args map[ArgKey]string
|
||||
files map[string]file
|
||||
}
|
||||
|
||||
// New creates a Resource where its files will
|
||||
// be located in the given directory name.
|
||||
func New(logger xlog.Logger, directoryName string) (Resource, error) {
|
||||
const op string = "resource.New"
|
||||
resolver := func() (string, error) {
|
||||
dirPath := fmt.Sprintf("%s/%s", TemporaryDirectory, directoryName)
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
absDirPath, err := filepath.Abs(dirPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return absDirPath, nil
|
||||
}
|
||||
dirPath, err := resolver()
|
||||
if err != nil {
|
||||
return Resource{}, xerror.New(op, err)
|
||||
}
|
||||
logger.DebugfOp(op, "resource directory '%s' created", directoryName)
|
||||
return Resource{
|
||||
logger: logger,
|
||||
dirPath: dirPath,
|
||||
args: make(map[ArgKey]string),
|
||||
files: make(map[string]file),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close removes the working directory of the
|
||||
// Resource if it exists.
|
||||
func (r Resource) Close() error {
|
||||
const op string = "resource.Resource.Close"
|
||||
if _, err := os.Stat(r.dirPath); os.IsNotExist(err) {
|
||||
r.logger.DebugfOp(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
|
||||
return nil
|
||||
}
|
||||
if err := os.RemoveAll(r.dirPath); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
r.logger.DebugfOp(op, "resource directory '%s' removed", r.dirPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithArg add a new argument to the Resource.
|
||||
func (r *Resource) WithArg(key ArgKey, value string) {
|
||||
const op string = "resource.Resource.WithArg"
|
||||
r.args[key] = value
|
||||
r.logger.DebugfOp(op, "added '%s' with value '%s' to resource args", key, value)
|
||||
}
|
||||
|
||||
// WithFile add a new file to the Resource.
|
||||
func (r *Resource) WithFile(filename string, in io.Reader) error {
|
||||
const op string = "resource.Resource.WithFile"
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
file := file{fpath: fpath}
|
||||
if err := file.write(in); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
r.files[filename] = file
|
||||
r.logger.DebugfOp(op, "resource file '%s' created", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DirPath returns the directory path
|
||||
// of the Resource.
|
||||
func (r Resource) DirPath() string {
|
||||
return r.dirPath
|
||||
}
|
||||
|
||||
// HasArg returns true if given key exists
|
||||
// among the Resource and its value is not empty.
|
||||
func (r Resource) HasArg(key ArgKey) bool {
|
||||
if v, ok := r.args[key]; ok {
|
||||
return v != ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
StringArg returns the value of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.String.
|
||||
*/
|
||||
func (r Resource) StringArg(key ArgKey, defaultValue string, rules ...xassert.RuleString) (string, error) {
|
||||
const op string = "resource.Resource.StringArg"
|
||||
result, err := xassert.String(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Int64Arg returns the int64 representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Int64.
|
||||
*/
|
||||
func (r Resource) Int64Arg(key ArgKey, defaultValue int64, rules ...xassert.RuleInt64) (int64, error) {
|
||||
const op string = "resource.Resource.Int64Arg"
|
||||
result, err := xassert.Int64(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Float64Arg returns the float64 representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Float64.
|
||||
*/
|
||||
func (r Resource) Float64Arg(key ArgKey, defaultValue float64, rules ...xassert.RuleFloat64) (float64, error) {
|
||||
const op string = "resource.Resource.Float64Arg"
|
||||
result, err := xassert.Float64(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
BoolArg returns the boolean representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Bool.
|
||||
*/
|
||||
func (r Resource) BoolArg(key ArgKey, defaultValue bool) (bool, error) {
|
||||
const op string = "resource.Resource.BoolArg"
|
||||
result, err := xassert.Bool(string(key), r.args[key], defaultValue)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Fpath returns the path of the given filename.
|
||||
// This filename should exist whithin the Resource.
|
||||
func (r Resource) Fpath(filename string) (string, error) {
|
||||
const op string = "resource.Resource.Fpath"
|
||||
file, ok := r.files[filename]
|
||||
if !ok {
|
||||
return "", xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("resource file '%s' does not exist", filename),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return file.fpath, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Fpaths returns the paths of the files
|
||||
having one of the given file extensions.
|
||||
|
||||
It should found at least one path.
|
||||
*/
|
||||
func (r Resource) Fpaths(exts ...string) ([]string, error) {
|
||||
const op string = "resource.Resource.Fpaths"
|
||||
var fpaths []string
|
||||
for filename, file := range r.files {
|
||||
for _, ext := range exts {
|
||||
if filepath.Ext(filename) == ext {
|
||||
fpaths = append(fpaths, file.fpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return nil, xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("no resource file found for extensions '%v'", exts),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return fpaths, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Fcontent returns the string content of the
|
||||
given filename.
|
||||
|
||||
If filename does not exist within the Resource,
|
||||
returns the default value.
|
||||
*/
|
||||
func (r Resource) Fcontent(filename, defaultValue string) (string, error) {
|
||||
const op string = "resource.Resource.Fcontent"
|
||||
file, ok := r.files[filename]
|
||||
if !ok {
|
||||
return defaultValue, nil
|
||||
}
|
||||
content, err := file.content()
|
||||
if err != nil {
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
32
internal/app/xhttp/xhttp.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
)
|
||||
|
||||
// New returns a custom echo.Echo.
|
||||
func New(config conf.Config) *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
srv.Use(contextMiddleware(config))
|
||||
srv.Use(loggerMiddleware())
|
||||
srv.Use(cleanupMiddleware())
|
||||
srv.Use(errorMiddleware())
|
||||
srv.GET(pingEndpoint, pingHandler)
|
||||
srv.POST(mergeEndpoint, mergeHandler)
|
||||
if config.DisableGoogleChrome() && config.DisableUnoconv() {
|
||||
return srv
|
||||
}
|
||||
g := srv.Group(convertGroupEndpoint)
|
||||
if !config.DisableGoogleChrome() {
|
||||
g.POST(htmlEndpoint, htmlHandler)
|
||||
g.POST(urlEndpoint, urlHandler)
|
||||
g.POST(markdownEndpoint, markdownHandler)
|
||||
}
|
||||
if !config.DisableUnoconv() {
|
||||
g.POST(officeEndpoint, officeHandler)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
126
internal/app/xhttp/xhttp_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestDisableChromeEndpoints(t *testing.T) {
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 404.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL endpoint should return 404.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office endpoint should return 200.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
|
||||
}
|
||||
|
||||
func TestDisableUnoconvEndpoints(t *testing.T) {
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "1")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 200.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// URL endpoint should return 200.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Office endpoint should return 404.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "0")
|
||||
}
|
||||
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "1")
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "1")
|
||||
config, err := conf.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Ping endpoint should return 200.
|
||||
req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge endpoint should return 200.
|
||||
body, contentType := test.MergeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML endpoint should return 404.
|
||||
body, contentType = test.HTMLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL endpoint should return 404.
|
||||
body, contentType = test.URLMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown endpoint should return 404.
|
||||
body, contentType = test.MarkdownMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office endpoint should return 404.
|
||||
body, contentType = test.OfficeMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// finally...
|
||||
os.Setenv(conf.DisableGoogleChromeEnvVar, "0")
|
||||
os.Setenv(conf.DisableUnoconvEnvVar, "0")
|
||||
}
|
||||
170
internal/pkg/chrome/chrome.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package chrome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mafredri/cdp/devtool"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
)
|
||||
|
||||
// Start starts Google Chrome headless in background.
|
||||
func Start(logger xlog.Logger) error {
|
||||
const op string = "chrome.Start"
|
||||
logger.DebugOp(op, "starting new Google Chrome headless process on port 9222...")
|
||||
resolver := func() error {
|
||||
cmd, err := cmd(logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we try to start the process.
|
||||
xexec.LogBeforeExecute(logger, cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
// if the process failed to start correctly,
|
||||
// we have to restart it.
|
||||
if !isViable(logger) {
|
||||
return restart(logger, cmd.Process)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmd(logger xlog.Logger) (*exec.Cmd, error) {
|
||||
const op string = "chrome.cmd"
|
||||
binary := "google-chrome-stable"
|
||||
args := []string{
|
||||
"--no-sandbox",
|
||||
"--headless",
|
||||
// see https://github.com/GoogleChrome/puppeteer/issues/2410.
|
||||
"--font-render-hinting=medium",
|
||||
"--remote-debugging-port=9222",
|
||||
"--disable-gpu",
|
||||
"--disable-translate",
|
||||
"--disable-extensions",
|
||||
"--disable-background-networking",
|
||||
"--safebrowsing-disable-auto-update",
|
||||
"--disable-sync",
|
||||
"--disable-default-apps",
|
||||
"--hide-scrollbars",
|
||||
"--metrics-recording-only",
|
||||
"--mute-audio",
|
||||
"--no-first-run",
|
||||
}
|
||||
cmd, err := xexec.Command(logger, binary, args...)
|
||||
if err != nil {
|
||||
return nil, xerror.New(op, err)
|
||||
}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func kill(logger xlog.Logger, proc *os.Process) error {
|
||||
const op string = "chrome.kill"
|
||||
logger.DebugOp(op, "killing Google Chrome headless process using port 9222...")
|
||||
resolver := func() error {
|
||||
err := syscall.Kill(-proc.Pid, syscall.SIGKILL)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(err.Error(), "no such process") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restart(logger xlog.Logger, proc *os.Process) error {
|
||||
const op string = "chrome.restart"
|
||||
logger.DebugOp(op, "restarting Google Chrome headless process using port 9222...")
|
||||
resolver := func() error {
|
||||
// kill the existing process first.
|
||||
if err := kill(logger, proc); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd, err := cmd(logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we try to restart the process.
|
||||
xexec.LogBeforeExecute(logger, cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
// if the process failed to restart correctly,
|
||||
// we have to restart it again.
|
||||
if !isViable(logger) {
|
||||
return restart(logger, cmd.Process)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isViable(logger xlog.Logger) bool {
|
||||
const (
|
||||
op string = "chrome.isViable"
|
||||
maxViabilityTests int = 20
|
||||
)
|
||||
viable := func() bool {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
endpoint := "http://localhost:9222"
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"checking Google Chrome headless process viability via endpoint '%s/json/version'",
|
||||
endpoint,
|
||||
)
|
||||
v, err := devtool.New(endpoint).Version(ctx)
|
||||
if err != nil {
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"Google Chrome headless is not viable as endpoint returned '%v'",
|
||||
err.Error(),
|
||||
)
|
||||
return false
|
||||
}
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"Google Chrome headless is viable as endpoint returned '%v'",
|
||||
v,
|
||||
)
|
||||
return true
|
||||
}
|
||||
result := false
|
||||
for i := 0; i < maxViabilityTests && !result; i++ {
|
||||
warmup(logger)
|
||||
result = viable()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func warmup(logger xlog.Logger) {
|
||||
const op string = "chrome.warmup"
|
||||
warmupTime := xtime.Duration(0.5)
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"waiting '%v' for allowing Google Chrome to warmup",
|
||||
warmupTime,
|
||||
)
|
||||
time.Sleep(warmupTime)
|
||||
}
|
||||
3
internal/pkg/chrome/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package chrome helps starting
|
||||
// Google Chrome headless in background.
|
||||
package chrome
|
||||
255
internal/pkg/conf/conf.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaximumWaitTimeoutEnvVar contains the name
|
||||
// of the environment variable "MAXIMUM_WAIT_TIMEOUT".
|
||||
MaximumWaitTimeoutEnvVar string = "MAXIMUM_WAIT_TIMEOUT"
|
||||
// MaximumWaitDelayEnvVar contains the name
|
||||
// of the environment variable "MAXIMUM_WAIT_DELAY".
|
||||
MaximumWaitDelayEnvVar string = "MAXIMUM_WAIT_DELAY"
|
||||
// MaximumWebhookURLTimeoutEnvVar contains the name
|
||||
// of the environment variable "MAXIMUM_WEBHOOK_URL_TIMEOUT".
|
||||
MaximumWebhookURLTimeoutEnvVar string = "MAXIMUM_WEBHOOK_URL_TIMEOUT"
|
||||
// DefaultWaitTimeoutEnvVar contains the name
|
||||
// of the environment variable "DEFAULT_WAIT_TIMEOUT".
|
||||
DefaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_TIMEOUT"
|
||||
// DefaultWebhookURLTimeoutEnvVar contains the name
|
||||
// of the environment variable "DEFAULT_WEBHOOK_URL_TIMEOUT".
|
||||
DefaultWebhookURLTimeoutEnvVar string = "DEFAULT_WEBHOOK_URL_TIMEOUT"
|
||||
// DefaultListenPortEnvVar contains the name
|
||||
// of the environment variable "DEFAULT_LISTEN_PORT".
|
||||
DefaultListenPortEnvVar string = "DEFAULT_LISTEN_PORT"
|
||||
// DisableGoogleChromeEnvVar contains the name
|
||||
// of the environment variable "DISABLE_GOOGLE_CHROME".
|
||||
DisableGoogleChromeEnvVar string = "DISABLE_GOOGLE_CHROME"
|
||||
// DisableUnoconvEnvVar contains the name
|
||||
// of the environment variable "DISABLE_UNOCONV".
|
||||
DisableUnoconvEnvVar string = "DISABLE_UNOCONV"
|
||||
// LogLevelEnvVar contains the name
|
||||
// of the environment variable "LOG_LEVEL".
|
||||
LogLevelEnvVar string = "LOG_LEVEL"
|
||||
// DefaultGoogleChromeRpccBufferSizeEnvVar contains the name
|
||||
// of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE".
|
||||
DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE"
|
||||
)
|
||||
|
||||
// Config contains the application
|
||||
// configuration.
|
||||
type Config struct {
|
||||
maximumWaitTimeout float64
|
||||
maximumWaitDelay float64
|
||||
maximumWebhookURLTimeout float64
|
||||
defaultWaitTimeout float64
|
||||
defaultWebhookURLTimeout float64
|
||||
defaultListenPort int64
|
||||
disableGoogleChrome bool
|
||||
disableUnoconv bool
|
||||
logLevel xlog.Level
|
||||
maximumGoogleChromeRpccBufferSize int64
|
||||
defaultGoogleChromeRpccBufferSize int64
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default
|
||||
// configuration.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
maximumWaitTimeout: 30.0,
|
||||
maximumWaitDelay: 10.0,
|
||||
maximumWebhookURLTimeout: 30.0,
|
||||
defaultWaitTimeout: 10.0,
|
||||
defaultWebhookURLTimeout: 10.0,
|
||||
defaultListenPort: 3000,
|
||||
disableGoogleChrome: false,
|
||||
disableUnoconv: false,
|
||||
logLevel: xlog.InfoLevel,
|
||||
maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB
|
||||
defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FromEnv returns a Conf according
|
||||
to environment variables.
|
||||
*/
|
||||
func FromEnv() (Config, error) {
|
||||
const op string = "conf.FromEnv"
|
||||
resolver := func() (Config, error) {
|
||||
c := DefaultConfig()
|
||||
maximumWaitTimeout, err := xassert.Float64FromEnv(
|
||||
MaximumWaitTimeoutEnvVar,
|
||||
c.maximumWaitTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
c.maximumWaitTimeout = maximumWaitTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
maximumWaitDelay, err := xassert.Float64FromEnv(
|
||||
MaximumWaitDelayEnvVar,
|
||||
c.maximumWaitDelay,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
c.maximumWaitDelay = maximumWaitDelay
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
maximumWebhookURLTimeout, err := xassert.Float64FromEnv(
|
||||
MaximumWebhookURLTimeoutEnvVar,
|
||||
c.maximumWebhookURLTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
c.maximumWebhookURLTimeout = maximumWebhookURLTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultWaitTimeout, err := xassert.Float64FromEnv(
|
||||
DefaultWaitTimeoutEnvVar,
|
||||
c.defaultWaitTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
xassert.Float64NotSuperiorTo(c.maximumWaitTimeout),
|
||||
)
|
||||
c.defaultWaitTimeout = defaultWaitTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultWebhookURLTimeout, err := xassert.Float64FromEnv(
|
||||
DefaultWebhookURLTimeoutEnvVar,
|
||||
c.defaultWebhookURLTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
xassert.Float64NotSuperiorTo(c.defaultWebhookURLTimeout),
|
||||
)
|
||||
c.defaultWebhookURLTimeout = defaultWebhookURLTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultListenPort, err := xassert.Int64FromEnv(
|
||||
DefaultListenPortEnvVar,
|
||||
c.defaultListenPort,
|
||||
xassert.Int64NotInferiorTo(0),
|
||||
xassert.Int64NotSuperiorTo(65535),
|
||||
)
|
||||
c.defaultListenPort = defaultListenPort
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
disableGoogleChrome, err := xassert.BoolFromEnv(
|
||||
DisableGoogleChromeEnvVar,
|
||||
c.disableGoogleChrome,
|
||||
)
|
||||
c.disableGoogleChrome = disableGoogleChrome
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
disableUnoconv, err := xassert.BoolFromEnv(
|
||||
DisableUnoconvEnvVar,
|
||||
c.disableUnoconv,
|
||||
)
|
||||
c.disableUnoconv = disableUnoconv
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
logLevel, err := xassert.StringFromEnv(
|
||||
LogLevelEnvVar,
|
||||
string(c.logLevel),
|
||||
xassert.StringOneOf(xlog.Levels()),
|
||||
)
|
||||
c.logLevel = xlog.MustParseLevel(logLevel)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv(
|
||||
DefaultGoogleChromeRpccBufferSizeEnvVar,
|
||||
c.defaultGoogleChromeRpccBufferSize,
|
||||
xassert.Int64NotInferiorTo(0),
|
||||
xassert.Int64NotSuperiorTo(c.MaximumGoogleChromeRpccBufferSize()),
|
||||
)
|
||||
c.defaultGoogleChromeRpccBufferSize = defaultGoogleChromeRpccBufferSize
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
result, err := resolver()
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MaximumWaitTimeout returns the maximum
|
||||
// wait timeout from the configuration.
|
||||
func (c Config) MaximumWaitTimeout() float64 {
|
||||
return c.maximumWaitTimeout
|
||||
}
|
||||
|
||||
// MaximumWaitDelay returns the maximum
|
||||
// wait timeout from the configuration.
|
||||
func (c Config) MaximumWaitDelay() float64 {
|
||||
return c.maximumWaitDelay
|
||||
}
|
||||
|
||||
// MaximumWebhookURLTimeout returns the maximum
|
||||
// webhook URL wait timeout from the configuration.
|
||||
func (c Config) MaximumWebhookURLTimeout() float64 {
|
||||
return c.maximumWebhookURLTimeout
|
||||
}
|
||||
|
||||
// DefaultWaitTimeout returns the default
|
||||
// wait timeout from the configuration.
|
||||
func (c Config) DefaultWaitTimeout() float64 {
|
||||
return c.defaultWaitTimeout
|
||||
}
|
||||
|
||||
// DefaultWebhookURLTimeout returns the default
|
||||
// webhook URL wait timeout from the configuration.
|
||||
func (c Config) DefaultWebhookURLTimeout() float64 {
|
||||
return c.defaultWebhookURLTimeout
|
||||
}
|
||||
|
||||
// DefaultListenPort returns the default
|
||||
// listen port from the configuration.
|
||||
func (c Config) DefaultListenPort() int64 {
|
||||
return c.defaultListenPort
|
||||
}
|
||||
|
||||
/*
|
||||
DisableGoogleChrome returns true if
|
||||
Google Chrome is disabled in the
|
||||
configuration.
|
||||
*/
|
||||
func (c Config) DisableGoogleChrome() bool {
|
||||
return c.disableGoogleChrome
|
||||
}
|
||||
|
||||
/*
|
||||
DisableUnoconv returns true if
|
||||
Unoconv is disabled in the
|
||||
configuration.
|
||||
*/
|
||||
func (c Config) DisableUnoconv() bool {
|
||||
return c.disableUnoconv
|
||||
}
|
||||
|
||||
// LogLevel returns the xlog.Level from
|
||||
// the configuration.
|
||||
func (c Config) LogLevel() xlog.Level {
|
||||
return c.logLevel
|
||||
}
|
||||
|
||||
// MaximumGoogleChromeRpccBufferSize returns the maximum
|
||||
// Google Chrome rpcc buffer size from the configuration.
|
||||
func (c Config) MaximumGoogleChromeRpccBufferSize() int64 {
|
||||
return c.maximumGoogleChromeRpccBufferSize
|
||||
}
|
||||
|
||||
// DefaultGoogleChromeRpccBufferSize returns the default
|
||||
// Google Chrome rpcc buffer size from the configuration.
|
||||
func (c Config) DefaultGoogleChromeRpccBufferSize() int64 {
|
||||
return c.defaultGoogleChromeRpccBufferSize
|
||||
}
|
||||
373
internal/pkg/conf/conf_test.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestEmptyFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// no environment variables set,
|
||||
// values should be equal to default config.
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestMaximumWaitTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// MAXIMUM_WAIT_TIMEOUT correctly set.
|
||||
os.Setenv(MaximumWaitTimeoutEnvVar, "10.0")
|
||||
expected = DefaultConfig()
|
||||
expected.maximumWaitTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWaitTimeoutEnvVar)
|
||||
// MAXIMUM_WAIT_TIMEOUT wrongly set.
|
||||
os.Setenv(MaximumWaitTimeoutEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWaitTimeoutEnvVar)
|
||||
// MAXIMUM_WAIT_TIMEOUT < 0.
|
||||
os.Setenv(MaximumWaitTimeoutEnvVar, "-1.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWaitTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestMaximumWaitDelayFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// MAXIMUM_WAIT_DELAY correctly set.
|
||||
os.Setenv(MaximumWaitDelayEnvVar, "10.0")
|
||||
expected = DefaultConfig()
|
||||
expected.maximumWaitDelay = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWaitDelayEnvVar)
|
||||
// MAXIMUM_WAIT_DELAY wrongly set.
|
||||
os.Setenv(MaximumWaitDelayEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWaitDelayEnvVar)
|
||||
// MAXIMUM_WAIT_DELAY < 0.
|
||||
os.Setenv(MaximumWaitDelayEnvVar, "-1.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWaitDelayEnvVar)
|
||||
}
|
||||
|
||||
func TestMaximumWebhookURLTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// MAXIMUM_WEBHOOK_URL_TIMEOUT correctly set.
|
||||
os.Setenv(MaximumWebhookURLTimeoutEnvVar, "10.0")
|
||||
expected = DefaultConfig()
|
||||
expected.maximumWebhookURLTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWebhookURLTimeoutEnvVar)
|
||||
// MAXIMUM_WEBHOOK_URL_TIMEOUT wrongly set.
|
||||
os.Setenv(MaximumWebhookURLTimeoutEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWebhookURLTimeoutEnvVar)
|
||||
// MAXIMUM_WEBHOOK_URL_TIMEOUT < 0.
|
||||
os.Setenv(MaximumWebhookURLTimeoutEnvVar, "-1.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(MaximumWebhookURLTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultWaitTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_WAIT_TIMEOUT correctly set.
|
||||
os.Setenv(DefaultWaitTimeoutEnvVar, "10.0")
|
||||
expected = DefaultConfig()
|
||||
expected.defaultWaitTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWaitTimeoutEnvVar)
|
||||
// DEFAULT_WAIT_TIMEOUT wrongly set.
|
||||
os.Setenv(DefaultWaitTimeoutEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWaitTimeoutEnvVar)
|
||||
// DEFAULT_WAIT_TIMEOUT < 0.
|
||||
os.Setenv(DefaultWaitTimeoutEnvVar, "-1.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWaitTimeoutEnvVar)
|
||||
// DEFAULT_WAIT_TIMEOUT > MAXIMUM_WAIT_TIMEOUT.
|
||||
os.Setenv(DefaultWaitTimeoutEnvVar, "40.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWaitTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultWebhookURLTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT correctly set.
|
||||
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "10.0")
|
||||
expected = DefaultConfig()
|
||||
expected.defaultWebhookURLTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT wrongly set.
|
||||
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT < 0.
|
||||
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "-1.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT > MAXIMUM_WEBHOOK_URL_TIMEOUT.
|
||||
os.Setenv(DefaultWebhookURLTimeoutEnvVar, "40.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultWebhookURLTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultListenPortFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_LISTEN_PORT correctly set.
|
||||
os.Setenv(DefaultListenPortEnvVar, "80")
|
||||
expected = DefaultConfig()
|
||||
expected.defaultListenPort = 80
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultListenPortEnvVar)
|
||||
// DEFAULT_LISTEN_PORT wrongly set.
|
||||
os.Setenv(DefaultListenPortEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultListenPortEnvVar)
|
||||
// DEFAULT_LISTEN_PORT < 0.
|
||||
os.Setenv(DefaultListenPortEnvVar, "-1.0")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultListenPortEnvVar)
|
||||
// DEFAULT_LISTEN_PORT > 65535.
|
||||
os.Setenv(DefaultListenPortEnvVar, "65536")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultListenPortEnvVar)
|
||||
}
|
||||
|
||||
func TestDisableGoogleChromeFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DISABLE_GOOGLE_CHROME correctly set.
|
||||
os.Setenv(DisableGoogleChromeEnvVar, "1")
|
||||
expected = DefaultConfig()
|
||||
expected.disableGoogleChrome = true
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DisableGoogleChromeEnvVar)
|
||||
os.Setenv(DisableGoogleChromeEnvVar, "0")
|
||||
expected = DefaultConfig()
|
||||
expected.disableGoogleChrome = false
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DisableGoogleChromeEnvVar)
|
||||
// DISABLE_GOOGLE_CHROME wrongly set.
|
||||
os.Setenv(DisableGoogleChromeEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DisableGoogleChromeEnvVar)
|
||||
}
|
||||
|
||||
func TestDisableUnoconvFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DISABLE_UNOCONV correctly set.
|
||||
os.Setenv(DisableUnoconvEnvVar, "1")
|
||||
expected = DefaultConfig()
|
||||
expected.disableUnoconv = true
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DisableUnoconvEnvVar)
|
||||
os.Setenv(DisableUnoconvEnvVar, "0")
|
||||
expected = DefaultConfig()
|
||||
expected.disableUnoconv = false
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DisableUnoconvEnvVar)
|
||||
// DISABLE_UNOCONV wrongly set.
|
||||
os.Setenv(DisableUnoconvEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DisableUnoconvEnvVar)
|
||||
}
|
||||
|
||||
func TestLogLevelFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// LOG_LEVEL correctly set.
|
||||
os.Setenv(LogLevelEnvVar, "DEBUG")
|
||||
expected = DefaultConfig()
|
||||
expected.logLevel = xlog.DebugLevel
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(LogLevelEnvVar)
|
||||
os.Setenv(LogLevelEnvVar, "INFO")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(LogLevelEnvVar)
|
||||
os.Setenv(LogLevelEnvVar, "ERROR")
|
||||
expected = DefaultConfig()
|
||||
expected.logLevel = xlog.ErrorLevel
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(LogLevelEnvVar)
|
||||
// LOG_LEVEL wrongly set.
|
||||
os.Setenv(LogLevelEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(LogLevelEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE correctly set.
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "100")
|
||||
expected = DefaultConfig()
|
||||
expected.defaultGoogleChromeRpccBufferSize = 100
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE wrongly set.
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "foo")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE < 0.
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "-1")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
// DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE > 100 MB (maximumGoogleChromeRpccBufferSize).
|
||||
os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "104857601")
|
||||
expected = DefaultConfig()
|
||||
result, err = FromEnv()
|
||||
test.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar)
|
||||
}
|
||||
|
||||
func TestGetters(t *testing.T) {
|
||||
result := DefaultConfig()
|
||||
assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout())
|
||||
assert.Equal(t, result.maximumWaitDelay, result.MaximumWaitDelay())
|
||||
assert.Equal(t, result.maximumWebhookURLTimeout, result.MaximumWebhookURLTimeout())
|
||||
assert.Equal(t, result.defaultWaitTimeout, result.DefaultWaitTimeout())
|
||||
assert.Equal(t, result.defaultWebhookURLTimeout, result.DefaultWebhookURLTimeout())
|
||||
assert.Equal(t, result.defaultListenPort, result.DefaultListenPort())
|
||||
assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome())
|
||||
assert.Equal(t, result.disableUnoconv, result.DisableUnoconv())
|
||||
assert.Equal(t, result.logLevel, result.LogLevel())
|
||||
assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize())
|
||||
assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize())
|
||||
}
|
||||
3
internal/pkg/conf/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package conf gathers all
|
||||
// configuration data.
|
||||
package conf
|
||||
9
internal/pkg/normalize/doc.go
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Package normalize helps removing special
|
||||
characters from a string.
|
||||
|
||||
Fixes: https://github.com/thecodingmachine/gotenberg/issues/104
|
||||
|
||||
Credits: https://medium.com/@swdream/golang-remove-all-accents-in-string-319abf6a7f5b
|
||||
*/
|
||||
package normalize
|
||||
21
internal/pkg/normalize/normalize.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// String normalizes given string.
|
||||
func String(str string) (string, error) {
|
||||
const op string = "normalize.String"
|
||||
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
result, _, err := transform.String(t, str)
|
||||
if err != nil {
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
17
internal/pkg/normalize/normalize_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
const (
|
||||
toNormalize string = "analise da aplicação"
|
||||
expected string = "analise da aplicacao"
|
||||
)
|
||||
v, err := String(toNormalize)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, v)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/*
|
||||
Package notify is used across the application
|
||||
to display nice outputs to the user.
|
||||
*/
|
||||
package notify
|
||||
@@ -1,30 +0,0 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/gommon/color"
|
||||
)
|
||||
|
||||
var (
|
||||
stdout *color.Color
|
||||
stderr *color.Color
|
||||
)
|
||||
|
||||
func init() {
|
||||
stdout = color.New()
|
||||
stdout.SetOutput(os.Stdout)
|
||||
stderr = color.New()
|
||||
stderr.SetOutput(os.Stderr)
|
||||
}
|
||||
|
||||
// Println prints a message to stdout.
|
||||
func Println(message string) {
|
||||
stdout.Printf("⇨ %s\n", message)
|
||||
}
|
||||
|
||||
// ErrPrintln prints an error to stderr.
|
||||
func ErrPrintln(err error) {
|
||||
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
||||
}
|
||||