mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +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:
|
||||
include:
|
||||
- stage: tests
|
||||
script: make lint
|
||||
- stage: tests
|
||||
script: make tests
|
||||
script: make lint tests
|
||||
- stage: publish
|
||||
if: tag IS present
|
||||
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
|
||||
DOCKER_USER=
|
||||
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.
|
||||
doc:
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:docs -f build/docs/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/docs" thecodingmachine/gotenberg:docs
|
||||
# build the base Docker image.
|
||||
base:
|
||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
|
||||
|
||||
# build the workspace Docker image.
|
||||
workspace:
|
||||
make base
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:workspace -f build/workspace/Dockerfile .
|
||||
|
||||
# gofmt and goimports all go files.
|
||||
fmt:
|
||||
@@ -15,24 +30,32 @@ fmt:
|
||||
|
||||
# run all linters.
|
||||
lint:
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg:lint
|
||||
make workspace
|
||||
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile .
|
||||
docker run --rm -it $(DOCKER_REPOSITORY)/gotenberg:lint
|
||||
|
||||
# run all tests.
|
||||
tests:
|
||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:tests -f build/tests/Dockerfile .
|
||||
docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg:tests
|
||||
make workspace
|
||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:tests -f build/tests/Dockerfile .
|
||||
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:
|
||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
make workspace
|
||||
docker build --build-arg VERSION=$(VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||
|
||||
# start the API using previously built Docker image.
|
||||
gotenberg:
|
||||
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
|
||||
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -p "3000:$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||
|
||||
# publish Gotenberg images according to version.
|
||||
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
|
||||
@@ -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
|
||||
# | 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 - &&\
|
||||
@@ -94,3 +94,17 @@ COPY build/base/* /usr/share/fonts/
|
||||
|
||||
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Default user
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | All processes in the Docker container will run as a dedicated
|
||||
# | non-root user.
|
||||
# |
|
||||
|
||||
RUN groupadd --gid 1001 gotenberg \
|
||||
&& useradd --uid 1001 --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \
|
||||
&& mkdir /gotenberg \
|
||||
&& chown gotenberg: /gotenberg
|
||||
|
||||
ENV PM2_HOME=/gotenberg/.pm2
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch
|
||||
FROM thecodingmachine/gotenberg:workspace
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | static
|
||||
@@ -19,6 +17,6 @@ RUN go get github.com/apex/static/cmd/static-docs
|
||||
# | 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." ]
|
||||
@@ -10,6 +10,8 @@ You may start it with:
|
||||
$ 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).
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
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`.
|
||||
|
||||
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:
|
||||
> [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`.
|
||||
|
||||
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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
@@ -339,4 +340,4 @@ $request = new HTMLRequest($index);
|
||||
$request->setWaitDelay(5.5);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch
|
||||
FROM thecodingmachine/gotenberg:workspace
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | GolangCI-Lint
|
||||
@@ -10,7 +8,7 @@ FROM golang:${GOLANG_VERSION}-stretch
|
||||
# | 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} &&\
|
||||
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.
|
||||
# |
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /lint
|
||||
# Define our working directory outside of $GOPATH (we're using go modules).
|
||||
USER gotenberg
|
||||
WORKDIR /gotenberg/lint
|
||||
|
||||
# Copy our module dependencies definitions.
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
# Copy our code source.
|
||||
COPY --chown=gotenberg:gotenberg . .
|
||||
|
||||
# Install module dependencies.
|
||||
RUN go mod download
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
||||
@@ -1,5 +1,3 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Binary
|
||||
# |--------------------------------------------------------------------------
|
||||
@@ -7,7 +5,7 @@ ARG GOLANG_VERSION
|
||||
# | Buils Gotenberg binary.
|
||||
# |
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
||||
FROM thecodingmachine/gotenberg:workspace AS workspace
|
||||
|
||||
ARG VERSION
|
||||
|
||||
@@ -16,13 +14,16 @@ ENV GOOS=linux \
|
||||
CGO_ENABLED=0
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /gotenberg
|
||||
WORKDIR /gotenberg/package
|
||||
|
||||
# Copy our source code.
|
||||
COPY . .
|
||||
COPY internal ./internal
|
||||
COPY cmd ./cmd
|
||||
COPY go.sum go.sum
|
||||
COPY go.mod go.mod
|
||||
|
||||
# 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
|
||||
@@ -35,8 +36,9 @@ FROM thecodingmachine/gotenberg:base
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
|
||||
COPY --from=workspace /gotenberg/package/gotenberg /usr/local/bin/
|
||||
|
||||
USER gotenberg
|
||||
WORKDIR /gotenberg
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -1,48 +1,14 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
||||
|
||||
FROM thecodingmachine/gotenberg:base
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Common libraries
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Libraries used in the build process of this image.
|
||||
# |
|
||||
|
||||
RUN apt-get install -y git gcc
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Golang
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs Golang.
|
||||
# |
|
||||
|
||||
COPY --from=golang /usr/local/go /usr/local/go
|
||||
|
||||
RUN export PATH="/usr/local/go/bin:$PATH" &&\
|
||||
go version
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Final touch
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
FROM thecodingmachine/gotenberg:workspace
|
||||
|
||||
# Define our workding outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /tests
|
||||
USER gotenberg
|
||||
WORKDIR /gotenberg/tests
|
||||
|
||||
# Copy our module dependencies definitions.
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
# Copy our code source.
|
||||
COPY --chown=gotenberg:gotenberg . .
|
||||
|
||||
# Install module dependencies.
|
||||
RUN go mod download
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
||||
@@ -2,16 +2,33 @@
|
||||
|
||||
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
|
||||
# spending to much time on each tests depending on
|
||||
# them.
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
||||
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
||||
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
||||
|
||||
# Running others tests.
|
||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
||||
#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/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.
|
||||
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 TestChromeShutdown
|
||||
#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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"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.
|
||||
// nolint: gochecknoglobals
|
||||
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() {
|
||||
notify.Printf("Gotenberg %s", version)
|
||||
opts := mustParseEnvVar()
|
||||
srv := api.New(opts)
|
||||
processes := mustStartProcesses(opts)
|
||||
// run our API in a goroutine so that it doesn't block.s
|
||||
const op = "main"
|
||||
config, err := conf.FromEnv()
|
||||
systemLogger := xlog.New(config.LogLevel(), "system")
|
||||
if err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
systemLogger.InfofOp(op, "Gotenberg %s", version)
|
||||
systemLogger.DebugfOp(op, "configuration: %+v", config)
|
||||
// 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() {
|
||||
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)
|
||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||
@@ -139,8 +57,22 @@ func main() {
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
// block until we receive our signal.
|
||||
<-quit
|
||||
mustShutdownAPI(srv)
|
||||
mustShutdownProcesses(processes)
|
||||
notify.Print("bye!")
|
||||
// create a deadline to wait for.
|
||||
ctx, cancel := xcontext.WithTimeout(systemLogger, 120)
|
||||
defer cancel()
|
||||
// doesn't block if no connections, but will otherwise wait
|
||||
// until the timeout deadline.
|
||||
systemLogger.InfoOp(op, "shutting down http server...")
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
systemLogger.FatalOp(op, err)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:5
|
||||
</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>
|
||||
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
|
||||
</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>
|
||||
</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
|
||||
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>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>
|
||||
<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>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">
|
||||
<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>
|
||||
|
||||
<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 class="Page" id="html">
|
||||
@@ -601,7 +620,8 @@ $client->store($request, $dest);
|
||||
</a>Wait delay</h2>
|
||||
|
||||
<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>
|
||||
<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
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.2.0 // indirect
|
||||
github.com/google/go-cmp v0.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.0.0
|
||||
github.com/labstack/gommon v0.2.8
|
||||
github.com/mafredri/cdp v0.22.0
|
||||
github.com/mattn/go-colorable v0.1.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.7 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.1
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/labstack/echo/v4 v4.1.6
|
||||
github.com/labstack/gommon v0.2.9
|
||||
github.com/mafredri/cdp v0.23.4
|
||||
github.com/mattn/go-isatty v0.0.8
|
||||
github.com/microcosm-cc/bluemonday v1.0.2
|
||||
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/valyala/fasttemplate v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // 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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
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/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
|
||||
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/mafredri/cdp v0.22.0 h1:BV17j8hXLDWczo2SZIAFuOjMpQMIOq5DOcd9sgB2hv0=
|
||||
github.com/mafredri/cdp v0.22.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv4=
|
||||
github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
|
||||
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
|
||||
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
|
||||
github.com/mafredri/cdp v0.23.4 h1:ffp4qq6slfCL4rFWBDeRHapkLE776gER4tX5Z3LS8CY=
|
||||
github.com/mafredri/cdp v0.23.4/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/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/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-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-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-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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"
|
||||
|
||||
"github.com/mafredri/cdp/devtool"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
type chrome struct {
|
||||
manager *processManager
|
||||
type chromeProcess struct {
|
||||
logger xlog.Logger
|
||||
}
|
||||
|
||||
// NewChrome retruns a Google Chrome
|
||||
// NewChromeProcess returns a Google Chrome
|
||||
// headless process.
|
||||
func NewChrome() Process {
|
||||
return &chrome{
|
||||
manager: &processManager{},
|
||||
func NewChromeProcess(logger xlog.Logger) Process {
|
||||
return chromeProcess{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *chrome) Fullname() string {
|
||||
func (p chromeProcess) Fullname() string {
|
||||
return "Google Chrome headless"
|
||||
}
|
||||
|
||||
func (p *chrome) Start() error {
|
||||
return p.manager.start(p)
|
||||
func (p chromeProcess) Start() error {
|
||||
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 {
|
||||
return p.manager.shutdown(p)
|
||||
func (p chromeProcess) IsViable() bool {
|
||||
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{
|
||||
"--no-sandbox",
|
||||
"--headless",
|
||||
@@ -50,23 +89,25 @@ func (p *chrome) args() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *chrome) name() string {
|
||||
func (p chromeProcess) binary() string {
|
||||
return "google-chrome-stable"
|
||||
}
|
||||
|
||||
func (p *chrome) viable() bool {
|
||||
// check if Google Chrome is correctly running.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
_, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (p *chrome) warmup() {
|
||||
time.Sleep(5 * time.Second)
|
||||
func (p chromeProcess) warmup() {
|
||||
const (
|
||||
op string = "pm2.chromeProcess.warmup"
|
||||
warmupTime time.Duration = 10 * 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.
|
||||
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
|
||||
processes on which our API depends.
|
||||
processes on which our application depends.
|
||||
|
||||
For instance, it may start Google Chrome headless and
|
||||
unoconv listener with PM2.
|
||||
|
||||
@@ -2,82 +2,101 @@ package pm2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
stoppedState = iota
|
||||
runningState
|
||||
errorState
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// Process is a type that can start or
|
||||
// shutdown a process with PM2.
|
||||
// stop a process with PM2.
|
||||
type Process interface {
|
||||
Fullname() string
|
||||
Start() error
|
||||
Shutdown() error
|
||||
IsViable() bool
|
||||
Stop() error
|
||||
args() []string
|
||||
name() string
|
||||
viable() bool
|
||||
binary() string
|
||||
warmup()
|
||||
}
|
||||
|
||||
type processManager struct {
|
||||
heuristicState int32
|
||||
}
|
||||
type pm2Command string
|
||||
|
||||
func (m *processManager) start(p Process) error {
|
||||
if err := m.pm2(p, "start"); err != nil {
|
||||
return err
|
||||
}
|
||||
p.warmup()
|
||||
if !p.viable() {
|
||||
attempts := 0
|
||||
for attempts < 5 && !p.viable() {
|
||||
if err := m.pm2(p, "restart"); err != nil {
|
||||
m.heuristicState = errorState
|
||||
return err
|
||||
const (
|
||||
startCommand pm2Command = "start"
|
||||
restartCommand pm2Command = "restart"
|
||||
stopCommand pm2Command = "stop"
|
||||
logsCommand pm2Command = "logs"
|
||||
)
|
||||
|
||||
func start(logger xlog.Logger, process Process) error {
|
||||
const (
|
||||
op string = "pm2.start"
|
||||
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() {
|
||||
m.heuristicState = errorState
|
||||
return fmt.Errorf("failed to launch %s", p.Fullname())
|
||||
// the process is viable, let's log its
|
||||
// output.
|
||||
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
|
||||
}
|
||||
if err := m.pm2(p, "stop"); err != nil {
|
||||
m.heuristicState = errorState
|
||||
return err
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
m.heuristicState = stoppedState
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *processManager) pm2(p Process, cmdName string) error {
|
||||
cmdArgs := []string{
|
||||
cmdName,
|
||||
p.name(),
|
||||
}
|
||||
if cmdName == "start" {
|
||||
cmdArgs = append(cmdArgs, "--interpreter=none", "--")
|
||||
cmdArgs = append(cmdArgs, p.args()...)
|
||||
}
|
||||
cmd := exec.Command(
|
||||
"pm2",
|
||||
cmdArgs...,
|
||||
)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err)
|
||||
func stop(logger xlog.Logger, process Process) error {
|
||||
const op string = "pm2.stop"
|
||||
if err := run(logger, stopCommand, process); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func run(logger xlog.Logger, pm2Cmd pm2Command, process Process) error {
|
||||
const op string = "pm2.run"
|
||||
resolver := func() error {
|
||||
args := []string{
|
||||
string(pm2Cmd),
|
||||
process.binary(),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,52 +1,77 @@
|
||||
package pm2
|
||||
|
||||
type unoconv struct {
|
||||
manager *processManager
|
||||
import (
|
||||
"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.
|
||||
func NewUnoconv() Process {
|
||||
return &unoconv{
|
||||
manager: &processManager{},
|
||||
func NewUnoconvProcess(logger xlog.Logger) Process {
|
||||
return unoconvProcess{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *unoconv) Fullname() string {
|
||||
func (p unoconvProcess) Fullname() string {
|
||||
return "unoconv listener"
|
||||
}
|
||||
|
||||
func (p *unoconv) Start() error {
|
||||
return p.manager.start(p)
|
||||
}
|
||||
|
||||
func (p *unoconv) Shutdown() error {
|
||||
return p.manager.shutdown(p)
|
||||
}
|
||||
|
||||
func (p *unoconv) args() []string {
|
||||
return []string{
|
||||
"--listener",
|
||||
"--verbose",
|
||||
func (p unoconvProcess) Start() error {
|
||||
const op string = "pm2.unoconvProcess.Start"
|
||||
if err := start(p.logger, p); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *unoconv) name() string {
|
||||
return "unoconv"
|
||||
}
|
||||
|
||||
func (p *unoconv) viable() bool {
|
||||
func (p unoconvProcess) IsViable() bool {
|
||||
// TODO find a way to check if
|
||||
// the unoconv listener
|
||||
// is correctly started?
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *unoconv) warmup() {
|
||||
// let's do nothing.
|
||||
func (p unoconvProcess) Stop() error {
|
||||
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.
|
||||
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/target"
|
||||
"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"
|
||||
)
|
||||
|
||||
type chrome struct {
|
||||
url string
|
||||
opts *ChromeOptions
|
||||
type chromePrinter struct {
|
||||
logger xlog.Logger
|
||||
url string
|
||||
opts ChromePrinterOptions
|
||||
}
|
||||
|
||||
// ChromeOptions helps customizing the
|
||||
// ChromePrinterOptions helps customizing the
|
||||
// Google Chrome printer behaviour.
|
||||
type ChromeOptions struct {
|
||||
type ChromePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
WaitDelay float64
|
||||
HeaderHTML string
|
||||
@@ -36,120 +41,137 @@ type ChromeOptions struct {
|
||||
Landscape bool
|
||||
}
|
||||
|
||||
func (p *chrome) Print(destination string) error {
|
||||
duration := time.Duration(p.opts.WaitTimeout+p.opts.WaitDelay) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), duration)
|
||||
func (p chromePrinter) Print(destination string) error {
|
||||
const op string = "printer.chromePrinter.Print"
|
||||
logOptions(p.logger, p.opts)
|
||||
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout+p.opts.WaitDelay)
|
||||
defer cancel()
|
||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
resolver := func() error {
|
||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
||||
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer devtConn.Close() // nolint: errcheck
|
||||
// create a new CDP Client that uses conn.
|
||||
devtClient := cdp.NewClient(devtConn)
|
||||
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
||||
if err != nil {
|
||||
return 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.
|
||||
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer devtConn.Close() // nolint: errcheck
|
||||
// create a new CDP Client that uses conn.
|
||||
devtClient := cdp.NewClient(devtConn)
|
||||
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new browser context: %v", err)
|
||||
}
|
||||
// create a new blank target with the new browser context.
|
||||
createTargetArgs := target.
|
||||
NewCreateTargetArgs("about:blank").
|
||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
||||
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new blank target: %v", err)
|
||||
}
|
||||
// connect the client to the new target.
|
||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
||||
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting client to blank target: %v", err)
|
||||
}
|
||||
defer newContextConn.Close() // nolint: errcheck
|
||||
// create a new CDP Client that uses newContextConn.
|
||||
targetClient := cdp.NewClient(newContextConn)
|
||||
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
||||
// close the target when done.
|
||||
defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck
|
||||
if err := runBatch(
|
||||
// enable all the domain events that we're interested in.
|
||||
func() error { return targetClient.DOM.Enable(ctx) },
|
||||
func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) },
|
||||
func() error { return targetClient.Page.Enable(ctx) },
|
||||
func() error { return targetClient.Runtime.Enable(ctx) },
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.navigate(ctx, targetClient); err != nil {
|
||||
return err
|
||||
}
|
||||
print, err := targetClient.Page.PrintToPDF(
|
||||
ctx,
|
||||
page.NewPrintToPDFArgs().
|
||||
SetPaperWidth(p.opts.PaperWidth).
|
||||
SetPaperHeight(p.opts.PaperHeight).
|
||||
SetMarginTop(p.opts.MarginTop).
|
||||
SetMarginBottom(p.opts.MarginBottom).
|
||||
SetMarginLeft(p.opts.MarginLeft).
|
||||
SetMarginRight(p.opts.MarginRight).
|
||||
SetLandscape(p.opts.Landscape).
|
||||
SetDisplayHeaderFooter(true).
|
||||
SetHeaderTemplate(p.opts.HeaderHTML).
|
||||
SetFooterTemplate(p.opts.FooterHTML).
|
||||
SetPrintBackground(true),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("printing page to PDF: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
||||
return fmt.Errorf("%s: writing file: %v", destination, err)
|
||||
if err := resolver(); err != nil {
|
||||
return xcontext.MustHandleError(
|
||||
ctx,
|
||||
xerror.New(op, err),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error {
|
||||
// make sure Page events are enabled.
|
||||
if err := client.Page.Enable(ctx); err != nil {
|
||||
return err
|
||||
func (p chromePrinter) navigate(ctx context.Context, client *cdp.Client) error {
|
||||
const op string = "printer.chromePrinter.navigate"
|
||||
resolver := func() error {
|
||||
// make sure Page events are enabled.
|
||||
if err := client.Page.Enable(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// make sure Network events are enabled.
|
||||
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
// create all clients for events.
|
||||
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer domContentEventFired.Close() // nolint: errcheck
|
||||
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer loadEventFired.Close() // nolint: errcheck
|
||||
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer loadingFinished.Close() // nolint: errcheck
|
||||
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runBatch(
|
||||
// wait for all events.
|
||||
func() error { _, err := domContentEventFired.Recv(); return err },
|
||||
func() error { _, err := loadEventFired.Recv(); return err },
|
||||
func() error { _, err := loadingFinished.Recv(); return err },
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
// wait for a given amount of time (useful for javascript delay).
|
||||
time.Sleep(xtime.Duration(p.opts.WaitDelay))
|
||||
return nil
|
||||
}
|
||||
// make sure Network events are enabled.
|
||||
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||
return err
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, 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
|
||||
}
|
||||
|
||||
@@ -165,5 +187,5 @@ func runBatch(fn ...func() error) error {
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Printer(new(chrome))
|
||||
_ = Printer(new(chromePrinter))
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/*
|
||||
Package printer contains structs which convert
|
||||
a specific file type to PDF.
|
||||
*/
|
||||
// Package printer helps converting
|
||||
// a specific file type to PDF.
|
||||
package printer
|
||||
|
||||
@@ -2,13 +2,17 @@ package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// NewHTML returns an HTML printer.
|
||||
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
||||
// NewHTMLPrinter returns a Printer which
|
||||
// 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)
|
||||
return &chrome{
|
||||
url: URL,
|
||||
opts: opts,
|
||||
return chromePrinter{
|
||||
logger: logger,
|
||||
url: URL,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,36 +9,46 @@ import (
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"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.
|
||||
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
||||
tmpl, err := template.
|
||||
New(filepath.Base(fpath)).
|
||||
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||
ParseFiles(fpath)
|
||||
// NewMarkdownPrinter returns a Printer which
|
||||
// is able to convert Markdown files to PDF.
|
||||
func NewMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) (Printer, error) {
|
||||
const op string = "printer.NewMarkdownPrinter"
|
||||
resolver := func() (string, error) {
|
||||
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 {
|
||||
return nil, fmt.Errorf("%s: parsing template: %v", fpath, err)
|
||||
return chromePrinter{}, xerror.New(op, err)
|
||||
}
|
||||
dirPath := filepath.Dir(fpath)
|
||||
data := &templateData{DirPath: dirPath}
|
||||
var buffer bytes.Buffer
|
||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||
return nil, fmt.Errorf("%s: executing template: %v", fpath, err)
|
||||
}
|
||||
baseFilename, err := rand.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
||||
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
||||
return nil, fmt.Errorf("%s: writing file: %v", dst, err)
|
||||
}
|
||||
URL := fmt.Sprintf("file://%s", dst)
|
||||
return &chrome{
|
||||
url: URL,
|
||||
opts: opts,
|
||||
return chromePrinter{
|
||||
logger: logger,
|
||||
url: URL,
|
||||
opts: opts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -47,10 +57,11 @@ type templateData struct {
|
||||
}
|
||||
|
||||
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
||||
const op string = "printer.markdownToHTML"
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
unsafe := blackfriday.Run(b)
|
||||
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||
|
||||
@@ -2,49 +2,71 @@ package printer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type merge struct {
|
||||
type mergePrinter struct {
|
||||
ctx context.Context
|
||||
logger xlog.Logger
|
||||
fpaths []string
|
||||
opts *MergeOptions
|
||||
opts MergePrinterOptions
|
||||
}
|
||||
|
||||
// MergeOptions helps customizing the
|
||||
// merge printer behaviour.
|
||||
type MergeOptions struct {
|
||||
// MergePrinterOptions helps customizing the
|
||||
// merge Printer behaviour.
|
||||
type MergePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
}
|
||||
|
||||
// NewMerge returns a merge printer.
|
||||
func NewMerge(fpaths []string, opts *MergeOptions) Printer {
|
||||
return &merge{
|
||||
// NewMergePrinter returns a Printer which
|
||||
// is able to merge PDFs.
|
||||
func NewMergePrinter(logger xlog.Logger, fpaths []string, opts MergePrinterOptions) Printer {
|
||||
return mergePrinter{
|
||||
logger: logger,
|
||||
fpaths: fpaths,
|
||||
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 {
|
||||
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()
|
||||
p.ctx = ctx
|
||||
}
|
||||
var cmdArgs []string
|
||||
cmdArgs = append(cmdArgs, p.fpaths...)
|
||||
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
||||
cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...)
|
||||
_, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pdtk: %v", err)
|
||||
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
|
||||
resolver := func() error {
|
||||
var args []string
|
||||
args = append(args, p.fpaths...)
|
||||
args = append(args, "cat", "output", destination)
|
||||
cmd, err := xexec.CommandContext(p.ctx, p.logger, "pdftk", args...)
|
||||
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
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"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
|
||||
opts *OfficeOptions
|
||||
opts OfficePrinterOptions
|
||||
}
|
||||
|
||||
// OfficeOptions helps customizing the
|
||||
// Office printer behaviour.
|
||||
type OfficeOptions struct {
|
||||
// OfficePrinterOptions helps customizing the
|
||||
// Office Printer behaviour.
|
||||
type OfficePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
Landscape bool
|
||||
}
|
||||
|
||||
// NewOffice returns an Office printer.
|
||||
func NewOffice(fpaths []string, opts *OfficeOptions) Printer {
|
||||
return &office{
|
||||
// NewOfficePrinter returns a Printer which
|
||||
// is able to convert Office documents to PDF.
|
||||
func NewOfficePrinter(logger xlog.Logger, fpaths []string, opts OfficePrinterOptions) Printer {
|
||||
return officePrinter{
|
||||
logger: logger,
|
||||
fpaths: fpaths,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *office) Print(destination string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
||||
func (p officePrinter) Print(destination string) error {
|
||||
const op string = "printer.officePrinter.Print"
|
||||
logOptions(p.logger, p.opts)
|
||||
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||
defer cancel()
|
||||
fpaths := make([]string, len(p.fpaths))
|
||||
dirPath := filepath.Dir(destination)
|
||||
for i, fpath := range p.fpaths {
|
||||
baseFilename, err := rand.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
resolver := func() error {
|
||||
fpaths := make([]string, len(p.fpaths))
|
||||
dirPath := filepath.Dir(destination)
|
||||
for i, fpath := range p.fpaths {
|
||||
baseFilename := xrand.Get()
|
||||
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 err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
||||
return err
|
||||
if len(fpaths) == 1 {
|
||||
p.logger.DebugOp(op, "only one PDF created, nothing to merge")
|
||||
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 {
|
||||
return os.Rename(fpaths[0], destination)
|
||||
}
|
||||
m := &merge{
|
||||
ctx: ctx,
|
||||
fpaths: fpaths,
|
||||
}
|
||||
return m.Print(destination)
|
||||
}
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var mu sync.Mutex
|
||||
|
||||
func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
cmdArgs := []string{
|
||||
"--format",
|
||||
"pdf",
|
||||
}
|
||||
if opts.Landscape {
|
||||
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "--output", destination, fpath)
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
"unoconv",
|
||||
cmdArgs...,
|
||||
)
|
||||
_, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unoconv: %v", err)
|
||||
if err := resolver(); err != nil {
|
||||
return xcontext.MustHandleError(
|
||||
ctx,
|
||||
xerror.New(op, err),
|
||||
)
|
||||
}
|
||||
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.
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// Printer is a type that can create a PDF file from a source.
|
||||
// The source is defined in the underlying implementation.
|
||||
type Printer interface {
|
||||
Print(destination string) error
|
||||
}
|
||||
|
||||
func logOptions(logger xlog.Logger, opts interface{}) {
|
||||
const op string = "printer.logOptions"
|
||||
logger.DebugfOp(op, "options: %+v", opts)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package printer
|
||||
|
||||
// NewURL returns a URL printer.
|
||||
func NewURL(url string, opts *ChromeOptions) Printer {
|
||||
return &chrome{
|
||||
url: url,
|
||||
opts: opts,
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// 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
|
||||
fi
|
||||
|
||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build \
|
||||
--build-arg GOLANG_VERSION=${GOLANG_VERSION} \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
-t thecodingmachine/gotenberg:latest \
|
||||
-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
|
||||
|
||||
import (
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// and wait until execution has completed
|
||||
// or an error is encountered.
|
||||
@@ -39,6 +52,16 @@ func AssertConcurrent(t *testing.T, fn func() error, amount int) {
|
||||
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
|
||||
// for a multipate/form-data request with all
|
||||
// files under "html" folder.
|
||||
|
||||
Reference in New Issue
Block a user