mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 20:32:13 +01:00
Merge branch '6.0.0' into noto-emoji-font
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
@@ -17,9 +17,7 @@ stages:
|
|||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- stage: tests
|
- stage: tests
|
||||||
script: make lint
|
script: make lint tests
|
||||||
- stage: tests
|
|
||||||
script: make tests
|
|
||||||
- stage: publish
|
- stage: publish
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS
|
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS
|
||||||
51
Makefile
51
Makefile
@@ -2,11 +2,26 @@ GOLANG_VERSION=1.12
|
|||||||
VERSION=snapshot
|
VERSION=snapshot
|
||||||
DOCKER_USER=
|
DOCKER_USER=
|
||||||
DOCKER_PASSWORD=
|
DOCKER_PASSWORD=
|
||||||
|
DOCKER_REPOSITORY=thecodingmachine
|
||||||
|
GOLANGCI_LINT_VERSION=1.17.1
|
||||||
|
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
|
||||||
|
|
||||||
# generate documentation.
|
# build the base Docker image.
|
||||||
doc:
|
base:
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:docs -f build/docs/Dockerfile .
|
docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
|
||||||
docker run --rm -it -v "$(PWD):/docs" thecodingmachine/gotenberg:docs
|
|
||||||
|
# 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.
|
# gofmt and goimports all go files.
|
||||||
fmt:
|
fmt:
|
||||||
@@ -15,24 +30,32 @@ fmt:
|
|||||||
|
|
||||||
# run all linters.
|
# run all linters.
|
||||||
lint:
|
lint:
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:lint -f build/lint/Dockerfile .
|
make workspace
|
||||||
docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg:lint
|
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile .
|
||||||
|
docker run --rm -it $(DOCKER_REPOSITORY)/gotenberg:lint
|
||||||
|
|
||||||
# run all tests.
|
# run all tests.
|
||||||
tests:
|
tests:
|
||||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
make workspace
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:tests -f build/tests/Dockerfile .
|
docker build -t $(DOCKER_REPOSITORY)/gotenberg:tests -f build/tests/Dockerfile .
|
||||||
docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg:tests
|
docker run --rm -it $(DOCKER_REPOSITORY)/gotenberg:tests
|
||||||
|
|
||||||
# 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:
|
image:
|
||||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
make workspace
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
docker build --build-arg VERSION=$(VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||||
|
|
||||||
# start the API using previously built Docker image.
|
# start the API using previously built Docker image.
|
||||||
gotenberg:
|
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) -p "3000:$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||||
|
|
||||||
# publish Gotenberg images according to version.
|
# publish Gotenberg images according to version.
|
||||||
publish:
|
publish:
|
||||||
./scripts/publish.sh $(GOLANG_VERSION) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
make workspace
|
||||||
|
./scripts/publish.sh $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:9.5-slim
|
FROM debian:9-slim
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Common libraries
|
# | Common libraries
|
||||||
@@ -16,7 +16,7 @@ RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free"
|
|||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# |
|
# |
|
||||||
# | Installs PM2 for launching programs in background and with failure
|
# | Installs PM2 for launching programs in background and with failure
|
||||||
# | recovering. In our case: Chrome (headless) and Office (headless).
|
# | recovering. In our case: Google Chrome (headless) and unoconv.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
||||||
@@ -94,3 +94,17 @@ COPY build/base/* /usr/share/fonts/
|
|||||||
|
|
||||||
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
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
|
||||||
|
|
||||||
|
ENV PM2_HOME=/gotenberg/.pm2
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
ARG GOLANG_VERSION
|
FROM thecodingmachine/gotenberg:workspace
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | static
|
# | static
|
||||||
@@ -19,6 +17,6 @@ RUN go get github.com/apex/static/cmd/static-docs
|
|||||||
# | Last instructions of this build.
|
# | Last instructions of this build.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
WORKDIR /docs
|
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." ]
|
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." ]
|
||||||
@@ -10,6 +10,8 @@ You may start it with:
|
|||||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
|
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
All processes in the Docker container runs as a dedicated non-root user called `gotenberg` with user id `1001` from the working dir `/gotenberg`.
|
||||||
|
|
||||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
## Docker Compose
|
## Docker Compose
|
||||||
@@ -38,5 +40,12 @@ Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoc
|
|||||||
|
|
||||||
> The more resources are granted, the quicker will be the conversions.
|
> The more resources are granted, the quicker will be the conversions.
|
||||||
|
|
||||||
|
Also, in the deployment spec of the pod, specify the uid `1001` of the user `gotenberg`:
|
||||||
|
```
|
||||||
|
securityContext:
|
||||||
|
privileged: false
|
||||||
|
runAsUser: 1001
|
||||||
|
```
|
||||||
|
|
||||||
In the following examples, we will assume your
|
In the following examples, we will assume your
|
||||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||||
@@ -8,7 +8,7 @@ You may customize the API behaviour thanks to environment variables.
|
|||||||
|
|
||||||
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`.
|
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`.
|
||||||
|
|
||||||
It takes the strings `"0"` or `"1"` as value.
|
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:
|
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
|
||||||
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
||||||
@@ -37,7 +37,7 @@ By default, the API will add a log entry when the [healthcheck endpoint](#ping)
|
|||||||
|
|
||||||
You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable `DISABLE_HEALTHCHECK_LOGGING`.
|
You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable `DISABLE_HEALTHCHECK_LOGGING`.
|
||||||
|
|
||||||
This environment variable operates in the same manner as the `DISABLE_GOOGLE_CHROME` and `DISABLE_UNOCONV` variables operate in that it accepts the strings `"0"` or `"1"` as values.
|
This environment variable operates in the same manner as the `DISABLE_GOOGLE_CHROME` and `DISABLE_UNOCONV` variables operate in that it accepts the strings `"0"` or `"1"` as values, where `"1"` is enabled.
|
||||||
|
|
||||||
## Default listen port
|
## Default listen port
|
||||||
|
|
||||||
@@ -45,4 +45,12 @@ By default, the API will listen on port `3000`. For most use cases this is perfe
|
|||||||
|
|
||||||
You may customize this port location with the environment variable `DEFAULT_LISTEN_PORT`.
|
You may customize this port location with the environment variable `DEFAULT_LISTEN_PORT`.
|
||||||
|
|
||||||
This environment variable accepts any string that can be turned into a port number (e.g., the string `"0"` up to the string `"65535"`).
|
This environment variable accepts any string that can be turned into a port number (e.g., the string `"0"` up to the string `"65535"`).
|
||||||
|
|
||||||
|
## Debug logging of process startup
|
||||||
|
|
||||||
|
By default, `stdout` and `stderr` messages from the started processes are disabled.
|
||||||
|
|
||||||
|
You may enable some debug logging from starting the process by setting the environment variable `DEBUG_PROCESS_STARTUP`.
|
||||||
|
|
||||||
|
This environment variable operates in the same manner as the `DISABLE_GOOGLE_CHROME` and `DISABLE_UNOCONV` variables operate in that it accepts the strings `"0"` or `"1"` as values, where `1` means `true`.
|
||||||
|
|||||||
@@ -296,7 +296,8 @@ $client->store($request, $dest);
|
|||||||
## Wait delay
|
## Wait delay
|
||||||
|
|
||||||
In some cases, you may want to wait a certain amount of time to make sure the
|
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.
|
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).
|
> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds).
|
||||||
|
|
||||||
@@ -339,4 +340,4 @@ $request = new HTMLRequest($index);
|
|||||||
$request->setWaitDelay(5.5);
|
$request->setWaitDelay(5.5);
|
||||||
$dest = "result.pdf";
|
$dest = "result.pdf";
|
||||||
$client->store($request, $dest);
|
$client->store($request, $dest);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
ARG GOLANG_VERSION
|
FROM thecodingmachine/gotenberg:workspace
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | GolangCI-Lint
|
# | GolangCI-Lint
|
||||||
@@ -10,7 +8,7 @@ FROM golang:${GOLANG_VERSION}-stretch
|
|||||||
# | than gometalinter.
|
# | than gometalinter.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
ENV GOLANGCI_LINT_VERSION 1.16.0
|
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} &&\
|
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
|
golangci-lint --version
|
||||||
@@ -22,14 +20,15 @@ RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.s
|
|||||||
# | Last instructions of this build.
|
# | Last instructions of this build.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
# Define our workding outside of $GOPATH (we're using go modules).
|
# Define our working directory outside of $GOPATH (we're using go modules).
|
||||||
WORKDIR /lint
|
USER gotenberg
|
||||||
|
WORKDIR /gotenberg/lint
|
||||||
|
|
||||||
# Copy our module dependencies definitions.
|
# Copy our code source.
|
||||||
COPY go.mod .
|
COPY --chown=gotenberg:gotenberg . .
|
||||||
COPY go.sum .
|
|
||||||
|
|
||||||
# Install module dependencies.
|
# Install module dependencies.
|
||||||
RUN go mod download
|
RUN go mod download &&\
|
||||||
|
go mod verify
|
||||||
|
|
||||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
ARG GOLANG_VERSION
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Binary
|
# | Binary
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
@@ -7,7 +5,7 @@ ARG GOLANG_VERSION
|
|||||||
# | Buils Gotenberg binary.
|
# | Buils Gotenberg binary.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
FROM thecodingmachine/gotenberg:workspace AS workspace
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
|
||||||
@@ -16,13 +14,16 @@ ENV GOOS=linux \
|
|||||||
CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
|
|
||||||
# Define our workding outside of $GOPATH (we're using go modules).
|
# Define our workding outside of $GOPATH (we're using go modules).
|
||||||
WORKDIR /gotenberg
|
WORKDIR /gotenberg/package
|
||||||
|
|
||||||
# Copy our source code.
|
# Copy our source code.
|
||||||
COPY . .
|
COPY internal ./internal
|
||||||
|
COPY cmd ./cmd
|
||||||
|
COPY go.sum go.sum
|
||||||
|
COPY go.mod go.mod
|
||||||
|
|
||||||
# Build our binary.
|
# 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
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Final touch
|
# | Final touch
|
||||||
@@ -35,8 +36,9 @@ FROM thecodingmachine/gotenberg:base
|
|||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
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
|
WORKDIR /gotenberg
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -1,48 +1,14 @@
|
|||||||
ARG GOLANG_VERSION
|
FROM thecodingmachine/gotenberg:workspace
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
|
||||||
|
|
||||||
FROM thecodingmachine/gotenberg:base
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Common libraries
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Libraries used in the build process of this image.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN apt-get install -y git gcc
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Golang
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs Golang.
|
|
||||||
# |
|
|
||||||
|
|
||||||
COPY --from=golang /usr/local/go /usr/local/go
|
|
||||||
|
|
||||||
RUN export PATH="/usr/local/go/bin:$PATH" &&\
|
|
||||||
go version
|
|
||||||
|
|
||||||
ENV GOPATH /go
|
|
||||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Final touch
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Last instructions of this build.
|
|
||||||
# |
|
|
||||||
|
|
||||||
# Define our workding outside of $GOPATH (we're using go modules).
|
# Define our workding outside of $GOPATH (we're using go modules).
|
||||||
WORKDIR /tests
|
USER gotenberg
|
||||||
|
WORKDIR /gotenberg/tests
|
||||||
|
|
||||||
# Copy our module dependencies definitions.
|
# Copy our code source.
|
||||||
COPY go.mod .
|
COPY --chown=gotenberg:gotenberg . .
|
||||||
COPY go.sum .
|
|
||||||
|
|
||||||
# Install module dependencies.
|
# Install module dependencies.
|
||||||
RUN go mod download
|
RUN go mod download &&\
|
||||||
|
go mod verify
|
||||||
|
|
||||||
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
||||||
@@ -2,16 +2,33 @@
|
|||||||
|
|
||||||
set -xe
|
set -xe
|
||||||
|
|
||||||
|
# Make sure the user running the
|
||||||
|
# tests is the Gotenberg user.
|
||||||
|
CURRENT_USER=$(whoami)
|
||||||
|
if [ "$CURRENT_USER" != "gotenberg" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the PM2 processes
|
||||||
|
# (Google Chrome headless & unoconv listener).
|
||||||
|
go run github.com/thecodingmachine/gotenberg/test/cmd/pm2
|
||||||
|
|
||||||
|
# Run our tests.
|
||||||
|
go test -race -cover ./...
|
||||||
|
|
||||||
# Testing PM2 processes launch separatly for avoiding
|
# Testing PM2 processes launch separatly for avoiding
|
||||||
# spending to much time on each tests depending on
|
# spending to much time on each tests depending on
|
||||||
# them.
|
# them.
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
||||||
|
|
||||||
# Running others tests.
|
# 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/config
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
#go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/random
|
||||||
|
#go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/standarderror
|
||||||
|
#go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/timeout
|
||||||
|
#go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
||||||
|
|
||||||
# Finally testing processes shutdown.
|
# 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 TestChromeShutdown
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
||||||
52
build/workspace/Dockerfile
Normal file
52
build/workspace/Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
ARG GOLANG_VERSION
|
||||||
|
|
||||||
|
FROM golang:${GOLANG_VERSION}-stretch as golang
|
||||||
|
|
||||||
|
FROM thecodingmachine/gotenberg:base
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | Common libraries
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# |
|
||||||
|
# | Libraries used in the build process of this image.
|
||||||
|
# |
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
git \
|
||||||
|
g++ \
|
||||||
|
gcc \
|
||||||
|
libc6-dev \
|
||||||
|
make \
|
||||||
|
pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | Golang
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# |
|
||||||
|
# | Installs Golang.
|
||||||
|
# |
|
||||||
|
|
||||||
|
COPY --from=golang /usr/local/go /usr/local/go
|
||||||
|
|
||||||
|
ENV GOPATH /gotenberg/go
|
||||||
|
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||||
|
|
||||||
|
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" &&\
|
||||||
|
chmod -R 777 "$GOPATH"
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | Final touch
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# |
|
||||||
|
# | Last instructions of this build.
|
||||||
|
# |
|
||||||
|
|
||||||
|
# Make sure the Gotenber user is able to
|
||||||
|
# call the Go binary.
|
||||||
|
USER gotenberg
|
||||||
|
|
||||||
|
RUN go version &&\
|
||||||
|
go env
|
||||||
|
|
||||||
|
USER root
|
||||||
@@ -1,137 +1,55 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/thecodingmachine/gotenberg/internal/app/xhttp"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set on build time.
|
// version will be set on build time.
|
||||||
// nolint: gochecknoglobals
|
// nolint: gochecknoglobals
|
||||||
var version = "snapshot"
|
var version = "snapshot"
|
||||||
|
|
||||||
const (
|
|
||||||
defaultWaitTimeoutEnvVar = "DEFAULT_WAIT_TIMEOUT"
|
|
||||||
defaultListenPortEnvVar = "DEFAULT_LISTEN_PORT"
|
|
||||||
disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME"
|
|
||||||
disableUnoconvEnvVar = "DISABLE_UNOCONV"
|
|
||||||
disableHealthcheckLoggingEnvVar = "DISABLE_HEALTHCHECK_LOGGING"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mustParseEnvVar() *api.Options {
|
|
||||||
opts := api.DefaultOptions()
|
|
||||||
if os.Getenv(defaultWaitTimeoutEnvVar) != "" {
|
|
||||||
defaultWaitTimeout, err := strconv.ParseFloat(os.Getenv(defaultWaitTimeoutEnvVar), 64)
|
|
||||||
if err != nil {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want float got %v", defaultWaitTimeoutEnvVar, err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.DefaultWaitTimeout = defaultWaitTimeout
|
|
||||||
}
|
|
||||||
if v, ok := os.LookupEnv(defaultListenPortEnvVar); ok {
|
|
||||||
defaultListener, err := strconv.ParseUint(os.Getenv(defaultListenPortEnvVar), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want uint got %v", defaultListenPortEnvVar, err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if defaultListener > 65535 {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want uint < 65535 got %v", defaultListenPortEnvVar, defaultListener))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.DefaultListenPort = v
|
|
||||||
}
|
|
||||||
if v, ok := os.LookupEnv(disableGoogleChromeEnvVar); ok {
|
|
||||||
if v != "1" && v != "0" {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableGoogleChromeEnvVar, v))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.EnableChromeEndpoints = v != "1"
|
|
||||||
}
|
|
||||||
if v, ok := os.LookupEnv(disableUnoconvEnvVar); ok {
|
|
||||||
if v != "1" && v != "0" {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableUnoconvEnvVar, v))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.EnableUnoconvEndpoints = v != "1"
|
|
||||||
}
|
|
||||||
if v, ok := os.LookupEnv(disableHealthcheckLoggingEnvVar); ok {
|
|
||||||
if v != "1" && v != "0" {
|
|
||||||
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableHealthcheckLoggingEnvVar, v))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
opts.EnableHealthcheckLogging = v != "1"
|
|
||||||
}
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustStartProcesses(opts *api.Options) []pm2.Process {
|
|
||||||
var processes []pm2.Process
|
|
||||||
if opts.EnableChromeEndpoints {
|
|
||||||
processes = append(processes, pm2.NewChrome())
|
|
||||||
}
|
|
||||||
if opts.EnableUnoconvEndpoints {
|
|
||||||
processes = append(processes, pm2.NewUnoconv())
|
|
||||||
}
|
|
||||||
for _, p := range processes {
|
|
||||||
notify.Printf("starting %s with PM2...", p.Fullname())
|
|
||||||
if err := p.Start(); err != nil {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return processes
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustStartAPI(srv *echo.Echo, port string) {
|
|
||||||
notify.Printf("http server started on port %v", port)
|
|
||||||
if err := srv.Start(fmt.Sprintf(":%v", port)); err != nil {
|
|
||||||
if err != http.ErrServerClosed {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustShutdownProcesses(processes []pm2.Process) {
|
|
||||||
for _, p := range processes {
|
|
||||||
notify.Printf("shutting down %s with PM2... (Ctrl+C to force)", p.Fullname())
|
|
||||||
if err := p.Shutdown(); err != nil {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustShutdownAPI(srv *echo.Echo) {
|
|
||||||
// create a deadline to wait for.
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
// doesn't block if no connections, but will otherwise wait
|
|
||||||
// until the timeout deadline.
|
|
||||||
notify.Print("shutting down http server... (Ctrl+C to force)")
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
|
||||||
notify.ErrPrint(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
notify.Printf("Gotenberg %s", version)
|
const op = "main"
|
||||||
opts := mustParseEnvVar()
|
config, err := conf.FromEnv()
|
||||||
srv := api.New(opts)
|
systemLogger := xlog.New(config.LogLevel(), "system")
|
||||||
processes := mustStartProcesses(opts)
|
if err != nil {
|
||||||
// run our API in a goroutine so that it doesn't block.s
|
systemLogger.FatalOp(op, err)
|
||||||
|
}
|
||||||
|
systemLogger.InfofOp(op, "Gotenberg %s", version)
|
||||||
|
systemLogger.DebugfOp(op, "configuration: %+v", config)
|
||||||
|
// start PM2 processes.
|
||||||
|
var processes []pm2.Process
|
||||||
|
if !config.DisableGoogleChrome() {
|
||||||
|
processes = append(processes, pm2.NewChromeProcess(systemLogger))
|
||||||
|
}
|
||||||
|
if !config.DisableUnoconv() {
|
||||||
|
processes = append(processes, pm2.NewUnoconvProcess(systemLogger))
|
||||||
|
}
|
||||||
|
for _, p := range processes {
|
||||||
|
systemLogger.InfofOp(op, "starting '%s' with PM2...", p.Fullname())
|
||||||
|
if err := p.Start(); err != nil {
|
||||||
|
systemLogger.FatalOp(op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create our API.
|
||||||
|
srv := xhttp.New(config, processes...)
|
||||||
|
// run our API in a goroutine so that it doesn't block.
|
||||||
go func() {
|
go func() {
|
||||||
mustStartAPI(srv, opts.DefaultListenPort)
|
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)
|
quit := make(chan os.Signal, 1)
|
||||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||||
@@ -139,8 +57,22 @@ func main() {
|
|||||||
signal.Notify(quit, os.Interrupt)
|
signal.Notify(quit, os.Interrupt)
|
||||||
// block until we receive our signal.
|
// block until we receive our signal.
|
||||||
<-quit
|
<-quit
|
||||||
mustShutdownAPI(srv)
|
// create a deadline to wait for.
|
||||||
mustShutdownProcesses(processes)
|
ctx, cancel := xcontext.WithTimeout(systemLogger, 120)
|
||||||
notify.Print("bye!")
|
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)
|
||||||
|
}
|
||||||
|
// shutdown PM2 processes.
|
||||||
|
for _, p := range processes {
|
||||||
|
systemLogger.InfofOp(op, "shutting down '%s' with PM2...", p.Fullname())
|
||||||
|
if err := p.Stop(); err != nil {
|
||||||
|
systemLogger.FatalOp(op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
systemLogger.InfoOp(op, "bye!")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,8 @@
|
|||||||
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:5
|
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:5
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
<p>All processes in the Docker container runs as a dedicated non-root user called <code>gotenberg</code> with user id <code>1001</code> from the working dir <code>/gotenberg</code>.</p>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
@@ -176,6 +178,13 @@ Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoc
|
|||||||
<p>The more resources are granted, the quicker will be the conversions.</p>
|
<p>The more resources are granted, the quicker will be the conversions.</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
|
<p>Also, in the deployment spec of the pod, specify the uid <code>1001</code> of the user <code>gotenberg</code>:</p>
|
||||||
|
|
||||||
|
<pre class="chroma">securityContext:
|
||||||
|
privileged: false
|
||||||
|
runAsUser: 1001
|
||||||
|
</pre>
|
||||||
|
|
||||||
<p>In the following examples, we will assume your
|
<p>In the following examples, we will assume your
|
||||||
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
||||||
|
|
||||||
@@ -222,7 +231,7 @@ Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3
|
|||||||
|
|
||||||
<p>In order to save some resources, the Gotenberg image accepts the environment variable <code>DISABLE_GOOGLE_CHROME</code>.</p>
|
<p>In order to save some resources, the Gotenberg image accepts the environment variable <code>DISABLE_GOOGLE_CHROME</code>.</p>
|
||||||
|
|
||||||
<p>It takes the strings <code>"0"</code> or <code>"1"</code> as value.</p>
|
<p>It takes the strings <code>"0"</code> or <code>"1"</code> as value where <code>1</code> means <code>true</code></p>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>If Google Chrome is disabled, the following conversions will <strong>not</strong> be available anymore:
|
<p>If Google Chrome is disabled, the following conversions will <strong>not</strong> be available anymore:
|
||||||
@@ -263,7 +272,7 @@ See the <a href="#timeout">timeout section</a>.</p>
|
|||||||
|
|
||||||
<p>You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable <code>DISABLE_HEALTHCHECK_LOGGING</code>.</p>
|
<p>You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable <code>DISABLE_HEALTHCHECK_LOGGING</code>.</p>
|
||||||
|
|
||||||
<p>This environment variable operates in the same manner as the <code>DISABLE_GOOGLE_CHROME</code> and <code>DISABLE_UNOCONV</code> variables operate in that it accepts the strings <code>"0"</code> or <code>"1"</code> as values.</p>
|
<p>This environment variable operates in the same manner as the <code>DISABLE_GOOGLE_CHROME</code> and <code>DISABLE_UNOCONV</code> variables operate in that it accepts the strings <code>"0"</code> or <code>"1"</code> as values, where <code>"1"</code> is enabled.</p>
|
||||||
|
|
||||||
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.default_listen_port" href="#environment_variables.default_listen_port">
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.default_listen_port" href="#environment_variables.default_listen_port">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||||
@@ -275,6 +284,16 @@ See the <a href="#timeout">timeout section</a>.</p>
|
|||||||
|
|
||||||
<p>This environment variable accepts any string that can be turned into a port number (e.g., the string <code>"0"</code> up to the string <code>"65535"</code>).</p>
|
<p>This environment variable accepts any string that can be turned into a port number (e.g., the string <code>"0"</code> up to the string <code>"65535"</code>).</p>
|
||||||
|
|
||||||
|
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.debug_logging_of_process_startup" href="#environment_variables.debug_logging_of_process_startup">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||||
|
</a>Debug logging of process startup</h2>
|
||||||
|
|
||||||
|
<p>By default, <code>stdout</code> and <code>stderr</code> messages from the started processes are disabled.</p>
|
||||||
|
|
||||||
|
<p>You may enable some debug logging from starting the process by setting the environment variable <code>DEBUG_PROCESS_STARTUP</code>.</p>
|
||||||
|
|
||||||
|
<p>This environment variable operates in the same manner as the <code>DISABLE_GOOGLE_CHROME</code> and <code>DISABLE_UNOCONV</code> variables operate in that it accepts the strings <code>"0"</code> or <code>"1"</code> as values, where <code>1</code> means <code>true</code>.</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="Page" id="html">
|
<div class="Page" id="html">
|
||||||
@@ -601,7 +620,8 @@ $client->store($request, $dest);
|
|||||||
</a>Wait delay</h2>
|
</a>Wait delay</h2>
|
||||||
|
|
||||||
<p>In some cases, you may want to wait a certain amount of time to make sure the
|
<p>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.</p>
|
page you’re trying to generate is fully rendered. For instance, if your page relies
|
||||||
|
a lot on JavaScript for rendering.</p>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>The wait delay is a duration in <strong>seconds</strong> (e.g <code>2.5</code> for 2.5 seconds).</p>
|
<p>The wait delay is a duration in <strong>seconds</strong> (e.g <code>2.5</code> for 2.5 seconds).</p>
|
||||||
|
|||||||
27
go.mod
27
go.mod
@@ -3,21 +3,20 @@ module github.com/thecodingmachine/gotenberg
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/google/go-cmp v0.3.0 // indirect
|
||||||
github.com/google/go-cmp v0.2.0 // indirect
|
|
||||||
github.com/gorilla/websocket v1.4.0 // indirect
|
github.com/gorilla/websocket v1.4.0 // indirect
|
||||||
github.com/labstack/echo/v4 v4.0.0
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
github.com/labstack/gommon v0.2.8
|
github.com/labstack/echo/v4 v4.1.6
|
||||||
github.com/mafredri/cdp v0.22.0
|
github.com/labstack/gommon v0.2.9
|
||||||
github.com/mattn/go-colorable v0.1.1 // indirect
|
github.com/mafredri/cdp v0.23.4
|
||||||
github.com/mattn/go-isatty v0.0.7 // indirect
|
github.com/mattn/go-isatty v0.0.8
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1
|
github.com/microcosm-cc/bluemonday v1.0.2
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/russross/blackfriday/v2 v2.0.1
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.4.2
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
81
go.sum
81
go.sum
@@ -3,51 +3,66 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/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/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.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
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/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/mafredri/cdp v0.22.0 h1:BV17j8hXLDWczo2SZIAFuOjMpQMIOq5DOcd9sgB2hv0=
|
github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv4=
|
||||||
github.com/mafredri/cdp v0.22.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
|
||||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
github.com/mafredri/cdp v0.23.4 h1:ffp4qq6slfCL4rFWBDeRHapkLE776gER4tX5Z3LS8CY=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
github.com/mafredri/cdp v0.23.4/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||||
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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 v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
|
||||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||||
|
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
const pingEndpoint = "/ping"
|
|
||||||
|
|
||||||
// Options allows to customize the behaviour
|
|
||||||
// of the API.
|
|
||||||
type Options struct {
|
|
||||||
DefaultWaitTimeout float64
|
|
||||||
DefaultListenPort string
|
|
||||||
EnableChromeEndpoints bool
|
|
||||||
EnableUnoconvEndpoints bool
|
|
||||||
EnableHealthcheckLogging bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultOptions returns default options.
|
|
||||||
func DefaultOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
DefaultWaitTimeout: 10,
|
|
||||||
DefaultListenPort: "3000",
|
|
||||||
EnableChromeEndpoints: true,
|
|
||||||
EnableUnoconvEndpoints: true,
|
|
||||||
EnableHealthcheckLogging: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns an API.
|
|
||||||
func New(opts *Options) *echo.Echo {
|
|
||||||
api := echo.New()
|
|
||||||
api.HideBanner = true
|
|
||||||
api.HidePort = true
|
|
||||||
api.Use(handleLogging(opts.EnableHealthcheckLogging))
|
|
||||||
api.GET(pingEndpoint, func(c echo.Context) error { return nil })
|
|
||||||
g := api.Group("/convert")
|
|
||||||
g.Use(handleContext(opts))
|
|
||||||
g.Use(handleError())
|
|
||||||
g.POST("/merge", merge)
|
|
||||||
if !opts.EnableChromeEndpoints && !opts.EnableUnoconvEndpoints {
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
if opts.EnableChromeEndpoints {
|
|
||||||
g.POST("/html", convertHTML)
|
|
||||||
g.POST("/url", convertURL)
|
|
||||||
g.POST("/markdown", convertMarkdown)
|
|
||||||
}
|
|
||||||
if opts.EnableUnoconvEndpoints {
|
|
||||||
g.POST("/office", convertOffice)
|
|
||||||
}
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultWaitTimeout(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.DefaultWaitTimeout = 0
|
|
||||||
srv := New(opts)
|
|
||||||
// testing if timeout.
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
// testing if no timeout.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "10"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisableChromeEndpoints(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.EnableChromeEndpoints = false
|
|
||||||
srv := New(opts)
|
|
||||||
// Ping.
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Merge.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// HTML.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Markdown.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// URL.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Office.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisableUnoconvEndpoints(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.EnableUnoconvEndpoints = false
|
|
||||||
srv := New(opts)
|
|
||||||
// Ping.
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Merge.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// HTML.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Markdown.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// URL.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Office.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
}
|
|
||||||
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.EnableChromeEndpoints = false
|
|
||||||
opts.EnableUnoconvEndpoints = false
|
|
||||||
srv := New(opts)
|
|
||||||
// Ping.
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Merge.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// HTML.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Markdown.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// URL.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
// Office.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
// Package api helps managing the HTTP server behind Gotenberg.
|
|
||||||
package api
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
type errBadRequest struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *errBadRequest) Error() string {
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func merge(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.mergePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpaths, err := ctx.resource.fpaths(".pdf")
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewMerge(fpaths, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertHTML(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.chromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpath, err := ctx.resource.fpath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewHTML(fpath, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertMarkdown(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.chromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpath, err := ctx.resource.fpath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p, err := printer.NewMarkdown(fpath, opts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertURL(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.chromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
remote, err := ctx.resource.get(remoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewURL(remote, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertOffice(c echo.Context) error {
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
opts, err := ctx.resource.officePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
fpaths, err := ctx.resource.fpaths(
|
|
||||||
".txt",
|
|
||||||
".rtf",
|
|
||||||
".fodt",
|
|
||||||
".doc",
|
|
||||||
".docx",
|
|
||||||
".odt",
|
|
||||||
".xls",
|
|
||||||
".xlsx",
|
|
||||||
".ods",
|
|
||||||
".ppt",
|
|
||||||
".pptx",
|
|
||||||
".odp",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
p := printer.NewOffice(fpaths, opts)
|
|
||||||
return convert(ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convert(ctx *resourceContext, p printer.Printer) error {
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
|
||||||
fpath := fmt.Sprintf("%s/%s", ctx.resource.formFilesDirPath, filename)
|
|
||||||
// if no webhook URL given, run conversion
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or an error.
|
|
||||||
if !ctx.resource.has(webhookURL) {
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if ctx.resource.has(resultFilename) {
|
|
||||||
filename, err = ctx.resource.get(resultFilename)
|
|
||||||
if err != nil {
|
|
||||||
return &errBadRequest{err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctx.Attachment(fpath, filename)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
defer ctx.resource.close() // nolint: errcheck
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close() // nolint: errcheck
|
|
||||||
webhook, err := ctx.resource.get(webhookURL)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := http.Post(webhook, "application/pdf", f) /* #nosec */
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger().Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() // nolint: errcheck
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestURL(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
// OK.
|
|
||||||
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// Bad request.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// Timeout.
|
|
||||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConcurrent(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
opts.DefaultWaitTimeout = 30
|
|
||||||
srv := New(opts)
|
|
||||||
// Merge.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// HTML.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// Markdown.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// URL.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
// Office.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWebhook(t *testing.T) {
|
|
||||||
status := make(chan error, 2)
|
|
||||||
rcv := echo.New()
|
|
||||||
rcv.POST("/foo", func(c echo.Context) error {
|
|
||||||
if c.Request().Header.Get("Content-type") != "application/pdf" {
|
|
||||||
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(c.Request().Body)
|
|
||||||
if err != nil {
|
|
||||||
status <- err
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if body == nil || len(body) == 0 {
|
|
||||||
status <- errors.New("empty body")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
status <- nil
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
go func() {
|
|
||||||
rcv.Start(":3001")
|
|
||||||
}()
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{webhookURL: "http://localhost:3001/foo"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
err := <-status
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResultFilename(t *testing.T) {
|
|
||||||
opts := DefaultOptions()
|
|
||||||
srv := New(opts)
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resultFilename: "foo.pdf"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
srv.ServeHTTP(rec, req)
|
|
||||||
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleLogging(enableHealthcheckLogging bool) echo.MiddlewareFunc {
|
|
||||||
if enableHealthcheckLogging {
|
|
||||||
// default logging middleware.
|
|
||||||
return middleware.Logger()
|
|
||||||
}
|
|
||||||
// middleware for skipping logging when the ping endpoint is called.
|
|
||||||
return middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
||||||
Skipper: func(c echo.Context) bool {
|
|
||||||
return c.Request().URL.Path == pingEndpoint
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleContext(opts *Options) echo.MiddlewareFunc {
|
|
||||||
// middleware for extending default context with our
|
|
||||||
// custom constext.
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
ctx := &resourceContext{c, opts, nil}
|
|
||||||
r, err := newResource(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if resourceErr := r.close(); resourceErr != nil {
|
|
||||||
c.Logger().Error(resourceErr)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ctx.resource = r
|
|
||||||
return next(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleError() echo.MiddlewareFunc {
|
|
||||||
// middleware for handling errors and removing resources
|
|
||||||
// once the request has been handled.
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
err := next(c)
|
|
||||||
ctx := c.(*resourceContext)
|
|
||||||
// if a webhookURL has been given,
|
|
||||||
// do not remove the resources here because
|
|
||||||
// we don't know if the result file has been
|
|
||||||
// generated or sent.
|
|
||||||
if !ctx.resource.has(webhookURL) {
|
|
||||||
if resourceErr := ctx.resource.close(); resourceErr != nil {
|
|
||||||
c.Logger().Error(resourceErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*echo.HTTPError); ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, ok := err.(*errBadRequest); ok {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
|
||||||
return echo.NewHTTPError(http.StatusRequestTimeout)
|
|
||||||
}
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
resultFilename string = "resultFilename"
|
|
||||||
waitTimeout string = "waitTimeout"
|
|
||||||
webhookURL string = "webhookURL"
|
|
||||||
remoteURL string = "remoteURL"
|
|
||||||
waitDelay string = "waitDelay"
|
|
||||||
paperWidth string = "paperWidth"
|
|
||||||
paperHeight string = "paperHeight"
|
|
||||||
marginTop string = "marginTop"
|
|
||||||
marginBottom string = "marginBottom"
|
|
||||||
marginLeft string = "marginLeft"
|
|
||||||
marginRight string = "marginRight"
|
|
||||||
landscape string = "landscape"
|
|
||||||
)
|
|
||||||
|
|
||||||
type resource struct {
|
|
||||||
formValues map[string]string
|
|
||||||
formFilesDirPath string
|
|
||||||
opts *Options
|
|
||||||
}
|
|
||||||
|
|
||||||
type resourceContext struct {
|
|
||||||
echo.Context
|
|
||||||
opts *Options
|
|
||||||
resource *resource
|
|
||||||
}
|
|
||||||
|
|
||||||
func newResource(ctx *resourceContext) (*resource, error) {
|
|
||||||
r := &resource{
|
|
||||||
formValues: formValues(ctx),
|
|
||||||
opts: ctx.opts,
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
r.formFilesDirPath = dirPath
|
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
|
||||||
}
|
|
||||||
if err := formFiles(ctx, dirPath); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formValues(ctx *resourceContext) map[string]string {
|
|
||||||
v := make(map[string]string)
|
|
||||||
v[resultFilename] = ctx.FormValue(resultFilename)
|
|
||||||
v[waitTimeout] = ctx.FormValue(waitTimeout)
|
|
||||||
v[webhookURL] = ctx.FormValue(webhookURL)
|
|
||||||
v[remoteURL] = ctx.FormValue(remoteURL)
|
|
||||||
v[waitDelay] = ctx.FormValue(waitDelay)
|
|
||||||
v[paperWidth] = ctx.FormValue(paperWidth)
|
|
||||||
v[paperHeight] = ctx.FormValue(paperHeight)
|
|
||||||
v[marginTop] = ctx.FormValue(marginTop)
|
|
||||||
v[marginBottom] = ctx.FormValue(marginBottom)
|
|
||||||
v[marginLeft] = ctx.FormValue(marginLeft)
|
|
||||||
v[marginRight] = ctx.FormValue(marginRight)
|
|
||||||
v[landscape] = ctx.FormValue(landscape)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func formFiles(ctx *resourceContext, dirPath string) error {
|
|
||||||
form, err := ctx.MultipartForm()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting multipart form: %v", err)
|
|
||||||
}
|
|
||||||
for _, files := range form.File {
|
|
||||||
for _, fh := range files {
|
|
||||||
in, err := fh.Open()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
|
||||||
}
|
|
||||||
defer in.Close() // nolint: errcheck
|
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
defer out.Close() // nolint: errcheck
|
|
||||||
if err := out.Chmod(0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := out.Seek(0, 0); err != nil {
|
|
||||||
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) close() error {
|
|
||||||
if _, err := os.Stat(r.formFilesDirPath); os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return os.RemoveAll(r.formFilesDirPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
|
||||||
|
|
||||||
func (r *resource) chromePrinterOptions() (*printer.ChromeOptions, error) {
|
|
||||||
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
delay, err := r.float64(waitDelay, 0.0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
header, err := r.content("header.html", defaultHeaderFooterHTML)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
footer, err := r.content("footer.html", defaultHeaderFooterHTML)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
width, err := r.float64(paperWidth, 8.27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
height, err := r.float64(paperHeight, 11.7)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
top, err := r.float64(marginTop, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bottom, err := r.float64(marginBottom, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
left, err := r.float64(marginLeft, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
right, err := r.float64(marginRight, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
landscape, err := r.bool(landscape, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &printer.ChromeOptions{
|
|
||||||
WaitTimeout: timeout,
|
|
||||||
WaitDelay: delay,
|
|
||||||
HeaderHTML: header,
|
|
||||||
FooterHTML: footer,
|
|
||||||
PaperWidth: width,
|
|
||||||
PaperHeight: height,
|
|
||||||
MarginTop: top,
|
|
||||||
MarginBottom: bottom,
|
|
||||||
MarginLeft: left,
|
|
||||||
MarginRight: right,
|
|
||||||
Landscape: landscape,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) officePrinterOptions() (*printer.OfficeOptions, error) {
|
|
||||||
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
landscape, err := r.bool(landscape, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &printer.OfficeOptions{
|
|
||||||
WaitTimeout: timeout,
|
|
||||||
Landscape: landscape,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) mergePrinterOptions() (*printer.MergeOptions, error) {
|
|
||||||
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &printer.MergeOptions{
|
|
||||||
WaitTimeout: timeout,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) has(key string) bool {
|
|
||||||
v, ok := r.formValues[key]
|
|
||||||
if ok {
|
|
||||||
ok = v != ""
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) hasFile(filename string) bool {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
|
||||||
_, err := os.Stat(fpath)
|
|
||||||
return !os.IsNotExist(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) get(key string) (string, error) {
|
|
||||||
v, ok := r.formValues[key]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("form value %s does not exist", key)
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) float64(key string, defaultValue float64) (float64, error) {
|
|
||||||
if !r.has(key) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
v, err := r.get(key)
|
|
||||||
if err != nil {
|
|
||||||
return 0.0, err
|
|
||||||
}
|
|
||||||
f, err := strconv.ParseFloat(v, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0.0, fmt.Errorf("form value %s: %v", key, err)
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) bool(key string, defaultValue bool) (bool, error) {
|
|
||||||
if !r.has(key) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
v, err := r.get(key)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
b, err := strconv.ParseBool(v)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("form value %s: %v", key, err)
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) fpath(filename string) (string, error) {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
|
||||||
_, err := os.Stat(fpath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return "", fmt.Errorf("%s: form file does not exist", filename)
|
|
||||||
}
|
|
||||||
absPath, err := filepath.Abs(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: getting absolute path: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return absPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) content(filename string, defaultValue string) (string, error) {
|
|
||||||
if !r.hasFile(filename) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
fpath, err := r.fpath(filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: reading form file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) fpaths(exts ...string) ([]string, error) {
|
|
||||||
var fpaths []string
|
|
||||||
err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error {
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fpath, err := r.fpath(info.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, ext := range exts {
|
|
||||||
if filepath.Ext(fpath) == ext {
|
|
||||||
fpaths = append(fpaths, fpath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(fpaths) == 0 {
|
|
||||||
return nil, fmt.Errorf("no form files found for extensions: %v", exts)
|
|
||||||
}
|
|
||||||
return fpaths, nil
|
|
||||||
}
|
|
||||||
3
internal/app/xhttp/doc.go
Normal file
3
internal/app/xhttp/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package xhttp defines our own implementation
|
||||||
|
// of echo.Echo.
|
||||||
|
package xhttp
|
||||||
299
internal/app/xhttp/handler.go
Normal file
299
internal/app/xhttp/handler.go
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
ctx.XLogger().DebugOp(op, "handling ping request...")
|
||||||
|
if err := ctx.ProcessesHealthcheck(); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
|
}
|
||||||
|
// TODO return processes info
|
||||||
|
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()
|
||||||
|
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
|
||||||
|
webhookURL, err := r.StringArg(resource.WebhookURLArgKey, "")
|
||||||
|
if err != nil {
|
||||||
|
xerr := xerror.New(op, err)
|
||||||
|
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.DebugfOp(
|
||||||
|
op,
|
||||||
|
"sending result file '%s' to '%s'",
|
||||||
|
filename,
|
||||||
|
webhookURL,
|
||||||
|
)
|
||||||
|
// TODO timeout
|
||||||
|
resp, err := http.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
|
||||||
|
}
|
||||||
127
internal/app/xhttp/middleware.go
Normal file
127
internal/app/xhttp/middleware.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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/pm2"
|
||||||
|
"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, processes ...pm2.Process) 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, processes...)
|
||||||
|
// 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 {
|
||||||
|
// required to have a correct status code.
|
||||||
|
ctx.Error(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 {
|
||||||
|
const op string = "xhttp.cleanupMiddleware"
|
||||||
|
err := next(c)
|
||||||
|
ctx := context.MustCastFromEchoContext(c)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
// if it's an error from echo
|
||||||
|
// like 404 not found and so on.
|
||||||
|
if echoHTTPErr, ok := err.(*echo.HTTPError); ok {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
internal/app/xhttp/option.go
Normal file
93
internal/app/xhttp/option.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return printer.ChromePrinterOptions{}, err
|
||||||
|
}
|
||||||
|
paperWidth, paperHeight,
|
||||||
|
err := resource.PaperSizeArgs(r)
|
||||||
|
if err != nil {
|
||||||
|
return printer.ChromePrinterOptions{}, err
|
||||||
|
}
|
||||||
|
marginTop, marginBottom, marginLeft, marginRight,
|
||||||
|
err := resource.MarginArgs(r)
|
||||||
|
if err != nil {
|
||||||
|
return printer.ChromePrinterOptions{}, err
|
||||||
|
}
|
||||||
|
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
|
||||||
|
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,
|
||||||
|
}, 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
|
||||||
|
}
|
||||||
211
internal/app/xhttp/pkg/context/context.go
Normal file
211
internal/app/xhttp/pkg/context/context.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"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/pm2"
|
||||||
|
"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
|
||||||
|
processes []pm2.Process
|
||||||
|
resource resource.Resource
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Context.
|
||||||
|
func New(c echo.Context, logger xlog.Logger, config conf.Config, processess ...pm2.Process) Context {
|
||||||
|
return Context{
|
||||||
|
c,
|
||||||
|
logger,
|
||||||
|
config,
|
||||||
|
processess,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessesHealthcheck returns an error if
|
||||||
|
// one of the processes is not viable.
|
||||||
|
func (ctx Context) ProcessesHealthcheck() error {
|
||||||
|
const op string = "context.Context.ProcessesHealthcheck"
|
||||||
|
for _, process := range ctx.processes {
|
||||||
|
if !process.IsViable() {
|
||||||
|
return xerror.New(
|
||||||
|
op,
|
||||||
|
fmt.Errorf("'%s' is not viable", process.Fullname()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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
|
||||||
|
if err := r.WithFile(fh.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)
|
||||||
|
}
|
||||||
7
internal/app/xhttp/pkg/context/doc.go
Normal file
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
|
||||||
253
internal/app/xhttp/pkg/resource/arg.go
Normal file
253
internal/app/xhttp/pkg/resource/arg.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PaperSizeArgs is a helper for retrieving
|
||||||
|
the "paperWidth" and "paperHeight" arguments
|
||||||
|
as float64.
|
||||||
|
*/
|
||||||
|
func PaperSizeArgs(r Resource) (float64, float64, error) {
|
||||||
|
const (
|
||||||
|
op string = "resource.PaperSizeArgs"
|
||||||
|
defaultPaperWidth float64 = 8.27
|
||||||
|
defaultPaperHeight float64 = 11.7
|
||||||
|
)
|
||||||
|
resolver := func() (float64, float64, error) {
|
||||||
|
paperWidth, err := r.Float64Arg(
|
||||||
|
PaperWidthArgKey,
|
||||||
|
defaultPaperWidth,
|
||||||
|
xassert.Float64NotInferiorTo(0.0),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return defaultPaperWidth,
|
||||||
|
defaultPaperHeight,
|
||||||
|
err
|
||||||
|
}
|
||||||
|
paperHeight, err := r.Float64Arg(
|
||||||
|
PaperHeightArgKey,
|
||||||
|
defaultPaperHeight,
|
||||||
|
xassert.Float64NotInferiorTo(0.0),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return defaultPaperWidth,
|
||||||
|
defaultPaperHeight,
|
||||||
|
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) (float64, float64, float64, float64, error) {
|
||||||
|
const (
|
||||||
|
op string = "resource.MarginArgs"
|
||||||
|
defaultMarginTop float64 = 1.0
|
||||||
|
defaultMarginBottom float64 = 1.0
|
||||||
|
defaultMarginLeft float64 = 1.0
|
||||||
|
defaultMarginRight float64 = 1.0
|
||||||
|
)
|
||||||
|
resolver := func() (float64, float64, float64, float64, error) {
|
||||||
|
marginTop, err := r.Float64Arg(
|
||||||
|
MarginTopArgKey,
|
||||||
|
defaultMarginTop,
|
||||||
|
xassert.Float64NotInferiorTo(0.0),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return defaultMarginTop,
|
||||||
|
defaultMarginBottom,
|
||||||
|
defaultMarginLeft,
|
||||||
|
defaultMarginRight,
|
||||||
|
err
|
||||||
|
}
|
||||||
|
marginBottom, err := r.Float64Arg(
|
||||||
|
MarginBottomArgKey,
|
||||||
|
defaultMarginBottom,
|
||||||
|
xassert.Float64NotInferiorTo(0.0),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return defaultMarginTop,
|
||||||
|
defaultMarginBottom,
|
||||||
|
defaultMarginLeft,
|
||||||
|
defaultMarginRight,
|
||||||
|
err
|
||||||
|
}
|
||||||
|
marginLeft, err := r.Float64Arg(
|
||||||
|
MarginLeftArgKey,
|
||||||
|
defaultMarginLeft,
|
||||||
|
xassert.Float64NotInferiorTo(0.0),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return defaultMarginTop,
|
||||||
|
defaultMarginBottom,
|
||||||
|
defaultMarginLeft,
|
||||||
|
defaultMarginRight,
|
||||||
|
err
|
||||||
|
}
|
||||||
|
marginRight, err := r.Float64Arg(
|
||||||
|
MarginRightArgKey,
|
||||||
|
defaultMarginRight,
|
||||||
|
xassert.Float64NotInferiorTo(0.0),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return defaultMarginTop,
|
||||||
|
defaultMarginBottom,
|
||||||
|
defaultMarginLeft,
|
||||||
|
defaultMarginRight,
|
||||||
|
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
|
||||||
|
}
|
||||||
8
internal/app/xhttp/pkg/resource/doc.go
Normal file
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
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/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) (string, string, error) {
|
||||||
|
const (
|
||||||
|
op string = "resource.HeaderFooterContents"
|
||||||
|
defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||||
|
)
|
||||||
|
resolver := func() (string, string, error) {
|
||||||
|
headerHTML, err := r.Fcontent("header.html", defaultHeaderFooterHTML)
|
||||||
|
if err != nil {
|
||||||
|
return defaultHeaderFooterHTML,
|
||||||
|
defaultHeaderFooterHTML,
|
||||||
|
err
|
||||||
|
}
|
||||||
|
footerHTML, err := r.Fcontent("footer.html", defaultHeaderFooterHTML)
|
||||||
|
if err != nil {
|
||||||
|
return defaultHeaderFooterHTML,
|
||||||
|
defaultHeaderFooterHTML,
|
||||||
|
err
|
||||||
|
}
|
||||||
|
return headerHTML,
|
||||||
|
footerHTML,
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
headerHTML, footerHTML,
|
||||||
|
err := resolver()
|
||||||
|
if err != nil {
|
||||||
|
return headerHTML,
|
||||||
|
footerHTML,
|
||||||
|
xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return headerHTML,
|
||||||
|
footerHTML,
|
||||||
|
nil
|
||||||
|
}
|
||||||
227
internal/app/xhttp/pkg/resource/resource.go
Normal file
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
|
||||||
|
}
|
||||||
33
internal/app/xhttp/xhttp.go
Normal file
33
internal/app/xhttp/xhttp.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package xhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a custom echo.Echo.
|
||||||
|
func New(config conf.Config, processes ...pm2.Process) *echo.Echo {
|
||||||
|
srv := echo.New()
|
||||||
|
srv.HideBanner = true
|
||||||
|
srv.HidePort = true
|
||||||
|
srv.Use(contextMiddleware(config, processes...))
|
||||||
|
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
|
||||||
|
}
|
||||||
206
internal/pkg/conf/conf.go
Normal file
206
internal/pkg/conf/conf.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
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 string = "MAXIMUM_WAIT_TIMEOUT"
|
||||||
|
maximumWaitDelayEnvVar string = "MAXIMUM_WAIT_DELAY"
|
||||||
|
maximumWebhookURLTimeoutEnvVar string = "MAXIMUM_WEBHOOK_URL_TIMEOUT"
|
||||||
|
defaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_TIMEOUT"
|
||||||
|
defaultWebhookURLTimeoutEnvVar string = "DEFAULT_WEBHOOK_URL_TIMEOUT"
|
||||||
|
defaultListenPortEnvVar string = "DEFAULT_LISTEN_PORT"
|
||||||
|
disableGoogleChromeEnvVar string = "DISABLE_GOOGLE_CHROME"
|
||||||
|
disableUnoconvEnvVar string = "DISABLE_UNOCONV"
|
||||||
|
logLevelEnvVar string = "LOG_LEVEL"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
334
internal/pkg/conf/conf_test.go
Normal file
334
internal/pkg/conf/conf_test.go
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
os.Unsetenv(defaultListenPortEnvVar)
|
||||||
|
// DEFAULT_LISTEN_PORT > 65535.
|
||||||
|
os.Setenv(defaultListenPortEnvVar, "65536")
|
||||||
|
expected = defaultConfig()
|
||||||
|
result, err = FromEnv()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.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()
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
os.Unsetenv(logLevelEnvVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
3
internal/pkg/conf/doc.go
Normal file
3
internal/pkg/conf/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package conf gathers all
|
||||||
|
// configuration data.
|
||||||
|
package conf
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/*
|
|
||||||
Package notify helps displaying nice outputs
|
|
||||||
to the user.
|
|
||||||
*/
|
|
||||||
package notify
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package notify
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/gommon/color"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Print prints a message to stdout.
|
|
||||||
func Print(message string) {
|
|
||||||
stdout := color.New()
|
|
||||||
stdout.SetOutput(os.Stdout)
|
|
||||||
stdout.Printf("⇨ %s\n", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf prints a formatted message to stdout.
|
|
||||||
func Printf(format string, a ...interface{}) {
|
|
||||||
message := fmt.Sprintf(format, a...)
|
|
||||||
Print(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WarnPrint prints a warning to stderr.
|
|
||||||
func WarnPrint(err error) {
|
|
||||||
stderr := color.New()
|
|
||||||
stderr.SetOutput(os.Stderr)
|
|
||||||
stderr.Printf("%s\n", color.Yellow(fmt.Sprintf("⇨ warn: %v", err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrPrint prints an error to stderr.
|
|
||||||
func ErrPrint(err error) {
|
|
||||||
stderr := color.New()
|
|
||||||
stderr.SetOutput(os.Stderr)
|
|
||||||
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
|
||||||
}
|
|
||||||
@@ -5,33 +5,72 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
"github.com/mafredri/cdp/devtool"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type chrome struct {
|
type chromeProcess struct {
|
||||||
manager *processManager
|
logger xlog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChrome retruns a Google Chrome
|
// NewChromeProcess returns a Google Chrome
|
||||||
// headless process.
|
// headless process.
|
||||||
func NewChrome() Process {
|
func NewChromeProcess(logger xlog.Logger) Process {
|
||||||
return &chrome{
|
return chromeProcess{
|
||||||
manager: &processManager{},
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Fullname() string {
|
func (p chromeProcess) Fullname() string {
|
||||||
return "Google Chrome headless"
|
return "Google Chrome headless"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Start() error {
|
func (p chromeProcess) Start() error {
|
||||||
return p.manager.start(p)
|
const op string = "pm2.chromeProcess.Start"
|
||||||
|
if err := start(p.logger, p); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Shutdown() error {
|
func (p chromeProcess) IsViable() bool {
|
||||||
return p.manager.shutdown(p)
|
const op string = "pm2.chromeProcess.IsViable"
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
p.logger.DebugfOp(
|
||||||
|
op,
|
||||||
|
"checking '%s' viability via endpoint '%s'",
|
||||||
|
p.Fullname(),
|
||||||
|
"http://localhost:9222/json/version",
|
||||||
|
)
|
||||||
|
v, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||||
|
if err != nil {
|
||||||
|
p.logger.ErrorfOp(
|
||||||
|
op,
|
||||||
|
"'%s' is not viable as endpoint returned '%v'",
|
||||||
|
p.Fullname(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
p.logger.DebugfOp(
|
||||||
|
op,
|
||||||
|
"'%s' is viable as endpoint returned '%v'",
|
||||||
|
p.Fullname(),
|
||||||
|
v,
|
||||||
|
)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) args() []string {
|
func (p chromeProcess) Stop() error {
|
||||||
|
const op string = "pm2.chromeProcess.Stop"
|
||||||
|
if err := stop(p.logger, p); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p chromeProcess) args() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--headless",
|
"--headless",
|
||||||
@@ -50,23 +89,25 @@ func (p *chrome) args() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) name() string {
|
func (p chromeProcess) binary() string {
|
||||||
return "google-chrome-stable"
|
return "google-chrome-stable"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) viable() bool {
|
func (p chromeProcess) warmup() {
|
||||||
// check if Google Chrome is correctly running.
|
const (
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
op string = "pm2.chromeProcess.warmup"
|
||||||
defer cancel()
|
warmupTime time.Duration = 10 * time.Second
|
||||||
_, err := devtool.New("http://localhost:9222").Version(ctx)
|
)
|
||||||
return err == nil
|
p.logger.DebugfOp(
|
||||||
}
|
op,
|
||||||
|
"waiting '%v' for allowing '%s' to warmup",
|
||||||
func (p *chrome) warmup() {
|
warmupTime,
|
||||||
time.Sleep(5 * time.Second)
|
p.Fullname(),
|
||||||
|
)
|
||||||
|
time.Sleep(warmupTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(chrome))
|
_ = Process(new(chromeProcess))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChromeStart(t *testing.T) {
|
|
||||||
p := NewChrome()
|
|
||||||
err := p.Start()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChromeShutdown(t *testing.T) {
|
|
||||||
p := NewChrome()
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Package pm2 facilitates starting external
|
Package pm2 facilitates starting external
|
||||||
processes on which our API depends.
|
processes on which our application depends.
|
||||||
|
|
||||||
For instance, it may start Google Chrome headless and
|
For instance, it may start Google Chrome headless and
|
||||||
unoconv listener with PM2.
|
unoconv listener with PM2.
|
||||||
|
|||||||
@@ -2,82 +2,101 @@ package pm2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
stoppedState = iota
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||||
runningState
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
errorState
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process is a type that can start or
|
// Process is a type that can start or
|
||||||
// shutdown a process with PM2.
|
// stop a process with PM2.
|
||||||
type Process interface {
|
type Process interface {
|
||||||
Fullname() string
|
Fullname() string
|
||||||
Start() error
|
Start() error
|
||||||
Shutdown() error
|
IsViable() bool
|
||||||
|
Stop() error
|
||||||
args() []string
|
args() []string
|
||||||
name() string
|
binary() string
|
||||||
viable() bool
|
|
||||||
warmup()
|
warmup()
|
||||||
}
|
}
|
||||||
|
|
||||||
type processManager struct {
|
type pm2Command string
|
||||||
heuristicState int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *processManager) start(p Process) error {
|
const (
|
||||||
if err := m.pm2(p, "start"); err != nil {
|
startCommand pm2Command = "start"
|
||||||
return err
|
restartCommand pm2Command = "restart"
|
||||||
}
|
stopCommand pm2Command = "stop"
|
||||||
p.warmup()
|
logsCommand pm2Command = "logs"
|
||||||
if !p.viable() {
|
)
|
||||||
attempts := 0
|
|
||||||
for attempts < 5 && !p.viable() {
|
func start(logger xlog.Logger, process Process) error {
|
||||||
if err := m.pm2(p, "restart"); err != nil {
|
const (
|
||||||
m.heuristicState = errorState
|
op string = "pm2.start"
|
||||||
return err
|
maximumAttempts int = 3
|
||||||
|
)
|
||||||
|
resolver := func() error {
|
||||||
|
// first, we try to start the process.
|
||||||
|
if err := run(logger, startCommand, process); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// we wait the process to be ready.
|
||||||
|
process.warmup()
|
||||||
|
// if the process failed to start correctly,
|
||||||
|
// we have to restart it.
|
||||||
|
if !process.IsViable() {
|
||||||
|
attempts := 0
|
||||||
|
for attempts < maximumAttempts && !process.IsViable() {
|
||||||
|
if err := run(logger, restartCommand, process); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
process.warmup()
|
||||||
|
attempts++
|
||||||
|
}
|
||||||
|
if !process.IsViable() {
|
||||||
|
return fmt.Errorf("failed to start '%s'", process.Fullname())
|
||||||
}
|
}
|
||||||
p.warmup()
|
|
||||||
attempts++
|
|
||||||
}
|
}
|
||||||
if !p.viable() {
|
// the process is viable, let's log its
|
||||||
m.heuristicState = errorState
|
// output.
|
||||||
return fmt.Errorf("failed to launch %s", p.Fullname())
|
if err := run(logger, logsCommand, process); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
m.heuristicState = runningState
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *processManager) shutdown(p Process) error {
|
|
||||||
if m.heuristicState != runningState {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := m.pm2(p, "stop"); err != nil {
|
if err := resolver(); err != nil {
|
||||||
m.heuristicState = errorState
|
return xerror.New(op, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
m.heuristicState = stoppedState
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *processManager) pm2(p Process, cmdName string) error {
|
func stop(logger xlog.Logger, process Process) error {
|
||||||
cmdArgs := []string{
|
const op string = "pm2.stop"
|
||||||
cmdName,
|
if err := run(logger, stopCommand, process); err != nil {
|
||||||
p.name(),
|
return xerror.New(op, err)
|
||||||
}
|
}
|
||||||
if cmdName == "start" {
|
return nil
|
||||||
cmdArgs = append(cmdArgs, "--interpreter=none", "--")
|
}
|
||||||
cmdArgs = append(cmdArgs, p.args()...)
|
|
||||||
}
|
func run(logger xlog.Logger, pm2Cmd pm2Command, process Process) error {
|
||||||
cmd := exec.Command(
|
const op string = "pm2.run"
|
||||||
"pm2",
|
resolver := func() error {
|
||||||
cmdArgs...,
|
args := []string{
|
||||||
)
|
string(pm2Cmd),
|
||||||
if err := cmd.Start(); err != nil {
|
process.binary(),
|
||||||
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err)
|
}
|
||||||
|
if pm2Cmd == startCommand {
|
||||||
|
args = append(args, "--interpreter=none", "--")
|
||||||
|
args = append(args, process.args()...)
|
||||||
|
}
|
||||||
|
cmd, err := xexec.Command(logger, "pm2", args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
xexec.LogBeforeExecute(logger, cmd)
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
if err := resolver(); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,77 @@
|
|||||||
package pm2
|
package pm2
|
||||||
|
|
||||||
type unoconv struct {
|
import (
|
||||||
manager *processManager
|
"time"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type unoconvProcess struct {
|
||||||
|
logger xlog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUnoconv retruns a unoconv listener
|
// NewUnoconvProcess returns a unoconv listener
|
||||||
// process.
|
// process.
|
||||||
func NewUnoconv() Process {
|
func NewUnoconvProcess(logger xlog.Logger) Process {
|
||||||
return &unoconv{
|
return unoconvProcess{
|
||||||
manager: &processManager{},
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) Fullname() string {
|
func (p unoconvProcess) Fullname() string {
|
||||||
return "unoconv listener"
|
return "unoconv listener"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) Start() error {
|
func (p unoconvProcess) Start() error {
|
||||||
return p.manager.start(p)
|
const op string = "pm2.unoconvProcess.Start"
|
||||||
}
|
if err := start(p.logger, p); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
func (p *unoconv) Shutdown() error {
|
|
||||||
return p.manager.shutdown(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) args() []string {
|
|
||||||
return []string{
|
|
||||||
"--listener",
|
|
||||||
"--verbose",
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) name() string {
|
func (p unoconvProcess) IsViable() bool {
|
||||||
return "unoconv"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) viable() bool {
|
|
||||||
// TODO find a way to check if
|
// TODO find a way to check if
|
||||||
// the unoconv listener
|
// the unoconv listener
|
||||||
// is correctly started?
|
// is correctly started?
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) warmup() {
|
func (p unoconvProcess) Stop() error {
|
||||||
// let's do nothing.
|
const op string = "pm2.unoconvProcess.Stop"
|
||||||
|
if err := stop(p.logger, p); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p unoconvProcess) args() []string {
|
||||||
|
return []string{
|
||||||
|
"--listener",
|
||||||
|
"--verbose",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p unoconvProcess) binary() string {
|
||||||
|
return "unoconv"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p unoconvProcess) warmup() {
|
||||||
|
const (
|
||||||
|
op string = "pm2.unoconvProcess.warmup"
|
||||||
|
warmupTime time.Duration = 3 * time.Second
|
||||||
|
)
|
||||||
|
p.logger.DebugfOp(
|
||||||
|
op,
|
||||||
|
"waiting '%v' for allowing '%s' to warmup",
|
||||||
|
warmupTime,
|
||||||
|
p.Fullname(),
|
||||||
|
)
|
||||||
|
time.Sleep(warmupTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(unoconv))
|
_ = Process(new(unoconvProcess))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnoconvStart(t *testing.T) {
|
|
||||||
p := NewUnoconv()
|
|
||||||
err := p.Start()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconvShutdown(t *testing.T) {
|
|
||||||
p := NewUnoconv()
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -12,17 +12,22 @@ import (
|
|||||||
"github.com/mafredri/cdp/protocol/page"
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
"github.com/mafredri/cdp/protocol/target"
|
"github.com/mafredri/cdp/protocol/target"
|
||||||
"github.com/mafredri/cdp/rpcc"
|
"github.com/mafredri/cdp/rpcc"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type chrome struct {
|
type chromePrinter struct {
|
||||||
url string
|
logger xlog.Logger
|
||||||
opts *ChromeOptions
|
url string
|
||||||
|
opts ChromePrinterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChromeOptions helps customizing the
|
// ChromePrinterOptions helps customizing the
|
||||||
// Google Chrome printer behaviour.
|
// Google Chrome printer behaviour.
|
||||||
type ChromeOptions struct {
|
type ChromePrinterOptions struct {
|
||||||
WaitTimeout float64
|
WaitTimeout float64
|
||||||
WaitDelay float64
|
WaitDelay float64
|
||||||
HeaderHTML string
|
HeaderHTML string
|
||||||
@@ -36,120 +41,137 @@ type ChromeOptions struct {
|
|||||||
Landscape bool
|
Landscape bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Print(destination string) error {
|
func (p chromePrinter) Print(destination string) error {
|
||||||
duration := time.Duration(p.opts.WaitTimeout+p.opts.WaitDelay) * time.Second
|
const op string = "printer.chromePrinter.Print"
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), duration)
|
logOptions(p.logger, p.opts)
|
||||||
|
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout+p.opts.WaitDelay)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
resolver := func() error {
|
||||||
if err != nil {
|
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
||||||
|
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer devtConn.Close() // nolint: errcheck
|
||||||
|
// create a new CDP Client that uses conn.
|
||||||
|
devtClient := cdp.NewClient(devtConn)
|
||||||
|
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// create a new blank target with the new browser context.
|
||||||
|
createTargetArgs := target.
|
||||||
|
NewCreateTargetArgs("about:blank").
|
||||||
|
SetBrowserContextID(newContextTarget.BrowserContextID)
|
||||||
|
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// connect the client to the new target.
|
||||||
|
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
||||||
|
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer newContextConn.Close() // nolint: errcheck
|
||||||
|
// create a new CDP Client that uses newContextConn.
|
||||||
|
targetClient := cdp.NewClient(newContextConn)
|
||||||
|
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
||||||
|
// close the target when done.
|
||||||
|
defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck
|
||||||
|
if err := runBatch(
|
||||||
|
// enable all the domain events that we're interested in.
|
||||||
|
func() error { return targetClient.DOM.Enable(ctx) },
|
||||||
|
func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) },
|
||||||
|
func() error { return targetClient.Page.Enable(ctx) },
|
||||||
|
func() error { return targetClient.Runtime.Enable(ctx) },
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.navigate(ctx, targetClient); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
print, err := targetClient.Page.PrintToPDF(
|
||||||
|
ctx,
|
||||||
|
page.NewPrintToPDFArgs().
|
||||||
|
SetPaperWidth(p.opts.PaperWidth).
|
||||||
|
SetPaperHeight(p.opts.PaperHeight).
|
||||||
|
SetMarginTop(p.opts.MarginTop).
|
||||||
|
SetMarginBottom(p.opts.MarginBottom).
|
||||||
|
SetMarginLeft(p.opts.MarginLeft).
|
||||||
|
SetMarginRight(p.opts.MarginRight).
|
||||||
|
SetLandscape(p.opts.Landscape).
|
||||||
|
SetDisplayHeaderFooter(true).
|
||||||
|
SetHeaderTemplate(p.opts.HeaderHTML).
|
||||||
|
SetFooterTemplate(p.opts.FooterHTML).
|
||||||
|
SetPrintBackground(true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
if err := resolver(); err != nil {
|
||||||
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
return xcontext.MustHandleError(
|
||||||
if err != nil {
|
ctx,
|
||||||
return err
|
xerror.New(op, err),
|
||||||
}
|
)
|
||||||
defer devtConn.Close() // nolint: errcheck
|
|
||||||
// create a new CDP Client that uses conn.
|
|
||||||
devtClient := cdp.NewClient(devtConn)
|
|
||||||
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new browser context: %v", err)
|
|
||||||
}
|
|
||||||
// create a new blank target with the new browser context.
|
|
||||||
createTargetArgs := target.
|
|
||||||
NewCreateTargetArgs("about:blank").
|
|
||||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
|
||||||
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new blank target: %v", err)
|
|
||||||
}
|
|
||||||
// connect the client to the new target.
|
|
||||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
|
||||||
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connecting client to blank target: %v", err)
|
|
||||||
}
|
|
||||||
defer newContextConn.Close() // nolint: errcheck
|
|
||||||
// create a new CDP Client that uses newContextConn.
|
|
||||||
targetClient := cdp.NewClient(newContextConn)
|
|
||||||
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
|
||||||
// close the target when done.
|
|
||||||
defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck
|
|
||||||
if err := runBatch(
|
|
||||||
// enable all the domain events that we're interested in.
|
|
||||||
func() error { return targetClient.DOM.Enable(ctx) },
|
|
||||||
func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) },
|
|
||||||
func() error { return targetClient.Page.Enable(ctx) },
|
|
||||||
func() error { return targetClient.Runtime.Enable(ctx) },
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.navigate(ctx, targetClient); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
print, err := targetClient.Page.PrintToPDF(
|
|
||||||
ctx,
|
|
||||||
page.NewPrintToPDFArgs().
|
|
||||||
SetPaperWidth(p.opts.PaperWidth).
|
|
||||||
SetPaperHeight(p.opts.PaperHeight).
|
|
||||||
SetMarginTop(p.opts.MarginTop).
|
|
||||||
SetMarginBottom(p.opts.MarginBottom).
|
|
||||||
SetMarginLeft(p.opts.MarginLeft).
|
|
||||||
SetMarginRight(p.opts.MarginRight).
|
|
||||||
SetLandscape(p.opts.Landscape).
|
|
||||||
SetDisplayHeaderFooter(true).
|
|
||||||
SetHeaderTemplate(p.opts.HeaderHTML).
|
|
||||||
SetFooterTemplate(p.opts.FooterHTML).
|
|
||||||
SetPrintBackground(true),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("printing page to PDF: %v", err)
|
|
||||||
}
|
|
||||||
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", destination, err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error {
|
func (p chromePrinter) navigate(ctx context.Context, client *cdp.Client) error {
|
||||||
// make sure Page events are enabled.
|
const op string = "printer.chromePrinter.navigate"
|
||||||
if err := client.Page.Enable(ctx); err != nil {
|
resolver := func() error {
|
||||||
return err
|
// make sure Page events are enabled.
|
||||||
|
if err := client.Page.Enable(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// make sure Network events are enabled.
|
||||||
|
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// create all clients for events.
|
||||||
|
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer domContentEventFired.Close() // nolint: errcheck
|
||||||
|
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadEventFired.Close() // nolint: errcheck
|
||||||
|
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadingFinished.Close() // nolint: errcheck
|
||||||
|
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runBatch(
|
||||||
|
// wait for all events.
|
||||||
|
func() error { _, err := domContentEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadingFinished.Recv(); return err },
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// wait for a given amount of time (useful for javascript delay).
|
||||||
|
time.Sleep(xtime.Duration(p.opts.WaitDelay))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
// make sure Network events are enabled.
|
if err := resolver(); err != nil {
|
||||||
if err := client.Network.Enable(ctx, nil); err != nil {
|
return xerror.New(op, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
// create all clients for events.
|
|
||||||
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer domContentEventFired.Close() // nolint: errcheck
|
|
||||||
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer loadEventFired.Close() // nolint: errcheck
|
|
||||||
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer loadingFinished.Close() // nolint: errcheck
|
|
||||||
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := runBatch(
|
|
||||||
// wait for all events.
|
|
||||||
func() error { _, err := domContentEventFired.Recv(); return err },
|
|
||||||
func() error { _, err := loadEventFired.Recv(); return err },
|
|
||||||
func() error { _, err := loadingFinished.Recv(); return err },
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// wait for a given amount of time (useful for javascript delay).
|
|
||||||
time.Sleep(time.Duration(p.opts.WaitDelay) * time.Second)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,5 +187,5 @@ func runBatch(fn ...func() error) error {
|
|||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(chrome))
|
_ = Printer(new(chromePrinter))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/*
|
// Package printer helps converting
|
||||||
Package printer contains structs which convert
|
// a specific file type to PDF.
|
||||||
a specific file type to PDF.
|
|
||||||
*/
|
|
||||||
package printer
|
package printer
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHTML returns an HTML printer.
|
// NewHTMLPrinter returns a Printer which
|
||||||
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
// is able to convert an HTML file to PDF.
|
||||||
|
func NewHTMLPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) Printer {
|
||||||
URL := fmt.Sprintf("file://%s", fpath)
|
URL := fmt.Sprintf("file://%s", fpath)
|
||||||
return &chrome{
|
return chromePrinter{
|
||||||
url: URL,
|
logger: logger,
|
||||||
opts: opts,
|
url: URL,
|
||||||
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,36 +9,46 @@ import (
|
|||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/russross/blackfriday/v2"
|
"github.com/russross/blackfriday/v2"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMarkdown returns a Markdown printer.
|
// NewMarkdownPrinter returns a Printer which
|
||||||
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
// is able to convert Markdown files to PDF.
|
||||||
tmpl, err := template.
|
func NewMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) (Printer, error) {
|
||||||
New(filepath.Base(fpath)).
|
const op string = "printer.NewMarkdownPrinter"
|
||||||
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
resolver := func() (string, error) {
|
||||||
ParseFiles(fpath)
|
tmpl, err := template.
|
||||||
|
New(filepath.Base(fpath)).
|
||||||
|
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||||
|
ParseFiles(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dirPath := filepath.Dir(fpath)
|
||||||
|
data := &templateData{DirPath: dirPath}
|
||||||
|
logger.DebugOp(op, "converting Markdown files to HTML...")
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
baseFilename := xrand.Get()
|
||||||
|
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
||||||
|
logger.DebugOp(op, "writing the HTML from previous conversion into new file...")
|
||||||
|
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("file://%s", dst), nil
|
||||||
|
}
|
||||||
|
URL, err := resolver()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: parsing template: %v", fpath, err)
|
return chromePrinter{}, xerror.New(op, err)
|
||||||
}
|
}
|
||||||
dirPath := filepath.Dir(fpath)
|
return chromePrinter{
|
||||||
data := &templateData{DirPath: dirPath}
|
logger: logger,
|
||||||
var buffer bytes.Buffer
|
url: URL,
|
||||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
opts: opts,
|
||||||
return nil, fmt.Errorf("%s: executing template: %v", fpath, err)
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
|
||||||
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: writing file: %v", dst, err)
|
|
||||||
}
|
|
||||||
URL := fmt.Sprintf("file://%s", dst)
|
|
||||||
return &chrome{
|
|
||||||
url: URL,
|
|
||||||
opts: opts,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +57,11 @@ type templateData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
||||||
|
const op string = "printer.markdownToHTML"
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||||
b, err := ioutil.ReadFile(fpath)
|
b, err := ioutil.ReadFile(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
return "", xerror.New(op, err)
|
||||||
}
|
}
|
||||||
unsafe := blackfriday.Run(b)
|
unsafe := blackfriday.Run(b)
|
||||||
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||||
|
|||||||
@@ -2,49 +2,71 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
"time"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type merge struct {
|
type mergePrinter struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
logger xlog.Logger
|
||||||
fpaths []string
|
fpaths []string
|
||||||
opts *MergeOptions
|
opts MergePrinterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeOptions helps customizing the
|
// MergePrinterOptions helps customizing the
|
||||||
// merge printer behaviour.
|
// merge Printer behaviour.
|
||||||
type MergeOptions struct {
|
type MergePrinterOptions struct {
|
||||||
WaitTimeout float64
|
WaitTimeout float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMerge returns a merge printer.
|
// NewMergePrinter returns a Printer which
|
||||||
func NewMerge(fpaths []string, opts *MergeOptions) Printer {
|
// is able to merge PDFs.
|
||||||
return &merge{
|
func NewMergePrinter(logger xlog.Logger, fpaths []string, opts MergePrinterOptions) Printer {
|
||||||
|
return mergePrinter{
|
||||||
|
logger: logger,
|
||||||
fpaths: fpaths,
|
fpaths: fpaths,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *merge) Print(destination string) error {
|
func (p mergePrinter) Print(destination string) error {
|
||||||
|
const op string = "printer.mergePrinter.Print"
|
||||||
|
/*
|
||||||
|
context.Context may be providen from
|
||||||
|
an officePrinter which needs to merge
|
||||||
|
its result files.
|
||||||
|
*/
|
||||||
if p.ctx == nil {
|
if p.ctx == nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
logOptions(p.logger, p.opts)
|
||||||
|
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
p.ctx = ctx
|
p.ctx = ctx
|
||||||
}
|
}
|
||||||
var cmdArgs []string
|
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
|
||||||
cmdArgs = append(cmdArgs, p.fpaths...)
|
resolver := func() error {
|
||||||
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
var args []string
|
||||||
cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...)
|
args = append(args, p.fpaths...)
|
||||||
_, err := cmd.Output()
|
args = append(args, "cat", "output", destination)
|
||||||
if err != nil {
|
cmd, err := xexec.CommandContext(p.ctx, p.logger, "pdftk", args...)
|
||||||
return fmt.Errorf("pdtk: %v", err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
xexec.LogBeforeExecute(p.logger, cmd)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
if err := resolver(); err != nil {
|
||||||
|
return xcontext.MustHandleError(
|
||||||
|
p.ctx,
|
||||||
|
xerror.New(op, err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(merge))
|
_ = Printer(new(mergePrinter))
|
||||||
)
|
)
|
||||||
|
|||||||
46
internal/pkg/printer/merge_test.go
Normal file
46
internal/pkg/printer/merge_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/printertest"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMergePrinter(t *testing.T) {
|
||||||
|
var (
|
||||||
|
logger xlog.Logger = xlogtest.DebugLogger()
|
||||||
|
fpaths []string = printertest.MergeFpaths(t)
|
||||||
|
opts MergePrinterOptions
|
||||||
|
dest string
|
||||||
|
p Printer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// default options.
|
||||||
|
opts = MergePrinterOptions{
|
||||||
|
WaitTimeout: 10.0,
|
||||||
|
}
|
||||||
|
p = NewMergePrinter(logger, fpaths, opts)
|
||||||
|
dest = printertest.GenerateDestination()
|
||||||
|
err = p.Print(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = os.RemoveAll(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK as context.Context
|
||||||
|
// should timeout.
|
||||||
|
opts = MergePrinterOptions{
|
||||||
|
WaitTimeout: 0.0,
|
||||||
|
}
|
||||||
|
p = NewMergePrinter(logger, fpaths, opts)
|
||||||
|
dest = printertest.GenerateDestination()
|
||||||
|
err = p.Print(dest)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
assert.Equal(t, xerror.TimeoutCode, xerror.Code(err))
|
||||||
|
err = os.RemoveAll(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
@@ -4,87 +4,125 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
|
"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/xrand"
|
||||||
)
|
)
|
||||||
|
|
||||||
type office struct {
|
type officePrinter struct {
|
||||||
|
logger xlog.Logger
|
||||||
fpaths []string
|
fpaths []string
|
||||||
opts *OfficeOptions
|
opts OfficePrinterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// OfficeOptions helps customizing the
|
// OfficePrinterOptions helps customizing the
|
||||||
// Office printer behaviour.
|
// Office Printer behaviour.
|
||||||
type OfficeOptions struct {
|
type OfficePrinterOptions struct {
|
||||||
WaitTimeout float64
|
WaitTimeout float64
|
||||||
Landscape bool
|
Landscape bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOffice returns an Office printer.
|
// NewOfficePrinter returns a Printer which
|
||||||
func NewOffice(fpaths []string, opts *OfficeOptions) Printer {
|
// is able to convert Office documents to PDF.
|
||||||
return &office{
|
func NewOfficePrinter(logger xlog.Logger, fpaths []string, opts OfficePrinterOptions) Printer {
|
||||||
|
return officePrinter{
|
||||||
|
logger: logger,
|
||||||
fpaths: fpaths,
|
fpaths: fpaths,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *office) Print(destination string) error {
|
func (p officePrinter) Print(destination string) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
const op string = "printer.officePrinter.Print"
|
||||||
|
logOptions(p.logger, p.opts)
|
||||||
|
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
fpaths := make([]string, len(p.fpaths))
|
resolver := func() error {
|
||||||
dirPath := filepath.Dir(destination)
|
fpaths := make([]string, len(p.fpaths))
|
||||||
for i, fpath := range p.fpaths {
|
dirPath := filepath.Dir(destination)
|
||||||
baseFilename, err := rand.Get()
|
for i, fpath := range p.fpaths {
|
||||||
if err != nil {
|
baseFilename := xrand.Get()
|
||||||
return err
|
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
||||||
|
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
|
||||||
|
if err := unoconv(ctx, p.logger, fpath, tmpDest, p.opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename)
|
||||||
|
fpaths[i] = tmpDest
|
||||||
}
|
}
|
||||||
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
if len(fpaths) == 1 {
|
||||||
if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
p.logger.DebugOp(op, "only one PDF created, nothing to merge")
|
||||||
return err
|
if err := os.Rename(fpaths[0], destination); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
fpaths[i] = tmpDest
|
m := mergePrinter{
|
||||||
|
logger: p.logger,
|
||||||
|
ctx: ctx,
|
||||||
|
fpaths: fpaths,
|
||||||
|
}
|
||||||
|
return m.Print(destination)
|
||||||
}
|
}
|
||||||
if len(fpaths) == 1 {
|
if err := resolver(); err != nil {
|
||||||
return os.Rename(fpaths[0], destination)
|
return xcontext.MustHandleError(
|
||||||
}
|
ctx,
|
||||||
m := &merge{
|
xerror.New(op, err),
|
||||||
ctx: ctx,
|
)
|
||||||
fpaths: fpaths,
|
|
||||||
}
|
|
||||||
return m.Print(destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint: gochecknoglobals
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
cmdArgs := []string{
|
|
||||||
"--format",
|
|
||||||
"pdf",
|
|
||||||
}
|
|
||||||
if opts.Landscape {
|
|
||||||
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "--output", destination, fpath)
|
|
||||||
cmd := exec.CommandContext(
|
|
||||||
ctx,
|
|
||||||
"unoconv",
|
|
||||||
cmdArgs...,
|
|
||||||
)
|
|
||||||
_, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unoconv: %v", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint: gochecknoglobals
|
||||||
|
var lock = make(chan struct{}, 1)
|
||||||
|
|
||||||
|
func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, opts OfficePrinterOptions) error {
|
||||||
|
const op string = "printer.unoconv"
|
||||||
|
resolver := func() error {
|
||||||
|
args := []string{
|
||||||
|
"--format",
|
||||||
|
"pdf",
|
||||||
|
}
|
||||||
|
if opts.Landscape {
|
||||||
|
args = append(args, "--printer", "PaperOrientation=landscape")
|
||||||
|
}
|
||||||
|
args = append(args, "--output", destination, fpath)
|
||||||
|
cmd, err := xexec.CommandContext(
|
||||||
|
ctx,
|
||||||
|
logger,
|
||||||
|
"unoconv",
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
xexec.LogBeforeExecute(logger, cmd)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
logger.DebugOp(op, "waiting lock to be acquired...")
|
||||||
|
select {
|
||||||
|
case lock <- struct{}{}:
|
||||||
|
// lock acquired.
|
||||||
|
logger.DebugOp(op, "lock acquired")
|
||||||
|
if err := resolver(); err != nil {
|
||||||
|
<-lock // we release the lock.
|
||||||
|
return xerror.New(op, err)
|
||||||
|
}
|
||||||
|
<-lock // we release the lock.
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
// failed to acquire lock before
|
||||||
|
// deadline.
|
||||||
|
logger.DebugOp(op, "failed to acquire lock before context.Context deadline")
|
||||||
|
return xerror.New(op, ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(office))
|
_ = Printer(new(officePrinter))
|
||||||
)
|
)
|
||||||
|
|||||||
70
internal/pkg/printer/office_test.go
Normal file
70
internal/pkg/printer/office_test.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/printertest"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOfficePrinter(t *testing.T) {
|
||||||
|
var (
|
||||||
|
logger xlog.Logger = xlogtest.DebugLogger()
|
||||||
|
fpaths []string = printertest.OfficeFpaths(t)
|
||||||
|
opts OfficePrinterOptions
|
||||||
|
dest string
|
||||||
|
p Printer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// default options.
|
||||||
|
opts = OfficePrinterOptions{
|
||||||
|
WaitTimeout: 10.0,
|
||||||
|
Landscape: false,
|
||||||
|
}
|
||||||
|
p = NewOfficePrinter(logger, fpaths, opts)
|
||||||
|
dest = printertest.GenerateDestination()
|
||||||
|
err = p.Print(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = os.RemoveAll(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// using one file.
|
||||||
|
opts = OfficePrinterOptions{
|
||||||
|
WaitTimeout: 10.0,
|
||||||
|
Landscape: false,
|
||||||
|
}
|
||||||
|
p = NewOfficePrinter(logger, []string{fpaths[0]}, opts)
|
||||||
|
dest = printertest.GenerateDestination()
|
||||||
|
err = p.Print(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = os.RemoveAll(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// options with landscape.
|
||||||
|
opts = OfficePrinterOptions{
|
||||||
|
WaitTimeout: 10.0,
|
||||||
|
Landscape: true,
|
||||||
|
}
|
||||||
|
p = NewOfficePrinter(logger, fpaths, opts)
|
||||||
|
dest = printertest.GenerateDestination()
|
||||||
|
err = p.Print(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = os.RemoveAll(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK as context.Context
|
||||||
|
// should timeout.
|
||||||
|
opts = OfficePrinterOptions{
|
||||||
|
WaitTimeout: 0.0,
|
||||||
|
Landscape: true,
|
||||||
|
}
|
||||||
|
p = NewOfficePrinter(logger, fpaths, opts)
|
||||||
|
dest = printertest.GenerateDestination()
|
||||||
|
err = p.Print(dest)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
assert.Equal(t, xerror.TimeoutCode, xerror.Code(err))
|
||||||
|
err = os.RemoveAll(dest)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
// Printer is a type that can create a PDF file from a source.
|
// Printer is a type that can create a PDF file from a source.
|
||||||
// The source is defined in the underlying implementation.
|
// The source is defined in the underlying implementation.
|
||||||
type Printer interface {
|
type Printer interface {
|
||||||
Print(destination string) error
|
Print(destination string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logOptions(logger xlog.Logger, opts interface{}) {
|
||||||
|
const op string = "printer.logOptions"
|
||||||
|
logger.DebugfOp(op, "options: %+v", opts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
// NewURL returns a URL printer.
|
import (
|
||||||
func NewURL(url string, opts *ChromeOptions) Printer {
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
return &chrome{
|
)
|
||||||
url: url,
|
|
||||||
opts: opts,
|
// NewURLPrinter returns a Printer which
|
||||||
|
// is able to convert a URL to PDF.
|
||||||
|
func NewURLPrinter(logger xlog.Logger, url string, opts ChromePrinterOptions) Printer {
|
||||||
|
return chromePrinter{
|
||||||
|
logger: logger,
|
||||||
|
url: url,
|
||||||
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
/*
|
|
||||||
Package rand helps generating a random string.
|
|
||||||
|
|
||||||
It should be used for creating directory and
|
|
||||||
file names in order to avoid collision.
|
|
||||||
*/
|
|
||||||
package rand
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package rand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get returns a random string.
|
|
||||||
func Get() (string, error) {
|
|
||||||
randBytes := make([]byte, 16)
|
|
||||||
_, err := rand.Read(randBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("creating random string: %v", err)
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(randBytes), nil
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package rand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
|
||||||
rand1, err := Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
rand2, err := Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.NotEqual(t, rand1, rand2)
|
|
||||||
}
|
|
||||||
8
internal/pkg/xassert/doc.go
Normal file
8
internal/pkg/xassert/doc.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Package xassert is a helper for converting
|
||||||
|
and/or validating strings.
|
||||||
|
|
||||||
|
All functions return our standard xerror.Error
|
||||||
|
in case of error.
|
||||||
|
*/
|
||||||
|
package xassert
|
||||||
88
internal/pkg/xassert/float64.go
Normal file
88
internal/pkg/xassert/float64.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RuleFloat64 is an interface for
|
||||||
|
// validating a float64.
|
||||||
|
type RuleFloat64 interface {
|
||||||
|
with(key string, value float64)
|
||||||
|
validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseRuleFloat64 struct {
|
||||||
|
key string
|
||||||
|
value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *baseRuleFloat64) with(key string, value float64) {
|
||||||
|
r.key = key
|
||||||
|
r.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruleFloat64NotInferiorTo struct {
|
||||||
|
*baseRuleFloat64
|
||||||
|
lowerBound float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ruleFloat64NotInferiorTo) validate() error {
|
||||||
|
const op string = "xassert.ruleFloat64NotInferiorTo.validate"
|
||||||
|
if r.value < r.lowerBound {
|
||||||
|
return xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' should be > '%f', got '%f'", r.key, r.lowerBound, r.value),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Float64NotInferiorTo returns a RuleFloat64 for
|
||||||
|
validating that a float64 is not inferior to
|
||||||
|
given lower bound.
|
||||||
|
*/
|
||||||
|
func Float64NotInferiorTo(lowerBound float64) RuleFloat64 {
|
||||||
|
return ruleFloat64NotInferiorTo{
|
||||||
|
&baseRuleFloat64{},
|
||||||
|
lowerBound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruleFloat64NotSuperiorTo struct {
|
||||||
|
*baseRuleFloat64
|
||||||
|
upperBound float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ruleFloat64NotSuperiorTo) validate() error {
|
||||||
|
const op string = "xassert.ruleFloat64NotSuperiorTo.validate"
|
||||||
|
if r.value > r.upperBound {
|
||||||
|
return xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' should be < '%f', got '%f'", r.key, r.upperBound, r.value),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Float64NotSuperiorTo returns a RuleFloat64 for
|
||||||
|
validating that a float64 is not superior to
|
||||||
|
given upper bound.
|
||||||
|
*/
|
||||||
|
func Float64NotSuperiorTo(upperBound float64) RuleFloat64 {
|
||||||
|
return ruleFloat64NotSuperiorTo{
|
||||||
|
&baseRuleFloat64{},
|
||||||
|
upperBound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = RuleFloat64(new(ruleFloat64NotInferiorTo))
|
||||||
|
_ = RuleFloat64(new(ruleFloat64NotSuperiorTo))
|
||||||
|
)
|
||||||
32
internal/pkg/xassert/float64_test.go
Normal file
32
internal/pkg/xassert/float64_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFloat64NotInferiorTo(t *testing.T) {
|
||||||
|
rule := Float64NotInferiorTo(0.0)
|
||||||
|
// should be OK.
|
||||||
|
rule.with("FOO", 10.0)
|
||||||
|
err := rule.validate()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK.
|
||||||
|
rule.with("FOO", -10.0)
|
||||||
|
err = rule.validate()
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloat64NotSuperiorTo(t *testing.T) {
|
||||||
|
rule := Float64NotSuperiorTo(0.0)
|
||||||
|
// should be OK.
|
||||||
|
rule.with("FOO", -10.0)
|
||||||
|
err := rule.validate()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK.
|
||||||
|
rule.with("FOO", 10.0)
|
||||||
|
err = rule.validate()
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
88
internal/pkg/xassert/int64.go
Normal file
88
internal/pkg/xassert/int64.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RuleInt64 is an interface for
|
||||||
|
// validating an int64.
|
||||||
|
type RuleInt64 interface {
|
||||||
|
with(key string, value int64)
|
||||||
|
validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseRuleInt64 struct {
|
||||||
|
key string
|
||||||
|
value int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *baseRuleInt64) with(key string, value int64) {
|
||||||
|
r.key = key
|
||||||
|
r.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruleInt64NotInferiorTo struct {
|
||||||
|
*baseRuleInt64
|
||||||
|
lowerBound int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ruleInt64NotInferiorTo) validate() error {
|
||||||
|
const op string = "xassert.ruleInt64NotInferiorTo.validate"
|
||||||
|
if r.value < r.lowerBound {
|
||||||
|
return xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' should be > '%d', got '%d'", r.key, r.lowerBound, r.value),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Int64NotInferiorTo returns a RuleInt64 for
|
||||||
|
validating that an int64 is not inferior to
|
||||||
|
given lower bound.
|
||||||
|
*/
|
||||||
|
func Int64NotInferiorTo(lowerBound int64) RuleInt64 {
|
||||||
|
return &ruleInt64NotInferiorTo{
|
||||||
|
&baseRuleInt64{},
|
||||||
|
lowerBound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruleInt64NotSuperiorTo struct {
|
||||||
|
*baseRuleInt64
|
||||||
|
upperBound int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ruleInt64NotSuperiorTo) validate() error {
|
||||||
|
const op string = "xassert.ruleInt64NotSuperiorTo.validate"
|
||||||
|
if r.value > r.upperBound {
|
||||||
|
return xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' should be < '%d', got '%d'", r.key, r.upperBound, r.value),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Int64NotSuperiorTo returns a RuleInt64 for
|
||||||
|
validating that an int64 is not superior to
|
||||||
|
given upper bound.
|
||||||
|
*/
|
||||||
|
func Int64NotSuperiorTo(upperBound int64) RuleInt64 {
|
||||||
|
return ruleInt64NotSuperiorTo{
|
||||||
|
&baseRuleInt64{},
|
||||||
|
upperBound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = RuleInt64(new(ruleInt64NotInferiorTo))
|
||||||
|
_ = RuleInt64(new(ruleInt64NotSuperiorTo))
|
||||||
|
)
|
||||||
32
internal/pkg/xassert/int64_test.go
Normal file
32
internal/pkg/xassert/int64_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInt64NotInferiorTo(t *testing.T) {
|
||||||
|
rule := Int64NotInferiorTo(0)
|
||||||
|
// should be OK.
|
||||||
|
rule.with("FOO", 10)
|
||||||
|
err := rule.validate()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK.
|
||||||
|
rule.with("FOO", -10)
|
||||||
|
err = rule.validate()
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInt64NotSuperiorTo(t *testing.T) {
|
||||||
|
rule := Int64NotSuperiorTo(0)
|
||||||
|
// should be OK.
|
||||||
|
rule.with("FOO", -10)
|
||||||
|
err := rule.validate()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK.
|
||||||
|
rule.with("FOO", 10)
|
||||||
|
err = rule.validate()
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
60
internal/pkg/xassert/string.go
Normal file
60
internal/pkg/xassert/string.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RuleString is an interface for
|
||||||
|
// validating a string.
|
||||||
|
type RuleString interface {
|
||||||
|
with(key, value string)
|
||||||
|
validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseRuleString struct {
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *baseRuleString) with(key, value string) {
|
||||||
|
r.key = key
|
||||||
|
r.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruleStringOneOf struct {
|
||||||
|
*baseRuleString
|
||||||
|
values []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ruleStringOneOf) validate() error {
|
||||||
|
const op string = "xassert.ruleStringOneOf.validate"
|
||||||
|
for _, v := range r.values {
|
||||||
|
if r.value == v {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' should be one of '%v', got '%s'", r.key, r.values, r.value),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringOneOf returns a RuleString for
|
||||||
|
validating that a string is one of given
|
||||||
|
values.
|
||||||
|
*/
|
||||||
|
func StringOneOf(values []string) RuleString {
|
||||||
|
return ruleStringOneOf{
|
||||||
|
&baseRuleString{},
|
||||||
|
values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = RuleString(new(ruleStringOneOf))
|
||||||
|
)
|
||||||
20
internal/pkg/xassert/string_test.go
Normal file
20
internal/pkg/xassert/string_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringOfOne(t *testing.T) {
|
||||||
|
rule := StringOneOf([]string{"foo", "bar", "baz"})
|
||||||
|
// should be OK.
|
||||||
|
rule.with("FOO", "foo")
|
||||||
|
err := rule.validate()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK.
|
||||||
|
rule.with("FOO", "qux")
|
||||||
|
err = rule.validate()
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
185
internal/pkg/xassert/xassert.go
Normal file
185
internal/pkg/xassert/xassert.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
String applies validation on a string.
|
||||||
|
|
||||||
|
If string is empty or validation fails,
|
||||||
|
returns the default value.
|
||||||
|
|
||||||
|
The key is used to identify the value.
|
||||||
|
*/
|
||||||
|
func String(key, value, defaultValue string, rules ...RuleString) (string, error) {
|
||||||
|
const op string = "xassert.String"
|
||||||
|
result := defaultValue
|
||||||
|
if value != "" {
|
||||||
|
result = value
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
rule.with(key, result)
|
||||||
|
if err := rule.validate(); err != nil {
|
||||||
|
return defaultValue, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringFromEnv returns the value of given environment
|
||||||
|
variable or the default value if not found or
|
||||||
|
validation fails.
|
||||||
|
*/
|
||||||
|
func StringFromEnv(envVar, defaultValue string, rules ...RuleString) (string, error) {
|
||||||
|
const op string = "xassert.StringFromEnv"
|
||||||
|
value := os.Getenv(envVar)
|
||||||
|
result, err := String(envVar, value, defaultValue, rules...)
|
||||||
|
if err != nil {
|
||||||
|
return result, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Int64 tries to convert a string to an int64.
|
||||||
|
|
||||||
|
If string is empty, conversion or validation fails,
|
||||||
|
returns the default value.
|
||||||
|
|
||||||
|
The key is used to identify the value.
|
||||||
|
*/
|
||||||
|
func Int64(key, value string, defaultValue int64, rules ...RuleInt64) (int64, error) {
|
||||||
|
const op string = "xassert.Int64"
|
||||||
|
result := defaultValue
|
||||||
|
if value != "" {
|
||||||
|
parsedValue, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue, xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' is not an integer, got '%s'", key, value),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result = parsedValue
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
rule.with(key, result)
|
||||||
|
if err := rule.validate(); err != nil {
|
||||||
|
return defaultValue, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Int64FromEnv returns the int64 representation of the
|
||||||
|
value of given environment variable.
|
||||||
|
|
||||||
|
If not found, empty, conversion or validation fails,
|
||||||
|
returns the default value.
|
||||||
|
*/
|
||||||
|
func Int64FromEnv(envVar string, defaultValue int64, rules ...RuleInt64) (int64, error) {
|
||||||
|
const op string = "xassert.Int64FromEnv"
|
||||||
|
value := os.Getenv(envVar)
|
||||||
|
result, err := Int64(envVar, value, defaultValue, rules...)
|
||||||
|
if err != nil {
|
||||||
|
return result, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Float64 tries to convert a string to a float64.
|
||||||
|
|
||||||
|
If string is empty, conversion or validation fails,
|
||||||
|
returns the default value.
|
||||||
|
|
||||||
|
The key is used to identify the value.
|
||||||
|
*/
|
||||||
|
func Float64(key, value string, defaultValue float64, rules ...RuleFloat64) (float64, error) {
|
||||||
|
const op string = "xassert.Float64"
|
||||||
|
result := defaultValue
|
||||||
|
if value != "" {
|
||||||
|
parsedValue, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue, xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' is not a float, got '%s'", key, value),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result = parsedValue
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
rule.with(key, result)
|
||||||
|
if err := rule.validate(); err != nil {
|
||||||
|
return defaultValue, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Float64FromEnv returns the float64 representation of the
|
||||||
|
value of given environment variable.
|
||||||
|
|
||||||
|
If not found, empty, conversion or validation fails,
|
||||||
|
returns the default value.
|
||||||
|
*/
|
||||||
|
func Float64FromEnv(envVar string, defaultValue float64, rules ...RuleFloat64) (float64, error) {
|
||||||
|
const op string = "xassert.Float64FromEnv"
|
||||||
|
value := os.Getenv(envVar)
|
||||||
|
result, err := Float64(envVar, value, defaultValue, rules...)
|
||||||
|
if err != nil {
|
||||||
|
return result, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bool tries to convert a string to a boolean.
|
||||||
|
|
||||||
|
If string is empty or conversion fails, returns the
|
||||||
|
default value.
|
||||||
|
|
||||||
|
The key is used to identify the value.
|
||||||
|
*/
|
||||||
|
func Bool(key, value string, defaultValue bool) (bool, error) {
|
||||||
|
const op string = "xassert.Bool"
|
||||||
|
result := defaultValue
|
||||||
|
if value != "" {
|
||||||
|
parsedValue, err := strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue, xerror.Invalid(
|
||||||
|
op,
|
||||||
|
fmt.Sprintf("'%s' is not a boolean, got '%s'", key, value),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result = parsedValue
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
BoolFromEnv returns the boolean representation of the
|
||||||
|
value of given environment variable.
|
||||||
|
|
||||||
|
If not found, empty or conversion fails, returns the
|
||||||
|
default value.
|
||||||
|
*/
|
||||||
|
func BoolFromEnv(envVar string, defaultValue bool) (bool, error) {
|
||||||
|
const op string = "xassert.BoolFromEnv"
|
||||||
|
value := os.Getenv(envVar)
|
||||||
|
result, err := Bool(envVar, value, defaultValue)
|
||||||
|
if err != nil {
|
||||||
|
return result, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
289
internal/pkg/xassert/xassert_test.go
Normal file
289
internal/pkg/xassert/xassert_test.go
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
package xassert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
const (
|
||||||
|
defaultValue string = "FOO"
|
||||||
|
)
|
||||||
|
var expected string
|
||||||
|
rule := StringOneOf([]string{"FOO", "BAR"})
|
||||||
|
// empty value, result should be equal
|
||||||
|
// to the default value.
|
||||||
|
v, err := String("foo", "", defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to given value
|
||||||
|
// as it is one of "FOO" and "BAR".
|
||||||
|
expected = "FOO"
|
||||||
|
v, err = String("foo", expected, defaultValue, rule)
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// should not be OK as given value is not
|
||||||
|
// one of "FOO" and "BAR".
|
||||||
|
v, err = String("foo", "BAZ", defaultValue, rule)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringFromEnv(t *testing.T) {
|
||||||
|
const (
|
||||||
|
envVar string = "FOO"
|
||||||
|
defaultValue string = "FOO"
|
||||||
|
)
|
||||||
|
var expected string
|
||||||
|
rule := StringOneOf([]string{"FOO", "BAR"})
|
||||||
|
// no environment variable set,
|
||||||
|
// value should be equal to default value.
|
||||||
|
v, err := StringFromEnv(envVar, defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to environment variable
|
||||||
|
// value as it is one of "FOO" and "BAR".
|
||||||
|
expected = "BAR"
|
||||||
|
os.Setenv(envVar, expected)
|
||||||
|
v, err = StringFromEnv(envVar, defaultValue, rule)
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
// should not be OK as environment variable
|
||||||
|
// value is not one of "FOO" and "BAR".
|
||||||
|
os.Setenv(envVar, "BAZ")
|
||||||
|
v, err = StringFromEnv(envVar, defaultValue, rule)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInt64(t *testing.T) {
|
||||||
|
const (
|
||||||
|
defaultValue int64 = 10
|
||||||
|
)
|
||||||
|
var expected int64
|
||||||
|
rule := Int64NotInferiorTo(6)
|
||||||
|
// empty value, result should be equal
|
||||||
|
// to the default value.
|
||||||
|
v, err := Int64("foo", "", defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to given value
|
||||||
|
// but as integer.
|
||||||
|
v, err = Int64("foo", "5", 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.
|
||||||
|
v, err = Int64("foo", "foo", defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
// should not be OK as given value does not
|
||||||
|
// validate the rule x >= 6.
|
||||||
|
v, err = Int64("foo", "5", defaultValue, rule)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInt64FromEnv(t *testing.T) {
|
||||||
|
const (
|
||||||
|
envVar string = "FOO"
|
||||||
|
defaultValue int64 = 10
|
||||||
|
)
|
||||||
|
var expected int64
|
||||||
|
rule := Int64NotInferiorTo(6)
|
||||||
|
// no environment variable set,
|
||||||
|
// value should be equal to default value.
|
||||||
|
v, err := Int64FromEnv(envVar, defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to environment variable
|
||||||
|
// value but as integer.
|
||||||
|
os.Setenv(envVar, "5")
|
||||||
|
v, err = Int64FromEnv(envVar, defaultValue)
|
||||||
|
expected = 5
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
// should not be OK as environment variable
|
||||||
|
// value is not a string representation of an integer.
|
||||||
|
os.Setenv(envVar, "foo")
|
||||||
|
v, err = Int64FromEnv(envVar, defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
// should not be OK as environment variable
|
||||||
|
// value does not validate the rule x >= 6.
|
||||||
|
os.Setenv(envVar, "5")
|
||||||
|
v, err = Int64FromEnv(envVar, defaultValue, rule)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloat64(t *testing.T) {
|
||||||
|
const defaultValue float64 = 10.0
|
||||||
|
var expected float64
|
||||||
|
rule := Float64NotInferiorTo(6.0)
|
||||||
|
// empty value, result should be equal
|
||||||
|
// to the default value.
|
||||||
|
v, err := Float64("foo", "", defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to given value
|
||||||
|
// but as float.
|
||||||
|
v, err = Float64("foo", "5.5", 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.
|
||||||
|
v, err = Float64("foo", "foo", defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
// should not be OK as given value does not
|
||||||
|
// validate the rule x >= 6.
|
||||||
|
v, err = Float64("foo", "5", defaultValue, rule)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloat64FromEnv(t *testing.T) {
|
||||||
|
const (
|
||||||
|
envVar string = "FOO"
|
||||||
|
defaultValue float64 = 10.0
|
||||||
|
)
|
||||||
|
var expected float64
|
||||||
|
rule := Float64NotInferiorTo(6.0)
|
||||||
|
// no environment variable set,
|
||||||
|
// value should be equal to default value.
|
||||||
|
v, err := Float64FromEnv(envVar, defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to environment variable
|
||||||
|
// value but as float.
|
||||||
|
os.Setenv(envVar, "5.5")
|
||||||
|
v, err = Float64FromEnv(envVar, defaultValue)
|
||||||
|
expected = 5.5
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
// should not be OK as environment variable
|
||||||
|
// value is not a string representation of a float.
|
||||||
|
os.Setenv(envVar, "foo")
|
||||||
|
v, err = Float64FromEnv(envVar, defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
// should not be OK as environment variable
|
||||||
|
// value does not validate the rule x >= 6.
|
||||||
|
os.Setenv(envVar, "5")
|
||||||
|
v, err = Float64FromEnv(envVar, defaultValue, rule)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBool(t *testing.T) {
|
||||||
|
const defaultValue bool = true
|
||||||
|
var expected bool
|
||||||
|
// empty value, result should be equal
|
||||||
|
// to the default value.
|
||||||
|
v, err := Bool("foo", "", defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to given value
|
||||||
|
// but as boolean.
|
||||||
|
v, err = Bool("foo", "1", defaultValue)
|
||||||
|
expected = true
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
v, err = Bool("foo", "true", defaultValue)
|
||||||
|
expected = true
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
v, err = Bool("foo", "0", defaultValue)
|
||||||
|
expected = false
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
v, err = Bool("foo", "false", 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.
|
||||||
|
v, err = Bool("foo", "foo", defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolFromEnv(t *testing.T) {
|
||||||
|
const (
|
||||||
|
envVar string = "FOO"
|
||||||
|
defaultValue bool = true
|
||||||
|
)
|
||||||
|
var expected bool
|
||||||
|
// no environment variable set,
|
||||||
|
// value should be equal to default value.
|
||||||
|
v, err := BoolFromEnv(envVar, defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// result should be equal to environment variable
|
||||||
|
// value but as boolean.
|
||||||
|
os.Setenv(envVar, "1")
|
||||||
|
v, err = BoolFromEnv(envVar, defaultValue)
|
||||||
|
expected = true
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
os.Setenv(envVar, "true")
|
||||||
|
v, err = BoolFromEnv(envVar, defaultValue)
|
||||||
|
expected = true
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
os.Setenv(envVar, "0")
|
||||||
|
v, err = BoolFromEnv(envVar, defaultValue)
|
||||||
|
expected = false
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
os.Setenv(envVar, "false")
|
||||||
|
v, err = BoolFromEnv(envVar, defaultValue)
|
||||||
|
expected = false
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
// should not be OK as environment variable
|
||||||
|
// value is not a string representation of a boolean.
|
||||||
|
os.Setenv(envVar, "foo")
|
||||||
|
v, err = BoolFromEnv(envVar, defaultValue)
|
||||||
|
expected = defaultValue
|
||||||
|
assert.Equal(t, expected, v)
|
||||||
|
xerrortest.AssertError(t, err)
|
||||||
|
os.Unsetenv(envVar)
|
||||||
|
}
|
||||||
3
internal/pkg/xcontext/doc.go
Normal file
3
internal/pkg/xcontext/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package xcontext helps managing
|
||||||
|
// context.Context with timeout.
|
||||||
|
package xcontext
|
||||||
56
internal/pkg/xcontext/xcontext.go
Normal file
56
internal/pkg/xcontext/xcontext.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package xcontext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithTimeout creates a context.Context which
|
||||||
|
// times out after given seconds.
|
||||||
|
func WithTimeout(logger xlog.Logger, seconds float64) (context.Context, context.CancelFunc) {
|
||||||
|
const op string = "xcontext.WithTimeout"
|
||||||
|
logger.DebugfOp(op, "creating context with '%.2fs' of timeout...", seconds)
|
||||||
|
return context.WithTimeout(context.Background(), xtime.Duration(seconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
MustHandleError checks if there is an error
|
||||||
|
in the given Context.
|
||||||
|
|
||||||
|
If no error, returns the previous error.
|
||||||
|
|
||||||
|
If context.DeadlineExceeded, wraps the previous
|
||||||
|
error inside an xerror.Error with xerror.TimeoutCode.
|
||||||
|
|
||||||
|
Otherwise wraps the previous error inside an
|
||||||
|
xerror.Error.
|
||||||
|
|
||||||
|
It panics if no previous error.
|
||||||
|
*/
|
||||||
|
func MustHandleError(ctx context.Context, previousErr error) error {
|
||||||
|
const op string = "xcontext.MustHandleError"
|
||||||
|
if previousErr == nil {
|
||||||
|
panic(fmt.Sprintf("%s: previous error should not be nil", op))
|
||||||
|
}
|
||||||
|
err := ctx.Err()
|
||||||
|
if err == nil {
|
||||||
|
// we do not wrap the previous error
|
||||||
|
// as it should be wrapped by the caller.
|
||||||
|
return previousErr
|
||||||
|
}
|
||||||
|
// context has timed out
|
||||||
|
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
||||||
|
return xerror.Timeout(op, "context has timed out", previousErr)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
context has another error: we do not
|
||||||
|
wrap the error from the Context as the previous
|
||||||
|
error should contain it.
|
||||||
|
*/
|
||||||
|
return xerror.New(op, previousErr)
|
||||||
|
}
|
||||||
43
internal/pkg/xcontext/xcontext_test.go
Normal file
43
internal/pkg/xcontext/xcontext_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package xcontext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMustHandleError(t *testing.T) {
|
||||||
|
previousErr := errors.New("previous error")
|
||||||
|
logger := xlogtest.DebugLogger()
|
||||||
|
// context should not have an error.
|
||||||
|
ctx, cancel := WithTimeout(logger, 5)
|
||||||
|
defer cancel()
|
||||||
|
err := MustHandleError(ctx, previousErr)
|
||||||
|
assert.Equal(t, previousErr, err)
|
||||||
|
// should panic.
|
||||||
|
ctx, cancel = WithTimeout(logger, 5)
|
||||||
|
defer cancel()
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
MustHandleError(ctx, nil)
|
||||||
|
})
|
||||||
|
// context should timed out.
|
||||||
|
ctx, cancel = WithTimeout(logger, 0.5)
|
||||||
|
defer cancel()
|
||||||
|
time.Sleep(xtime.Duration(1))
|
||||||
|
err = MustHandleError(ctx, previousErr)
|
||||||
|
xerr := xerrortest.AssertError(t, err)
|
||||||
|
assert.Equal(t, xerror.TimeoutCode, xerror.Code(xerr))
|
||||||
|
// context should have an error different
|
||||||
|
// than context.DeadlineExceeded.
|
||||||
|
ctx, cancel = WithTimeout(logger, 5)
|
||||||
|
cancel()
|
||||||
|
err = MustHandleError(ctx, previousErr)
|
||||||
|
xerr = xerrortest.AssertError(t, err)
|
||||||
|
assert.Equal(t, xerror.InternalCode, xerror.Code(xerr))
|
||||||
|
}
|
||||||
7
internal/pkg/xerror/doc.go
Normal file
7
internal/pkg/xerror/doc.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
Package xerror helps standardizing
|
||||||
|
the errors through the application.
|
||||||
|
|
||||||
|
Credits: https://middlemost.com/failure-is-your-domain/
|
||||||
|
*/
|
||||||
|
package xerror
|
||||||
152
internal/pkg/xerror/xerror.go
Normal file
152
internal/pkg/xerror/xerror.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package xerror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorCode is machine-readable error code.
|
||||||
|
type ErrorCode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// InternalCode is an internal error.
|
||||||
|
InternalCode ErrorCode = "internal"
|
||||||
|
// InvalidCode occurs when a validation
|
||||||
|
// failed.
|
||||||
|
InvalidCode ErrorCode = "invalid"
|
||||||
|
// TimeoutCode occurs when something
|
||||||
|
// timed out.
|
||||||
|
TimeoutCode ErrorCode = "timeout"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error defines our standard application
|
||||||
|
// error.
|
||||||
|
type Error struct {
|
||||||
|
code ErrorCode
|
||||||
|
message string
|
||||||
|
op string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the string representation of the error message.
|
||||||
|
func (e Error) Error() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
// if wrapping an error, print its Error() message.
|
||||||
|
// Otherwise print the error code & message.
|
||||||
|
if e.err != nil {
|
||||||
|
buf.WriteString(e.err.Error())
|
||||||
|
} else {
|
||||||
|
if e.code != "" {
|
||||||
|
fmt.Fprintf(&buf, "<%s> ", e.code)
|
||||||
|
}
|
||||||
|
buf.WriteString(e.message)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
New returns a xerror.Error.
|
||||||
|
|
||||||
|
Should be used for wrapping an error
|
||||||
|
at the end of a function.
|
||||||
|
*/
|
||||||
|
func New(op string, previous error) error {
|
||||||
|
return &Error{
|
||||||
|
op: op,
|
||||||
|
err: previous,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Invalid returns a xerror.Error.
|
||||||
|
|
||||||
|
Should be used when an input
|
||||||
|
is wrong.
|
||||||
|
*/
|
||||||
|
func Invalid(op, message string, previous error) error {
|
||||||
|
return &Error{
|
||||||
|
code: InvalidCode,
|
||||||
|
message: message,
|
||||||
|
op: op,
|
||||||
|
err: previous,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Timeout returns a xerror.Error.
|
||||||
|
|
||||||
|
Should be used when a timeout occurs.
|
||||||
|
*/
|
||||||
|
func Timeout(op, message string, previous error) error {
|
||||||
|
return &Error{
|
||||||
|
code: TimeoutCode,
|
||||||
|
message: message,
|
||||||
|
op: op,
|
||||||
|
err: previous,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code returns the code of the root error, if available.
|
||||||
|
// Otherwise returns InternalCode.
|
||||||
|
func Code(err error) ErrorCode {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
e, ok := err.(*Error)
|
||||||
|
if ok && e.code != "" {
|
||||||
|
return e.code
|
||||||
|
}
|
||||||
|
if ok && e.err != nil {
|
||||||
|
return Code(e.err)
|
||||||
|
}
|
||||||
|
return InternalCode
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultMessage string = "an internal error has occurred: please contact technical support"
|
||||||
|
|
||||||
|
// Message returns the human-readable message of the error, if available.
|
||||||
|
// Otherwise returns a generic error message.
|
||||||
|
func Message(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
e, ok := err.(*Error)
|
||||||
|
if ok && e.message != "" {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
if ok && e.err != nil {
|
||||||
|
return Message(e.err)
|
||||||
|
}
|
||||||
|
return defaultMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Op returns the logical operation of the error, if available.
|
||||||
|
// Otherwise returns an empty string.
|
||||||
|
func Op(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
e, ok := err.(*Error)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
nestedOp := Op(e.err)
|
||||||
|
if nestedOp != "" {
|
||||||
|
// we want to avoid having the same op chained.
|
||||||
|
if e.op != "" && !strings.Contains(nestedOp, e.op) {
|
||||||
|
fmt.Fprintf(&buf, "%s: %s", e.op, nestedOp)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&buf, "%s", nestedOp)
|
||||||
|
}
|
||||||
|
} else if e.op != "" {
|
||||||
|
fmt.Fprintf(&buf, "%s", e.op)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = error(new(Error))
|
||||||
|
)
|
||||||
97
internal/pkg/xerror/xerror_test.go
Normal file
97
internal/pkg/xerror/xerror_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package xerror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Error 1.0: op = "foo"
|
||||||
|
Error 1.1: op = "bar"
|
||||||
|
Error 1.2: code = "invalid", op = "baz", message = "nested error"
|
||||||
|
Error 1.3: message = "root error"
|
||||||
|
*/
|
||||||
|
func scenario1() error {
|
||||||
|
rootErr := errors.New("root error")
|
||||||
|
nestedErr := Invalid("baz", "nested error", rootErr)
|
||||||
|
wrappingErr := New("bar", nestedErr)
|
||||||
|
return New("foo", wrappingErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Error 2.0: op = "foo"
|
||||||
|
Error 2.1: op = "bar"
|
||||||
|
Error 2.2: code = "timeout", op = "bar", message = "nested error"
|
||||||
|
*/
|
||||||
|
func scenario2() error {
|
||||||
|
nestedErr := Timeout("bar", "nested error", nil)
|
||||||
|
wrappingErr := New("bar", nestedErr)
|
||||||
|
return New("foo", wrappingErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 3.0: code = "", op = "foo"
|
||||||
|
func scenario3() error {
|
||||||
|
return New("foo", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
// should return the Error 1.3
|
||||||
|
// message.
|
||||||
|
err := scenario1()
|
||||||
|
assert.Equal(t, "root error", err.Error())
|
||||||
|
// should return the Error 2.2 message with
|
||||||
|
// its code.
|
||||||
|
err = scenario2()
|
||||||
|
assert.Equal(t, "<timeout> nested error", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCode(t *testing.T) {
|
||||||
|
// should be an empty code if no error.
|
||||||
|
assert.Equal(t, "", fmt.Sprintf("%s", Code(nil)))
|
||||||
|
// should be the code of Error 1.2.
|
||||||
|
err := scenario1()
|
||||||
|
assert.Equal(t, InvalidCode, Code(err))
|
||||||
|
// should be the code of Error 2.2.
|
||||||
|
err = scenario2()
|
||||||
|
assert.Equal(t, TimeoutCode, Code(err))
|
||||||
|
// should be the default code.
|
||||||
|
err = scenario3()
|
||||||
|
assert.Equal(t, InternalCode, Code(err))
|
||||||
|
err = errors.New("some error")
|
||||||
|
assert.Equal(t, InternalCode, Code(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessage(t *testing.T) {
|
||||||
|
// should be an empty message if no error.
|
||||||
|
assert.Equal(t, "", Message(nil))
|
||||||
|
// should be the message of Error 1.2.
|
||||||
|
err := scenario1()
|
||||||
|
assert.Equal(t, "nested error", Message(err))
|
||||||
|
// should be the default message.
|
||||||
|
err = errors.New("some error")
|
||||||
|
assert.Equal(t, defaultMessage, Message(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOp(t *testing.T) {
|
||||||
|
// should be an empty op if no error.
|
||||||
|
assert.Equal(t, "", Op(nil))
|
||||||
|
// should be the chain of op in this order:
|
||||||
|
// Error 1.0 -> Error 1.1 -> Error 1.2.
|
||||||
|
err := scenario1()
|
||||||
|
assert.Equal(t, "foo: bar: baz", Op(err))
|
||||||
|
/*
|
||||||
|
should be the chain of op in this order:
|
||||||
|
Error 2.0 -> Error 2.1.
|
||||||
|
|
||||||
|
As Error 2.1 and Error 2.2 shares the same
|
||||||
|
op, Error 2.2 op is not displayed.
|
||||||
|
*/
|
||||||
|
err = scenario2()
|
||||||
|
assert.Equal(t, "foo: bar", Op(err))
|
||||||
|
// should be an empty op if not Error.
|
||||||
|
err = errors.New("some error")
|
||||||
|
assert.Equal(t, "", Op(err))
|
||||||
|
}
|
||||||
8
internal/pkg/xexec/doc.go
Normal file
8
internal/pkg/xexec/doc.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Package xexec helps creating exec.Cmd
|
||||||
|
with logging.
|
||||||
|
|
||||||
|
All functions return our standard xerror.Error
|
||||||
|
in case of error.
|
||||||
|
*/
|
||||||
|
package xexec
|
||||||
99
internal/pkg/xexec/xexec.go
Normal file
99
internal/pkg/xexec/xexec.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package xexec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Command is a wrapper around exec.Command.
|
||||||
|
|
||||||
|
If given xlog.Logger has a xlog.DebugLevel,
|
||||||
|
also logs the output from the command.
|
||||||
|
*/
|
||||||
|
func Command(logger xlog.Logger, binary string, args ...string) (*exec.Cmd, error) {
|
||||||
|
const op string = "xexec.Command"
|
||||||
|
cmd := exec.Command(binary, args...)
|
||||||
|
if err := pipe(logger, cmd); err != nil {
|
||||||
|
return nil, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CommandContext is a wrapper around exec.CommandContext.
|
||||||
|
|
||||||
|
If given xlog.Logger has a xlog.DebugLevel,
|
||||||
|
also logs the output from the command.
|
||||||
|
*/
|
||||||
|
func CommandContext(ctx context.Context, logger xlog.Logger, binary string, args ...string) (*exec.Cmd, error) {
|
||||||
|
const op string = "xexec.CommandContext"
|
||||||
|
cmd := exec.CommandContext(ctx, binary, args...)
|
||||||
|
if err := pipe(logger, cmd); err != nil {
|
||||||
|
return nil, xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogBeforeExecute logs a command before its execution.
|
||||||
|
func LogBeforeExecute(logger xlog.Logger, cmd *exec.Cmd) {
|
||||||
|
const op string = "xexec.LogBeforeExecute"
|
||||||
|
logger.DebugfOp(op, "executing command: %s", strings.Join(cmd.Args, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipe(logger xlog.Logger, cmd *exec.Cmd) error {
|
||||||
|
const op string = "xexec.pipe"
|
||||||
|
if logger.Level() != xlog.DebugLevel {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// if xlog.DebugLevel, log the output
|
||||||
|
// from the command.
|
||||||
|
resolver := func() error {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go logCommandOutput(logger, stdout, "stdout", cmd)
|
||||||
|
go logCommandOutput(logger, stderr, "stderr", cmd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := resolver(); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func logCommandOutput(logger xlog.Logger, reader io.ReadCloser, outputType string, cmd *exec.Cmd) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(outputType)
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
buf.WriteString(fmt.Sprintf(".%s", arg))
|
||||||
|
}
|
||||||
|
op := buf.String()
|
||||||
|
r := bufio.NewReader(reader)
|
||||||
|
defer reader.Close() // nolint: errcheck
|
||||||
|
for {
|
||||||
|
line, _, err := r.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
logger.ErrorOp(op, err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if len(line) != 0 {
|
||||||
|
logger.DebugOp(op, string(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
internal/pkg/xexec/xexec_test.go
Normal file
39
internal/pkg/xexec/xexec_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package xexec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommand(t *testing.T) {
|
||||||
|
logger := xlogtest.DebugLogger()
|
||||||
|
// should pipe command output as
|
||||||
|
// xlog.Logger has a xlog.DebugLevel.
|
||||||
|
cmd, err := Command(logger, "echo", "Hello", "World")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
LogBeforeExecute(logger, cmd)
|
||||||
|
// should not pipe command output as
|
||||||
|
// xlog.Logger has a xlog.InfoLevel.
|
||||||
|
logger = xlogtest.InfoLogger()
|
||||||
|
cmd, err = Command(logger, "echo", "Hello", "World")
|
||||||
|
LogBeforeExecute(logger, cmd)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandContext(t *testing.T) {
|
||||||
|
logger := xlogtest.DebugLogger()
|
||||||
|
// should pipe command output as
|
||||||
|
// xlog.Logger has a xlog.DebugLevel.
|
||||||
|
cmd, err := CommandContext(context.Background(), logger, "echo", "Hello", "World")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
LogBeforeExecute(logger, cmd)
|
||||||
|
// should not pipe command output as
|
||||||
|
// xlog.Logger has a xlog.InfoLevel.
|
||||||
|
logger = xlogtest.InfoLogger()
|
||||||
|
cmd, err = CommandContext(context.Background(), logger, "echo", "Hello", "World")
|
||||||
|
LogBeforeExecute(logger, cmd)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
17
internal/pkg/xlog/doc.go
Normal file
17
internal/pkg/xlog/doc.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Package xlog defines a standard logger
|
||||||
|
for the application.
|
||||||
|
|
||||||
|
It uses structured logging thanks to
|
||||||
|
https://github.com/sirupsen/logrus.
|
||||||
|
|
||||||
|
All messages have at least two fields:
|
||||||
|
|
||||||
|
A "trace" field which helps to identify
|
||||||
|
messages belonging to the same context.
|
||||||
|
|
||||||
|
An "op" field which helps to identify
|
||||||
|
the logical operation associated
|
||||||
|
with the message.
|
||||||
|
*/
|
||||||
|
package xlog
|
||||||
141
internal/pkg/xlog/xlog.go
Normal file
141
internal/pkg/xlog/xlog.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package xlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Level helps setting the severity
|
||||||
|
// of the messages displayed.
|
||||||
|
type Level string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DebugLevel is the lowest level.
|
||||||
|
DebugLevel Level = "DEBUG"
|
||||||
|
// InfoLevel is the intermediate level.
|
||||||
|
InfoLevel Level = "INFO"
|
||||||
|
// ErrorLevel is the highest level.
|
||||||
|
ErrorLevel Level = "ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger enforces specific log message formats.
|
||||||
|
type Logger struct {
|
||||||
|
entry *logrus.Entry
|
||||||
|
level Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a xlog.Logger.
|
||||||
|
func New(level Level, trace string) Logger {
|
||||||
|
l := logrus.New()
|
||||||
|
l.SetLevel(mustLogrusLevel(level))
|
||||||
|
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
l.SetFormatter(&logrus.JSONFormatter{})
|
||||||
|
}
|
||||||
|
return Logger{
|
||||||
|
entry: l.WithField("trace", trace),
|
||||||
|
level: level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustLogrusLevel(level Level) logrus.Level {
|
||||||
|
const op string = "xlog.mustLogrusLevel"
|
||||||
|
switch level {
|
||||||
|
case DebugLevel:
|
||||||
|
return logrus.DebugLevel
|
||||||
|
case InfoLevel:
|
||||||
|
return logrus.InfoLevel
|
||||||
|
case ErrorLevel:
|
||||||
|
return logrus.ErrorLevel
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("%s: '%s' is not associated with any logrus.Level", op, level))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levels returns a slice of string
|
||||||
|
// with all severities.
|
||||||
|
func Levels() []string {
|
||||||
|
return []string{
|
||||||
|
string(DebugLevel),
|
||||||
|
string(InfoLevel),
|
||||||
|
string(ErrorLevel),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
MustParseLevel returns the Level corresponding
|
||||||
|
to given string.
|
||||||
|
|
||||||
|
It panics if no correspondence.
|
||||||
|
*/
|
||||||
|
func MustParseLevel(level string) Level {
|
||||||
|
const op string = "xlog.MustParseLevel"
|
||||||
|
switch level {
|
||||||
|
case string(DebugLevel):
|
||||||
|
return DebugLevel
|
||||||
|
case string(InfoLevel):
|
||||||
|
return InfoLevel
|
||||||
|
case string(ErrorLevel):
|
||||||
|
return ErrorLevel
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("%s: '%s' is not one of '%v'", op, level, Levels()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level returns the current Level.
|
||||||
|
func (l Logger) Level() Level {
|
||||||
|
return l.level
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFields returns a new xlog.Logger with
|
||||||
|
// given fields.
|
||||||
|
func (l Logger) WithFields(fields map[string]interface{}) Logger {
|
||||||
|
return Logger{
|
||||||
|
entry: l.entry.WithFields(fields),
|
||||||
|
level: l.level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugOp logs a debug message for given
|
||||||
|
// logical operation.
|
||||||
|
func (l Logger) DebugOp(op, message string) {
|
||||||
|
l.entry.WithField("op", op).Debug(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugfOp logs a debug message for given
|
||||||
|
// logical operation and format.
|
||||||
|
func (l Logger) DebugfOp(op, format string, args ...interface{}) {
|
||||||
|
l.entry.WithField("op", op).Debugf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoOp logs an info message for given
|
||||||
|
// logical operation.
|
||||||
|
func (l Logger) InfoOp(op, message string) {
|
||||||
|
l.entry.WithField("op", op).Info(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfofOp logs an info message for given
|
||||||
|
// logical operation and format.
|
||||||
|
func (l Logger) InfofOp(op, format string, args ...interface{}) {
|
||||||
|
l.entry.WithField("op", op).Infof(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorOp logs an error for given
|
||||||
|
// logical operation.
|
||||||
|
func (l Logger) ErrorOp(op string, err error) {
|
||||||
|
l.entry.WithField("op", op).Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorfOp logs an error message for given
|
||||||
|
// logical operation and format.
|
||||||
|
func (l Logger) ErrorfOp(op, format string, args ...interface{}) {
|
||||||
|
l.entry.WithField("op", op).Errorf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FatalOp logs an error for given
|
||||||
|
// logical operation and exit 1.
|
||||||
|
func (l Logger) FatalOp(op string, err error) {
|
||||||
|
l.entry.WithField("op", op).Fatal(err.Error())
|
||||||
|
}
|
||||||
3
internal/pkg/xrand/doc.go
Normal file
3
internal/pkg/xrand/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package xrand helps generating
|
||||||
|
// random strings.
|
||||||
|
package xrand
|
||||||
10
internal/pkg/xrand/xrand.go
Normal file
10
internal/pkg/xrand/xrand.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package xrand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/gommon/random"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get returns a random string.
|
||||||
|
func Get() string {
|
||||||
|
return random.String(32)
|
||||||
|
}
|
||||||
28
internal/pkg/xrand/xrand_test.go
Normal file
28
internal/pkg/xrand/xrand_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package xrand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
var rands []string
|
||||||
|
// use case: for 1 000 concurrent
|
||||||
|
// requests (which is a big Gotenberg instance),
|
||||||
|
// none should have the same identifier.
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
rands = append(rands, Get())
|
||||||
|
}
|
||||||
|
unique := func() bool {
|
||||||
|
for i, rand := range rands {
|
||||||
|
for j, current := range rands {
|
||||||
|
if i != j && rand == current {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
assert.Equal(t, true, unique())
|
||||||
|
}
|
||||||
6
internal/pkg/xtime/doc.go
Normal file
6
internal/pkg/xtime/doc.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
Package xtime helps generating
|
||||||
|
time.Duration from seconds represented
|
||||||
|
as float64.
|
||||||
|
*/
|
||||||
|
package xtime
|
||||||
10
internal/pkg/xtime/xtime.go
Normal file
10
internal/pkg/xtime/xtime.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package xtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Duration creates a time.Duration from seconds.
|
||||||
|
func Duration(seconds float64) time.Duration {
|
||||||
|
return time.Duration(1000*seconds) * time.Millisecond
|
||||||
|
}
|
||||||
14
internal/pkg/xtime/xtime_test.go
Normal file
14
internal/pkg/xtime/xtime_test.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package xtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDuration(t *testing.T) {
|
||||||
|
expected := time.Duration(1500) * time.Millisecond
|
||||||
|
result := Duration(1.5)
|
||||||
|
assert.Equal(t, expected.String(), result.String())
|
||||||
|
}
|
||||||
@@ -18,9 +18,7 @@ if [ $VERSION_LENGTH -ne 3 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg GOLANG_VERSION=${GOLANG_VERSION} \
|
|
||||||
--build-arg VERSION=${VERSION} \
|
--build-arg VERSION=${VERSION} \
|
||||||
-t thecodingmachine/gotenberg:latest \
|
-t thecodingmachine/gotenberg:latest \
|
||||||
-t thecodingmachine/gotenberg:${SEMVER[0]} \
|
-t thecodingmachine/gotenberg:${SEMVER[0]} \
|
||||||
|
|||||||
18
test/cmd/pm2/pm2.go
Normal file
18
test/cmd/pm2/pm2.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := xlogtest.DebugLogger()
|
||||||
|
process := pm2.NewChromeProcess(logger)
|
||||||
|
if err := process.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
process = pm2.NewUnoconvProcess(logger)
|
||||||
|
if err := process.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
test/doc.go
Normal file
3
test/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package test contains useful
|
||||||
|
// functions used across tests.
|
||||||
|
package test
|
||||||
6
test/internalpkg/printertest/doc.go
Normal file
6
test/internalpkg/printertest/doc.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
Package printertest contains useful
|
||||||
|
functions for tests related
|
||||||
|
to printer package.
|
||||||
|
*/
|
||||||
|
package printertest
|
||||||
49
test/internalpkg/printertest/printertest.go
Normal file
49
test/internalpkg/printertest/printertest.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package printertest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
testdataDirectoryPath should be
|
||||||
|
the absolute of the testdata INSIDE
|
||||||
|
the Docker image.
|
||||||
|
*/
|
||||||
|
const testdataDirectoryPath string = "/gotenberg/tests/test/testdata"
|
||||||
|
|
||||||
|
// GenerateDestination simply generates
|
||||||
|
// a path for a resulting PDF file.
|
||||||
|
func GenerateDestination() string {
|
||||||
|
return fmt.Sprintf("/tmp/%s.pdf", xrand.Get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeFpaths return the paths
|
||||||
|
// of the PDF files used in tests.
|
||||||
|
func MergeFpaths(t *testing.T) []string {
|
||||||
|
return []string{
|
||||||
|
fpath(t, "pdf", "gotenberg.pdf"),
|
||||||
|
fpath(t, "pdf", "gotenberg_bis.pdf"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfficeFpaths return the paths
|
||||||
|
// of the Office documents used in tests.
|
||||||
|
func OfficeFpaths(t *testing.T) []string {
|
||||||
|
return []string{
|
||||||
|
fpath(t, "office", "document.docx"),
|
||||||
|
fpath(t, "office", "document.rtf"),
|
||||||
|
fpath(t, "office", "document.txt"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fpath(t *testing.T, kind, filename string) string {
|
||||||
|
require.NotEmpty(t, kind)
|
||||||
|
require.NotEmpty(t, filename)
|
||||||
|
fpath := fmt.Sprintf("%s/%s/%s", testdataDirectoryPath, kind, filename)
|
||||||
|
require.FileExists(t, fpath)
|
||||||
|
return fpath
|
||||||
|
}
|
||||||
6
test/internalpkg/xerrortest/doc.go
Normal file
6
test/internalpkg/xerrortest/doc.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
Package xerrortest contains useful
|
||||||
|
functions for tests related
|
||||||
|
to xerror package.
|
||||||
|
*/
|
||||||
|
package xerrortest
|
||||||
18
test/internalpkg/xerrortest/xerrortest.go
Normal file
18
test/internalpkg/xerrortest/xerrortest.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package xerrortest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssertError validates that given error
|
||||||
|
// is of an instance of xerror.Error.
|
||||||
|
// If so, returns the instance of xerror.Error.
|
||||||
|
func AssertError(t *testing.T, err error) *xerror.Error {
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
standardized, ok := err.(*xerror.Error)
|
||||||
|
assert.Equal(t, true, ok)
|
||||||
|
return standardized
|
||||||
|
}
|
||||||
6
test/internalpkg/xlogtest/doc.go
Normal file
6
test/internalpkg/xlogtest/doc.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
Package xlogtest contains useful
|
||||||
|
functions for tests related
|
||||||
|
to xlog package.
|
||||||
|
*/
|
||||||
|
package xlogtest
|
||||||
23
test/internalpkg/xlogtest/xlogtest.go
Normal file
23
test/internalpkg/xlogtest/xlogtest.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package xlogtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DebugLogger creates a xlog.Logger
|
||||||
|
// with xlog.DebugLevel for our tests.
|
||||||
|
func DebugLogger() xlog.Logger {
|
||||||
|
return xlog.New(xlog.DebugLevel, "tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoLogger creates a xlog.Logger
|
||||||
|
// with xlog.InfoLevel for our tests.
|
||||||
|
func InfoLogger() xlog.Logger {
|
||||||
|
return xlog.New(xlog.DebugLevel, "tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorLogger creates a xlog.Logger
|
||||||
|
// with xlog.ErrorLevel for our tests.
|
||||||
|
func ErrorLogger() xlog.Logger {
|
||||||
|
return xlog.New(xlog.ErrorLevel, "tests")
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package test contains useful functions used across tests.
|
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +27,19 @@ func AssertStatusCode(t *testing.T, expectedStatusCode int, srv http.Handler, re
|
|||||||
assert.Equal(t, expectedStatusCode, rec.Code)
|
assert.Equal(t, expectedStatusCode, rec.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertDirectoryEmpty checks if given directory
|
||||||
|
// is empty.
|
||||||
|
func AssertDirectoryEmpty(t *testing.T, directory string) {
|
||||||
|
f, err := os.Open(directory)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer f.Close() // nolint: errcheck
|
||||||
|
_, err = f.Readdir(1)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, io.EOF, err)
|
||||||
|
}
|
||||||
|
|
||||||
// AssertConcurrent runs all functions simultaneously
|
// AssertConcurrent runs all functions simultaneously
|
||||||
// and wait until execution has completed
|
// and wait until execution has completed
|
||||||
// or an error is encountered.
|
// or an error is encountered.
|
||||||
@@ -39,6 +52,16 @@ func AssertConcurrent(t *testing.T, fn func() error, amount int) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertStandardError validates that given error
|
||||||
|
// is of an instance of xerror.Error.
|
||||||
|
// If so, returns the instance of xerror.Error.
|
||||||
|
func AssertStandardError(t *testing.T, err error) *xerror.Error {
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
standardized, ok := err.(*xerror.Error)
|
||||||
|
assert.Equal(t, true, ok)
|
||||||
|
return standardized
|
||||||
|
}
|
||||||
|
|
||||||
// HTMLTestMultipartForm returns the body
|
// HTMLTestMultipartForm returns the body
|
||||||
// for a multipate/form-data request with all
|
// for a multipate/form-data request with all
|
||||||
// files under "html" folder.
|
// files under "html" folder.
|
||||||
|
|||||||
Reference in New Issue
Block a user