mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 04:32:15 +01:00
huge refactoring
This commit is contained in:
@@ -1,2 +0,0 @@
|
|||||||
scripts
|
|
||||||
test
|
|
||||||
@@ -17,9 +17,7 @@ stages:
|
|||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- stage: tests
|
- stage: tests
|
||||||
script: make lint
|
script: make lint tests
|
||||||
- stage: tests
|
|
||||||
script: make tests
|
|
||||||
- stage: publish
|
- stage: publish
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS
|
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS
|
||||||
49
Makefile
49
Makefile
@@ -1,18 +1,27 @@
|
|||||||
GOLANG_VERSION=1.12
|
GOLANG_VERSION=1.12.7
|
||||||
VERSION=snapshot
|
VERSION=snapshot
|
||||||
DOCKER_USER=
|
DOCKER_USER=
|
||||||
DOCKER_PASSWORD=
|
DOCKER_PASSWORD=
|
||||||
DOCKER_REPOSITORY=thecodingmachine
|
DOCKER_REPOSITORY=thecodingmachine
|
||||||
DEFAULT_WAIT_TIMEOUT=10
|
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
|
DEFAULT_LISTEN_PORT=3000
|
||||||
DISABLE_GOOGLE_CHROME=0
|
DISABLE_GOOGLE_CHROME=0
|
||||||
DISABLE_UNOCONV=0
|
DISABLE_UNOCONV=0
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
# generate documentation.
|
# build the base Docker image.
|
||||||
doc:
|
base:
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile .
|
docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
|
||||||
docker run --rm -it -v "$(PWD):/docs" $(DOCKER_REPOSITORY)/gotenberg:docs
|
|
||||||
|
# build the workspace Docker image.
|
||||||
|
workspace:
|
||||||
|
make base
|
||||||
|
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:workspace -f build/workspace/Dockerfile .
|
||||||
|
|
||||||
# gofmt and goimports all go files.
|
# gofmt and goimports all go files.
|
||||||
fmt:
|
fmt:
|
||||||
@@ -21,24 +30,32 @@ fmt:
|
|||||||
|
|
||||||
# run all linters.
|
# run all linters.
|
||||||
lint:
|
lint:
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile .
|
make workspace
|
||||||
docker run --rm -it -v "$(PWD):/lint" $(DOCKER_REPOSITORY)/gotenberg:lint
|
docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile .
|
||||||
|
docker run --rm -it $(DOCKER_REPOSITORY)/gotenberg:lint
|
||||||
|
|
||||||
# run all tests.
|
# run all tests.
|
||||||
tests:
|
tests:
|
||||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
|
make workspace
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:tests -f build/tests/Dockerfile .
|
docker build -t $(DOCKER_REPOSITORY)/gotenberg:tests -f build/tests/Dockerfile .
|
||||||
docker run --rm -it -v "$(PWD):/tests" $(DOCKER_REPOSITORY)/gotenberg:tests
|
docker run --rm -it $(DOCKER_REPOSITORY)/gotenberg:tests
|
||||||
|
|
||||||
# build Docker image.
|
# generate documentation.
|
||||||
|
doc:
|
||||||
|
make workspace
|
||||||
|
docker build -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile .
|
||||||
|
docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REPOSITORY)/gotenberg:docs
|
||||||
|
|
||||||
|
# build Gotenberg Docker image.
|
||||||
image:
|
image:
|
||||||
docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile .
|
make workspace
|
||||||
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
docker build --build-arg VERSION=$(VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile .
|
||||||
|
|
||||||
# start the API using previously built Docker image.
|
# start the API using previously built Docker image.
|
||||||
gotenberg:
|
gotenberg:
|
||||||
docker run -it --rm -e DEFAULT_WAIT_TIMEOUT=$(DEFAULT_WAIT_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)
|
docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -p "3000:$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION)
|
||||||
|
|
||||||
# publish Gotenberg images according to version.
|
# publish Gotenberg images according to version.
|
||||||
publish:
|
publish:
|
||||||
./scripts/publish.sh $(GOLANG_VERSION) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
make workspace
|
||||||
|
./scripts/publish.sh $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
||||||
@@ -16,7 +16,7 @@ RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free"
|
|||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# |
|
# |
|
||||||
# | Installs PM2 for launching programs in background and with failure
|
# | Installs PM2 for launching programs in background and with failure
|
||||||
# | recovering. In our case: Chrome (headless) and Office (headless).
|
# | recovering. In our case: Google Chrome (headless) and unoconv.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
||||||
@@ -101,6 +101,8 @@ COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
|||||||
# |
|
# |
|
||||||
|
|
||||||
RUN groupadd --gid 1001 gotenberg \
|
RUN groupadd --gid 1001 gotenberg \
|
||||||
&& useradd --uid 1001 --gid gotenberg --shell /bin/bash --no-create-home gotenberg \
|
&& useradd --uid 1001 --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \
|
||||||
&& mkdir /gotenberg \
|
&& mkdir /gotenberg \
|
||||||
&& chown gotenberg: /gotenberg
|
&& chown gotenberg: /gotenberg
|
||||||
|
|
||||||
|
ENV PM2_HOME=/gotenberg/.pm2
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
ARG GOLANG_VERSION
|
FROM thecodingmachine/gotenberg:workspace
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | static
|
# | static
|
||||||
@@ -19,6 +17,6 @@ RUN go get github.com/apex/static/cmd/static-docs
|
|||||||
# | Last instructions of this build.
|
# | Last instructions of this build.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
WORKDIR /docs
|
WORKDIR /gotenberg/docs
|
||||||
|
|
||||||
CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ]
|
CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ]
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
ARG GOLANG_VERSION
|
FROM thecodingmachine/gotenberg:workspace
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | GolangCI-Lint
|
# | GolangCI-Lint
|
||||||
@@ -10,7 +8,7 @@ FROM golang:${GOLANG_VERSION}-stretch
|
|||||||
# | than gometalinter.
|
# | than gometalinter.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
ENV GOLANGCI_LINT_VERSION 1.16.0
|
ARG GOLANGCI_LINT_VERSION
|
||||||
|
|
||||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
||||||
golangci-lint --version
|
golangci-lint --version
|
||||||
@@ -23,13 +21,14 @@ RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.s
|
|||||||
# |
|
# |
|
||||||
|
|
||||||
# Define our working directory outside of $GOPATH (we're using go modules).
|
# Define our working directory outside of $GOPATH (we're using go modules).
|
||||||
WORKDIR /lint
|
USER gotenberg
|
||||||
|
WORKDIR /gotenberg/lint
|
||||||
|
|
||||||
# Copy our module dependencies definitions.
|
# Copy our code source.
|
||||||
COPY go.mod .
|
COPY --chown=gotenberg:gotenberg . .
|
||||||
COPY go.sum .
|
|
||||||
|
|
||||||
# Install module dependencies.
|
# Install module dependencies.
|
||||||
RUN go mod download
|
RUN go mod download &&\
|
||||||
|
go mod verify
|
||||||
|
|
||||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
ARG GOLANG_VERSION
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Binary
|
# | Binary
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
@@ -7,7 +5,7 @@ ARG GOLANG_VERSION
|
|||||||
# | Buils Gotenberg binary.
|
# | Buils Gotenberg binary.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
FROM thecodingmachine/gotenberg:workspace AS workspace
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
|
||||||
@@ -16,7 +14,7 @@ ENV GOOS=linux \
|
|||||||
CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
|
|
||||||
# Define our workding outside of $GOPATH (we're using go modules).
|
# Define our workding outside of $GOPATH (we're using go modules).
|
||||||
WORKDIR /gotenberg
|
WORKDIR /gotenberg/package
|
||||||
|
|
||||||
# Copy our source code.
|
# Copy our source code.
|
||||||
COPY internal ./internal
|
COPY internal ./internal
|
||||||
@@ -25,7 +23,7 @@ COPY go.sum go.sum
|
|||||||
COPY go.mod go.mod
|
COPY go.mod go.mod
|
||||||
|
|
||||||
# Build our binary.
|
# Build our binary.
|
||||||
RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
|
RUN go build -o gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Final touch
|
# | Final touch
|
||||||
@@ -38,9 +36,8 @@ FROM thecodingmachine/gotenberg:base
|
|||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||||
|
|
||||||
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
|
COPY --from=workspace /gotenberg/package/gotenberg /usr/local/bin/
|
||||||
|
|
||||||
ENV PM2_HOME=/gotenberg/.pm2
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /gotenberg
|
WORKDIR /gotenberg
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,14 @@
|
|||||||
ARG GOLANG_VERSION
|
FROM thecodingmachine/gotenberg:workspace
|
||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch AS golang
|
# Define our workding outside of $GOPATH (we're using go modules).
|
||||||
|
USER gotenberg
|
||||||
|
WORKDIR /gotenberg/tests
|
||||||
|
|
||||||
FROM thecodingmachine/gotenberg:base
|
# Copy our code source.
|
||||||
|
COPY --chown=gotenberg:gotenberg . .
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Common libraries
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Libraries used in the build process of this image.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN apt-get install -y git gcc
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Golang
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs Golang.
|
|
||||||
# |
|
|
||||||
|
|
||||||
COPY --from=golang /usr/local/go /usr/local/go
|
|
||||||
|
|
||||||
RUN export PATH="/usr/local/go/bin:$PATH" &&\
|
|
||||||
go version
|
|
||||||
|
|
||||||
ENV GOPATH /go
|
|
||||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Final touch
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Last instructions of this build.
|
|
||||||
# |
|
|
||||||
|
|
||||||
# Define our working directory outside of $GOPATH (we're using go modules).
|
|
||||||
WORKDIR /tests
|
|
||||||
|
|
||||||
# Copy our module dependencies definitions.
|
|
||||||
COPY go.mod .
|
|
||||||
COPY go.sum .
|
|
||||||
|
|
||||||
# Install module dependencies.
|
# Install module dependencies.
|
||||||
RUN go mod download
|
RUN go mod download &&\
|
||||||
|
go mod verify
|
||||||
|
|
||||||
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
||||||
@@ -2,19 +2,33 @@
|
|||||||
|
|
||||||
set -xe
|
set -xe
|
||||||
|
|
||||||
|
# Make sure the user running the
|
||||||
|
# tests is the Gotenberg user.
|
||||||
|
CURRENT_USER=$(whoami)
|
||||||
|
if [ "$CURRENT_USER" != "gotenberg" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the PM2 processes
|
||||||
|
# (Google Chrome headless & unoconv listener).
|
||||||
|
go run github.com/thecodingmachine/gotenberg/test/cmd/pm2
|
||||||
|
|
||||||
|
# Run our tests.
|
||||||
|
go test -race -cover ./...
|
||||||
|
|
||||||
# Testing PM2 processes launch separatly for avoiding
|
# Testing PM2 processes launch separatly for avoiding
|
||||||
# spending to much time on each tests depending on
|
# spending to much time on each tests depending on
|
||||||
# them.
|
# them.
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
||||||
|
|
||||||
# Running others tests.
|
# Running others tests.
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/config
|
#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/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/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/pkg/timeout
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
#go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
||||||
|
|
||||||
# Finally testing processes shutdown.
|
# Finally testing processes shutdown.
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
|
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
#go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
||||||
52
build/workspace/Dockerfile
Normal file
52
build/workspace/Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
FROM thecodingmachine/gotenberg:base
|
||||||
|
|
||||||
|
ARG GOLANG_VERSION
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | 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.
|
||||||
|
# |
|
||||||
|
|
||||||
|
RUN wget https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz &&\
|
||||||
|
tar -xvf go${GOLANG_VERSION}.linux-amd64.tar.gz &&\
|
||||||
|
mv go /usr/local
|
||||||
|
|
||||||
|
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,17 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
"github.com/thecodingmachine/gotenberg/internal/app/xhttp"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set on build time.
|
// version will be set on build time.
|
||||||
@@ -20,8 +19,8 @@ var version = "snapshot"
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
const op = "main"
|
const op = "main"
|
||||||
config, err := config.FromEnv()
|
config, err := conf.FromEnv()
|
||||||
systemLogger := logger.New(config.LogLevel(), "system")
|
systemLogger := xlog.New(config.LogLevel(), "system")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
systemLogger.FatalOp(op, err)
|
systemLogger.FatalOp(op, err)
|
||||||
}
|
}
|
||||||
@@ -29,24 +28,24 @@ func main() {
|
|||||||
systemLogger.DebugfOp(op, "configuration: %+v", config)
|
systemLogger.DebugfOp(op, "configuration: %+v", config)
|
||||||
// start PM2 processes.
|
// start PM2 processes.
|
||||||
var processes []pm2.Process
|
var processes []pm2.Process
|
||||||
if config.EnableChromeEndpoints() {
|
if !config.DisableGoogleChrome() {
|
||||||
processes = append(processes, pm2.NewChrome(systemLogger))
|
processes = append(processes, pm2.NewChromeProcess(systemLogger))
|
||||||
}
|
}
|
||||||
if config.EnableUnoconvEndpoints() {
|
if !config.DisableUnoconv() {
|
||||||
processes = append(processes, pm2.NewUnoconv(systemLogger))
|
processes = append(processes, pm2.NewUnoconvProcess(systemLogger))
|
||||||
}
|
}
|
||||||
for _, p := range processes {
|
for _, p := range processes {
|
||||||
systemLogger.InfofOp(op, "starting %s with PM2...", p.Fullname())
|
systemLogger.InfofOp(op, "starting '%s' with PM2...", p.Fullname())
|
||||||
if err := p.Start(); err != nil {
|
if err := p.Start(); err != nil {
|
||||||
systemLogger.FatalOp(op, err)
|
systemLogger.FatalOp(op, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// run our API in a goroutine so that it doesn't block.
|
|
||||||
// create our API.
|
// create our API.
|
||||||
srv := api.New(config)
|
srv := xhttp.New(config, processes...)
|
||||||
|
// run our API in a goroutine so that it doesn't block.
|
||||||
go func() {
|
go func() {
|
||||||
systemLogger.InfofOp(op, "http server started on port %s", config.DefaultListenPort())
|
systemLogger.InfofOp(op, "http server started on port '%d'", config.DefaultListenPort())
|
||||||
if err := srv.Start(fmt.Sprintf(":%s", config.DefaultListenPort())); err != nil {
|
if err := srv.Start(fmt.Sprintf(":%d", config.DefaultListenPort())); err != nil {
|
||||||
if err != http.ErrServerClosed {
|
if err != http.ErrServerClosed {
|
||||||
systemLogger.FatalOp(op, err)
|
systemLogger.FatalOp(op, err)
|
||||||
}
|
}
|
||||||
@@ -59,21 +58,21 @@ func main() {
|
|||||||
// block until we receive our signal.
|
// block until we receive our signal.
|
||||||
<-quit
|
<-quit
|
||||||
// create a deadline to wait for.
|
// create a deadline to wait for.
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
ctx, cancel := xcontext.WithTimeout(systemLogger, 120)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
// doesn't block if no connections, but will otherwise wait
|
// doesn't block if no connections, but will otherwise wait
|
||||||
// until the timeout deadline.
|
// until the timeout deadline.
|
||||||
systemLogger.InfofOp(op, "shutting down http server...")
|
systemLogger.InfoOp(op, "shutting down http server...")
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
systemLogger.FatalOp(op, err)
|
systemLogger.FatalOp(op, err)
|
||||||
}
|
}
|
||||||
// shutdown PM2 processes.
|
// shutdown PM2 processes.
|
||||||
for _, p := range processes {
|
for _, p := range processes {
|
||||||
systemLogger.InfofOp(op, "shutting down %s with PM2...", p.Fullname())
|
systemLogger.InfofOp(op, "shutting down '%s' with PM2...", p.Fullname())
|
||||||
if err := p.Shutdown(); err != nil {
|
if err := p.Stop(); err != nil {
|
||||||
systemLogger.FatalOp(op, err)
|
systemLogger.FatalOp(op, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
systemLogger.InfofOp(op, "bye!")
|
systemLogger.InfoOp(op, "bye!")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/middleware"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// New returns an API.
|
|
||||||
func New(config *config.Config) *echo.Echo {
|
|
||||||
api := echo.New()
|
|
||||||
api.HideBanner = true
|
|
||||||
api.HidePort = true
|
|
||||||
api.Use(middleware.Context(config))
|
|
||||||
api.Use(middleware.Logger())
|
|
||||||
api.Use(middleware.Cleanup())
|
|
||||||
api.Use(middleware.Error())
|
|
||||||
api.GET(handler.PingEndpoint, handler.Ping)
|
|
||||||
api.POST(handler.MergeEndpoint, handler.Merge)
|
|
||||||
if !config.EnableChromeEndpoints() && !config.EnableUnoconvEndpoints() {
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
g := api.Group(handler.ConvertGroupEndpoint)
|
|
||||||
if config.EnableChromeEndpoints() {
|
|
||||||
g.POST(handler.HTMLEndpoint, handler.HTML)
|
|
||||||
g.POST(handler.URLEndpoint, handler.URL)
|
|
||||||
g.POST(handler.MarkdownEndpoint, handler.Markdown)
|
|
||||||
}
|
|
||||||
if config.EnableUnoconvEndpoints() {
|
|
||||||
g.POST(handler.OfficeEndpoint, handler.Office)
|
|
||||||
}
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/middleware"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPing(t *testing.T) {
|
|
||||||
endpoint := handler.PingEndpoint
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
// should be OK.
|
|
||||||
req := httptest.NewRequest(http.MethodGet, endpoint, nil)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
|
||||||
endpoint := handler.MergeEndpoint
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
// should be OK.
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// bad request.
|
|
||||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// timeout.
|
|
||||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
// should have no more resources.
|
|
||||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
|
||||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
|
||||||
endpoint := fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.HTMLEndpoint)
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
// should be OK.
|
|
||||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// bad request.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.WaitDelayFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.PaperWidthFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.PaperHeightFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginTopFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginBottomFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginLeftFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginRightFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.LandscapeFormField: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// timeout.
|
|
||||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
// should have no more resources.
|
|
||||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
|
||||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
|
||||||
endpoint := fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.MarkdownEndpoint)
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
// should be OK.
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// bad request.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitDelayFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.PaperWidthFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.PaperHeightFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginTopFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginBottomFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginLeftFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginRightFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.LandscapeFormField: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// timeout.
|
|
||||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
// should have no more resources.
|
|
||||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
|
||||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestURL(t *testing.T) {
|
|
||||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
|
||||||
endpoint := fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.URLEndpoint)
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
// should be OK.
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
// bad request.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.WaitDelayFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.PaperWidthFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.PaperHeightFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginTopFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginBottomFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginLeftFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginRightFormField: "not a float"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.LandscapeFormField: "not a bool"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
|
||||||
// timeout.
|
|
||||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
|
||||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
|
||||||
// should have no more resources.
|
|
||||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
|
||||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConcurrent(t *testing.T) {
|
|
||||||
const concurrentRequests int = 4
|
|
||||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
// Merge.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, handler.MergeEndpoint, 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
|
|
||||||
},
|
|
||||||
concurrentRequests,
|
|
||||||
)
|
|
||||||
// HTML.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.HTMLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.HTMLEndpoint), 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
|
|
||||||
},
|
|
||||||
concurrentRequests,
|
|
||||||
)
|
|
||||||
// Markdown.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.MarkdownEndpoint), 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
|
|
||||||
},
|
|
||||||
concurrentRequests,
|
|
||||||
)
|
|
||||||
// URL.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.URLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.URLEndpoint), 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
|
|
||||||
},
|
|
||||||
concurrentRequests,
|
|
||||||
)
|
|
||||||
// Office.
|
|
||||||
test.AssertConcurrent(
|
|
||||||
t,
|
|
||||||
func() error {
|
|
||||||
body, contentType := test.OfficeTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.OfficeEndpoint), 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
|
|
||||||
},
|
|
||||||
concurrentRequests,
|
|
||||||
)
|
|
||||||
// should have no more resources.
|
|
||||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
|
||||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}()
|
|
||||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resource.WebhookURLFormField: "http://localhost:3001/foo"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
|
||||||
err = <-status
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// should have no more resources.
|
|
||||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
|
||||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResultFilename(t *testing.T) {
|
|
||||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
|
||||||
config, err := config.FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
srv := New(config)
|
|
||||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resource.ResultFilenameFormField: "foo.pdf"})
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/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"))
|
|
||||||
// should have no more resources.
|
|
||||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
|
||||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
// Package api helps managing the HTTP server behind Gotenberg.
|
|
||||||
package api
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package context
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Context extends the default echo.Context.
|
|
||||||
type Context struct {
|
|
||||||
echo.Context
|
|
||||||
logger *logger.Logger
|
|
||||||
config *config.Config
|
|
||||||
resource *resource.Resource
|
|
||||||
startTime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new context.
|
|
||||||
func New(c echo.Context, logger *logger.Logger, config *config.Config) *Context {
|
|
||||||
return &Context{
|
|
||||||
c,
|
|
||||||
logger,
|
|
||||||
config,
|
|
||||||
nil,
|
|
||||||
time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustCastFromEchoContext cast an echo.Context to our custom
|
|
||||||
// context. If something goes wrong, panic.
|
|
||||||
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 a custom context", op))
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// StandardLogger returns the custom logger.
|
|
||||||
// This method should be used instead of the
|
|
||||||
// default Logger() method coming from
|
|
||||||
// the echo.Context!
|
|
||||||
func (ctx *Context) StandardLogger() *logger.Logger {
|
|
||||||
return ctx.logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resource returns the associated resource
|
|
||||||
// to the context.
|
|
||||||
func (ctx *Context) Resource() *resource.Resource {
|
|
||||||
return ctx.resource
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithResource adds a resource to the context.
|
|
||||||
func (ctx *Context) WithResource(resourceDirPath string) error {
|
|
||||||
const op string = "context.WithResource"
|
|
||||||
r, err := resource.New(ctx, ctx.logger, ctx.config, resourceDirPath)
|
|
||||||
ctx.resource = r
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{
|
|
||||||
Op: op,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogRequestResult logs the result of a request.
|
|
||||||
// This method should only be used by a middleware!
|
|
||||||
func (ctx *Context) LogRequestResult(err error, isDebug bool) error {
|
|
||||||
const op string = "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)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package context helps extending
|
|
||||||
// the default echo.Context.
|
|
||||||
package context
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package handler contains all
|
|
||||||
// the endpoint methods of the API.
|
|
||||||
package handler
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/random"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// PingEndpoint is the route for healthcheck.
|
|
||||||
PingEndpoint = "/ping"
|
|
||||||
// MergeEndpoint is the route for merging PDF files.
|
|
||||||
MergeEndpoint = "/merge"
|
|
||||||
// ConvertGroupEndpoint is the route of the group
|
|
||||||
// in charge of converting files to PDF.
|
|
||||||
ConvertGroupEndpoint = "/convert"
|
|
||||||
// HTMLEndpoint is the route for converting
|
|
||||||
// HTML to PDF.
|
|
||||||
HTMLEndpoint = "/html"
|
|
||||||
// URLEndpoint is the route for converting
|
|
||||||
// a URL to PDF.
|
|
||||||
URLEndpoint = "/url"
|
|
||||||
// MarkdownEndpoint is the route for converting
|
|
||||||
// Markdown to PDF.
|
|
||||||
MarkdownEndpoint = "/markdown"
|
|
||||||
// OfficeEndpoint is the route for converting
|
|
||||||
// Office files to PDF.
|
|
||||||
OfficeEndpoint = "/office"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convert(ctx *context.Context, p printer.Printer) error {
|
|
||||||
const op string = "handler.convert"
|
|
||||||
r := ctx.Resource()
|
|
||||||
logger := ctx.StandardLogger()
|
|
||||||
baseFilename := random.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.Has(resource.WebhookURLFormField) {
|
|
||||||
logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLFormField)
|
|
||||||
if err := convertSync(filename, fpath, ctx, p); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// 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.WebhookURLFormField)
|
|
||||||
return convertAsync(filename, fpath, ctx, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertSync(filename, fpath string, ctx *context.Context, p printer.Printer) error {
|
|
||||||
const op = "handler.convertSync"
|
|
||||||
r := ctx.Resource()
|
|
||||||
logger := ctx.StandardLogger()
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
if !r.Has(resource.ResultFilenameFormField) {
|
|
||||||
logger.DebugfOp(
|
|
||||||
op,
|
|
||||||
"no '%s' found, using generated filename '%s'",
|
|
||||||
resource.ResultFilenameFormField,
|
|
||||||
filename,
|
|
||||||
)
|
|
||||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
logger.DebugfOp(
|
|
||||||
op,
|
|
||||||
"'%s' found, so not using generated filename",
|
|
||||||
resource.ResultFilenameFormField,
|
|
||||||
)
|
|
||||||
filename, err := r.Get(resource.ResultFilenameFormField)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertAsync(filename, fpath string, ctx *context.Context, p printer.Printer) error {
|
|
||||||
const op = "handler.convertAsync"
|
|
||||||
r := ctx.Resource()
|
|
||||||
logger := ctx.StandardLogger()
|
|
||||||
go func() {
|
|
||||||
defer r.Close() // nolint: errcheck
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
logger.ErrorOp(
|
|
||||||
op,
|
|
||||||
&standarderror.Error{Op: op, Err: err},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
logger.ErrorOp(
|
|
||||||
op,
|
|
||||||
&standarderror.Error{Op: op, Err: err},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close() // nolint: errcheck
|
|
||||||
webhookURL, err := r.Get(resource.WebhookURLFormField)
|
|
||||||
if err != nil {
|
|
||||||
logger.ErrorOp(
|
|
||||||
op,
|
|
||||||
&standarderror.Error{Op: op, Err: err},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.DebugfOp(
|
|
||||||
op,
|
|
||||||
"sending result file '%s' to '%s'",
|
|
||||||
filename,
|
|
||||||
webhookURL,
|
|
||||||
)
|
|
||||||
resp, err := http.Post(webhookURL, "application/pdf", f) /* #nosec */
|
|
||||||
if err != nil {
|
|
||||||
logger.ErrorOp(
|
|
||||||
op,
|
|
||||||
&standarderror.Error{Op: op, Err: err},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() // nolint: errcheck
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTML is the endpoint for converting
|
|
||||||
// HTML to PDF.
|
|
||||||
func HTML(c echo.Context) error {
|
|
||||||
const op string = "handler.HTML"
|
|
||||||
ctx := context.MustCastFromEchoContext(c)
|
|
||||||
ctx.StandardLogger().DebugfOp(op, "html request")
|
|
||||||
r := ctx.Resource()
|
|
||||||
opts, err := r.ChromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
fpath, err := r.Fpath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
p := printer.NewHTML(fpath, opts)
|
|
||||||
if err := convert(ctx, p); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Markdown is the endpoint for converting
|
|
||||||
// Markdown to PDF.
|
|
||||||
func Markdown(c echo.Context) error {
|
|
||||||
const op string = "handler.Markdown"
|
|
||||||
ctx := context.MustCastFromEchoContext(c)
|
|
||||||
ctx.StandardLogger().DebugfOp(op, "markdown request")
|
|
||||||
r := ctx.Resource()
|
|
||||||
opts, err := r.ChromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
fpath, err := r.Fpath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
p, err := printer.NewMarkdown(fpath, opts)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
if err := convert(ctx, p); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Merge is the endpoint for
|
|
||||||
// merging PDF files.
|
|
||||||
func Merge(c echo.Context) error {
|
|
||||||
const op string = "handler.Merge"
|
|
||||||
ctx := context.MustCastFromEchoContext(c)
|
|
||||||
ctx.StandardLogger().DebugfOp(op, "merge request")
|
|
||||||
r := ctx.Resource()
|
|
||||||
opts, err := r.MergePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
fpaths, err := r.Fpaths(".pdf")
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
p := printer.NewMerge(fpaths, opts)
|
|
||||||
if err := convert(ctx, p); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Office is the endpoint for converting
|
|
||||||
// Office files to PDF.
|
|
||||||
func Office(c echo.Context) error {
|
|
||||||
const op string = "handler.Office"
|
|
||||||
ctx := context.MustCastFromEchoContext(c)
|
|
||||||
ctx.StandardLogger().DebugfOp(op, "office request")
|
|
||||||
r := ctx.Resource()
|
|
||||||
opts, err := r.OfficePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
fpaths, err := r.Fpaths(
|
|
||||||
".txt",
|
|
||||||
".rtf",
|
|
||||||
".fodt",
|
|
||||||
".doc",
|
|
||||||
".docx",
|
|
||||||
".odt",
|
|
||||||
".xls",
|
|
||||||
".xlsx",
|
|
||||||
".ods",
|
|
||||||
".ppt",
|
|
||||||
".pptx",
|
|
||||||
".odp",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
p := printer.NewOffice(fpaths, opts)
|
|
||||||
if err := convert(ctx, p); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ping is the endpoint for healthcheck.
|
|
||||||
func Ping(c echo.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// URL is the endpoint for converting
|
|
||||||
// a URL to PDF.
|
|
||||||
func URL(c echo.Context) error {
|
|
||||||
const op string = "handler.URL"
|
|
||||||
ctx := context.MustCastFromEchoContext(c)
|
|
||||||
ctx.StandardLogger().DebugfOp(op, "url request")
|
|
||||||
r := ctx.Resource()
|
|
||||||
opts, err := r.ChromePrinterOptions()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
remoteURL, err := r.Get(resource.RemoteURLFormField)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
p := printer.NewURL(remoteURL, opts)
|
|
||||||
if err := convert(ctx, p); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cleanup helps removing a resource at the end of a request.
|
|
||||||
func Cleanup() echo.MiddlewareFunc {
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
const op string = "middleware.Cleanup"
|
|
||||||
err := next(c)
|
|
||||||
ctx := context.MustCastFromEchoContext(c)
|
|
||||||
r := ctx.Resource()
|
|
||||||
if r == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// if a webhook URL has been given,
|
|
||||||
// do not remove the resource here because
|
|
||||||
// we don't know if the result file has been
|
|
||||||
// generated or sent.
|
|
||||||
if r.Has(resource.WebhookURLFormField) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// a resource is associated with our custom context.
|
|
||||||
if resourceErr := r.Close(); resourceErr != nil {
|
|
||||||
ctx.StandardLogger().ErrorOp(op, &standarderror.Error{
|
|
||||||
Op: op,
|
|
||||||
Err: resourceErr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/random"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// TestingTraceEnvVar is an environment
|
|
||||||
// variable used in some tests.
|
|
||||||
TestingTraceEnvVar string = "TESTING_TRACE"
|
|
||||||
// TestsTracePrefix helps
|
|
||||||
// creating all resources inside a prefix.
|
|
||||||
// Only used in some tests
|
|
||||||
// to check if the resources
|
|
||||||
// have been removed.
|
|
||||||
TestsTracePrefix string = "tmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Context helps extending the default echo.Context with
|
|
||||||
// our custom context.
|
|
||||||
func Context(config *config.Config) echo.MiddlewareFunc {
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
var trace string
|
|
||||||
if os.Getenv(TestingTraceEnvVar) == "1" {
|
|
||||||
trace = fmt.Sprintf("%s/%s", TestsTracePrefix, random.Get())
|
|
||||||
} else {
|
|
||||||
// generate a unique identifier for the request.
|
|
||||||
trace = random.Get()
|
|
||||||
}
|
|
||||||
// create the logger for this request using
|
|
||||||
// the previous identifier as trace.
|
|
||||||
logger := logger.New(config.LogLevel(), trace)
|
|
||||||
// extend the current echo context with our custom
|
|
||||||
// context.
|
|
||||||
ctx := context.New(c, logger, config)
|
|
||||||
// if its an healthcheck request, there
|
|
||||||
// is no resource associated to it.
|
|
||||||
if ctx.Path() == handler.PingEndpoint {
|
|
||||||
return next(ctx)
|
|
||||||
}
|
|
||||||
// if the endpoint is not for healthcheck, associate a
|
|
||||||
// resource to our custom context.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package middleware contains the
|
|
||||||
// middleware of the API.
|
|
||||||
package middleware
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error helps handling errors (if any).
|
|
||||||
func Error() 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 := standarderror.Op(err)
|
|
||||||
logger := ctx.StandardLogger()
|
|
||||||
logger.ErrorOp(errOp, err)
|
|
||||||
// handle our custom HTTP error.
|
|
||||||
var httpErr error
|
|
||||||
errCode := standarderror.Code(err)
|
|
||||||
errMessage := standarderror.Message(err)
|
|
||||||
switch errCode {
|
|
||||||
case standarderror.Invalid:
|
|
||||||
httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage)
|
|
||||||
case standarderror.Timeout:
|
|
||||||
httpErr = echo.NewHTTPError(http.StatusRequestTimeout, errMessage)
|
|
||||||
default:
|
|
||||||
httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage)
|
|
||||||
}
|
|
||||||
// required to have a correct status code.
|
|
||||||
ctx.Error(httpErr)
|
|
||||||
return httpErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger helps logging the result of a request.
|
|
||||||
func Logger() 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() == handler.PingEndpoint
|
|
||||||
return ctx.LogRequestResult(err, isDebug)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Package resource helps creating a folder
|
|
||||||
// containing all uploaded files and the resulting
|
|
||||||
// PDF file. It also helps centralizing all
|
|
||||||
// the form values.
|
|
||||||
package resource
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
package resource
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ResultFilenameFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
ResultFilenameFormField string = "resultFilename"
|
|
||||||
// WaitTimeoutFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
WaitTimeoutFormField string = "waitTimeout"
|
|
||||||
// WebhookURLFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
WebhookURLFormField string = "webhookURL"
|
|
||||||
// RemoteURLFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
RemoteURLFormField string = "remoteURL"
|
|
||||||
// WaitDelayFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
WaitDelayFormField string = "waitDelay"
|
|
||||||
// PaperWidthFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
PaperWidthFormField string = "paperWidth"
|
|
||||||
// PaperHeightFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
PaperHeightFormField string = "paperHeight"
|
|
||||||
// MarginTopFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
MarginTopFormField string = "marginTop"
|
|
||||||
// MarginBottomFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
MarginBottomFormField string = "marginBottom"
|
|
||||||
// MarginLeftFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
MarginLeftFormField string = "marginLeft"
|
|
||||||
// MarginRightFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
MarginRightFormField string = "marginRight"
|
|
||||||
// LandscapeFormField contains the name
|
|
||||||
// of a form field.
|
|
||||||
LandscapeFormField string = "landscape"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resource helps retrieving form values
|
|
||||||
// and form files from a request.
|
|
||||||
type Resource struct {
|
|
||||||
logger *logger.Logger
|
|
||||||
config *config.Config
|
|
||||||
formValues map[string]string
|
|
||||||
formFilesDirPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new resource.
|
|
||||||
func New(c echo.Context, logger *logger.Logger, config *config.Config, dirPath string) (*Resource, error) {
|
|
||||||
const op string = "resource.New"
|
|
||||||
r := &Resource{
|
|
||||||
logger: logger,
|
|
||||||
config: config,
|
|
||||||
formValues: formValues(c, logger),
|
|
||||||
formFilesDirPath: dirPath,
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
r.logger.DebugfOp(op, "directory '%s' created", dirPath)
|
|
||||||
if err := formFiles(c, logger, dirPath); err != nil {
|
|
||||||
return r, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formValues(c echo.Context, logger *logger.Logger) map[string]string {
|
|
||||||
const op string = "resource.formValues"
|
|
||||||
v := make(map[string]string)
|
|
||||||
fetch := func(formField string) string {
|
|
||||||
value := c.FormValue(formField)
|
|
||||||
if value == "" {
|
|
||||||
logger.DebugfOp(op, "'%s' is empty", formField)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
logger.DebugfOp(op, "'%s' retrieved, got '%s'", formField, value)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
v[ResultFilenameFormField] = fetch(ResultFilenameFormField)
|
|
||||||
v[WaitTimeoutFormField] = fetch(WaitTimeoutFormField)
|
|
||||||
v[WebhookURLFormField] = fetch(WebhookURLFormField)
|
|
||||||
v[RemoteURLFormField] = fetch(RemoteURLFormField)
|
|
||||||
v[WaitDelayFormField] = fetch(WaitDelayFormField)
|
|
||||||
v[PaperWidthFormField] = fetch(PaperWidthFormField)
|
|
||||||
v[PaperHeightFormField] = fetch(PaperHeightFormField)
|
|
||||||
v[MarginTopFormField] = fetch(MarginTopFormField)
|
|
||||||
v[MarginBottomFormField] = fetch(MarginBottomFormField)
|
|
||||||
v[MarginLeftFormField] = fetch(MarginLeftFormField)
|
|
||||||
v[MarginRightFormField] = fetch(MarginRightFormField)
|
|
||||||
v[LandscapeFormField] = fetch(LandscapeFormField)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func formFiles(c echo.Context, logger *logger.Logger, dirPath string) error {
|
|
||||||
const op string = "resource.formFiles"
|
|
||||||
form, err := c.MultipartForm()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
for _, files := range form.File {
|
|
||||||
for _, fh := range files {
|
|
||||||
in, err := fh.Open()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
defer in.Close() // nolint: errcheck
|
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
defer out.Close() // nolint: errcheck
|
|
||||||
if err := out.Chmod(0644); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
if _, err := out.Seek(0, 0); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
logger.DebugfOp(op, "'%s' created", fh.Filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirPath returns the directory
|
|
||||||
// path where are stored the form
|
|
||||||
// files and the resulting PDF file.
|
|
||||||
func (r *Resource) DirPath() string {
|
|
||||||
return r.formFilesDirPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close deletes the working directory of the
|
|
||||||
// resource if it exists.
|
|
||||||
func (r *Resource) Close() error {
|
|
||||||
const op string = "resource.Close"
|
|
||||||
if _, err := os.Stat(r.formFilesDirPath); os.IsNotExist(err) {
|
|
||||||
r.logger.DebugfOp(op, "directory '%s' does not exist, nothing to remove", r.formFilesDirPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(r.formFilesDirPath); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
r.logger.DebugfOp(op, "directory '%s' removed", r.formFilesDirPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
|
||||||
|
|
||||||
// ChromePrinterOptions returns the Chrome printer options
|
|
||||||
// thanks to the form values and form files from the request
|
|
||||||
// plus the default values from the configuration.
|
|
||||||
func (r *Resource) ChromePrinterOptions() (*printer.ChromeOptions, error) {
|
|
||||||
const op string = "resource.ChromePrinterOptions"
|
|
||||||
waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout())
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
waitDelay, err := r.float64(WaitDelayFormField, 0.0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
headerHTML, err := r.content("header.html", defaultHeaderFooterHTML)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
footerHTML, err := r.content("footer.html", defaultHeaderFooterHTML)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
paperWidth, err := r.float64(PaperWidthFormField, 8.27)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
paperHeight, err := r.float64(PaperHeightFormField, 11.7)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
marginTop, err := r.float64(MarginTopFormField, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
marginBottom, err := r.float64(MarginBottomFormField, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
marginLeft, err := r.float64(MarginLeftFormField, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
marginRight, err := r.float64(MarginRightFormField, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
landscape, err := r.bool(LandscapeFormField, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
opts := &printer.ChromeOptions{
|
|
||||||
WaitTimeout: waitTimeout,
|
|
||||||
WaitDelay: waitDelay,
|
|
||||||
HeaderHTML: headerHTML,
|
|
||||||
FooterHTML: footerHTML,
|
|
||||||
PaperWidth: paperWidth,
|
|
||||||
PaperHeight: paperHeight,
|
|
||||||
MarginTop: marginTop,
|
|
||||||
MarginBottom: marginBottom,
|
|
||||||
MarginLeft: marginLeft,
|
|
||||||
MarginRight: marginRight,
|
|
||||||
Landscape: landscape,
|
|
||||||
}
|
|
||||||
r.logger.DebugfOp(op, "printer options: %+v", opts)
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OfficePrinterOptions returns the Office printer options
|
|
||||||
// thanks to the form values from the request
|
|
||||||
// plus the default values from the configuration.
|
|
||||||
func (r *Resource) OfficePrinterOptions() (*printer.OfficeOptions, error) {
|
|
||||||
const op string = "resource.OfficePrinterOptions"
|
|
||||||
waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout())
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
landscape, err := r.bool(LandscapeFormField, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
opts := &printer.OfficeOptions{
|
|
||||||
WaitTimeout: waitTimeout,
|
|
||||||
Landscape: landscape,
|
|
||||||
}
|
|
||||||
r.logger.DebugfOp(op, "printer options: %+v", opts)
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergePrinterOptions returns the merge printer options
|
|
||||||
// thanks to the form values from the request
|
|
||||||
// plus the default values from the configuration.
|
|
||||||
func (r *Resource) MergePrinterOptions() (*printer.MergeOptions, error) {
|
|
||||||
const op string = "resource.MergePrinterOptions"
|
|
||||||
waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout())
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
opts := &printer.MergeOptions{
|
|
||||||
WaitTimeout: waitTimeout,
|
|
||||||
}
|
|
||||||
r.logger.DebugfOp(op, "printer options: %+v", opts)
|
|
||||||
return opts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has returns true if the resource
|
|
||||||
// contains the given form field and
|
|
||||||
// its value is not empty.
|
|
||||||
func (r *Resource) Has(formField string) bool {
|
|
||||||
v, ok := r.formValues[formField]
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the form field value.
|
|
||||||
func (r *Resource) Get(formField string) (string, error) {
|
|
||||||
const op string = "resource.Get"
|
|
||||||
v, err := r.value(formField)
|
|
||||||
if err != nil {
|
|
||||||
return "", &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resource) value(formField string) (string, error) {
|
|
||||||
const op string = "resource.value"
|
|
||||||
v, ok := r.formValues[formField]
|
|
||||||
if !ok {
|
|
||||||
return "", &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' does not exist", formField),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resource) float64(formField string, defaultValue float64) (float64, error) {
|
|
||||||
const op string = "resource.float64"
|
|
||||||
if !r.Has(formField) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
v, err := r.value(formField)
|
|
||||||
if err != nil {
|
|
||||||
return 0.0, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
f, err := strconv.ParseFloat(v, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0.0, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' is not a float, got '%s'", formField, v),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resource) bool(formField string, defaultValue bool) (bool, error) {
|
|
||||||
const op string = "resource.bool"
|
|
||||||
if !r.Has(formField) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
v, err := r.value(formField)
|
|
||||||
if err != nil {
|
|
||||||
return false, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
b, err := strconv.ParseBool(v)
|
|
||||||
if err != nil {
|
|
||||||
return false, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' is not a boolean, got '%s'", formField, v),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fpath returns the path of the given filename.
|
|
||||||
// This filename should be the name of a form file.
|
|
||||||
func (r *Resource) Fpath(filename string) (string, error) {
|
|
||||||
const op string = "resource.Fpath"
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
|
||||||
_, err := os.Stat(fpath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return "", &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("file '%s' does not exist", filename),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
absPath, err := filepath.Abs(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return absPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resource) content(filename string, defaultValue string) (string, error) {
|
|
||||||
const op string = "resource.content"
|
|
||||||
if !r.hasFile(filename) {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
fpath, err := r.Fpath(filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fpaths returns the list of files of the resource
|
|
||||||
// according to given file extensions.
|
|
||||||
func (r *Resource) Fpaths(exts ...string) ([]string, error) {
|
|
||||||
const op string = "resource.Fpaths"
|
|
||||||
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 &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
for _, ext := range exts {
|
|
||||||
if filepath.Ext(fpath) == ext {
|
|
||||||
fpaths = append(fpaths, fpath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
if len(fpaths) == 0 {
|
|
||||||
return nil, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("no file found for extentions %v", exts),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
||||||
298
internal/app/xhttp/handler.go
Normal file
298
internal/app/xhttp/handler.go
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
128
internal/app/xhttp/middleware.go
Normal file
128
internal/app/xhttp/middleware.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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:
|
||||||
|
// TODO status
|
||||||
|
httpErr = echo.NewHTTPError(http.StatusBadGateway, 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
|
||||||
|
}
|
||||||
210
internal/app/xhttp/pkg/context/context.go
Normal file
210
internal/app/xhttp/pkg/context/context.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasResource returns true if the Context
|
||||||
|
// has a resource.Resource.
|
||||||
|
func (ctx Context) HasResource() bool {
|
||||||
|
return &ctx.resource != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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,178 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_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 {
|
|
||||||
defaultWaitTimeout float64
|
|
||||||
defaultListenPort string
|
|
||||||
enableChromeEndpoints bool
|
|
||||||
enableUnoconvEndpoints bool
|
|
||||||
logLevel logrus.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultConfig() *Config {
|
|
||||||
return &Config{
|
|
||||||
defaultWaitTimeout: 10,
|
|
||||||
defaultListenPort: "3000",
|
|
||||||
enableChromeEndpoints: true,
|
|
||||||
enableUnoconvEndpoints: true,
|
|
||||||
logLevel: logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromEnv fetches configuration
|
|
||||||
// from environment variables.
|
|
||||||
func FromEnv() (*Config, error) {
|
|
||||||
const op string = "config.FromEnv"
|
|
||||||
c := defaultConfig()
|
|
||||||
defaultWaitTimeout, err := defaultWaitTimeoutFromEnv(defaultWaitTimeoutEnvVar, c.DefaultWaitTimeout())
|
|
||||||
c.defaultWaitTimeout = defaultWaitTimeout
|
|
||||||
if err != nil {
|
|
||||||
return c, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
defaultListenPort, err := defaultListenPortFromEnv(defaultListenPortEnvVar, c.DefaultListenPort())
|
|
||||||
c.defaultListenPort = defaultListenPort
|
|
||||||
if err != nil {
|
|
||||||
return c, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
disableChromeEndpoints, err := boolFromEnv(disableGoogleChromeEnvVar, !c.EnableChromeEndpoints())
|
|
||||||
c.enableChromeEndpoints = !disableChromeEndpoints
|
|
||||||
if err != nil {
|
|
||||||
return c, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
disableUnoconvEndpoints, err := boolFromEnv(disableUnoconvEnvVar, !c.EnableUnoconvEndpoints())
|
|
||||||
c.enableUnoconvEndpoints = !disableUnoconvEndpoints
|
|
||||||
if err != nil {
|
|
||||||
return c, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
logLevel, err := logLevelFromEnv(logLevelEnvVar, c.LogLevel())
|
|
||||||
c.logLevel = logLevel
|
|
||||||
if err != nil {
|
|
||||||
return c, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultWaitTimeout returns the default
|
|
||||||
// wait timeout from the configuration.
|
|
||||||
func (c *Config) DefaultWaitTimeout() float64 {
|
|
||||||
return c.defaultWaitTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultListenPort returns the default
|
|
||||||
// listen port from the configuration.
|
|
||||||
func (c *Config) DefaultListenPort() string {
|
|
||||||
return c.defaultListenPort
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableChromeEndpoints returns true if
|
|
||||||
// Chrome endpoints are enabled in the
|
|
||||||
// configuration.
|
|
||||||
func (c *Config) EnableChromeEndpoints() bool {
|
|
||||||
return c.enableChromeEndpoints
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableUnoconvEndpoints returns true if
|
|
||||||
// Unoconv endpoints are enabled in the
|
|
||||||
// configuration.
|
|
||||||
func (c *Config) EnableUnoconvEndpoints() bool {
|
|
||||||
return c.enableUnoconvEndpoints
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogLevel returns the logrus.Level from
|
|
||||||
// the configuration.
|
|
||||||
func (c *Config) LogLevel() logrus.Level {
|
|
||||||
return c.logLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultWaitTimeoutFromEnv(envVar string, defaultValue float64) (float64, error) {
|
|
||||||
const op string = "config.defaultWaitTimeoutFromEnv"
|
|
||||||
if v, ok := os.LookupEnv(envVar); ok {
|
|
||||||
waitTimeout, err := strconv.ParseFloat(v, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' is not a float, got '%s'", envVar, v),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return waitTimeout, nil
|
|
||||||
}
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultListenPortFromEnv(envVar string, defaultValue string) (string, error) {
|
|
||||||
const op string = "config.defaultListenPortFromEnv"
|
|
||||||
if v, ok := os.LookupEnv(envVar); ok {
|
|
||||||
portAsUint, err := strconv.ParseUint(v, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' is not a uint, got '%s'", envVar, v),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if portAsUint > 65535 {
|
|
||||||
return defaultValue, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' is not a uint < 65535, got '%d'", envVar, portAsUint),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolFromEnv(envVar string, defaultValue bool) (bool, error) {
|
|
||||||
const op string = "config.boolFromEnv"
|
|
||||||
if v, ok := os.LookupEnv(envVar); ok {
|
|
||||||
if v != "1" && v != "0" {
|
|
||||||
return defaultValue, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' is not '0' or '1', got %s", envVar, v),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v == "1", nil
|
|
||||||
}
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logLevelFromEnv(envVar string, defaultValue logrus.Level) (logrus.Level, error) {
|
|
||||||
const op string = "config.logLevelFromEnv"
|
|
||||||
if v, ok := os.LookupEnv(envVar); ok {
|
|
||||||
switch v {
|
|
||||||
case "DEBUG":
|
|
||||||
return logrus.DebugLevel, nil
|
|
||||||
case "INFO":
|
|
||||||
return logrus.InfoLevel, nil
|
|
||||||
case "ERROR":
|
|
||||||
return logrus.ErrorLevel, nil
|
|
||||||
default:
|
|
||||||
return defaultValue, &standarderror.Error{
|
|
||||||
Code: standarderror.Invalid,
|
|
||||||
Message: fmt.Sprintf("'%s' is not 'DEBUG', 'INFO' or 'ERROR', got '%s'", envVar, v),
|
|
||||||
Op: op,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultWaitTimeout(t *testing.T) {
|
|
||||||
// should be OK.
|
|
||||||
config, err := FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, 10.0, config.DefaultWaitTimeout())
|
|
||||||
os.Setenv(defaultWaitTimeoutEnvVar, "1.5")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, 1.5, config.DefaultWaitTimeout())
|
|
||||||
// should failed.
|
|
||||||
os.Setenv(defaultWaitTimeoutEnvVar, "foo")
|
|
||||||
_, err = FromEnv()
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized := test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
|
||||||
os.Unsetenv(defaultWaitTimeoutEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultListenPort(t *testing.T) {
|
|
||||||
// should be OK.
|
|
||||||
config, err := FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, "3000", config.DefaultListenPort())
|
|
||||||
os.Setenv(defaultListenPortEnvVar, "4000")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, "4000", config.DefaultListenPort())
|
|
||||||
// should failed.
|
|
||||||
os.Setenv(defaultListenPortEnvVar, "foo")
|
|
||||||
_, err = FromEnv()
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized := test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
|
||||||
os.Setenv(defaultListenPortEnvVar, "100000000")
|
|
||||||
_, err = FromEnv()
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized = test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
|
||||||
os.Unsetenv(defaultListenPortEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnableChromeEndpoints(t *testing.T) {
|
|
||||||
// should be OK.
|
|
||||||
config, err := FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, true, config.EnableChromeEndpoints())
|
|
||||||
os.Setenv(disableGoogleChromeEnvVar, "1")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, false, config.EnableChromeEndpoints())
|
|
||||||
os.Setenv(disableGoogleChromeEnvVar, "0")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, true, config.EnableChromeEndpoints())
|
|
||||||
// should failed.
|
|
||||||
os.Setenv(disableGoogleChromeEnvVar, "true")
|
|
||||||
_, err = FromEnv()
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized := test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
|
||||||
os.Unsetenv(disableGoogleChromeEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnableUnoconvEndpoints(t *testing.T) {
|
|
||||||
// should be OK.
|
|
||||||
config, err := FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, true, config.EnableUnoconvEndpoints())
|
|
||||||
os.Setenv(disableUnoconvEnvVar, "1")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, false, config.EnableUnoconvEndpoints())
|
|
||||||
os.Setenv(disableUnoconvEnvVar, "0")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, true, config.EnableUnoconvEndpoints())
|
|
||||||
// should failed.
|
|
||||||
os.Setenv(disableUnoconvEnvVar, "true")
|
|
||||||
_, err = FromEnv()
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized := test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
|
||||||
os.Unsetenv(disableUnoconvEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogLevel(t *testing.T) {
|
|
||||||
// should be OK.
|
|
||||||
config, err := FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, logrus.InfoLevel, config.LogLevel())
|
|
||||||
os.Setenv(logLevelEnvVar, "DEBUG")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, logrus.DebugLevel, config.LogLevel())
|
|
||||||
os.Setenv(logLevelEnvVar, "INFO")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, logrus.InfoLevel, config.LogLevel())
|
|
||||||
os.Setenv(logLevelEnvVar, "ERROR")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, logrus.ErrorLevel, config.LogLevel())
|
|
||||||
// should failed.
|
|
||||||
os.Setenv(logLevelEnvVar, "foo")
|
|
||||||
config, err = FromEnv()
|
|
||||||
assert.Equal(t, logrus.InfoLevel, config.LogLevel())
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized := test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
|
||||||
os.Unsetenv(logLevelEnvVar)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package config gathers all
|
|
||||||
// configuration data.
|
|
||||||
package config
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package logger defines a standard
|
|
||||||
// logger for the application.
|
|
||||||
package logger
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Logger enforces specific log message formats.
|
|
||||||
type Logger struct {
|
|
||||||
entry *logrus.Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
// New initializes the logger.
|
|
||||||
func New(level logrus.Level, trace string) *Logger {
|
|
||||||
l := logrus.New()
|
|
||||||
l.SetLevel(level)
|
|
||||||
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
|
||||||
l.SetFormatter(&logrus.JSONFormatter{})
|
|
||||||
}
|
|
||||||
return &Logger{
|
|
||||||
entry: l.WithField("trace", trace),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFields creates a new logger with
|
|
||||||
// given fields.
|
|
||||||
func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
|
|
||||||
return &Logger{
|
|
||||||
entry: l.entry.WithFields(fields),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DebugfOp logs a debug message for given
|
|
||||||
// logical operation.
|
|
||||||
func (l *Logger) DebugfOp(op string, format string, args ...interface{}) {
|
|
||||||
l.entry.WithField("op", op).Debugf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InfofOp logs an info message for given
|
|
||||||
// logical operation.
|
|
||||||
func (l *Logger) InfofOp(op string, 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.
|
|
||||||
func (l *Logger) ErrorfOp(op string, message string) {
|
|
||||||
l.entry.WithField("op", op).Error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FatalOp logs an error message for given
|
|
||||||
// logical operation.
|
|
||||||
func (l *Logger) FatalOp(op string, err error) {
|
|
||||||
l.entry.WithField("op", op).Error(err.Error())
|
|
||||||
}
|
|
||||||
@@ -5,45 +5,72 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
"github.com/mafredri/cdp/devtool"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const chromeWarmupTime time.Duration = 10 * time.Second
|
type chromeProcess struct {
|
||||||
|
logger xlog.Logger
|
||||||
type chrome struct {
|
|
||||||
manager *processManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChrome returns a Google Chrome
|
// NewChromeProcess returns a Google Chrome
|
||||||
// headless process.
|
// headless process.
|
||||||
func NewChrome(logger *logger.Logger) Process {
|
func NewChromeProcess(logger xlog.Logger) Process {
|
||||||
return &chrome{
|
return chromeProcess{
|
||||||
manager: &processManager{logger: logger},
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Fullname() string {
|
func (p chromeProcess) Fullname() string {
|
||||||
return "Google Chrome headless"
|
return "Google Chrome headless"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Start() error {
|
func (p chromeProcess) Start() error {
|
||||||
const op string = "pm2.chrome.Start"
|
const op string = "pm2.chromeProcess.Start"
|
||||||
if err := p.manager.start(p); err != nil {
|
if err := start(p.logger, p); err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return xerror.New(op, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Shutdown() error {
|
func (p chromeProcess) IsViable() bool {
|
||||||
const op string = "pm2.chrome.Shutdown"
|
const op string = "pm2.chromeProcess.IsViable"
|
||||||
if err := p.manager.shutdown(p); err != nil {
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
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 chromeProcess) Stop() error {
|
||||||
|
const op string = "pm2.chromeProcess.Stop"
|
||||||
|
if err := stop(p.logger, p); err != nil {
|
||||||
|
return xerror.New(op, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) args() []string {
|
func (p chromeProcess) args() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--headless",
|
"--headless",
|
||||||
@@ -62,47 +89,25 @@ func (p *chrome) args() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) name() string {
|
func (p chromeProcess) binary() string {
|
||||||
return "google-chrome-stable"
|
return "google-chrome-stable"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) viable() bool {
|
func (p chromeProcess) warmup() {
|
||||||
const op string = "pm2.chrome.viable"
|
const (
|
||||||
// check if Google Chrome is correctly running.
|
op string = "pm2.chromeProcess.warmup"
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
warmupTime time.Duration = 10 * time.Second
|
||||||
defer cancel()
|
|
||||||
p.manager.logger.DebugfOp(
|
|
||||||
op,
|
|
||||||
"checking liveness via debug version endpoint http://localhost:9222/json/version",
|
|
||||||
)
|
)
|
||||||
v, err := devtool.New("http://localhost:9222").Version(ctx)
|
p.logger.DebugfOp(
|
||||||
if err != nil {
|
|
||||||
p.manager.logger.DebugfOp(
|
|
||||||
op,
|
|
||||||
"debug version endpoint returned error: %v",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
p.manager.logger.DebugfOp(
|
|
||||||
op,
|
op,
|
||||||
"debug version endpoint returned version info: %+v",
|
"waiting '%v' for allowing '%s' to warmup",
|
||||||
*v,
|
warmupTime,
|
||||||
|
p.Fullname(),
|
||||||
)
|
)
|
||||||
return true
|
time.Sleep(warmupTime)
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chrome) warmup() {
|
|
||||||
const op string = "pm2.chrome.warmup"
|
|
||||||
p.manager.logger.DebugfOp(
|
|
||||||
op,
|
|
||||||
"allowing %v to startup",
|
|
||||||
chromeWarmupTime,
|
|
||||||
)
|
|
||||||
time.Sleep(chromeWarmupTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(chrome))
|
_ = Process(new(chromeProcess))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChromeStart(t *testing.T) {
|
|
||||||
p := NewChrome(test.CreateTestLogger())
|
|
||||||
err := p.Start()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChromeShutdown(t *testing.T) {
|
|
||||||
p := NewChrome(test.CreateTestLogger())
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Package pm2 facilitates starting external
|
Package pm2 facilitates starting external
|
||||||
processes on which our API depends.
|
processes on which our application depends.
|
||||||
|
|
||||||
For instance, it may start Google Chrome headless and
|
For instance, it may start Google Chrome headless and
|
||||||
unoconv listener with PM2.
|
unoconv listener with PM2.
|
||||||
|
|||||||
@@ -1,124 +1,102 @@
|
|||||||
package pm2
|
package pm2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||||
)
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
|
||||||
const (
|
|
||||||
stoppedState int32 = iota
|
|
||||||
runningState
|
|
||||||
errorState
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process is a type that can start or
|
// Process is a type that can start or
|
||||||
// shutdown a process with PM2.
|
// stop a process with PM2.
|
||||||
type Process interface {
|
type Process interface {
|
||||||
Fullname() string
|
Fullname() string
|
||||||
Start() error
|
Start() error
|
||||||
Shutdown() error
|
IsViable() bool
|
||||||
|
Stop() error
|
||||||
args() []string
|
args() []string
|
||||||
name() string
|
binary() string
|
||||||
viable() bool
|
|
||||||
warmup()
|
warmup()
|
||||||
}
|
}
|
||||||
|
|
||||||
type processManager struct {
|
type pm2Command string
|
||||||
heuristicState int32
|
|
||||||
logger *logger.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *processManager) start(p Process) error {
|
const (
|
||||||
const op string = "pm2.start"
|
startCommand pm2Command = "start"
|
||||||
if err := m.pm2(p, "start"); err != nil {
|
restartCommand pm2Command = "restart"
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
stopCommand pm2Command = "stop"
|
||||||
}
|
logsCommand pm2Command = "logs"
|
||||||
p.warmup()
|
)
|
||||||
if !p.viable() {
|
|
||||||
attempts := 0
|
func start(logger xlog.Logger, process Process) error {
|
||||||
for attempts < 5 && !p.viable() {
|
const (
|
||||||
if err := m.pm2(p, "restart"); err != nil {
|
op string = "pm2.start"
|
||||||
m.heuristicState = errorState
|
maximumAttempts int = 3
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
)
|
||||||
}
|
resolver := func() error {
|
||||||
p.warmup()
|
// first, we try to start the process.
|
||||||
attempts++
|
if err := run(logger, startCommand, process); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if !p.viable() {
|
// we wait the process to be ready.
|
||||||
m.heuristicState = errorState
|
process.warmup()
|
||||||
return &standarderror.Error{
|
// if the process failed to start correctly,
|
||||||
Op: op,
|
// we have to restart it.
|
||||||
Message: fmt.Sprintf("failed to launch %s", p.Fullname()),
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// the process is viable, let's log its
|
||||||
m.heuristicState = runningState
|
// output.
|
||||||
return nil
|
if err := run(logger, logsCommand, process); err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
func (m *processManager) shutdown(p Process) error {
|
|
||||||
const op string = "pm2.shutdown"
|
|
||||||
if m.heuristicState != runningState {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := m.pm2(p, "stop"); err != nil {
|
if err := resolver(); err != nil {
|
||||||
m.heuristicState = errorState
|
return xerror.New(op, err)
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
}
|
||||||
m.heuristicState = stoppedState
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *processManager) pm2(p Process, cmdName string) error {
|
func stop(logger xlog.Logger, process Process) error {
|
||||||
const op string = "pm2.pm2"
|
const op string = "pm2.stop"
|
||||||
cmdArgs := []string{
|
if err := run(logger, stopCommand, process); err != nil {
|
||||||
cmdName,
|
return xerror.New(op, err)
|
||||||
p.name(),
|
}
|
||||||
}
|
return nil
|
||||||
if cmdName == "start" {
|
}
|
||||||
cmdArgs = append(cmdArgs, "--interpreter=none", "--")
|
|
||||||
cmdArgs = append(cmdArgs, p.args()...)
|
func run(logger xlog.Logger, pm2Cmd pm2Command, process Process) error {
|
||||||
}
|
const op string = "pm2.run"
|
||||||
cmd := exec.Command(
|
resolver := func() error {
|
||||||
"pm2",
|
args := []string{
|
||||||
cmdArgs...,
|
string(pm2Cmd),
|
||||||
)
|
process.binary(),
|
||||||
m.logger.DebugfOp(op, "executing command: %s", strings.Join(cmd.Args, " "))
|
}
|
||||||
processStdOut, err := cmd.StdoutPipe()
|
if pm2Cmd == startCommand {
|
||||||
if err != nil {
|
args = append(args, "--interpreter=none", "--")
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
args = append(args, process.args()...)
|
||||||
}
|
}
|
||||||
processStdErr, err := cmd.StderrPipe()
|
cmd, err := xexec.Command(logger, "pm2", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
readFromPipe := func(outputType string, reader io.ReadCloser) {
|
xexec.LogBeforeExecute(logger, cmd)
|
||||||
readFromPipeOp := fmt.Sprintf("pm2.%s.%s", p.name(), outputType)
|
return cmd.Start()
|
||||||
r := bufio.NewReader(reader)
|
}
|
||||||
defer reader.Close() // nolint: errcheck
|
if err := resolver(); err != nil {
|
||||||
for {
|
return xerror.New(op, err)
|
||||||
line, _, err := r.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
if err != io.EOF {
|
|
||||||
m.logger.ErrorOp(readFromPipeOp, err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if len(line) != 0 {
|
|
||||||
m.logger.DebugfOp(readFromPipeOp, string(line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go readFromPipe("stdout", processStdOut)
|
|
||||||
go readFromPipe("stderr", processStdErr)
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,73 +3,75 @@ package pm2
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const unoconvWarmupTime time.Duration = 5 * time.Second
|
type unoconvProcess struct {
|
||||||
|
logger xlog.Logger
|
||||||
type unoconv struct {
|
|
||||||
manager *processManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUnoconv returns a unoconv listener
|
// NewUnoconvProcess returns a unoconv listener
|
||||||
// process.
|
// process.
|
||||||
func NewUnoconv(logger *logger.Logger) Process {
|
func NewUnoconvProcess(logger xlog.Logger) Process {
|
||||||
return &unoconv{
|
return unoconvProcess{
|
||||||
manager: &processManager{logger: logger},
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) Fullname() string {
|
func (p unoconvProcess) Fullname() string {
|
||||||
return "unoconv listener"
|
return "unoconv listener"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) Start() error {
|
func (p unoconvProcess) Start() error {
|
||||||
const op string = "pm2.unoconv.Start"
|
const op string = "pm2.unoconvProcess.Start"
|
||||||
if err := p.manager.start(p); err != nil {
|
if err := start(p.logger, p); err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return xerror.New(op, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) Shutdown() error {
|
func (p unoconvProcess) IsViable() bool {
|
||||||
const op string = "pm2.unoconv.Shutdown"
|
|
||||||
if err := p.manager.shutdown(p); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) args() []string {
|
|
||||||
return []string{
|
|
||||||
"--listener",
|
|
||||||
"--verbose",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) name() string {
|
|
||||||
return "unoconv"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *unoconv) viable() bool {
|
|
||||||
// TODO find a way to check if
|
// TODO find a way to check if
|
||||||
// the unoconv listener
|
// the unoconv listener
|
||||||
// is correctly started?
|
// is correctly started?
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *unoconv) warmup() {
|
func (p unoconvProcess) Stop() error {
|
||||||
const op string = "pm2.unoconv.warmup"
|
const op string = "pm2.unoconvProcess.Stop"
|
||||||
p.manager.logger.DebugfOp(
|
if err := stop(p.logger, p); err != nil {
|
||||||
op,
|
return xerror.New(op, err)
|
||||||
"allowing %v to startup",
|
}
|
||||||
unoconvWarmupTime,
|
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
|
||||||
)
|
)
|
||||||
time.Sleep(unoconvWarmupTime)
|
p.logger.DebugfOp(
|
||||||
|
op,
|
||||||
|
"waiting '%v' for allowing '%s' to warmup",
|
||||||
|
warmupTime,
|
||||||
|
p.Fullname(),
|
||||||
|
)
|
||||||
|
time.Sleep(warmupTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(unoconv))
|
_ = Process(new(unoconvProcess))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package pm2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnoconvStart(t *testing.T) {
|
|
||||||
p := NewUnoconv(test.CreateTestLogger())
|
|
||||||
err := p.Start()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconvShutdown(t *testing.T) {
|
|
||||||
p := NewUnoconv(test.CreateTestLogger())
|
|
||||||
err := p.Shutdown()
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -12,19 +12,22 @@ import (
|
|||||||
"github.com/mafredri/cdp/protocol/page"
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
"github.com/mafredri/cdp/protocol/target"
|
"github.com/mafredri/cdp/protocol/target"
|
||||||
"github.com/mafredri/cdp/rpcc"
|
"github.com/mafredri/cdp/rpcc"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/timeout"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type chrome struct {
|
type chromePrinter struct {
|
||||||
url string
|
logger xlog.Logger
|
||||||
opts *ChromeOptions
|
url string
|
||||||
|
opts ChromePrinterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChromeOptions helps customizing the
|
// ChromePrinterOptions helps customizing the
|
||||||
// Google Chrome printer behaviour.
|
// Google Chrome printer behaviour.
|
||||||
type ChromeOptions struct {
|
type ChromePrinterOptions struct {
|
||||||
WaitTimeout float64
|
WaitTimeout float64
|
||||||
WaitDelay float64
|
WaitDelay float64
|
||||||
HeaderHTML string
|
HeaderHTML string
|
||||||
@@ -38,26 +41,27 @@ type ChromeOptions struct {
|
|||||||
Landscape bool
|
Landscape bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) Print(destination string) error {
|
func (p chromePrinter) Print(destination string) error {
|
||||||
const op string = "printer.chrome.Print"
|
const op string = "printer.chromePrinter.Print"
|
||||||
ctx, cancel := timeout.Context(p.opts.WaitTimeout + p.opts.WaitDelay)
|
logOptions(p.logger, p.opts)
|
||||||
|
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout+p.opts.WaitDelay)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
resolver := func() error {
|
resolver := func() error {
|
||||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
||||||
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
defer devtConn.Close() // nolint: errcheck
|
defer devtConn.Close() // nolint: errcheck
|
||||||
// create a new CDP Client that uses conn.
|
// create a new CDP Client that uses conn.
|
||||||
devtClient := cdp.NewClient(devtConn)
|
devtClient := cdp.NewClient(devtConn)
|
||||||
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
// create a new blank target with the new browser context.
|
// create a new blank target with the new browser context.
|
||||||
createTargetArgs := target.
|
createTargetArgs := target.
|
||||||
@@ -65,13 +69,13 @@ func (p *chrome) Print(destination string) error {
|
|||||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
SetBrowserContextID(newContextTarget.BrowserContextID)
|
||||||
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
// connect the client to the new target.
|
// connect the client to the new target.
|
||||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
||||||
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
defer newContextConn.Close() // nolint: errcheck
|
defer newContextConn.Close() // nolint: errcheck
|
||||||
// create a new CDP Client that uses newContextConn.
|
// create a new CDP Client that uses newContextConn.
|
||||||
@@ -86,10 +90,10 @@ func (p *chrome) Print(destination string) error {
|
|||||||
func() error { return targetClient.Page.Enable(ctx) },
|
func() error { return targetClient.Page.Enable(ctx) },
|
||||||
func() error { return targetClient.Runtime.Enable(ctx) },
|
func() error { return targetClient.Runtime.Enable(ctx) },
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
if err := p.navigate(ctx, targetClient); err != nil {
|
if err := p.navigate(ctx, targetClient); err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
print, err := targetClient.Page.PrintToPDF(
|
print, err := targetClient.Page.PrintToPDF(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -107,58 +111,67 @@ func (p *chrome) Print(destination string) error {
|
|||||||
SetPrintBackground(true),
|
SetPrintBackground(true),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := resolver(); err != nil {
|
if err := resolver(); err != nil {
|
||||||
return timeout.Err(ctx, err)
|
return xcontext.MustHandleError(
|
||||||
|
ctx,
|
||||||
|
xerror.New(op, err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error {
|
func (p chromePrinter) navigate(ctx context.Context, client *cdp.Client) error {
|
||||||
const op string = "printer.chrome.navigate"
|
const op string = "printer.chromePrinter.navigate"
|
||||||
// make sure Page events are enabled.
|
resolver := func() error {
|
||||||
if err := client.Page.Enable(ctx); err != nil {
|
// make sure Page events are enabled.
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
if err := client.Page.Enable(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// make sure Network events are enabled.
|
||||||
|
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// create all clients for events.
|
||||||
|
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer domContentEventFired.Close() // nolint: errcheck
|
||||||
|
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadEventFired.Close() // nolint: errcheck
|
||||||
|
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadingFinished.Close() // nolint: errcheck
|
||||||
|
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runBatch(
|
||||||
|
// wait for all events.
|
||||||
|
func() error { _, err := domContentEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadingFinished.Recv(); return err },
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// wait for a given amount of time (useful for javascript delay).
|
||||||
|
time.Sleep(xtime.Duration(p.opts.WaitDelay))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
// make sure Network events are enabled.
|
if err := resolver(); err != nil {
|
||||||
if err := client.Network.Enable(ctx, nil); err != nil {
|
return xerror.New(op, err)
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
}
|
||||||
// create all clients for events.
|
|
||||||
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
defer domContentEventFired.Close() // nolint: errcheck
|
|
||||||
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
defer loadEventFired.Close() // nolint: errcheck
|
|
||||||
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
defer loadingFinished.Close() // nolint: errcheck
|
|
||||||
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: 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 &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
// wait for a given amount of time (useful for javascript delay).
|
|
||||||
time.Sleep(timeout.Duration(p.opts.WaitDelay))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,5 +187,5 @@ func runBatch(fn ...func() error) error {
|
|||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(chrome))
|
_ = Printer(new(chromePrinter))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/*
|
// Package printer helps converting
|
||||||
Package printer contains structs which convert
|
// a specific file type to PDF.
|
||||||
a specific file type to PDF.
|
|
||||||
*/
|
|
||||||
package printer
|
package printer
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHTML returns an HTML printer.
|
// NewHTMLPrinter returns a Printer which
|
||||||
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
// is able to convert an HTML file to PDF.
|
||||||
|
func NewHTMLPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) Printer {
|
||||||
URL := fmt.Sprintf("file://%s", fpath)
|
URL := fmt.Sprintf("file://%s", fpath)
|
||||||
return &chrome{
|
return chromePrinter{
|
||||||
url: URL,
|
logger: logger,
|
||||||
opts: opts,
|
url: URL,
|
||||||
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,37 +7,46 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/labstack/gommon/random"
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/russross/blackfriday/v2"
|
"github.com/russross/blackfriday/v2"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMarkdown returns a Markdown printer.
|
// NewMarkdownPrinter returns a Printer which
|
||||||
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
// is able to convert Markdown files to PDF.
|
||||||
const op string = "printer.NewMarkdown"
|
func NewMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) (Printer, error) {
|
||||||
tmpl, err := template.
|
const op string = "printer.NewMarkdownPrinter"
|
||||||
New(filepath.Base(fpath)).
|
resolver := func() (string, error) {
|
||||||
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
tmpl, err := template.
|
||||||
ParseFiles(fpath)
|
New(filepath.Base(fpath)).
|
||||||
|
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||||
|
ParseFiles(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dirPath := filepath.Dir(fpath)
|
||||||
|
data := &templateData{DirPath: dirPath}
|
||||||
|
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)
|
||||||
|
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("file://%s", dst), nil
|
||||||
|
}
|
||||||
|
URL, err := resolver()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
return chromePrinter{}, xerror.New(op, err)
|
||||||
}
|
}
|
||||||
dirPath := filepath.Dir(fpath)
|
return chromePrinter{
|
||||||
data := &templateData{DirPath: dirPath}
|
logger: logger,
|
||||||
var buffer bytes.Buffer
|
url: URL,
|
||||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
opts: opts,
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
baseFilename := random.String(32)
|
|
||||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
|
||||||
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
|
||||||
return nil, &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
URL := fmt.Sprintf("file://%s", dst)
|
|
||||||
return &chrome{
|
|
||||||
url: URL,
|
|
||||||
opts: opts,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +59,7 @@ func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
|||||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||||
b, err := ioutil.ReadFile(fpath)
|
b, err := ioutil.ReadFile(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", &standarderror.Error{Op: op, Err: err}
|
return "", xerror.New(op, err)
|
||||||
}
|
}
|
||||||
unsafe := blackfriday.Run(b)
|
unsafe := blackfriday.Run(b)
|
||||||
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||||
|
|||||||
@@ -2,57 +2,71 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/timeout"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type merge struct {
|
type mergePrinter struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
logger xlog.Logger
|
||||||
fpaths []string
|
fpaths []string
|
||||||
opts *MergeOptions
|
opts MergePrinterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeOptions helps customizing the
|
// MergePrinterOptions helps customizing the
|
||||||
// merge printer behaviour.
|
// merge Printer behaviour.
|
||||||
type MergeOptions struct {
|
type MergePrinterOptions struct {
|
||||||
WaitTimeout float64
|
WaitTimeout float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMerge returns a merge printer.
|
// NewMergePrinter returns a Printer which
|
||||||
func NewMerge(fpaths []string, opts *MergeOptions) Printer {
|
// is able to merge PDFs.
|
||||||
return &merge{
|
func NewMergePrinter(logger xlog.Logger, fpaths []string, opts MergePrinterOptions) Printer {
|
||||||
|
return mergePrinter{
|
||||||
|
logger: logger,
|
||||||
fpaths: fpaths,
|
fpaths: fpaths,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *merge) Print(destination string) error {
|
func (p mergePrinter) Print(destination string) error {
|
||||||
const op string = "printer.merge.Print"
|
const op string = "printer.mergePrinter.Print"
|
||||||
|
logOptions(p.logger, p.opts)
|
||||||
|
/*
|
||||||
|
context.Context may be providen from
|
||||||
|
an officePrinter which needs to merge
|
||||||
|
its result files.
|
||||||
|
*/
|
||||||
if p.ctx == nil {
|
if p.ctx == nil {
|
||||||
ctx, cancel := timeout.Context(p.opts.WaitTimeout)
|
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
p.ctx = ctx
|
p.ctx = ctx
|
||||||
}
|
}
|
||||||
|
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
|
||||||
resolver := func() error {
|
resolver := func() error {
|
||||||
var cmdArgs []string
|
var args []string
|
||||||
cmdArgs = append(cmdArgs, p.fpaths...)
|
args = append(args, p.fpaths...)
|
||||||
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
args = append(args, "cat", "output", destination)
|
||||||
cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...)
|
cmd, err := xexec.CommandContext(p.ctx, p.logger, "pdftk", args...)
|
||||||
_, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
xexec.LogBeforeExecute(p.logger, cmd)
|
||||||
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
if err := resolver(); err != nil {
|
if err := resolver(); err != nil {
|
||||||
return timeout.Err(p.ctx, err)
|
return xcontext.MustHandleError(
|
||||||
|
p.ctx,
|
||||||
|
xerror.New(op, err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(merge))
|
_ = Printer(new(mergePrinter))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,67 +4,75 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/labstack/gommon/random"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/timeout"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||||
)
|
)
|
||||||
|
|
||||||
type office struct {
|
type officePrinter struct {
|
||||||
|
logger xlog.Logger
|
||||||
fpaths []string
|
fpaths []string
|
||||||
opts *OfficeOptions
|
opts OfficePrinterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// OfficeOptions helps customizing the
|
// OfficePrinterOptions helps customizing the
|
||||||
// Office printer behaviour.
|
// Office Printer behaviour.
|
||||||
type OfficeOptions struct {
|
type OfficePrinterOptions struct {
|
||||||
WaitTimeout float64
|
WaitTimeout float64
|
||||||
Landscape bool
|
Landscape bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOffice returns an Office printer.
|
// NewOfficePrinter returns a Printer which
|
||||||
func NewOffice(fpaths []string, opts *OfficeOptions) Printer {
|
// is able to convert Office documents to PDF.
|
||||||
return &office{
|
func NewOfficePrinter(logger xlog.Logger, fpaths []string, opts OfficePrinterOptions) Printer {
|
||||||
|
return officePrinter{
|
||||||
|
logger: logger,
|
||||||
fpaths: fpaths,
|
fpaths: fpaths,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *office) Print(destination string) error {
|
func (p officePrinter) Print(destination string) error {
|
||||||
const op string = "printer.office.Print"
|
const op string = "printer.officePrinter.Print"
|
||||||
ctx, cancel := timeout.Context(p.opts.WaitTimeout)
|
logOptions(p.logger, p.opts)
|
||||||
|
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
fpaths := make([]string, len(p.fpaths))
|
|
||||||
resolver := func() error {
|
resolver := func() error {
|
||||||
|
fpaths := make([]string, len(p.fpaths))
|
||||||
dirPath := filepath.Dir(destination)
|
dirPath := filepath.Dir(destination)
|
||||||
for i, fpath := range p.fpaths {
|
for i, fpath := range p.fpaths {
|
||||||
baseFilename := random.String(32)
|
baseFilename := xrand.Get()
|
||||||
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
||||||
if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
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
|
fpaths[i] = tmpDest
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
m := mergePrinter{
|
||||||
|
ctx: ctx,
|
||||||
|
fpaths: fpaths,
|
||||||
|
}
|
||||||
|
return m.Print(destination)
|
||||||
}
|
}
|
||||||
if err := resolver(); err != nil {
|
if err := resolver(); err != nil {
|
||||||
return timeout.Err(ctx, err)
|
return xcontext.MustHandleError(
|
||||||
}
|
ctx,
|
||||||
if len(fpaths) == 1 {
|
xerror.New(op, err),
|
||||||
if err := os.Rename(fpaths[0], destination); err != nil {
|
)
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m := &merge{
|
|
||||||
ctx: ctx,
|
|
||||||
fpaths: fpaths,
|
|
||||||
}
|
|
||||||
if err := m.Print(destination); err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -72,31 +80,41 @@ func (p *office) Print(destination string) error {
|
|||||||
// nolint: gochecknoglobals
|
// nolint: gochecknoglobals
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
|
|
||||||
func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error {
|
func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, opts OfficePrinterOptions) error {
|
||||||
const op string = "printer.unoconv"
|
const op string = "printer.unoconv"
|
||||||
|
// TODO check if timeout while waiting for the lock.
|
||||||
|
logger.DebugOp(op, "waiting lock to be released...")
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
cmdArgs := []string{
|
logger.DebugOp(op, "lock released")
|
||||||
"--format",
|
resolver := func() error {
|
||||||
"pdf",
|
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()
|
||||||
}
|
}
|
||||||
if opts.Landscape {
|
if err := resolver(); err != nil {
|
||||||
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
return xerror.New(op, err)
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, "--output", destination, fpath)
|
|
||||||
cmd := exec.CommandContext(
|
|
||||||
ctx,
|
|
||||||
"unoconv",
|
|
||||||
cmdArgs...,
|
|
||||||
)
|
|
||||||
_, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return &standarderror.Error{Op: op, Err: err}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(office))
|
_ = Printer(new(officePrinter))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
// Printer is a type that can create a PDF file from a source.
|
// Printer is a type that can create a PDF file from a source.
|
||||||
// The source is defined in the underlying implementation.
|
// The source is defined in the underlying implementation.
|
||||||
type Printer interface {
|
type Printer interface {
|
||||||
Print(destination string) error
|
Print(destination string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logOptions(logger xlog.Logger, opts interface{}) {
|
||||||
|
const op string = "printer.logOptions"
|
||||||
|
logger.DebugfOp(op, "options: %+v", opts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
// NewURL returns a URL printer.
|
import (
|
||||||
func NewURL(url string, opts *ChromeOptions) Printer {
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||||
return &chrome{
|
)
|
||||||
url: url,
|
|
||||||
opts: opts,
|
// NewURLPrinter returns a Printer which
|
||||||
|
// is able to convert a URL to PDF.
|
||||||
|
func NewURLPrinter(logger xlog.Logger, url string, opts ChromePrinterOptions) Printer {
|
||||||
|
return chromePrinter{
|
||||||
|
logger: logger,
|
||||||
|
url: url,
|
||||||
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package random helps generating
|
|
||||||
// a random string.
|
|
||||||
package random
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/*
|
|
||||||
Package standarderror helps standardizing
|
|
||||||
the errors in the application.
|
|
||||||
|
|
||||||
Credits: https://middlemost.com/failure-is-your-domain/
|
|
||||||
*/
|
|
||||||
package standarderror
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package standarderror
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Internal is a code
|
|
||||||
// for internal errors.
|
|
||||||
Internal = "internal"
|
|
||||||
// Invalid is a code
|
|
||||||
// for validation errors.
|
|
||||||
Invalid = "invalid"
|
|
||||||
// Timeout is a code
|
|
||||||
// for timeout errors.
|
|
||||||
Timeout = "timeout"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error defines a standard application
|
|
||||||
// error.
|
|
||||||
type Error struct {
|
|
||||||
// Code is a machine-readable
|
|
||||||
// error code.
|
|
||||||
Code string
|
|
||||||
// Message is a human-readable
|
|
||||||
// message.
|
|
||||||
Message string
|
|
||||||
// Op is a logical operation.
|
|
||||||
Op string
|
|
||||||
// Err is a nested error.
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the string representation of the error message.
|
|
||||||
func (err *Error) Error() string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
// if wrapping an error, print its Error() message.
|
|
||||||
// Otherwise print the error code & message.
|
|
||||||
if err.Err != nil {
|
|
||||||
buf.WriteString(err.Err.Error())
|
|
||||||
} else {
|
|
||||||
if err.Code != "" {
|
|
||||||
fmt.Fprintf(&buf, "<%s> ", err.Code)
|
|
||||||
}
|
|
||||||
buf.WriteString(err.Message)
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Code returns the code of the root error, if available.
|
|
||||||
// Otherwise returns Internal.
|
|
||||||
func Code(err error) string {
|
|
||||||
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 Internal
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
if e.Op != "" {
|
|
||||||
fmt.Fprintf(&buf, "%s", e.Op)
|
|
||||||
}
|
|
||||||
if nestedOp := Op(e.Err); nestedOp != "" {
|
|
||||||
fmt.Fprintf(&buf, ": %s", nestedOp)
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = error(new(Error))
|
|
||||||
)
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package standarderror
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func scenario1() error {
|
|
||||||
rootErr := errors.New("root error")
|
|
||||||
nestedErr := &Error{
|
|
||||||
Code: Invalid,
|
|
||||||
Op: "bar",
|
|
||||||
Message: "nested error",
|
|
||||||
Err: rootErr,
|
|
||||||
}
|
|
||||||
err := &Error{
|
|
||||||
Op: "foo",
|
|
||||||
Err: nestedErr,
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func scenario2() error {
|
|
||||||
nestedErr := &Error{
|
|
||||||
Code: Invalid,
|
|
||||||
Op: "bar",
|
|
||||||
Message: "nested error",
|
|
||||||
}
|
|
||||||
err := &Error{
|
|
||||||
Code: Internal,
|
|
||||||
Op: "foo",
|
|
||||||
Err: nestedErr,
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestError(t *testing.T) {
|
|
||||||
err := scenario1()
|
|
||||||
assert.Equal(t, "root error", err.Error())
|
|
||||||
err = scenario2()
|
|
||||||
assert.Equal(t, "<invalid> nested error", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCode(t *testing.T) {
|
|
||||||
assert.Equal(t, "", Code(nil))
|
|
||||||
err := scenario1()
|
|
||||||
assert.Equal(t, Invalid, Code(err))
|
|
||||||
err = scenario2()
|
|
||||||
assert.Equal(t, Internal, Code(err))
|
|
||||||
err = errors.New("some error")
|
|
||||||
assert.Equal(t, Internal, Code(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessage(t *testing.T) {
|
|
||||||
assert.Equal(t, "", Message(nil))
|
|
||||||
err := scenario1()
|
|
||||||
assert.Equal(t, "nested error", Message(err))
|
|
||||||
err = errors.New("some error")
|
|
||||||
assert.Equal(t, defaultMessage, Message(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOp(t *testing.T) {
|
|
||||||
assert.Equal(t, "", Op(nil))
|
|
||||||
err := scenario1()
|
|
||||||
assert.Equal(t, "foo: bar", Op(err))
|
|
||||||
err = errors.New("some error")
|
|
||||||
assert.Equal(t, "", Op(err))
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package timeout helps managing
|
|
||||||
// context with timeout.
|
|
||||||
package timeout
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package timeout
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Context creates a context with timeout for
|
|
||||||
// given second.
|
|
||||||
func Context(seconds float64) (context.Context, context.CancelFunc) {
|
|
||||||
return context.WithTimeout(context.Background(), Duration(seconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration creates a duration from seconds.
|
|
||||||
func Duration(seconds float64) time.Duration {
|
|
||||||
return time.Duration(1000*seconds) * time.Millisecond
|
|
||||||
}
|
|
||||||
|
|
||||||
// Err checks if there is an error in the given context
|
|
||||||
// and wraps the previous error inside a standarderror.Error.
|
|
||||||
func Err(ctx context.Context, previousErr error) error {
|
|
||||||
const op string = "timeout.Err"
|
|
||||||
if previousErr == nil {
|
|
||||||
panic(fmt.Sprintf("%s: previous error should not be nil", op))
|
|
||||||
}
|
|
||||||
err := ctx.Err()
|
|
||||||
if err == nil {
|
|
||||||
return previousErr
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
|
||||||
return &standarderror.Error{
|
|
||||||
Code: standarderror.Timeout,
|
|
||||||
Message: "context has timed out",
|
|
||||||
Op: op,
|
|
||||||
Err: previousErr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &standarderror.Error{
|
|
||||||
Message: "context finished with an error",
|
|
||||||
Op: op,
|
|
||||||
Err: previousErr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package timeout
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDuration(t *testing.T) {
|
|
||||||
expected := time.Duration(1500) * time.Millisecond
|
|
||||||
result := Duration(1.5)
|
|
||||||
assert.Equal(t, expected.String(), result.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErr(t *testing.T) {
|
|
||||||
previousErr := errors.New("previous error")
|
|
||||||
// should be OK.
|
|
||||||
ctx, cancel := Context(5)
|
|
||||||
defer cancel()
|
|
||||||
assert.NotNil(t, Err(ctx, previousErr))
|
|
||||||
// should timeout.
|
|
||||||
ctx, cancel = Context(0.5)
|
|
||||||
defer cancel()
|
|
||||||
time.Sleep(Duration(1))
|
|
||||||
err := Err(ctx, previousErr)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized := test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Timeout, standardized.Code)
|
|
||||||
// should failed.
|
|
||||||
ctx, cancel = Context(5)
|
|
||||||
cancel()
|
|
||||||
err = Err(ctx, previousErr)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
standardized = test.RequireStandardError(t, err)
|
|
||||||
assert.Equal(t, standarderror.Internal, standarderror.Code(err))
|
|
||||||
}
|
|
||||||
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"
|
||||||
|
"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 op string
|
||||||
|
if len(cmd.Args) >= 2 {
|
||||||
|
op = fmt.Sprintf("%s.%s.%s", cmd.Args[0], cmd.Args[1], outputType)
|
||||||
|
} else {
|
||||||
|
// len(cmd.Args) should always be >= 1.
|
||||||
|
op = fmt.Sprintf("%s.%s", cmd.Args[0], outputType)
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package random
|
package xrand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/labstack/gommon/random"
|
"github.com/labstack/gommon/random"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package random
|
package xrand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
6
internal/pkg/xtime/doc.go
Normal file
6
internal/pkg/xtime/doc.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
Package xtime helps generating
|
||||||
|
time.Duration from seconds represented
|
||||||
|
as float64.
|
||||||
|
*/
|
||||||
|
package xtime
|
||||||
10
internal/pkg/xtime/xtime.go
Normal file
10
internal/pkg/xtime/xtime.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package xtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Duration creates a time.Duration from seconds.
|
||||||
|
func Duration(seconds float64) time.Duration {
|
||||||
|
return time.Duration(1000*seconds) * time.Millisecond
|
||||||
|
}
|
||||||
14
internal/pkg/xtime/xtime_test.go
Normal file
14
internal/pkg/xtime/xtime_test.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package xtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDuration(t *testing.T) {
|
||||||
|
expected := time.Duration(1500) * time.Millisecond
|
||||||
|
result := Duration(1.5)
|
||||||
|
assert.Equal(t, expected.String(), result.String())
|
||||||
|
}
|
||||||
@@ -18,9 +18,7 @@ if [ $VERSION_LENGTH -ne 3 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg GOLANG_VERSION=${GOLANG_VERSION} \
|
|
||||||
--build-arg VERSION=${VERSION} \
|
--build-arg VERSION=${VERSION} \
|
||||||
-t thecodingmachine/gotenberg:latest \
|
-t thecodingmachine/gotenberg:latest \
|
||||||
-t thecodingmachine/gotenberg:${SEMVER[0]} \
|
-t thecodingmachine/gotenberg:${SEMVER[0]} \
|
||||||
|
|||||||
18
test/cmd/pm2/pm2.go
Normal file
18
test/cmd/pm2/pm2.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := xlogtest.DebugLogger()
|
||||||
|
process := pm2.NewChromeProcess(logger)
|
||||||
|
if err := process.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
process = pm2.NewUnoconvProcess(logger)
|
||||||
|
if err := process.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
test/doc.go
Normal file
3
test/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package test contains useful
|
||||||
|
// functions used across tests.
|
||||||
|
package test
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user