mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b49b35365 | ||
|
|
333ab7066c | ||
|
|
66dbdc09b5 | ||
|
|
79f396f6ff | ||
|
|
62ba4877ac | ||
|
|
e65a5e6d22 | ||
|
|
711b43070d | ||
|
|
b2f957d1d8 | ||
|
|
249745f256 | ||
|
|
8ff9d3bcf1 | ||
|
|
99c328c302 | ||
|
|
8a9f0a245e | ||
|
|
9daecc127a | ||
|
|
119bba4f04 | ||
|
|
bf205c579d | ||
|
|
aa57b17254 | ||
|
|
7df786c5c6 | ||
|
|
b315464e98 |
2
.github/workflows/continuous_integration.yml
vendored
2
.github/workflows/continuous_integration.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.60.3
|
||||
version: v1.61.0
|
||||
|
||||
tests:
|
||||
needs:
|
||||
|
||||
23
Makefile
23
Makefile
@@ -11,9 +11,10 @@ DOCKER_REPOSITORY=gotenberg
|
||||
GOTENBERG_VERSION=snapshot
|
||||
GOTENBERG_USER_GID=1001
|
||||
GOTENBERG_USER_UID=1001
|
||||
NOTO_COLOR_EMOJI_VERSION=v2.042 # See https://github.com/googlefonts/noto-emoji/releases.
|
||||
NOTO_COLOR_EMOJI_VERSION=v2.047 # See https://github.com/googlefonts/noto-emoji/releases.
|
||||
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||
GOLANGCI_LINT_VERSION=v1.60.3 # See https://github.com/golangci/golangci-lint/releases.
|
||||
PDFCPU_VERSION=v0.8.1 # See https://github.com/pdfcpu/pdfcpu/releases.
|
||||
GOLANGCI_LINT_VERSION=v1.61.0 # See https://github.com/golangci/golangci-lint/releases.
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the Gotenberg's Docker image
|
||||
@@ -24,12 +25,14 @@ build: ## Build the Gotenberg's Docker image
|
||||
--build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) \
|
||||
--build-arg NOTO_COLOR_EMOJI_VERSION=$(NOTO_COLOR_EMOJI_VERSION) \
|
||||
--build-arg PDFTK_VERSION=$(PDFTK_VERSION) \
|
||||
--build-arg PDFCPU_VERSION=$(PDFCPU_VERSION) \
|
||||
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||
-f build/Dockerfile .
|
||||
|
||||
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
||||
API_PORT=3000
|
||||
API_PORT_FROM_ENV=
|
||||
API_BIND_IP=
|
||||
API_START_TIMEOUT=30s
|
||||
API_TIMEOUT=30s
|
||||
API_BODY_LIMIT=
|
||||
@@ -69,6 +72,10 @@ LOG_LEVEL=info
|
||||
LOG_FORMAT=auto
|
||||
LOG_FIELDS_PREFIX=
|
||||
PDFENGINES_ENGINES=
|
||||
PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk
|
||||
PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine
|
||||
PDFENGINES_READ_METADATA_ENGINES=exiftool
|
||||
PDFENGINES_WRITE_METADATA_ENGINES=exiftool
|
||||
PDFENGINES_DISABLE_ROUTES=false
|
||||
PROMETHEUS_NAMESPACE=gotenberg
|
||||
PROMETHEUS_COLLECT_INTERVAL=1s
|
||||
@@ -95,6 +102,7 @@ run: ## Start a Gotenberg container
|
||||
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||
--api-port=$(API_PORT) \
|
||||
--api-port-from-env=$(API_PORT_FROM_ENV) \
|
||||
--api-bind-ip=$(API_BIND_IP) \
|
||||
--api-start-timeout=$(API_START_TIMEOUT) \
|
||||
--api-timeout=$(API_TIMEOUT) \
|
||||
--api-body-limit="$(API_BODY_LIMIT)" \
|
||||
@@ -102,9 +110,9 @@ run: ## Start a Gotenberg container
|
||||
--api-trace-header=$(API_TRACE_HEADER) \
|
||||
--api-enable-basic-auth=$(API_ENABLE_BASIC_AUTH) \
|
||||
--api-download-from-allow-list=$(API-DOWNLOAD-FROM-ALLOW-LIST) \
|
||||
--api-download-from-deny-list=$(API-DOWNLOAD-FROM-DENY-LIST) \
|
||||
--api-download-from-max-retry=$(API-DOWNLOAD-FROM-FROM-MAX-RETRY) \
|
||||
--api-disable-download-from=$(API-DISABLE-DOWNLOAD-FROM) \
|
||||
--api-download-from-deny-list=$(API-DOWNLOAD-FROM-DENY-LIST) \
|
||||
--api-download-from-max-retry=$(API-DOWNLOAD-FROM-FROM-MAX-RETRY) \
|
||||
--api-disable-download-from=$(API-DISABLE-DOWNLOAD-FROM) \
|
||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||
@@ -132,6 +140,10 @@ run: ## Start a Gotenberg container
|
||||
--log-format=$(LOG_FORMAT) \
|
||||
--log-fields-prefix=$(LOG_FIELDS_PREFIX) \
|
||||
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
||||
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
||||
--pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \
|
||||
--pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \
|
||||
--pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \
|
||||
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
|
||||
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
|
||||
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
||||
@@ -197,6 +209,7 @@ release: ## Build the Gotenberg's Docker image and push it to a Docker repositor
|
||||
$(GOTENBERG_USER_UID) \
|
||||
$(NOTO_COLOR_EMOJI_VERSION) \
|
||||
$(PDFTK_VERSION) \
|
||||
$(PDFCPU_VERSION) \
|
||||
$(DOCKER_REGISTRY) \
|
||||
$(DOCKER_REPOSITORY) \
|
||||
$(LINUX_AMD64_RELEASE)
|
||||
|
||||
@@ -3,10 +3,35 @@
|
||||
# stage that uses them.
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
# ----------------------------------------------
|
||||
# pdfcpu binary build stage
|
||||
# ----------------------------------------------
|
||||
# Note: this stage is required as pdfcpu does not release an armhf variant by
|
||||
# default.
|
||||
|
||||
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
|
||||
|
||||
ARG PDFCPU_VERSION
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Define the working directory outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /home
|
||||
|
||||
RUN curl -Ls "https://github.com/pdfcpu/pdfcpu/archive/refs/tags/$PDFCPU_VERSION.tar.gz" -o pdfcpu.tar.gz &&\
|
||||
tar --strip-components=1 -xvzf pdfcpu.tar.gz
|
||||
|
||||
# Install module dependencies.
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
RUN go build -o pdfcpu -ldflags "-s -w -X 'main.version=$PDFCPU_VERSION' -X 'github.com/pdfcpu/pdfcpu/pkg/pdfcpu.VersionStr=$PDFCPU_VERSION' -X main.builtBy=gotenberg" ./cmd/pdfcpu &&\
|
||||
# Verify installation.
|
||||
./pdfcpu version
|
||||
|
||||
# ----------------------------------------------
|
||||
# Gotenberg binary build stage
|
||||
# ----------------------------------------------
|
||||
FROM golang:$GOLANG_VERSION AS binary-stage
|
||||
FROM golang:$GOLANG_VERSION AS gotenberg-binary-stage
|
||||
|
||||
ARG GOTENBERG_VERSION
|
||||
ENV CGO_ENABLED=0
|
||||
@@ -187,8 +212,11 @@ RUN \
|
||||
# https://github.com/arachnys/athenapdf/commit/ba25a8d80a25d08d58865519c4cd8756dc9a336d.
|
||||
COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||
|
||||
# Copy the Gotenberg binary from the binary stage.
|
||||
COPY --from=binary-stage /home/gotenberg /usr/bin/
|
||||
# Copy the pdfcpu binary from the pdfcpu-binary-stage.
|
||||
COPY --from=pdfcpu-binary-stage /home/pdfcpu /usr/bin/
|
||||
|
||||
# Copy the Gotenberg binary from the gotenberg-binary-stage.
|
||||
COPY --from=gotenberg-binary-stage /home/gotenberg /usr/bin/
|
||||
|
||||
# Environment variables required by modules or else.
|
||||
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium
|
||||
@@ -197,6 +225,7 @@ ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
|
||||
ENV PDFTK_BIN_PATH=/usr/bin/pdftk
|
||||
ENV QPDF_BIN_PATH=/usr/bin/qpdf
|
||||
ENV EXIFTOOL_BIN_PATH=/usr/bin/exiftool
|
||||
ENV PDFCPU_BIN_PATH=/usr/bin/pdfcpu
|
||||
|
||||
USER gotenberg
|
||||
WORKDIR /home/gotenberg
|
||||
|
||||
38
go.mod
38
go.mod
@@ -4,15 +4,15 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/alexliesenfeld/health v0.8.0
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/barasher/go-exiftool v1.10.0
|
||||
github.com/chromedp/cdproto v0.0.0-20240919203636-12af5e8a671f
|
||||
github.com/chromedp/chromedp v0.10.0
|
||||
github.com/chromedp/cdproto v0.0.0-20241110205750-a72e6703cd9b
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
github.com/labstack/gommon v0.4.2
|
||||
@@ -20,21 +20,19 @@ require (
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/pdfcpu/pdfcpu v0.8.1
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/prometheus/client_golang v1.20.4
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/image v0.20.0 // indirect
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/term v0.24.0
|
||||
golang.org/x/text v0.18.0
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/sync v0.9.0
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/text v0.20.0
|
||||
)
|
||||
|
||||
require github.com/dlclark/regexp2 v1.11.4
|
||||
@@ -43,29 +41,23 @@ require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.59.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
)
|
||||
|
||||
90
go.sum
90
go.sum
@@ -1,8 +1,8 @@
|
||||
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
|
||||
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
|
||||
@@ -11,13 +11,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/cdproto v0.0.0-20240919203636-12af5e8a671f h1:dEjjp+iN34En5Pl9XIi978DmR2/CMwuOxoPWtiHixKQ=
|
||||
github.com/chromedp/cdproto v0.0.0-20240919203636-12af5e8a671f/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKab1E=
|
||||
github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/chromedp/cdproto v0.0.0-20241110205750-a72e6703cd9b h1:md1Gk5jkNE91SZxFDCMHmKqX0/GsEr1/VTejht0sCbY=
|
||||
github.com/chromedp/cdproto v0.0.0-20241110205750-a72e6703cd9b/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
|
||||
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
|
||||
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -52,24 +51,16 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
|
||||
github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||
@@ -85,8 +76,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -98,28 +87,19 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pdfcpu/pdfcpu v0.8.1 h1:AiWUb8uXlrXqJ73OmiYXBjDF0Qxt4OuM281eAfkAOMA=
|
||||
github.com/pdfcpu/pdfcpu v0.8.1/go.mod h1:M5SFotxdaw0fedxthpjbA/PADytAo6wJnGH0SSBWJ7s=
|
||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
|
||||
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
|
||||
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
|
||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@@ -140,38 +120,32 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
||||
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -29,6 +30,7 @@ func init() {
|
||||
// middlewares or health checks.
|
||||
type Api struct {
|
||||
port int
|
||||
bindIp string
|
||||
tlsCertFile string
|
||||
tlsKeyFile string
|
||||
startTimeout time.Duration
|
||||
@@ -171,11 +173,12 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
||||
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
||||
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
||||
fs.String("api-bind-ip", "", "Set the IP address the API should bind to for incoming connections")
|
||||
fs.String("api-tls-cert-file", "", "Path to the TLS/SSL certificate file - for HTTPS support")
|
||||
fs.String("api-tls-key-file", "", "Path to the TLS/SSL key file - for HTTPS support")
|
||||
fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
|
||||
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
||||
fs.String("api-body-limit", "", "Set the body limit for multipart/form-data requests")
|
||||
fs.String("api-body-limit", "", "Set the body limit for multipart/form-data requests - it accepts values like 5MB, 1GB, etc")
|
||||
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
||||
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
||||
fs.Bool("api-enable-basic-auth", false, "Enable basic authentication - will look for the GOTENBERG_API_BASIC_AUTH_USERNAME and GOTENBERG_API_BASIC_AUTH_PASSWORD environment variables")
|
||||
@@ -194,6 +197,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||
flags := ctx.ParsedFlags()
|
||||
a.port = flags.MustInt("api-port")
|
||||
a.bindIp = flags.MustString("api-bind-ip")
|
||||
a.tlsCertFile = flags.MustString("api-tls-cert-file")
|
||||
a.tlsKeyFile = flags.MustString("api-tls-key-file")
|
||||
a.startTimeout = flags.MustDuration("api-start-timeout")
|
||||
@@ -329,6 +333,10 @@ func (a *Api) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
if a.bindIp != "" && net.ParseIP(a.bindIp) == nil {
|
||||
err = multierr.Append(err, errors.New("IP must be a valid IP address"))
|
||||
}
|
||||
|
||||
if (a.tlsCertFile != "" && a.tlsKeyFile == "") || (a.tlsCertFile == "" && a.tlsKeyFile != "") {
|
||||
err = multierr.Append(err,
|
||||
errors.New("both TLS certificate and key files must be set"),
|
||||
@@ -522,11 +530,11 @@ func (a *Api) Start() error {
|
||||
var err error
|
||||
if a.tlsCertFile != "" && a.tlsKeyFile != "" {
|
||||
// Start an HTTPS server (supports HTTP/2).
|
||||
err = a.srv.StartTLS(fmt.Sprintf(":%d", a.port), a.tlsCertFile, a.tlsKeyFile)
|
||||
err = a.srv.StartTLS(fmt.Sprintf("%s:%d", a.bindIp, a.port), a.tlsCertFile, a.tlsKeyFile)
|
||||
} else {
|
||||
// Start an HTTP/2 Cleartext (non-HTTPS) server.
|
||||
server := &http2.Server{}
|
||||
err = a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server)
|
||||
err = a.srv.StartH2CServer(fmt.Sprintf("%s:%d", a.bindIp, a.port), server)
|
||||
}
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
a.logger.Fatal(err.Error())
|
||||
@@ -538,7 +546,11 @@ func (a *Api) Start() error {
|
||||
|
||||
// StartupMessage returns a custom startup message.
|
||||
func (a *Api) StartupMessage() string {
|
||||
return fmt.Sprintf("server listening on port %d", a.port)
|
||||
ip := a.bindIp
|
||||
if a.bindIp == "" {
|
||||
ip = "[::]"
|
||||
}
|
||||
return fmt.Sprintf("server started on %s:%d", ip, a.port)
|
||||
}
|
||||
|
||||
// Stop stops the HTTP server.
|
||||
|
||||
@@ -57,6 +57,30 @@ func TestApi_Provision(t *testing.T) {
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "port from env: invalid environment variable value",
|
||||
ctx: func() *gotenberg.Context {
|
||||
fs := new(Api).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: fs,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}(),
|
||||
setEnv: func() {
|
||||
err := os.Setenv("PORT", "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_USERNAME environment variable",
|
||||
ctx: func() *gotenberg.Context {
|
||||
@@ -99,30 +123,6 @@ func TestApi_Provision(t *testing.T) {
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "port from env: invalid environment variable value",
|
||||
ctx: func() *gotenberg.Context {
|
||||
fs := new(Api).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: fs,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}(),
|
||||
setEnv: func() {
|
||||
err := os.Setenv("PORT", "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no valid routers",
|
||||
ctx: func() *gotenberg.Context {
|
||||
@@ -462,6 +462,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
port int
|
||||
bindIp string
|
||||
tlsCertFile string
|
||||
tlsKeyFile string
|
||||
rootPath string
|
||||
@@ -473,6 +474,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid port (< 1)",
|
||||
port: 0,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: nil,
|
||||
@@ -482,6 +484,17 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid port (> 65535)",
|
||||
port: 65536,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: nil,
|
||||
middlewares: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid IP",
|
||||
port: 10,
|
||||
bindIp: "foo",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: nil,
|
||||
@@ -491,6 +504,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid TLS files: only cert file provided",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
tlsCertFile: "cert.pem",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
@@ -501,6 +515,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid TLS files: only key file provided",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
tlsKeyFile: "key.pem",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
@@ -511,6 +526,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid root path: missing / prefix",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "foo/",
|
||||
traceHeader: "foo",
|
||||
routes: nil,
|
||||
@@ -520,6 +536,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid root path: missing / suffix",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo",
|
||||
traceHeader: "foo",
|
||||
routes: nil,
|
||||
@@ -529,6 +546,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid trace header",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "",
|
||||
routes: nil,
|
||||
@@ -538,6 +556,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid route: empty path",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: []Route{
|
||||
@@ -551,6 +570,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid route: missing / prefix in path",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: []Route{
|
||||
@@ -564,6 +584,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid multipart route: no /forms prefix in path",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: []Route{
|
||||
@@ -578,6 +599,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid route: no method",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: []Route{
|
||||
@@ -592,6 +614,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid route: nil handler",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: []Route{
|
||||
@@ -607,6 +630,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid route: path already existing",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: []Route{
|
||||
@@ -627,6 +651,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "invalid middleware: nil handler",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: nil,
|
||||
@@ -641,6 +666,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
{
|
||||
scenario: "success",
|
||||
port: 10,
|
||||
bindIp: "127.0.0.1",
|
||||
rootPath: "/foo/",
|
||||
traceHeader: "foo",
|
||||
routes: []Route{
|
||||
@@ -694,6 +720,7 @@ func TestApi_Validate(t *testing.T) {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := Api{
|
||||
port: tc.port,
|
||||
bindIp: tc.bindIp,
|
||||
tlsCertFile: tc.tlsCertFile,
|
||||
tlsKeyFile: tc.tlsKeyFile,
|
||||
rootPath: tc.rootPath,
|
||||
@@ -918,15 +945,36 @@ func TestApi_Start(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApi_StartupMessage(t *testing.T) {
|
||||
mod := Api{
|
||||
port: 3000,
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
port int
|
||||
bindIp string
|
||||
expectMessage string
|
||||
}{
|
||||
{
|
||||
scenario: "no custom IP",
|
||||
port: 3000,
|
||||
bindIp: "",
|
||||
expectMessage: "server started on [::]:3000",
|
||||
},
|
||||
{
|
||||
scenario: "custom IP",
|
||||
port: 3000,
|
||||
bindIp: "127.0.0.1",
|
||||
expectMessage: "server started on 127.0.0.1:3000",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := Api{
|
||||
port: tc.port,
|
||||
bindIp: tc.bindIp,
|
||||
}
|
||||
|
||||
actual := mod.StartupMessage()
|
||||
expect := "server listening on port 3000"
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
actual := mod.StartupMessage()
|
||||
if actual != tc.expectMessage {
|
||||
t.Errorf("expected '%s' but got '%s'", tc.expectMessage, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -228,7 +228,6 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||
setCookiesActionFunc(logger, options.Cookies),
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||
forceExactColorsActionFunc(),
|
||||
@@ -252,7 +251,6 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
|
||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||
setCookiesActionFunc(logger, options.Cookies),
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||
forceExactColorsActionFunc(),
|
||||
@@ -291,17 +289,35 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
defer taskCancel()
|
||||
|
||||
// We validate all others requests against our allow / deny lists.
|
||||
// If a request does not pass the validation, we make it fail.
|
||||
listenForEventRequestPaused(taskCtx, logger, b.arguments.allowList, b.arguments.denyList)
|
||||
// If a request does not pass the validation, we make it fail. It also set
|
||||
// the extra HTTP headers, if any.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1011.
|
||||
listenForEventRequestPaused(taskCtx, logger, eventRequestPausedOptions{
|
||||
allowList: b.arguments.allowList,
|
||||
denyList: b.arguments.denyList,
|
||||
extraHttpHeaders: options.ExtraHttpHeaders,
|
||||
})
|
||||
|
||||
var (
|
||||
invalidHttpStatusCode error
|
||||
invalidHttpStatusCodeMu sync.RWMutex
|
||||
invalidHttpStatusCode error
|
||||
invalidHttpStatusCodeMu sync.RWMutex
|
||||
invalidResourceHttpStatusCode error
|
||||
invalidResourceHttpStatusCodeMu sync.RWMutex
|
||||
)
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||
if len(options.FailOnHttpStatusCodes) != 0 {
|
||||
listenForEventResponseReceived(taskCtx, logger, url, options.FailOnHttpStatusCodes, &invalidHttpStatusCode, &invalidHttpStatusCodeMu)
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/613.
|
||||
// https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
if len(options.FailOnHttpStatusCodes) != 0 || len(options.FailOnResourceHttpStatusCodes) != 0 {
|
||||
listenForEventResponseReceived(taskCtx, logger, eventResponseReceivedOptions{
|
||||
mainPageUrl: url,
|
||||
failOnHttpStatusCodes: options.FailOnHttpStatusCodes,
|
||||
invalidHttpStatusCode: &invalidHttpStatusCode,
|
||||
invalidHttpStatusCodeMu: &invalidHttpStatusCodeMu,
|
||||
failOnResourceOnHttpStatusCode: options.FailOnResourceHttpStatusCodes,
|
||||
invalidResourceHttpStatusCode: &invalidResourceHttpStatusCode,
|
||||
invalidResourceHttpStatusCodeMu: &invalidResourceHttpStatusCodeMu,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -315,14 +331,22 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
}
|
||||
|
||||
var (
|
||||
loadingFailed error
|
||||
loadingFailedMu sync.RWMutex
|
||||
loadingFailed error
|
||||
loadingFailedMu sync.RWMutex
|
||||
resourceLoadingFailed error
|
||||
resourceLoadingFailedMu sync.RWMutex
|
||||
)
|
||||
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/913
|
||||
// https://github.com/gotenberg/gotenberg/issues/959
|
||||
listenForEventLoadingFailed(taskCtx, logger, &loadingFailed, &loadingFailedMu)
|
||||
// https://github.com/gotenberg/gotenberg/issues/913.
|
||||
// https://github.com/gotenberg/gotenberg/issues/959.
|
||||
// https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
listenForEventLoadingFailed(taskCtx, logger, eventLoadingFailedOptions{
|
||||
loadingFailed: &loadingFailed,
|
||||
loadingFailedMu: &loadingFailedMu,
|
||||
resourceLoadingFailed: &resourceLoadingFailed,
|
||||
resourceLoadingFailedMu: &resourceLoadingFailedMu,
|
||||
})
|
||||
|
||||
err = chromedp.Run(taskCtx, tasks...)
|
||||
if err != nil {
|
||||
@@ -351,6 +375,14 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
return fmt.Errorf("%v: %w", invalidHttpStatusCode, ErrInvalidHttpStatusCode)
|
||||
}
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
invalidResourceHttpStatusCodeMu.RLock()
|
||||
defer invalidResourceHttpStatusCodeMu.RUnlock()
|
||||
|
||||
if invalidResourceHttpStatusCode != nil {
|
||||
return fmt.Errorf("%v: %w", invalidResourceHttpStatusCode, ErrInvalidResourceHttpStatusCode)
|
||||
}
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||
consoleExceptionsMu.RLock()
|
||||
defer consoleExceptionsMu.RUnlock()
|
||||
@@ -360,8 +392,8 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
}
|
||||
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/913
|
||||
// https://github.com/gotenberg/gotenberg/issues/959
|
||||
// https://github.com/gotenberg/gotenberg/issues/913.
|
||||
// https://github.com/gotenberg/gotenberg/issues/959.
|
||||
loadingFailedMu.RLock()
|
||||
defer loadingFailedMu.RUnlock()
|
||||
|
||||
@@ -369,6 +401,13 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
return fmt.Errorf("%v: %w", loadingFailed, ErrLoadingFailed)
|
||||
}
|
||||
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
if options.FailOnResourceLoadingFailed {
|
||||
if resourceLoadingFailed != nil {
|
||||
return fmt.Errorf("%v: %w", resourceLoadingFailed, ErrResourceLoadingFailed)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -446,6 +446,44 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
expectError: true,
|
||||
expectedError: ErrInvalidHttpStatusCode,
|
||||
},
|
||||
{
|
||||
scenario: "ErrInvalidResourceHttpStatusCode",
|
||||
browser: newChromiumBrowser(
|
||||
browserArguments{
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
wsUrlReadTimeout: 5 * time.Second,
|
||||
allowList: regexp2.MustCompile("", 0),
|
||||
denyList: regexp2.MustCompile("", 0),
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/style.css", fs.WorkingDirPath()), []byte("body{font-family: Arial, Helvetica, sans-serif;}"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"></head><body><h1>ErrInvalidResourceHttpStatusCode</h1></body></html>"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: PdfOptions{
|
||||
Options: Options{FailOnResourceHttpStatusCodes: []int64{200}},
|
||||
},
|
||||
noDeadline: false,
|
||||
start: true,
|
||||
expectError: true,
|
||||
expectedError: ErrInvalidResourceHttpStatusCode,
|
||||
},
|
||||
{
|
||||
scenario: "ErrConsoleExceptions",
|
||||
browser: newChromiumBrowser(
|
||||
@@ -505,6 +543,39 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
expectError: true,
|
||||
expectedError: ErrLoadingFailed,
|
||||
},
|
||||
{
|
||||
scenario: "ErrResourceLoadingFailed",
|
||||
browser: newChromiumBrowser(
|
||||
browserArguments{
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
wsUrlReadTimeout: 5 * time.Second,
|
||||
allowList: regexp2.MustCompile("", 0),
|
||||
denyList: regexp2.MustCompile("", 0),
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"http://localhost:100/style.css\"></head><body><h1>ErrResourceLoadingFailed</h1></body></html>"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: PdfOptions{
|
||||
Options: Options{FailOnResourceLoadingFailed: true},
|
||||
},
|
||||
noDeadline: false,
|
||||
start: true,
|
||||
expectError: true,
|
||||
expectedError: ErrResourceLoadingFailed,
|
||||
},
|
||||
{
|
||||
scenario: "clear cache",
|
||||
browser: newChromiumBrowser(
|
||||
@@ -702,8 +773,21 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
return fs
|
||||
}(),
|
||||
options: PdfOptions{
|
||||
Options: Options{ExtraHttpHeaders: map[string]string{
|
||||
"X-Foo": "Bar",
|
||||
Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{
|
||||
{
|
||||
Name: "X-Foo",
|
||||
Value: "foo",
|
||||
},
|
||||
{
|
||||
Name: "X-Bar",
|
||||
Value: "bar",
|
||||
Scope: regexp2.MustCompile(`.*index\.html.*`, 0),
|
||||
},
|
||||
{
|
||||
Name: "X-Baz",
|
||||
Value: "baz",
|
||||
Scope: regexp2.MustCompile(`.*another\.html.*`, 0),
|
||||
},
|
||||
}},
|
||||
},
|
||||
noDeadline: false,
|
||||
@@ -711,6 +795,10 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
expectError: false,
|
||||
expectedLogEntries: []string{
|
||||
"extra HTTP headers:",
|
||||
"extra HTTP header 'X-Foo' will be set for request URL",
|
||||
"extra HTTP header 'X-Bar' (scoped) will be set for request URL",
|
||||
"extra HTTP header 'X-Baz' (scoped) will not be set for request URL",
|
||||
"setting extra HTTP headers for request URL",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1520,6 +1608,44 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
expectError: true,
|
||||
expectedError: ErrInvalidHttpStatusCode,
|
||||
},
|
||||
{
|
||||
scenario: "ErrInvalidResourceHttpStatusCode",
|
||||
browser: newChromiumBrowser(
|
||||
browserArguments{
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
wsUrlReadTimeout: 5 * time.Second,
|
||||
allowList: regexp2.MustCompile("", 0),
|
||||
denyList: regexp2.MustCompile("", 0),
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/style.css", fs.WorkingDirPath()), []byte("body{font-family: Arial, Helvetica, sans-serif;}"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"></head><body><h1>ErrInvalidResourceHttpStatusCode</h1></body></html>"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: ScreenshotOptions{
|
||||
Options: Options{FailOnResourceHttpStatusCodes: []int64{299}},
|
||||
},
|
||||
noDeadline: false,
|
||||
start: true,
|
||||
expectError: true,
|
||||
expectedError: ErrInvalidResourceHttpStatusCode,
|
||||
},
|
||||
{
|
||||
scenario: "ErrConsoleExceptions",
|
||||
browser: newChromiumBrowser(
|
||||
@@ -1563,7 +1689,6 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
denyList: regexp2.MustCompile("", 0),
|
||||
},
|
||||
),
|
||||
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
|
||||
@@ -1580,6 +1705,39 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
expectError: true,
|
||||
expectedError: ErrLoadingFailed,
|
||||
},
|
||||
{
|
||||
scenario: "ErrResourceLoadingFailed",
|
||||
browser: newChromiumBrowser(
|
||||
browserArguments{
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
wsUrlReadTimeout: 5 * time.Second,
|
||||
allowList: regexp2.MustCompile("", 0),
|
||||
denyList: regexp2.MustCompile("", 0),
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"http://localhost:100/style.css\"></head><body><h1>ErrResourceLoadingFailed</h1></body></html>"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: ScreenshotOptions{
|
||||
Options: Options{FailOnResourceLoadingFailed: true},
|
||||
},
|
||||
noDeadline: false,
|
||||
start: true,
|
||||
expectError: true,
|
||||
expectedError: ErrResourceLoadingFailed,
|
||||
},
|
||||
{
|
||||
scenario: "clear cache",
|
||||
browser: newChromiumBrowser(
|
||||
@@ -1716,6 +1874,41 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
"set cookie",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "user agent override",
|
||||
browser: newChromiumBrowser(
|
||||
browserArguments{
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
wsUrlReadTimeout: 5 * time.Second,
|
||||
allowList: regexp2.MustCompile("", 0),
|
||||
denyList: regexp2.MustCompile("", 0),
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("<h1>User-Agent override</h1>"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: ScreenshotOptions{
|
||||
Options: Options{UserAgent: "foo"},
|
||||
},
|
||||
noDeadline: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
expectedLogEntries: []string{
|
||||
fmt.Sprintf("user agent override: foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "extra HTTP headers",
|
||||
browser: newChromiumBrowser(
|
||||
@@ -1742,8 +1935,21 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
return fs
|
||||
}(),
|
||||
options: ScreenshotOptions{
|
||||
Options: Options{ExtraHttpHeaders: map[string]string{
|
||||
"X-Foo": "Bar",
|
||||
Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{
|
||||
{
|
||||
Name: "X-Foo",
|
||||
Value: "foo",
|
||||
},
|
||||
{
|
||||
Name: "X-Bar",
|
||||
Value: "bar",
|
||||
Scope: regexp2.MustCompile(`.*index\.html.*`, 0),
|
||||
},
|
||||
{
|
||||
Name: "X-Baz",
|
||||
Value: "baz",
|
||||
Scope: regexp2.MustCompile(`.*another\.html.*`, 0),
|
||||
},
|
||||
}},
|
||||
},
|
||||
noDeadline: false,
|
||||
@@ -1751,6 +1957,10 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
expectError: false,
|
||||
expectedLogEntries: []string{
|
||||
"extra HTTP headers:",
|
||||
"extra HTTP header 'X-Foo' will be set for request URL",
|
||||
"extra HTTP header 'X-Bar' (scoped) will be set for request URL",
|
||||
"extra HTTP header 'X-Baz' (scoped) will not be set for request URL",
|
||||
"setting extra HTTP headers for request URL",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/dlclark/regexp2"
|
||||
flag "github.com/spf13/pflag"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -37,14 +38,22 @@ var (
|
||||
// matches with one of the entry in [Options.FailOnHttpStatusCodes].
|
||||
ErrInvalidHttpStatusCode = errors.New("invalid HTTP status code")
|
||||
|
||||
// ErrInvalidResourceHttpStatusCode happens when the status code from one
|
||||
// or more resources matches with one of the entry in
|
||||
// [Options.FailOnResourceHttpStatusCodes].
|
||||
ErrInvalidResourceHttpStatusCode = errors.New("invalid resource HTTP status code")
|
||||
|
||||
// ErrConsoleExceptions happens when there are exceptions in the Chromium
|
||||
// console. It also happens only if the [Options.FailOnConsoleExceptions]
|
||||
// is set to true.
|
||||
ErrConsoleExceptions = errors.New("console exceptions")
|
||||
|
||||
// ErrLoadingFailed happens when a URL failed to load.
|
||||
// ErrLoadingFailed happens when the main page failed to load.
|
||||
ErrLoadingFailed = errors.New("loading failed")
|
||||
|
||||
// ErrResourceLoadingFailed happens when one or more resources failed to load.
|
||||
ErrResourceLoadingFailed = errors.New("resource loading failed")
|
||||
|
||||
// PDF specific.
|
||||
|
||||
// ErrOmitBackgroundWithoutPrintBackground happens if
|
||||
@@ -85,6 +94,14 @@ type Options struct {
|
||||
// code from the main page matches with one of its entries.
|
||||
FailOnHttpStatusCodes []int64
|
||||
|
||||
// FailOnResourceHttpStatusCodes sets if the conversion should fail if the
|
||||
// status code from at least one resource matches with one if its entries.
|
||||
FailOnResourceHttpStatusCodes []int64
|
||||
|
||||
// FailOnResourceLoadingFailed sets if the conversion should fail like the
|
||||
// main page if Chromium fails to load at least one resource.
|
||||
FailOnResourceLoadingFailed bool
|
||||
|
||||
// FailOnConsoleExceptions sets if the conversion should fail if there are
|
||||
// exceptions in the Chromium console.
|
||||
FailOnConsoleExceptions bool
|
||||
@@ -109,7 +126,7 @@ type Options struct {
|
||||
|
||||
// ExtraHttpHeaders are extra HTTP headers to send by Chromium while
|
||||
// loading he HTML document.
|
||||
ExtraHttpHeaders map[string]string
|
||||
ExtraHttpHeaders []ExtraHttpHeader
|
||||
|
||||
// EmulatedMediaType is the media type to emulate, either "screen" or
|
||||
// "print".
|
||||
@@ -123,17 +140,19 @@ type Options struct {
|
||||
// DefaultOptions returns the default values for Options.
|
||||
func DefaultOptions() Options {
|
||||
return Options{
|
||||
SkipNetworkIdleEvent: true,
|
||||
FailOnHttpStatusCodes: []int64{499, 599},
|
||||
FailOnConsoleExceptions: false,
|
||||
WaitDelay: 0,
|
||||
WaitWindowStatus: "",
|
||||
WaitForExpression: "",
|
||||
Cookies: nil,
|
||||
UserAgent: "",
|
||||
ExtraHttpHeaders: nil,
|
||||
EmulatedMediaType: "",
|
||||
OmitBackground: false,
|
||||
SkipNetworkIdleEvent: true,
|
||||
FailOnHttpStatusCodes: []int64{499, 599},
|
||||
FailOnResourceHttpStatusCodes: nil,
|
||||
FailOnResourceLoadingFailed: false,
|
||||
FailOnConsoleExceptions: false,
|
||||
WaitDelay: 0,
|
||||
WaitWindowStatus: "",
|
||||
WaitForExpression: "",
|
||||
Cookies: nil,
|
||||
UserAgent: "",
|
||||
ExtraHttpHeaders: nil,
|
||||
EmulatedMediaType: "",
|
||||
OmitBackground: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,26 +213,31 @@ type PdfOptions struct {
|
||||
// PreferCssPageSize defines whether to prefer page size as defined by CSS.
|
||||
// If false, the content will be scaled to fit the paper size.
|
||||
PreferCssPageSize bool
|
||||
|
||||
// GenerateDocumentOutline defines whether the document outline should be
|
||||
// embedded into the PDF.
|
||||
GenerateDocumentOutline bool
|
||||
}
|
||||
|
||||
// DefaultPdfOptions returns the default values for PdfOptions.
|
||||
func DefaultPdfOptions() PdfOptions {
|
||||
return PdfOptions{
|
||||
Options: DefaultOptions(),
|
||||
Landscape: false,
|
||||
PrintBackground: false,
|
||||
Scale: 1.0,
|
||||
SinglePage: false,
|
||||
PaperWidth: 8.5,
|
||||
PaperHeight: 11,
|
||||
MarginTop: 0.39,
|
||||
MarginBottom: 0.39,
|
||||
MarginLeft: 0.39,
|
||||
MarginRight: 0.39,
|
||||
PageRanges: "",
|
||||
HeaderTemplate: "<html><head></head><body></body></html>",
|
||||
FooterTemplate: "<html><head></head><body></body></html>",
|
||||
PreferCssPageSize: false,
|
||||
Options: DefaultOptions(),
|
||||
Landscape: false,
|
||||
PrintBackground: false,
|
||||
Scale: 1.0,
|
||||
SinglePage: false,
|
||||
PaperWidth: 8.5,
|
||||
PaperHeight: 11,
|
||||
MarginTop: 0.39,
|
||||
MarginBottom: 0.39,
|
||||
MarginLeft: 0.39,
|
||||
MarginRight: 0.39,
|
||||
PageRanges: "",
|
||||
HeaderTemplate: "<html><head></head><body></body></html>",
|
||||
FooterTemplate: "<html><head></head><body></body></html>",
|
||||
PreferCssPageSize: false,
|
||||
GenerateDocumentOutline: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +313,22 @@ type Cookie struct {
|
||||
SameSite network.CookieSameSite `json:"sameSite,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraHttpHeader are extra HTTP headers to send by Chromium.
|
||||
type ExtraHttpHeader struct {
|
||||
// Name is the header name.
|
||||
// Required.
|
||||
Name string
|
||||
|
||||
// Value is the header value.
|
||||
// Required.
|
||||
Value string
|
||||
|
||||
// Scope is the header scope. If nil, the header will be applied to ALL
|
||||
// requests from the page.
|
||||
// Optional.
|
||||
Scope *regexp2.Regexp
|
||||
}
|
||||
|
||||
// Api helps to interact with Chromium for converting HTML documents to PDF.
|
||||
type Api interface {
|
||||
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||
|
||||
@@ -3,6 +3,7 @@ package chromium
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
@@ -20,10 +21,22 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
type eventRequestPausedOptions struct {
|
||||
allowList, denyList *regexp2.Regexp
|
||||
extraHttpHeaders []ExtraHttpHeader
|
||||
}
|
||||
|
||||
// listenForEventRequestPaused listens for requests to check if they are
|
||||
// allowed or not.network.SetBlockedURLS()
|
||||
// allowed or not. It also set the extra HTTP headers, if any.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1011.
|
||||
// TODO: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-setBlockedURLs (experimental for now).
|
||||
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp2.Regexp, denyList *regexp2.Regexp) {
|
||||
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, options eventRequestPausedOptions) {
|
||||
if len(options.extraHttpHeaders) == 0 {
|
||||
logger.Debug("no extra HTTP headers")
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", options.extraHttpHeaders))
|
||||
}
|
||||
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
switch e := ev.(type) {
|
||||
case *fetch.EventRequestPaused:
|
||||
@@ -37,7 +50,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
||||
return
|
||||
}
|
||||
|
||||
err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline)
|
||||
err := gotenberg.FilterDeadline(options.allowList, options.denyList, e.Request.URL, deadline)
|
||||
if err != nil {
|
||||
logger.Warn(err.Error())
|
||||
allow = false
|
||||
@@ -46,33 +59,114 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
||||
cctx := chromedp.FromContext(ctx)
|
||||
executorCtx := cdp.WithExecutor(ctx, cctx.Target)
|
||||
|
||||
if allow {
|
||||
req := fetch.ContinueRequest(e.RequestID)
|
||||
if !allow {
|
||||
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
|
||||
err = req.Do(executorCtx)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("continue request: %s", err))
|
||||
logger.Error(fmt.Sprintf("fail request: %s", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
|
||||
req := fetch.ContinueRequest(e.RequestID)
|
||||
|
||||
var extraHttpHeadersToSet []ExtraHttpHeader
|
||||
if len(options.extraHttpHeaders) > 0 {
|
||||
// The user want to set extra HTTP headers.
|
||||
|
||||
// First, we have to check if at least one header has to be
|
||||
// set for current request.
|
||||
for _, header := range options.extraHttpHeaders {
|
||||
if header.Scope == nil {
|
||||
// Non-scoped header.
|
||||
logger.Debug(fmt.Sprintf("extra HTTP header '%s' will be set for request URL '%s'", header.Name, e.Request.URL))
|
||||
extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
|
||||
continue
|
||||
}
|
||||
|
||||
ok, err := header.Scope.MatchString(e.Request.URL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("fail to match extra HTTP header '%s' scope with URL '%s': %s", header.Name, e.Request.URL, err))
|
||||
} else if ok {
|
||||
logger.Debug(fmt.Sprintf("extra HTTP header '%s' (scoped) will be set for request URL '%s'", header.Name, e.Request.URL))
|
||||
extraHttpHeadersToSet = append(extraHttpHeadersToSet, header)
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("scoped extra HTTP header '%s' (scoped) will not be set for request URL '%s'", header.Name, e.Request.URL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(extraHttpHeadersToSet) > 0 {
|
||||
logger.Debug(fmt.Sprintf("setting extra HTTP headers for request URL '%s': %+v", e.Request.URL, extraHttpHeadersToSet))
|
||||
|
||||
originalHeaders := e.Request.Headers
|
||||
headers := make(map[string]string)
|
||||
|
||||
for key, value := range originalHeaders {
|
||||
strValue, ok := value.(string)
|
||||
if ok {
|
||||
headers[key] = strValue
|
||||
} else {
|
||||
logger.Error(fmt.Sprintf("ignoring header '%s' for URL '%s' since it cannot be cast to a string", key, e.Request.URL))
|
||||
}
|
||||
}
|
||||
|
||||
var headersEntries []*fetch.HeaderEntry
|
||||
for key, value := range headers {
|
||||
headersEntries = append(headersEntries, &fetch.HeaderEntry{
|
||||
Name: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
for _, header := range extraHttpHeadersToSet {
|
||||
headersEntries = append(headersEntries, &fetch.HeaderEntry{
|
||||
Name: header.Name,
|
||||
Value: header.Value,
|
||||
})
|
||||
}
|
||||
|
||||
req.Headers = headersEntries
|
||||
}
|
||||
|
||||
err = req.Do(executorCtx)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("fail request: %s", err))
|
||||
logger.Error(fmt.Sprintf("continue request: %s", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type eventResponseReceivedOptions struct {
|
||||
mainPageUrl string
|
||||
failOnHttpStatusCodes []int64
|
||||
invalidHttpStatusCode *error
|
||||
invalidHttpStatusCodeMu *sync.RWMutex
|
||||
failOnResourceOnHttpStatusCode []int64
|
||||
invalidResourceHttpStatusCode *error
|
||||
invalidResourceHttpStatusCodeMu *sync.RWMutex
|
||||
}
|
||||
|
||||
// listenForEventResponseReceived listens for an invalid HTTP status code that
|
||||
// is returned by the main page.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||
func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url string, failOnHttpStatusCodes []int64, invalidHttpStatusCode *error, invalidHttpStatusCodeMu *sync.RWMutex) {
|
||||
// is returned by the main page or by one or more resources.
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/613.
|
||||
// https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
func listenForEventResponseReceived(
|
||||
ctx context.Context,
|
||||
logger *zap.Logger,
|
||||
options eventResponseReceivedOptions,
|
||||
) {
|
||||
for _, code := range []int64{199, 299, 399, 499, 599} {
|
||||
if slices.Contains(failOnHttpStatusCodes, code) {
|
||||
if slices.Contains(options.failOnHttpStatusCodes, code) {
|
||||
for i := code - 99; i <= code; i++ {
|
||||
failOnHttpStatusCodes = append(failOnHttpStatusCodes, i)
|
||||
options.failOnHttpStatusCodes = append(options.failOnHttpStatusCodes, i)
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(options.failOnResourceOnHttpStatusCode, code) {
|
||||
for i := code - 99; i <= code; i++ {
|
||||
options.failOnResourceOnHttpStatusCode = append(options.failOnResourceOnHttpStatusCode, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,41 +174,53 @@ func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
switch ev := ev.(type) {
|
||||
case *network.EventResponseReceived:
|
||||
if ev.Response.URL != url {
|
||||
if ev.Response.URL == options.mainPageUrl {
|
||||
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response))
|
||||
|
||||
if slices.Contains(options.failOnHttpStatusCodes, ev.Response.Status) {
|
||||
options.invalidHttpStatusCodeMu.Lock()
|
||||
defer options.invalidHttpStatusCodeMu.Unlock()
|
||||
|
||||
*options.invalidHttpStatusCode = fmt.Errorf("%d: %s", ev.Response.Status, ev.Response.StatusText)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response))
|
||||
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for a resource: %+v", ev.Response))
|
||||
|
||||
if slices.Contains(failOnHttpStatusCodes, ev.Response.Status) {
|
||||
invalidHttpStatusCodeMu.Lock()
|
||||
defer invalidHttpStatusCodeMu.Unlock()
|
||||
if slices.Contains(options.failOnResourceOnHttpStatusCode, ev.Response.Status) {
|
||||
options.invalidResourceHttpStatusCodeMu.Lock()
|
||||
defer options.invalidResourceHttpStatusCodeMu.Unlock()
|
||||
|
||||
*invalidHttpStatusCode = fmt.Errorf("%d: %s", ev.Response.Status, ev.Response.StatusText)
|
||||
*options.invalidResourceHttpStatusCode = multierr.Append(
|
||||
*options.invalidResourceHttpStatusCode,
|
||||
fmt.Errorf("%s - %d: %s", ev.Response.URL, ev.Response.Status, http.StatusText(int(ev.Response.Status))),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type eventLoadingFailedOptions struct {
|
||||
loadingFailed *error
|
||||
loadingFailedMu *sync.RWMutex
|
||||
resourceLoadingFailed *error
|
||||
resourceLoadingFailedMu *sync.RWMutex
|
||||
}
|
||||
|
||||
// listenForEventLoadingFailed listens for an event indicating that the main
|
||||
// page failed to load.
|
||||
// page or one or more resources failed to load.
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/913.
|
||||
// https://github.com/gotenberg/gotenberg/issues/959.
|
||||
func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, loadingFailed *error, loadingFailedMu *sync.RWMutex) {
|
||||
// https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, options eventLoadingFailedOptions) {
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
switch ev := ev.(type) {
|
||||
case *network.EventLoadingFailed:
|
||||
logger.Debug(fmt.Sprintf("event EventLoadingFailed fired: %+v", ev.ErrorText))
|
||||
|
||||
if ev.Type != network.ResourceTypeDocument {
|
||||
logger.Debug("skip EventLoadingFailed: is not resource type Document")
|
||||
return
|
||||
}
|
||||
|
||||
// Supposition: except iframe, an event loading failed with a
|
||||
// resource type Document is about the main page.
|
||||
|
||||
// We are looking for common errors.
|
||||
// TODO: sufficient?
|
||||
errors := []string{
|
||||
@@ -128,16 +234,35 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, loadin
|
||||
"net::ERR_ADDRESS_UNREACHABLE",
|
||||
"net::ERR_BLOCKED_BY_CLIENT",
|
||||
"net::ERR_BLOCKED_BY_RESPONSE",
|
||||
"net::ERR_FILE_NOT_FOUND",
|
||||
}
|
||||
if !slices.Contains(errors, ev.ErrorText) {
|
||||
logger.Debug(fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors))
|
||||
return
|
||||
}
|
||||
|
||||
loadingFailedMu.Lock()
|
||||
defer loadingFailedMu.Unlock()
|
||||
if ev.Type == network.ResourceTypeDocument {
|
||||
// Supposition: except iframe, an event loading failed with a
|
||||
// resource type Document is about the main page.
|
||||
logger.Debug("event EventLoadingFailed fired for main page")
|
||||
|
||||
*loadingFailed = fmt.Errorf("%s", ev.ErrorText)
|
||||
options.loadingFailedMu.Lock()
|
||||
defer options.loadingFailedMu.Unlock()
|
||||
|
||||
*options.loadingFailed = fmt.Errorf("%s", ev.ErrorText)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("event EventLoadingFailed fired for a resource")
|
||||
|
||||
options.resourceLoadingFailedMu.Lock()
|
||||
defer options.resourceLoadingFailedMu.Unlock()
|
||||
|
||||
*options.resourceLoadingFailed = multierr.Append(
|
||||
*options.resourceLoadingFailed,
|
||||
fmt.Errorf("resource %s: %s", ev.Type, ev.ErrorText),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
@@ -28,17 +29,19 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
defaultOptions := DefaultOptions()
|
||||
|
||||
var (
|
||||
skipNetworkIdleEvent bool
|
||||
failOnHttpStatusCodes []int64
|
||||
failOnConsoleExceptions bool
|
||||
waitDelay time.Duration
|
||||
waitWindowStatus string
|
||||
waitForExpression string
|
||||
cookies []Cookie
|
||||
userAgent string
|
||||
extraHttpHeaders map[string]string
|
||||
emulatedMediaType string
|
||||
omitBackground bool
|
||||
skipNetworkIdleEvent bool
|
||||
failOnHttpStatusCodes []int64
|
||||
failOnResourceHttpStatusCodes []int64
|
||||
failOnResourceLoadingFailed bool
|
||||
failOnConsoleExceptions bool
|
||||
waitDelay time.Duration
|
||||
waitWindowStatus string
|
||||
waitForExpression string
|
||||
cookies []Cookie
|
||||
userAgent string
|
||||
extraHttpHeaders []ExtraHttpHeader
|
||||
emulatedMediaType string
|
||||
omitBackground bool
|
||||
)
|
||||
|
||||
form := ctx.FormData().
|
||||
@@ -56,6 +59,20 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
|
||||
return nil
|
||||
}).
|
||||
Custom("failOnResourceHttpStatusCodes", func(value string) error {
|
||||
if value == "" {
|
||||
failOnResourceHttpStatusCodes = defaultOptions.FailOnResourceHttpStatusCodes
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(value), &failOnResourceHttpStatusCodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal failOnResourceHttpStatusCodes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}).
|
||||
Bool("failOnResourceLoadingFailed", &failOnResourceLoadingFailed, defaultOptions.FailOnResourceLoadingFailed).
|
||||
Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
|
||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||
@@ -86,12 +103,59 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
|
||||
var headers map[string]string
|
||||
err := json.Unmarshal([]byte(value), &headers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal extraHttpHeaders: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
for k, v := range headers {
|
||||
var scope string
|
||||
var valueTokens []string
|
||||
var invalidScopeToken bool
|
||||
|
||||
tokens := strings.Split(v, ";")
|
||||
for _, token := range tokens {
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(token)), "scope") {
|
||||
tokenNoSpaces := strings.Join(strings.Fields(token), "")
|
||||
parts := strings.SplitN(tokenNoSpaces, "=", 2)
|
||||
|
||||
if len(parts) == 2 && strings.ToLower(parts[0]) == "scope" && parts[1] != "" {
|
||||
scope = parts[1]
|
||||
} else {
|
||||
err = multierr.Append(err, fmt.Errorf("invalid scope '%s' for header '%s'", scope, k))
|
||||
invalidScopeToken = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if token != "" {
|
||||
valueTokens = append(valueTokens, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if invalidScopeToken {
|
||||
continue
|
||||
}
|
||||
|
||||
var scopeRegexp *regexp2.Regexp
|
||||
if len(scope) > 0 {
|
||||
p, errCompile := regexp2.Compile(scope, 0)
|
||||
if errCompile != nil {
|
||||
err = multierr.Append(err, fmt.Errorf("invalid scope regex pattern for header '%s': %w", k, errCompile))
|
||||
continue
|
||||
}
|
||||
scopeRegexp = p
|
||||
}
|
||||
|
||||
extraHttpHeaders = append(extraHttpHeaders, ExtraHttpHeader{
|
||||
Name: k,
|
||||
Value: strings.Join(valueTokens, "; "),
|
||||
Scope: scopeRegexp,
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}).
|
||||
Custom("emulatedMediaType", func(value string) error {
|
||||
if value == "" {
|
||||
@@ -110,17 +174,19 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground)
|
||||
|
||||
options := Options{
|
||||
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
||||
FailOnHttpStatusCodes: failOnHttpStatusCodes,
|
||||
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||
WaitDelay: waitDelay,
|
||||
WaitWindowStatus: waitWindowStatus,
|
||||
WaitForExpression: waitForExpression,
|
||||
Cookies: cookies,
|
||||
UserAgent: userAgent,
|
||||
ExtraHttpHeaders: extraHttpHeaders,
|
||||
EmulatedMediaType: emulatedMediaType,
|
||||
OmitBackground: omitBackground,
|
||||
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
||||
FailOnHttpStatusCodes: failOnHttpStatusCodes,
|
||||
FailOnResourceHttpStatusCodes: failOnResourceHttpStatusCodes,
|
||||
FailOnResourceLoadingFailed: failOnResourceLoadingFailed,
|
||||
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||
WaitDelay: waitDelay,
|
||||
WaitWindowStatus: waitWindowStatus,
|
||||
WaitForExpression: waitForExpression,
|
||||
Cookies: cookies,
|
||||
UserAgent: userAgent,
|
||||
ExtraHttpHeaders: extraHttpHeaders,
|
||||
EmulatedMediaType: emulatedMediaType,
|
||||
OmitBackground: omitBackground,
|
||||
}
|
||||
|
||||
return form, options
|
||||
@@ -139,6 +205,7 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||
pageRanges string
|
||||
headerTemplate, footerTemplate string
|
||||
preferCssPageSize bool
|
||||
generateDocumentOutline bool
|
||||
)
|
||||
|
||||
form.
|
||||
@@ -155,24 +222,26 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||
String("nativePageRanges", &pageRanges, defaultPdfOptions.PageRanges).
|
||||
Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate).
|
||||
Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).
|
||||
Bool("preferCssPageSize", &preferCssPageSize, defaultPdfOptions.PreferCssPageSize)
|
||||
Bool("preferCssPageSize", &preferCssPageSize, defaultPdfOptions.PreferCssPageSize).
|
||||
Bool("generateDocumentOutline", &generateDocumentOutline, defaultPdfOptions.GenerateDocumentOutline)
|
||||
|
||||
pdfOptions := PdfOptions{
|
||||
Options: options,
|
||||
Landscape: landscape,
|
||||
PrintBackground: printBackground,
|
||||
Scale: scale,
|
||||
SinglePage: singlePage,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
PageRanges: pageRanges,
|
||||
HeaderTemplate: headerTemplate,
|
||||
FooterTemplate: footerTemplate,
|
||||
PreferCssPageSize: preferCssPageSize,
|
||||
Options: options,
|
||||
Landscape: landscape,
|
||||
PrintBackground: printBackground,
|
||||
Scale: scale,
|
||||
SinglePage: singlePage,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
PageRanges: pageRanges,
|
||||
HeaderTemplate: headerTemplate,
|
||||
FooterTemplate: footerTemplate,
|
||||
PreferCssPageSize: preferCssPageSize,
|
||||
GenerateDocumentOutline: generateDocumentOutline,
|
||||
}
|
||||
|
||||
return form, pdfOptions
|
||||
@@ -678,12 +747,22 @@ func handleChromiumError(err error, options Options) error {
|
||||
)
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrInvalidResourceHttpStatusCode) {
|
||||
return api.WrapError(
|
||||
err,
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusConflict,
|
||||
fmt.Sprintf("Invalid HTTP status code from resources:\n%s", strings.ReplaceAll(err.Error(), fmt.Sprintf(": %s", ErrInvalidResourceHttpStatusCode.Error()), "")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrConsoleExceptions) {
|
||||
return api.WrapError(
|
||||
err,
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusConflict,
|
||||
fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")),
|
||||
fmt.Sprintf("Chromium console exceptions:\n%s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -698,5 +777,15 @@ func handleChromiumError(err error, options Options) error {
|
||||
)
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrResourceLoadingFailed) {
|
||||
return api.WrapError(
|
||||
err,
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusConflict,
|
||||
fmt.Sprintf("Chromium failed to load resources: %v", strings.ReplaceAll(err.Error(), fmt.Sprintf(": %s", ErrResourceLoadingFailed.Error()), "")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
@@ -19,14 +21,18 @@ import (
|
||||
|
||||
func TestFormDataChromiumOptions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions Options
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions Options
|
||||
compareWithoutDeepEqual bool
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultOptions(),
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid failOnHttpStatusCodes form field",
|
||||
@@ -44,6 +50,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.FailOnHttpStatusCodes = nil
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid failOnHttpStatusCodes form field",
|
||||
@@ -61,6 +69,46 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.FailOnHttpStatusCodes = []int64{399, 499, 599}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid failOnResourceHttpStatusCodes form field",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"failOnResourceHttpStatusCodes": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: func() Options {
|
||||
options := DefaultOptions()
|
||||
options.FailOnResourceHttpStatusCodes = nil
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid failOnResourceHttpStatusCodes form field",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"failOnResourceHttpStatusCodes": {
|
||||
`[399,499,599]`,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: func() Options {
|
||||
options := DefaultOptions()
|
||||
options.FailOnResourceHttpStatusCodes = []int64{399, 499, 599}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid cookies form field",
|
||||
@@ -73,7 +121,9 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid cookies form field (missing required values)",
|
||||
@@ -93,6 +143,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.Cookies = []Cookie{{}}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid cookies form field",
|
||||
@@ -114,9 +166,11 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
}}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid extraHttpHeaders form field",
|
||||
scenario: "invalid extraHttpHeaders form field: cannot unmarshall",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
@@ -126,7 +180,39 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid extraHttpHeaders form field: invalid scope",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"extraHttpHeaders": {
|
||||
`{"foo":"bar;scope;;"}`,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid extraHttpHeaders form field: invalid scope regex pattern",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"extraHttpHeaders": {
|
||||
`{"foo":"bar;scope=*."}`,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
compareWithoutDeepEqual: false,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid extraHttpHeaders form field",
|
||||
@@ -134,18 +220,28 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"extraHttpHeaders": {
|
||||
`{"foo":"bar"}`,
|
||||
`{"foo":"bar","baz":"qux;scope=https?:\\/\\/([a-zA-Z0-9-]+\\.)*qux\\.com\\/.*"}`,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: func() Options {
|
||||
options := DefaultOptions()
|
||||
options.ExtraHttpHeaders = map[string]string{
|
||||
"foo": "bar",
|
||||
options.ExtraHttpHeaders = []ExtraHttpHeader{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Name: "baz",
|
||||
Value: "qux",
|
||||
Scope: regexp2.MustCompile(`https?:\/\/([a-zA-Z0-9-]+\.)*qux\.com\/.*`, 0),
|
||||
},
|
||||
}
|
||||
return options
|
||||
}(),
|
||||
compareWithoutDeepEqual: true,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid emulatedMediaType form field",
|
||||
@@ -158,7 +254,8 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectedOptions: DefaultOptions(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid emulatedMediaType form field",
|
||||
@@ -176,14 +273,61 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
options.EmulatedMediaType = "screen"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
_, actual := FormDataChromiumOptions(tc.ctx.Context)
|
||||
form, actual := FormDataChromiumOptions(tc.ctx.Context)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
if tc.compareWithoutDeepEqual {
|
||||
if len(tc.expectedOptions.ExtraHttpHeaders) != len(actual.ExtraHttpHeaders) {
|
||||
t.Fatalf("expected %d extra HTTP headers, but got %d", len(tc.expectedOptions.ExtraHttpHeaders), len(actual.ExtraHttpHeaders))
|
||||
}
|
||||
|
||||
sort.Slice(tc.expectedOptions.ExtraHttpHeaders, func(i, j int) bool {
|
||||
return tc.expectedOptions.ExtraHttpHeaders[i].Name < tc.expectedOptions.ExtraHttpHeaders[j].Name
|
||||
})
|
||||
sort.Slice(actual.ExtraHttpHeaders, func(i, j int) bool {
|
||||
return actual.ExtraHttpHeaders[i].Name < actual.ExtraHttpHeaders[j].Name
|
||||
})
|
||||
|
||||
for i := range tc.expectedOptions.ExtraHttpHeaders {
|
||||
if tc.expectedOptions.ExtraHttpHeaders[i].Name != actual.ExtraHttpHeaders[i].Name {
|
||||
t.Fatalf("expected '%s' extra HTTP header, but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Name, tc.expectedOptions.ExtraHttpHeaders[i].Name)
|
||||
}
|
||||
|
||||
if tc.expectedOptions.ExtraHttpHeaders[i].Value != actual.ExtraHttpHeaders[i].Value {
|
||||
t.Fatalf("expected '%s' as value for extra HTTP header '%s', but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Value, tc.expectedOptions.ExtraHttpHeaders[i].Name, actual.ExtraHttpHeaders[i].Value)
|
||||
}
|
||||
|
||||
var expectedScope string
|
||||
if tc.expectedOptions.ExtraHttpHeaders[i].Scope != nil {
|
||||
expectedScope = tc.expectedOptions.ExtraHttpHeaders[i].Scope.String()
|
||||
}
|
||||
var actualScope string
|
||||
if actual.ExtraHttpHeaders[i].Scope != nil {
|
||||
actualScope = actual.ExtraHttpHeaders[i].Scope.String()
|
||||
}
|
||||
|
||||
if expectedScope != actualScope {
|
||||
t.Fatalf("expected '%s' as scope for extra HTTP header '%s', but got '%s'", expectedScope, tc.expectedOptions.ExtraHttpHeaders[i].Name, actualScope)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
}
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -191,14 +335,16 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
||||
|
||||
func TestFormDataChromiumPdfOptions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions PdfOptions
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions PdfOptions
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultPdfOptions(),
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultPdfOptions(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "custom form fields (Options & PdfOptions)",
|
||||
@@ -220,29 +366,42 @@ func TestFormDataChromiumPdfOptions(t *testing.T) {
|
||||
options.EmulatedMediaType = "screen"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
_, actual := FormDataChromiumPdfOptions(tc.ctx.Context)
|
||||
form, actual := FormDataChromiumPdfOptions(tc.ctx.Context)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions ScreenshotOptions
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedOptions ScreenshotOptions
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultScreenshotOptions(),
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedOptions: DefaultScreenshotOptions(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format form field",
|
||||
@@ -260,6 +419,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = ""
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid png format form field",
|
||||
@@ -277,6 +437,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = "png"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "valid jpeg format form field",
|
||||
@@ -294,6 +455,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = "jpeg"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "valid webp format form field",
|
||||
@@ -311,6 +473,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Format = "webp"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (not an integer)",
|
||||
@@ -328,6 +491,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 0
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (< 0)",
|
||||
@@ -345,6 +509,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 0
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (> 100)",
|
||||
@@ -362,6 +527,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 0
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid quality form field",
|
||||
@@ -379,6 +545,7 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.Quality = 50
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "custom form fields (Options & ScreenshotOptions)",
|
||||
@@ -412,29 +579,42 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
options.EmulatedMediaType = "screen"
|
||||
return options
|
||||
}(),
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
_, actual := FormDataChromiumScreenshotOptions(tc.ctx.Context)
|
||||
form, actual := FormDataChromiumScreenshotOptions(tc.ctx.Context)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedOptions) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataChromiumPdfFormats(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedPdfFormats gotenberg.PdfFormats
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedPdfFormats gotenberg.PdfFormats
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{},
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{},
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "pdfa and pdfua form fields",
|
||||
@@ -450,30 +630,44 @@ func TestFormDataChromiumPdfFormats(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
actual := FormDataChromiumPdfFormats(tc.ctx.Context.FormData())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataChromiumPdfFormats(form)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataPdfMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedMetadata map[string]interface{}
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedMetadata map[string]interface{}
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no metadata form field",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedMetadata: nil,
|
||||
scenario: "no metadata form field",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid metadata form field",
|
||||
@@ -486,7 +680,8 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedMetadata: nil,
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid metadata form field",
|
||||
@@ -502,15 +697,27 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
expectedMetadata: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
actual := FormDataPdfMetadata(tc.ctx.Context.FormData())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataPdfMetadata(form)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1423,6 +1630,18 @@ func TestConvertUrl(t *testing.T) {
|
||||
expectHttpStatus: http.StatusConflict,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrInvalidResourceHttpStatusCode",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return ErrInvalidResourceHttpStatusCode
|
||||
}},
|
||||
options: DefaultPdfOptions(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusConflict,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrConsoleExceptions",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
@@ -1447,6 +1666,18 @@ func TestConvertUrl(t *testing.T) {
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrResourceLoadingFailed",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return ErrResourceLoadingFailed
|
||||
}},
|
||||
options: DefaultPdfOptions(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusConflict,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from Chromium",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
@@ -1633,6 +1864,18 @@ func TestScreenshotUrl(t *testing.T) {
|
||||
expectHttpStatus: http.StatusConflict,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrInvalidResourceHttpStatusCode",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||
return ErrInvalidResourceHttpStatusCode
|
||||
}},
|
||||
options: DefaultScreenshotOptions(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusConflict,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrConsoleExceptions",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
@@ -1657,6 +1900,18 @@ func TestScreenshotUrl(t *testing.T) {
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrResourceLoadingFailed",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||
return ErrResourceLoadingFailed
|
||||
}},
|
||||
options: DefaultScreenshotOptions(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusConflict,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from Chromium",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
|
||||
@@ -48,6 +48,7 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOpti
|
||||
WithMarginRight(options.MarginRight).
|
||||
WithPageRanges(pageRanges).
|
||||
WithPreferCSSPageSize(options.PreferCssPageSize).
|
||||
WithGenerateDocumentOutline(options.GenerateDocumentOutline).
|
||||
// Does not seem to work.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/831.
|
||||
WithGenerateTaggedPDF(false)
|
||||
@@ -298,28 +299,33 @@ func userAgentOverride(logger *zap.Logger, userAgent string) chromedp.ActionFunc
|
||||
}
|
||||
}
|
||||
|
||||
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if len(extraHttpHeaders) == 0 {
|
||||
logger.Debug("no extra HTTP headers")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
|
||||
|
||||
headers := make(network.Headers, len(extraHttpHeaders))
|
||||
for key, value := range extraHttpHeaders {
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
err := network.SetExtraHTTPHeaders(headers).Do(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("set extra HTTP headers: %w", err)
|
||||
}
|
||||
}
|
||||
// This code has been replaced with the listenForEventRequestPaused function.
|
||||
// Indeed, the user may want to scope the headers per domain, but using
|
||||
// network.SetExtraHTTPHeaders set the headers for ALL requests from the page.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1011.
|
||||
//
|
||||
//func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||
// return func(ctx context.Context) error {
|
||||
// if len(extraHttpHeaders) == 0 {
|
||||
// logger.Debug("no extra HTTP headers")
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
|
||||
//
|
||||
// headers := make(network.Headers, len(extraHttpHeaders))
|
||||
// for key, value := range extraHttpHeaders {
|
||||
// headers[key] = value
|
||||
// }
|
||||
//
|
||||
// err := network.SetExtraHTTPHeaders(headers).Do(ctx)
|
||||
// if err == nil {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return fmt.Errorf("set extra HTTP headers: %w", err)
|
||||
// }
|
||||
//}
|
||||
|
||||
func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleEvent bool) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/barasher/go-exiftool"
|
||||
"go.uber.org/zap"
|
||||
@@ -109,6 +110,17 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
fileMetadata[0].SetString(key, val)
|
||||
case []string:
|
||||
fileMetadata[0].SetStrings(key, val)
|
||||
case []interface{}:
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1048.
|
||||
strings := make([]string, len(val))
|
||||
for i, entry := range val {
|
||||
if str, ok := entry.(string); ok {
|
||||
strings[i] = str
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
}
|
||||
fileMetadata[0].SetStrings(key, strings)
|
||||
case bool:
|
||||
fileMetadata[0].SetString(key, fmt.Sprintf("%t", val))
|
||||
case int:
|
||||
@@ -122,7 +134,7 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
// TODO: support more complex cases, e.g., arrays and nested objects
|
||||
// (limitations in underlying library).
|
||||
default:
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %w", gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,20 @@ func TestExiftool_WriteMetadata(t *testing.T) {
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported",
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (not string array)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Unsupported": []interface{}{
|
||||
"foo",
|
||||
1,
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (default)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
@@ -171,6 +184,24 @@ func TestExiftool_WriteMetadata(t *testing.T) {
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "success (interface array to string array)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Keywords": []interface{}{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
},
|
||||
expectMetadata: map[string]interface{}{
|
||||
"Keywords": []interface{}{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
createCopy: true,
|
||||
|
||||
@@ -365,13 +365,6 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
return ErrRuntimeException
|
||||
}
|
||||
|
||||
// Possible errors:
|
||||
// 1. LibreOffice failed for some reason.
|
||||
// 2. Context done.
|
||||
//
|
||||
// On the second scenario, LibreOffice might not have time to remove some
|
||||
// of its temporary files, as it has been killed without warning. The
|
||||
// garbage collector will delete them for us (if the module is loaded).
|
||||
return fmt.Errorf("convert to PDF: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
|
||||
// interface using the pdfcpu library. This package allows for:
|
||||
// interface using the pdfcpu command-line tool. This package allows for:
|
||||
//
|
||||
// 1. The merging of PDF files.
|
||||
//
|
||||
|
||||
@@ -2,11 +2,10 @@ package pdfcpu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
pdfcpuAPI "github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
pdfcpuLog "github.com/pdfcpu/pdfcpu/pkg/log"
|
||||
pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
@@ -16,10 +15,10 @@ func init() {
|
||||
gotenberg.MustRegisterModule(new(PdfCpu))
|
||||
}
|
||||
|
||||
// PdfCpu abstracts the pdfcpu library and implements the [gotenberg.PdfEngine]
|
||||
// interface.
|
||||
// PdfCpu abstracts the CLI tool pdfcpu and implements the
|
||||
// [gotenberg.PdfEngine] interface.
|
||||
type PdfCpu struct {
|
||||
conf *pdfcpuConfig.Configuration
|
||||
binPath string
|
||||
}
|
||||
|
||||
// Descriptor returns a [PdfCpu]'s module descriptor.
|
||||
@@ -32,16 +31,38 @@ func (engine *PdfCpu) Descriptor() gotenberg.ModuleDescriptor {
|
||||
|
||||
// Provision sets the engine properties.
|
||||
func (engine *PdfCpu) Provision(ctx *gotenberg.Context) error {
|
||||
pdfcpuConfig.ConfigPath = "disable"
|
||||
pdfcpuLog.DisableLoggers()
|
||||
engine.conf = pdfcpuConfig.NewDefaultConfiguration()
|
||||
binPath, ok := os.LookupEnv("PDFCPU_BIN_PATH")
|
||||
if !ok {
|
||||
return errors.New("PDFCPU_BIN_PATH environment variable is not set")
|
||||
}
|
||||
|
||||
engine.binPath = binPath
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the module properties.
|
||||
func (engine *PdfCpu) Validate() error {
|
||||
_, err := os.Stat(engine.binPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("pdfcpu binary path does not exist: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, false, engine.conf)
|
||||
var args []string
|
||||
args = append(args, "merge", outputPath)
|
||||
args = append(args, inputPaths...)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
_, err = cmd.Exec()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -68,5 +89,6 @@ func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, met
|
||||
var (
|
||||
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||
_ gotenberg.Provisioner = (*PdfCpu)(nil)
|
||||
_ gotenberg.Validator = (*PdfCpu)(nil)
|
||||
_ gotenberg.PdfEngine = (*PdfCpu)(nil)
|
||||
)
|
||||
|
||||
@@ -33,14 +33,59 @@ func TestPdfCpu_Provision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "empty bin path",
|
||||
binPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "bin path does not exist",
|
||||
binPath: "/foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "validate success",
|
||||
binPath: os.Getenv("PDFTK_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
engine.binPath = tc.binPath
|
||||
err := engine.Validate()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
inputPaths []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
@@ -48,6 +93,7 @@ func TestPdfCpu_Merge(t *testing.T) {
|
||||
},
|
||||
{
|
||||
scenario: "single file success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
},
|
||||
@@ -55,10 +101,12 @@ func TestPdfCpu_Merge(t *testing.T) {
|
||||
},
|
||||
{
|
||||
scenario: "many files success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
@@ -81,7 +129,7 @@ func TestPdfCpu_Merge(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
err = engine.Merge(nil, nil, tc.inputPaths, outputDir+"/foo.pdf")
|
||||
err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
|
||||
@@ -12,12 +12,23 @@ import (
|
||||
)
|
||||
|
||||
type multiPdfEngines struct {
|
||||
engines []gotenberg.PdfEngine
|
||||
mergeEngines []gotenberg.PdfEngine
|
||||
convertEngines []gotenberg.PdfEngine
|
||||
readMedataEngines []gotenberg.PdfEngine
|
||||
writeMedataEngines []gotenberg.PdfEngine
|
||||
}
|
||||
|
||||
func newMultiPdfEngines(engines ...gotenberg.PdfEngine) *multiPdfEngines {
|
||||
func newMultiPdfEngines(
|
||||
mergeEngines,
|
||||
convertEngines,
|
||||
readMetadataEngines,
|
||||
writeMedataEngines []gotenberg.PdfEngine,
|
||||
) *multiPdfEngines {
|
||||
return &multiPdfEngines{
|
||||
engines: engines,
|
||||
mergeEngines: mergeEngines,
|
||||
convertEngines: convertEngines,
|
||||
readMedataEngines: readMetadataEngines,
|
||||
writeMedataEngines: writeMedataEngines,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +38,7 @@ func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inp
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.engines {
|
||||
for _, engine := range multi.mergeEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
|
||||
}(engine)
|
||||
@@ -52,7 +63,7 @@ func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, f
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.engines {
|
||||
for _, engine := range multi.convertEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
|
||||
}(engine)
|
||||
@@ -80,16 +91,16 @@ func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logg
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
resultChan := make(chan readMetadataResult, len(multi.engines))
|
||||
resultChan := make(chan readMetadataResult, len(multi.readMedataEngines))
|
||||
|
||||
for _, engine := range multi.engines {
|
||||
for _, engine := range multi.readMedataEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
|
||||
resultChan <- readMetadataResult{metadata: metadata, err: err}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
for range multi.engines {
|
||||
for range multi.readMedataEngines {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
@@ -111,7 +122,7 @@ func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Log
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.engines {
|
||||
for _, engine := range multi.writeMedataEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
|
||||
}(engine)
|
||||
|
||||
@@ -20,11 +20,16 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
@@ -32,16 +37,21 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
@@ -49,16 +59,21 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
@@ -66,11 +81,16 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -105,43 +125,58 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
@@ -149,11 +184,16 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -188,43 +228,58 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
@@ -232,11 +287,16 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -271,9 +331,14 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -282,14 +347,19 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -298,14 +368,19 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -315,9 +390,14 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
@@ -27,9 +27,12 @@ func init() {
|
||||
// the [api.Router] interface to expose relevant PDF processing routes if
|
||||
// enabled.
|
||||
type PdfEngines struct {
|
||||
names []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
mergeNames []string
|
||||
convertNames []string
|
||||
readMetadataNames []string
|
||||
writeMedataNames []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
}
|
||||
|
||||
// Descriptor returns a PdfEngines' module descriptor.
|
||||
@@ -38,9 +41,18 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
ID: "pdfengines",
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
||||
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the PDF engines and their order - all by default")
|
||||
fs.StringSlice("pdfengines-merge-engines", []string{"qpdf", "pdfcpu", "pdftk"}, "Set the PDF engines and their order for the merge feature - empty means all")
|
||||
fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all")
|
||||
fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all")
|
||||
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
||||
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
||||
|
||||
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the default PDF engines and their default order - all by default")
|
||||
err := fs.MarkDeprecated("pdfengines-engines", "use other flags for a more granular selection of PDF engines per method")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
New: func() gotenberg.Module { return new(PdfEngines) },
|
||||
@@ -51,7 +63,10 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
// selected by the user thanks to the "engines" flag.
|
||||
func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
flags := ctx.ParsedFlags()
|
||||
names := flags.MustStringSlice("pdfengines-engines")
|
||||
mergeNames := flags.MustStringSlice("pdfengines-merge-engines")
|
||||
convertNames := flags.MustStringSlice("pdfengines-convert-engines")
|
||||
readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines")
|
||||
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
|
||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||
|
||||
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
||||
@@ -65,26 +80,37 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.engines[i] = engine.(gotenberg.PdfEngine)
|
||||
}
|
||||
|
||||
if len(names) > 0 {
|
||||
// Selection from user.
|
||||
mod.names = names
|
||||
|
||||
// Example in case of deprecated module name.
|
||||
//for i, name := range names {
|
||||
// if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
||||
// logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
||||
// mod.names[i] = "libreoffice-pdfengine"
|
||||
// }
|
||||
//}
|
||||
|
||||
return nil
|
||||
defaultNames := make([]string, len(mod.engines))
|
||||
for i, engine := range mod.engines {
|
||||
defaultNames[i] = engine.(gotenberg.Module).Descriptor().ID
|
||||
}
|
||||
|
||||
// No selection from user, use all PDF engines available.
|
||||
mod.names = make([]string, len(mod.engines))
|
||||
// Example in case of deprecated module name.
|
||||
//for i, name := range defaultNames {
|
||||
// if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
||||
// logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
||||
// mod.defaultNames[i] = "libreoffice-pdfengine"
|
||||
// }
|
||||
//}
|
||||
|
||||
for i, engine := range mod.engines {
|
||||
mod.names[i] = engine.(gotenberg.Module).Descriptor().ID
|
||||
mod.mergeNames = defaultNames
|
||||
if len(mergeNames) > 0 {
|
||||
mod.mergeNames = mergeNames
|
||||
}
|
||||
|
||||
mod.convertNames = defaultNames
|
||||
if len(convertNames) > 0 {
|
||||
mod.convertNames = convertNames
|
||||
}
|
||||
|
||||
mod.readMetadataNames = defaultNames
|
||||
if len(readMetadataNames) > 0 {
|
||||
mod.readMetadataNames = readMetadataNames
|
||||
}
|
||||
|
||||
mod.writeMedataNames = defaultNames
|
||||
if len(writeMetadataNames) > 0 {
|
||||
mod.writeMedataNames = writeMetadataNames
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -105,22 +131,40 @@ func (mod *PdfEngines) Validate() error {
|
||||
}
|
||||
|
||||
nonExistingEngines := make([]string, 0)
|
||||
findNonExistingEngines := func(names []string) {
|
||||
for _, name := range names {
|
||||
engineExists := false
|
||||
|
||||
for _, name := range mod.names {
|
||||
engineExists := false
|
||||
for _, engine := range mod.engines {
|
||||
if name == engine.(gotenberg.Module).Descriptor().ID {
|
||||
engineExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, engine := range mod.engines {
|
||||
if name == engine.(gotenberg.Module).Descriptor().ID {
|
||||
engineExists = true
|
||||
break
|
||||
if engineExists {
|
||||
continue
|
||||
}
|
||||
|
||||
alreadyInSlice := false
|
||||
for _, engine := range nonExistingEngines {
|
||||
if engine == name {
|
||||
alreadyInSlice = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !alreadyInSlice {
|
||||
nonExistingEngines = append(nonExistingEngines, name)
|
||||
}
|
||||
}
|
||||
|
||||
if !engineExists {
|
||||
nonExistingEngines = append(nonExistingEngines, name)
|
||||
}
|
||||
}
|
||||
|
||||
findNonExistingEngines(mod.mergeNames)
|
||||
findNonExistingEngines(mod.convertNames)
|
||||
findNonExistingEngines(mod.readMetadataNames)
|
||||
findNonExistingEngines(mod.writeMedataNames)
|
||||
|
||||
if len(nonExistingEngines) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -132,24 +176,35 @@ func (mod *PdfEngines) Validate() error {
|
||||
// modules.
|
||||
func (mod *PdfEngines) SystemMessages() []string {
|
||||
return []string{
|
||||
strings.Join(mod.names[:], " "),
|
||||
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
||||
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||
fmt.Sprintf("write medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")),
|
||||
}
|
||||
}
|
||||
|
||||
// PdfEngine returns a [gotenberg.PdfEngine].
|
||||
func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||
engines := make([]gotenberg.PdfEngine, len(mod.names))
|
||||
|
||||
for i, name := range mod.names {
|
||||
for _, engine := range mod.engines {
|
||||
if name == engine.(gotenberg.Module).Descriptor().ID {
|
||||
engines[i] = engine
|
||||
break
|
||||
engines := func(names []string) []gotenberg.PdfEngine {
|
||||
list := make([]gotenberg.PdfEngine, len(names))
|
||||
for i, name := range names {
|
||||
for _, engine := range mod.engines {
|
||||
if name == engine.(gotenberg.Module).Descriptor().ID {
|
||||
list[i] = engine
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
return newMultiPdfEngines(engines...), nil
|
||||
return newMultiPdfEngines(
|
||||
engines(mod.mergeNames),
|
||||
engines(mod.convertNames),
|
||||
engines(mod.readMetadataNames),
|
||||
engines(mod.writeMedataNames),
|
||||
), nil
|
||||
}
|
||||
|
||||
// Routes returns the HTTP routes.
|
||||
|
||||
@@ -2,6 +2,7 @@ package pdfengines
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -22,10 +23,13 @@ func TestPdfEngines_Descriptor(t *testing.T) {
|
||||
|
||||
func TestPdfEngines_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectedPdfEngines []string
|
||||
expectError bool
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectedMergePdfEngines []string
|
||||
expectedConvertPdfEngines []string
|
||||
expectedReadMetadataPdfEngines []string
|
||||
expectedWriteMetadataPdfEngines []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no selection from user",
|
||||
@@ -61,8 +65,11 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectedPdfEngines: []string{"bar"},
|
||||
expectError: false,
|
||||
expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"},
|
||||
expectedConvertPdfEngines: []string{"libreoffice-pdfengine"},
|
||||
expectedReadMetadataPdfEngines: []string{"exiftool"},
|
||||
expectedWriteMetadataPdfEngines: []string{"exiftool"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "selection from user",
|
||||
@@ -100,7 +107,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}
|
||||
|
||||
fs := new(PdfEngines).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
|
||||
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
@@ -116,8 +123,12 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectedPdfEngines: []string{"b", "a"},
|
||||
expectError: false,
|
||||
|
||||
expectedMergePdfEngines: []string{"b"},
|
||||
expectedConvertPdfEngines: []string{"b"},
|
||||
expectedReadMetadataPdfEngines: []string{"a"},
|
||||
expectedWriteMetadataPdfEngines: []string{"a"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "no valid PDF engine",
|
||||
@@ -167,13 +178,43 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if len(tc.expectedPdfEngines) != len(mod.names) {
|
||||
t.Fatalf("expected %d names but got %d", len(tc.expectedPdfEngines), len(mod.names))
|
||||
if len(tc.expectedMergePdfEngines) != len(mod.mergeNames) {
|
||||
t.Fatalf("expected %d merge names but got %d", len(tc.expectedMergePdfEngines), len(mod.mergeNames))
|
||||
}
|
||||
|
||||
for index, name := range mod.names {
|
||||
if name != tc.expectedPdfEngines[index] {
|
||||
t.Fatalf("expected scenario at index %d to be %s, but got: %s", index, name, tc.expectedPdfEngines[index])
|
||||
if len(tc.expectedConvertPdfEngines) != len(mod.convertNames) {
|
||||
t.Fatalf("expected %d convert names but got %d", len(tc.expectedConvertPdfEngines), len(mod.convertNames))
|
||||
}
|
||||
|
||||
if len(tc.expectedReadMetadataPdfEngines) != len(mod.readMetadataNames) {
|
||||
t.Fatalf("expected %d read metadata names but got %d", len(tc.expectedReadMetadataPdfEngines), len(mod.readMetadataNames))
|
||||
}
|
||||
|
||||
if len(tc.expectedWriteMetadataPdfEngines) != len(mod.writeMedataNames) {
|
||||
t.Fatalf("expected %d write metadata names but got %d", len(tc.expectedWriteMetadataPdfEngines), len(mod.writeMedataNames))
|
||||
}
|
||||
|
||||
for index, name := range mod.mergeNames {
|
||||
if name != tc.expectedMergePdfEngines[index] {
|
||||
t.Fatalf("expected merge name at index %d to be %s, but got: %s", index, name, tc.expectedMergePdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.convertNames {
|
||||
if name != tc.expectedConvertPdfEngines[index] {
|
||||
t.Fatalf("expected convert name at index %d to be %s, but got: %s", index, name, tc.expectedConvertPdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.readMetadataNames {
|
||||
if name != tc.expectedReadMetadataPdfEngines[index] {
|
||||
t.Fatalf("expected read metadata name at index %d to be %s, but got: %s", index, name, tc.expectedReadMetadataPdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.writeMedataNames {
|
||||
if name != tc.expectedWriteMetadataPdfEngines[index] {
|
||||
t.Fatalf("expected write metadat name at index %d to be %s, but got: %s", index, name, tc.expectedWriteMetadataPdfEngines[index])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -239,8 +280,11 @@ func TestPdfEngines_Validate(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
names: tc.names,
|
||||
engines: tc.engines,
|
||||
mergeNames: tc.names,
|
||||
convertNames: tc.names,
|
||||
readMetadataNames: tc.names,
|
||||
writeMedataNames: tc.names,
|
||||
engines: tc.engines,
|
||||
}
|
||||
|
||||
err := mod.Validate()
|
||||
@@ -258,22 +302,36 @@ func TestPdfEngines_Validate(t *testing.T) {
|
||||
|
||||
func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
mod.names = []string{"foo", "bar"}
|
||||
mod.mergeNames = []string{"foo", "bar"}
|
||||
mod.convertNames = []string{"foo", "bar"}
|
||||
mod.readMetadataNames = []string{"foo", "bar"}
|
||||
mod.writeMedataNames = []string{"foo", "bar"}
|
||||
|
||||
messages := mod.SystemMessages()
|
||||
if len(messages) != 1 {
|
||||
if len(messages) != 4 {
|
||||
t.Errorf("expected one and only one message, but got %d", len(messages))
|
||||
}
|
||||
|
||||
expect := strings.Join(mod.names[:], " ")
|
||||
if messages[0] != expect {
|
||||
t.Errorf("expected message '%s', but got '%s'", expect, messages[0])
|
||||
expect := []string{
|
||||
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
||||
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||
fmt.Sprintf("write medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")),
|
||||
}
|
||||
|
||||
for i, message := range messages {
|
||||
if message != expect[i] {
|
||||
t.Errorf("expected message at index %d to be %s, but got %s", i, message, expect[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfEngines_PdfEngine(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
names: []string{"foo", "bar"},
|
||||
mergeNames: []string{"foo", "bar"},
|
||||
convertNames: []string{"foo", "bar"},
|
||||
readMetadataNames: []string{"foo", "bar"},
|
||||
writeMedataNames: []string{"foo", "bar"},
|
||||
engines: func() []gotenberg.PdfEngine {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
|
||||
@@ -45,7 +45,7 @@ func (engine *PdfTk) Provision(ctx *gotenberg.Context) error {
|
||||
func (engine *PdfTk) Validate() error {
|
||||
_, err := os.Stat(engine.binPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("PdfTk binary path does not exist: %w", err)
|
||||
return fmt.Errorf("PDFtk binary path does not exist: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -97,6 +97,7 @@ func TestPdfTk_Merge(t *testing.T) {
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "many files success",
|
||||
@@ -105,6 +106,7 @@ func TestPdfTk_Merge(t *testing.T) {
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
|
||||
@@ -97,6 +97,7 @@ func TestQPdf_Merge(t *testing.T) {
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "many files success",
|
||||
@@ -105,6 +106,7 @@ func TestQPdf_Merge(t *testing.T) {
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
|
||||
@@ -113,13 +113,19 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve values from echo.Context before it get recycled.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1000.
|
||||
startTime := c.Get("startTime").(time.Time)
|
||||
traceHeader := c.Get("traceHeader").(string)
|
||||
trace := c.Get("trace").(string)
|
||||
|
||||
client := &client{
|
||||
url: webhookUrl,
|
||||
method: webhookMethod,
|
||||
errorUrl: webhookErrorUrl,
|
||||
errorMethod: webhookErrorMethod,
|
||||
extraHttpHeaders: extraHttpHeaders,
|
||||
startTime: c.Get("startTime").(time.Time),
|
||||
startTime: startTime,
|
||||
|
||||
client: &retryablehttp.Client{
|
||||
HTTPClient: &http.Client{
|
||||
@@ -157,8 +163,8 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
echo.HeaderContentType: echo.MIMEApplicationJSON,
|
||||
c.Get("traceHeader").(string): c.Get("trace").(string),
|
||||
echo.HeaderContentType: echo.MIMEApplicationJSON,
|
||||
traceHeader: trace,
|
||||
}
|
||||
|
||||
err = client.send(bytes.NewReader(b), headers, true)
|
||||
@@ -236,7 +242,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
echo.HeaderContentDisposition: fmt.Sprintf("attachement; filename=%q", ctx.OutputFilename(outputPath)),
|
||||
echo.HeaderContentType: http.DetectContentType(fileHeader),
|
||||
echo.HeaderContentLength: strconv.FormatInt(fileStat.Size(), 10),
|
||||
c.Get("traceHeader").(string): c.Get("trace").(string),
|
||||
traceHeader: trace,
|
||||
}
|
||||
|
||||
// Send the output file to the webhook.
|
||||
|
||||
@@ -9,9 +9,10 @@ GOTENBERG_USER_GID="$3"
|
||||
GOTENBERG_USER_UID="$4"
|
||||
NOTO_COLOR_EMOJI_VERSION="$5"
|
||||
PDFTK_VERSION="$6"
|
||||
DOCKER_REGISTRY="$7"
|
||||
DOCKER_REPOSITORY="$8"
|
||||
LINUX_AMD64_RELEASE="$9"
|
||||
PDFCPU_VERSION="$7"
|
||||
DOCKER_REGISTRY="$8"
|
||||
DOCKER_REPOSITORY="$9"
|
||||
LINUX_AMD64_RELEASE="${10}"
|
||||
|
||||
# Find out if given version is "semver".
|
||||
GOTENBERG_VERSION="${GOTENBERG_VERSION//v}"
|
||||
@@ -65,6 +66,7 @@ docker buildx build \
|
||||
--build-arg GOTENBERG_USER_UID="$GOTENBERG_USER_UID" \
|
||||
--build-arg NOTO_COLOR_EMOJI_VERSION="$NOTO_COLOR_EMOJI_VERSION" \
|
||||
--build-arg PDFTK_VERSION="$PDFTK_VERSION" \
|
||||
--build-arg PDFCPU_VERSION="$PDFCPU_VERSION" \
|
||||
$PLATFORM_FLAG \
|
||||
"${TAGS[@]}" \
|
||||
--push \
|
||||
|
||||
8
test/testdata/chromium/html/index.html
vendored
8
test/testdata/chromium/html/index.html
vendored
@@ -2,6 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- net::ERR_FILE_NOT_FOUND -->
|
||||
<link rel="stylesheet" type="text/css" href="doesnotexist.css">
|
||||
<!-- net::ERR_CONNECTION_REFUSED -->
|
||||
<link rel="stylesheet" type="text/css" href="http://localhost:100/style.css">
|
||||
<!-- 400 Bad Request -->
|
||||
<link rel="stylesheet" type="text/css" href="https://httpstat.us/400">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
|
||||
<title>Gutenberg</title>
|
||||
@@ -23,7 +29,7 @@
|
||||
|
||||
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
|
||||
<p>It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.</p>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
|
||||
8
test/testdata/chromium/markdown/index.html
vendored
8
test/testdata/chromium/markdown/index.html
vendored
@@ -2,6 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- net::ERR_FILE_NOT_FOUND -->
|
||||
<link rel="stylesheet" type="text/css" href="doesnotexist.css">
|
||||
<!-- net::ERR_CONNECTION_REFUSED -->
|
||||
<link rel="stylesheet" type="text/css" href="http://localhost:100/style.css">
|
||||
<!-- 400 Bad Request -->
|
||||
<link rel="stylesheet" type="text/css" href="https://httpstat.us/400">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
|
||||
<title>Gutenberg</title>
|
||||
@@ -15,7 +21,7 @@
|
||||
|
||||
<blockquote cite="https://sites.google.com/site/johanngutenbergper5/q">
|
||||
<p>It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.</p>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></cite></footer>
|
||||
<footer>— <a href="https://sites.google.com/site/johanngutenbergper5/q">Johannes Gutenberg</a></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user