mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f89a0db471 | ||
|
|
1d66b43da1 | ||
|
|
75c4a4f253 | ||
|
|
6a9c95b3b1 | ||
|
|
c11a2d0081 | ||
|
|
43a13ff2c5 | ||
|
|
8454d163eb | ||
|
|
4a11f6a39f | ||
|
|
c63235cd3d | ||
|
|
f4ce196a34 | ||
|
|
e644b27201 | ||
|
|
d2b14582ee | ||
|
|
ca9194fd96 | ||
|
|
773d3ab13c | ||
|
|
a02a4a07a5 | ||
|
|
0a8625227f | ||
|
|
dc613aa2bf | ||
|
|
2008ac5663 | ||
|
|
da326f1925 | ||
|
|
71911cb1a7 | ||
|
|
31e7582216 | ||
|
|
91f63047e0 | ||
|
|
c9aa4938f4 | ||
|
|
b9e6a38037 | ||
|
|
eb054146d2 |
@@ -10,20 +10,41 @@ linters-settings:
|
|||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
- decorder
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- dupword
|
||||||
|
- durationcheck
|
||||||
- errcheck
|
- errcheck
|
||||||
|
- errname
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
- gci
|
- gci
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
- gofumpt
|
- gofumpt
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
|
- importas
|
||||||
- misspell
|
- misspell
|
||||||
- prealloc
|
- prealloc
|
||||||
|
- promlinter
|
||||||
|
#- sloglint
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- tenv
|
||||||
|
- testableexamples
|
||||||
|
- tparallel
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 5m
|
timeout: 5m
|
||||||
@@ -31,6 +52,5 @@ run:
|
|||||||
tests: false
|
tests: false
|
||||||
|
|
||||||
output:
|
output:
|
||||||
format: 'colored-line-number'
|
|
||||||
print-issued-lines: true
|
print-issued-lines: true
|
||||||
print-linter-name: true
|
print-linter-name: true
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -33,6 +33,9 @@ API_START_TIMEOUT=30s
|
|||||||
API_TIMEOUT=30s
|
API_TIMEOUT=30s
|
||||||
API_ROOT_PATH=/
|
API_ROOT_PATH=/
|
||||||
API_TRACE_HEADER=Gotenberg-Trace
|
API_TRACE_HEADER=Gotenberg-Trace
|
||||||
|
API_ENABLE_BASIC_AUTH=false
|
||||||
|
GOTENBERG_API_BASIC_AUTH_USERNAME=
|
||||||
|
GOTENBERG_API_BASIC_AUTH_PASSWORD=
|
||||||
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||||
CHROMIUM_RESTART_AFTER=0
|
CHROMIUM_RESTART_AFTER=0
|
||||||
CHROMIUM_MAX_QUEUE_SIZE=0
|
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||||
@@ -79,6 +82,8 @@ WEBHOOK_DISABLE=false
|
|||||||
run: ## Start a Gotenberg container
|
run: ## Start a Gotenberg container
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-p $(API_PORT):$(API_PORT) \
|
-p $(API_PORT):$(API_PORT) \
|
||||||
|
-e GOTENBERG_API_BASIC_AUTH_USERNAME=$(GOTENBERG_API_BASIC_AUTH_USERNAME) \
|
||||||
|
-e GOTENBERG_API_BASIC_AUTH_PASSWORD=$(GOTENBERG_API_BASIC_AUTH_PASSWORD) \
|
||||||
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
|
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
|
||||||
gotenberg \
|
gotenberg \
|
||||||
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||||
@@ -88,6 +93,7 @@ run: ## Start a Gotenberg container
|
|||||||
--api-timeout=$(API_TIMEOUT) \
|
--api-timeout=$(API_TIMEOUT) \
|
||||||
--api-root-path=$(API_ROOT_PATH) \
|
--api-root-path=$(API_ROOT_PATH) \
|
||||||
--api-trace-header=$(API_TRACE_HEADER) \
|
--api-trace-header=$(API_TRACE_HEADER) \
|
||||||
|
--api-enable-basic-auth=$(API_ENABLE_BASIC_AUTH) \
|
||||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||||
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||||
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||||
|
|||||||
@@ -164,19 +164,20 @@ RUN \
|
|||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
# Install PDFtk & QPDF (PDF engines).
|
# Install PDFtk, QPDF & ExifTool (PDF engines).
|
||||||
# See https://github.com/gotenberg/gotenberg/pull/273.
|
# See https://github.com/gotenberg/gotenberg/pull/273.
|
||||||
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
|
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
|
||||||
chmod a+x /usr/bin/pdftk-all.jar &&\
|
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||||
echo '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
echo '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
||||||
chmod +x /usr/bin/pdftk &&\
|
chmod +x /usr/bin/pdftk &&\
|
||||||
apt-get update -qq &&\
|
apt-get update -qq &&\
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf exiftool &&\
|
||||||
# See https://github.com/nextcloud/docker/issues/380.
|
# See https://github.com/nextcloud/docker/issues/380.
|
||||||
mkdir -p /usr/share/man/man1 &&\
|
mkdir -p /usr/share/man/man1 &&\
|
||||||
# Verify installations.
|
# Verify installations.
|
||||||
pdftk --version &&\
|
pdftk --version &&\
|
||||||
qpdf --version &&\
|
qpdf --version &&\
|
||||||
|
exiftool --version &&\
|
||||||
# Cleanup.
|
# Cleanup.
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
@@ -195,6 +196,7 @@ ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin
|
|||||||
ENV UNOCONVERTER_BIN_PATH /usr/bin/unoconverter
|
ENV UNOCONVERTER_BIN_PATH /usr/bin/unoconverter
|
||||||
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
||||||
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
||||||
|
ENV EXIFTOOL_BIN_PATH /usr/bin/exiftool
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
|
|||||||
27
go.mod
27
go.mod
@@ -1,17 +1,18 @@
|
|||||||
module github.com/gotenberg/gotenberg/v8
|
module github.com/gotenberg/gotenberg/v8
|
||||||
|
|
||||||
go 1.22
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexliesenfeld/health v0.8.0
|
github.com/alexliesenfeld/health v0.8.0
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154
|
github.com/barasher/go-exiftool v1.10.0
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20240328024531-fe04f09ede24
|
||||||
github.com/chromedp/chromedp v0.9.5
|
github.com/chromedp/chromedp v0.9.5
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5
|
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||||
github.com/klauspost/compress v1.17.7 // indirect
|
github.com/klauspost/compress v1.17.8 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/labstack/echo/v4 v4.11.4
|
github.com/labstack/echo/v4 v4.11.4
|
||||||
github.com/labstack/gommon v0.4.2
|
github.com/labstack/gommon v0.4.2
|
||||||
@@ -24,15 +25,15 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.19.0
|
github.com/prometheus/client_golang v1.19.0
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.11.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.21.0 // indirect
|
golang.org/x/crypto v0.22.0 // indirect
|
||||||
golang.org/x/image v0.15.0 // indirect
|
golang.org/x/image v0.15.0 // indirect
|
||||||
golang.org/x/net v0.22.0
|
golang.org/x/net v0.24.0
|
||||||
golang.org/x/sync v0.6.0
|
golang.org/x/sync v0.7.0
|
||||||
golang.org/x/sys v0.18.0 // indirect
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
golang.org/x/term v0.18.0
|
golang.org/x/term v0.19.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,12 +42,13 @@ require github.com/dlclark/regexp2 v1.11.0
|
|||||||
require (
|
require (
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.3.2 // indirect
|
github.com/gobwas/ws v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||||
@@ -55,13 +57,14 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.0 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.50.0 // indirect
|
github.com/prometheus/common v0.52.3 // indirect
|
||||||
github.com/prometheus/procfs v0.13.0 // indirect
|
github.com/prometheus/procfs v0.13.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
53
go.sum
53
go.sum
@@ -5,17 +5,20 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
|
|||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
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/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
|
||||||
|
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154 h1:jeAmkzyOAQBPRmZMhX+i/CJv0VViLkHk1nF0qx8s0Mk=
|
github.com/chromedp/cdproto v0.0.0-20240328024531-fe04f09ede24 h1:XLG3KlHtG6Wg75ed/daLltJtcj8VXjw7F9mzYenzFL0=
|
||||||
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20240328024531-fe04f09ede24/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
||||||
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
||||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||||
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
@@ -29,6 +32,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
|||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
||||||
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
@@ -53,8 +58,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
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.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
@@ -98,10 +103,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
|
github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
|
||||||
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
|
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
@@ -113,15 +118,17 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
@@ -134,23 +141,25 @@ 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/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 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
|||||||
35
pkg/gotenberg/env.go
Normal file
35
pkg/gotenberg/env.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringEnv retrieves the value of the environment variable named by the key.
|
||||||
|
// If the variable is present in the environment and not empty, the value is
|
||||||
|
// returned.
|
||||||
|
func StringEnv(key string) (string, error) {
|
||||||
|
val, ok := os.LookupEnv(key)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("environment variable '%s' does not exist", key)
|
||||||
|
}
|
||||||
|
if val == "" {
|
||||||
|
return "", fmt.Errorf("environment variable '%s' is empty", key)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntEnv relies on [StringEnv] and converts the values if it exists and is not
|
||||||
|
// empty.
|
||||||
|
func IntEnv(key string) (int, error) {
|
||||||
|
val, err := StringEnv(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
intVal, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("get int value of environment variable '%s': %w", key, err)
|
||||||
|
}
|
||||||
|
return intVal, nil
|
||||||
|
}
|
||||||
134
pkg/gotenberg/env_test.go
Normal file
134
pkg/gotenberg/env_test.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringEnv(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
key string
|
||||||
|
setEnv func()
|
||||||
|
expectVal string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "non-existing environment variable",
|
||||||
|
key: "NON_EXISTING",
|
||||||
|
expectVal: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "empty environment variable",
|
||||||
|
key: "EMPTY_STRING",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EMPTY_STRING", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
key: "EXISTING_STRING_VALUE",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EXISTING_STRING_VALUE", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: "foo",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
if tc.setEnv != nil {
|
||||||
|
tc.setEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := StringEnv(tc.key)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectVal != val {
|
||||||
|
t.Errorf("expected value '%s' but got '%s'", tc.expectVal, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntEnv(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
key string
|
||||||
|
setEnv func()
|
||||||
|
expectVal int
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "empty environment variable",
|
||||||
|
key: "EMPTY_INT",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EMPTY_INT", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: 0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "non-integer value",
|
||||||
|
key: "NON_INTEGER",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("NON_INTEGER", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: 0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
key: "EXISTING_INT_VALUE",
|
||||||
|
setEnv: func() {
|
||||||
|
err := os.Setenv("EXISTING_INT_VALUE", "123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectVal: 123,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
if tc.setEnv != nil {
|
||||||
|
tc.setEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := IntEnv(tc.key)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectVal != val {
|
||||||
|
t.Errorf("expected value %d but got %d", tc.expectVal, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,8 +35,10 @@ func (mod *ValidatorMock) Validate() error {
|
|||||||
|
|
||||||
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
||||||
type PdfEngineMock struct {
|
type PdfEngineMock struct {
|
||||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
|
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||||
|
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
@@ -47,6 +49,14 @@ func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, fo
|
|||||||
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return engine.ReadMetadataMock(ctx, logger, inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
||||||
type PdfEngineProviderMock struct {
|
type PdfEngineProviderMock struct {
|
||||||
PdfEngineMock func() (PdfEngine, error)
|
PdfEngineMock func() (PdfEngine, error)
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ func TestPDFEngineMock(t *testing.T) {
|
|||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
@@ -66,6 +72,16 @@ func TestPDFEngineMock(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = mock.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineMock.ReadMetadata, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.WriteMetadata(context.Background(), zap.NewNop(), map[string]interface{}{}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineMock.WriteMetadata but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngineProviderMock(t *testing.T) {
|
func TestPDFEngineProviderMock(t *testing.T) {
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ var (
|
|||||||
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
||||||
// PdfEngine interface does not support a requested PDF format conversion.
|
// PdfEngine interface does not support a requested PDF format conversion.
|
||||||
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
|
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
|
||||||
|
|
||||||
|
// ErrPdfEngineMetadataValueNotSupported is returned when a metadata value
|
||||||
|
// is not supported.
|
||||||
|
ErrPdfEngineMetadataValueNotSupported = errors.New("metadata value not supported")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -64,6 +68,12 @@ type PdfEngine interface {
|
|||||||
// Convert transforms a given PDF to the specified formats defined in
|
// Convert transforms a given PDF to the specified formats defined in
|
||||||
// PdfFormats. If no format, it does nothing.
|
// PdfFormats. If no format, it does nothing.
|
||||||
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
|
|
||||||
|
// ReadMetadata extracts the metadata of a given PDF file.
|
||||||
|
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||||
|
|
||||||
|
// WriteMetadata writes the metadata into a given PDF file.
|
||||||
|
WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
// AlphanumericSort implements sort.Interface and helps to sort strings
|
// AlphanumericSort implements sort.Interface and helps to sort strings
|
||||||
// alphanumerically.
|
// alphanumerically.
|
||||||
//
|
//
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/805.
|
// See: https://github.com/gotenberg/gotenberg/issues/805.
|
||||||
type AlphanumericSort []string
|
type AlphanumericSort []string
|
||||||
|
|
||||||
func (s AlphanumericSort) Len() int {
|
func (s AlphanumericSort) Len() int {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -34,6 +32,8 @@ type Api struct {
|
|||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
rootPath string
|
rootPath string
|
||||||
traceHeader string
|
traceHeader string
|
||||||
|
basicAuthUsername string
|
||||||
|
basicAuthPassword string
|
||||||
disableHealthCheckLogging bool
|
disableHealthCheckLogging bool
|
||||||
|
|
||||||
routes []Route
|
routes []Route
|
||||||
@@ -163,8 +163,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
||||||
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
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.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")
|
||||||
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
New: func() gotenberg.Module { return new(Api) },
|
New: func() gotenberg.Module { return new(Api) },
|
||||||
@@ -184,24 +184,28 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
// Port from env?
|
// Port from env?
|
||||||
portEnvVar := flags.MustString("api-port-from-env")
|
portEnvVar := flags.MustString("api-port-from-env")
|
||||||
if portEnvVar != "" {
|
if portEnvVar != "" {
|
||||||
val, ok := os.LookupEnv(portEnvVar)
|
port, err := gotenberg.IntEnv(portEnvVar)
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("environment variable '%s' does not exist", portEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
return fmt.Errorf("environment variable '%s' is empty", portEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := strconv.Atoi(val)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get int value of environment variable '%s': %w", portEnvVar, err)
|
return fmt.Errorf("get API port from env: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.port = port
|
a.port = port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable basic auth?
|
||||||
|
enableBasicAuth := flags.MustBool("api-enable-basic-auth")
|
||||||
|
if enableBasicAuth {
|
||||||
|
basicAuthUsername, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_USERNAME")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get basic auth username from env: %w", err)
|
||||||
|
}
|
||||||
|
basicAuthPassword, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_PASSWORD")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get basic auth password from env: %w", err)
|
||||||
|
}
|
||||||
|
a.basicAuthUsername = basicAuthUsername
|
||||||
|
a.basicAuthPassword = basicAuthPassword
|
||||||
|
}
|
||||||
|
|
||||||
// Get routes from modules.
|
// Get routes from modules.
|
||||||
mods, err := ctx.Modules(new(Router))
|
mods, err := ctx.Modules(new(Router))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -394,6 +398,13 @@ func (a *Api) Start() error {
|
|||||||
loggerMiddleware(a.logger, disableLoggingForPaths),
|
loggerMiddleware(a.logger, disableLoggingForPaths),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Basic auth?
|
||||||
|
if a.basicAuthUsername != "" {
|
||||||
|
a.srv.Pre(
|
||||||
|
basicAuthMiddleware(a.basicAuthUsername, a.basicAuthPassword),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Add the modules' middlewares in their respective stacks.
|
// Add the modules' middlewares in their respective stacks.
|
||||||
var externalMultipartMiddlewares []Middleware
|
var externalMultipartMiddlewares []Middleware
|
||||||
for _, externalMiddleware := range a.externalMiddlewares {
|
for _, externalMiddleware := range a.externalMiddlewares {
|
||||||
@@ -402,7 +413,7 @@ func (a *Api) Start() error {
|
|||||||
a.srv.Pre(externalMiddleware.Handler)
|
a.srv.Pre(externalMiddleware.Handler)
|
||||||
case MultipartStack:
|
case MultipartStack:
|
||||||
externalMultipartMiddlewares = append(externalMultipartMiddlewares, externalMiddleware)
|
externalMultipartMiddlewares = append(externalMultipartMiddlewares, externalMiddleware)
|
||||||
default:
|
case DefaultStack:
|
||||||
a.srv.Use(externalMiddleware.Handler)
|
a.srv.Use(externalMiddleware.Handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,28 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "port from env: empty environment variable",
|
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_USERNAME environment variable",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
fs := new(Api).Descriptor().FlagSet
|
fs := new(Api).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
err := fs.Parse([]string{"--api-enable-basic-auth=true"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: fs,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_PASSWORD environment variable",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
fs := new(Api).Descriptor().FlagSet
|
||||||
|
err := fs.Parse([]string{"--api-enable-basic-auth=true"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -74,7 +92,7 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
setEnv: func() {
|
setEnv: func() {
|
||||||
err := os.Setenv("PORT", "")
|
err := os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -361,7 +379,7 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs := new(Api).Descriptor().FlagSet
|
fs := new(Api).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
err := fs.Parse([]string{"--api-port-from-env=PORT", "--api-enable-basic-auth=true"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -383,6 +401,14 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_PASSWORD", "bar")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
expectPort: 1337,
|
expectPort: 1337,
|
||||||
expectMiddlewares: []Middleware{
|
expectMiddlewares: []Middleware{
|
||||||
@@ -671,6 +697,8 @@ func TestApi_Start(t *testing.T) {
|
|||||||
mod.port = 3000
|
mod.port = 3000
|
||||||
mod.startTimeout = time.Duration(30) * time.Second
|
mod.startTimeout = time.Duration(30) * time.Second
|
||||||
mod.rootPath = "/"
|
mod.rootPath = "/"
|
||||||
|
mod.basicAuthUsername = "foo"
|
||||||
|
mod.basicAuthPassword = "bar"
|
||||||
mod.disableHealthCheckLogging = true
|
mod.disableHealthCheckLogging = true
|
||||||
mod.routes = []Route{
|
mod.routes = []Route{
|
||||||
{
|
{
|
||||||
@@ -755,6 +783,7 @@ func TestApi_Start(t *testing.T) {
|
|||||||
// health request.
|
// health request.
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
|
healthRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||||
|
|
||||||
mod.srv.ServeHTTP(recorder, healthRequest)
|
mod.srv.ServeHTTP(recorder, healthRequest)
|
||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
@@ -791,6 +820,7 @@ func TestApi_Start(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, url, body)
|
req := httptest.NewRequest(http.MethodPost, url, body)
|
||||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||||
|
req.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
|||||||
|
|
||||||
form, err := echoCtx.MultipartForm()
|
form, err := echoCtx.MultipartForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
if errors.Is(err, http.ErrNotMultipart) {
|
if errors.Is(err, http.ErrNotMultipart) {
|
||||||
return nil, cancel, WrapError(
|
return nil, cancel, WrapError(
|
||||||
fmt.Errorf("get multipart form: %w", err),
|
fmt.Errorf("get multipart form: %w", err),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -145,6 +146,91 @@ func (form *FormData) MandatoryDuration(key string, target *time.Duration) *Form
|
|||||||
return form.mustMandatoryField(key, target)
|
return form.mustMandatoryField(key, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inches binds a form field to a float64 variable. It populates an error
|
||||||
|
// if the value cannot be computed back to inches.
|
||||||
|
//
|
||||||
|
// var foo float64
|
||||||
|
//
|
||||||
|
// ctx.FormData().Inches("foo", &foo, 2.0)
|
||||||
|
func (form *FormData) Inches(key string, target *float64, defaultValue float64) *FormData {
|
||||||
|
form.inches(key, target)
|
||||||
|
if *target == -math.MaxFloat64 {
|
||||||
|
*target = defaultValue
|
||||||
|
}
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
// MandatoryInches binds a form field to a float64 variable. It populates
|
||||||
|
// an error if the value cannot be computed back to inches, is empty, or the
|
||||||
|
// "key" does not exist.
|
||||||
|
//
|
||||||
|
// var foo float64
|
||||||
|
//
|
||||||
|
// ctx.FormData().MandatoryInches("foo", &foo)
|
||||||
|
func (form *FormData) MandatoryInches(key string, target *float64) *FormData {
|
||||||
|
val, ok := form.values[key]
|
||||||
|
if !ok || val[0] == "" {
|
||||||
|
form.append(
|
||||||
|
fmt.Errorf("form field '%s' is required", key),
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
return form.inches(key, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inches tries to compute a string value to inches.
|
||||||
|
func (form *FormData) inches(key string, target *float64) *FormData {
|
||||||
|
var value string
|
||||||
|
form.mustValue(key, &value, "")
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
*target = -math.MaxFloat64
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, unit := range []string{"pt", "px", "in", "mm", "cm", "pc"} {
|
||||||
|
if !strings.HasSuffix(value, unit) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := strconv.ParseFloat(strings.TrimSuffix(value, unit), 64)
|
||||||
|
if err != nil {
|
||||||
|
form.append(
|
||||||
|
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
switch unit {
|
||||||
|
case "pt":
|
||||||
|
*target = val * (1.0 / 72.0)
|
||||||
|
case "px":
|
||||||
|
*target = val * (1.0 / 96.0)
|
||||||
|
case "in":
|
||||||
|
*target = val
|
||||||
|
case "mm":
|
||||||
|
*target = val * (1.0 / 25.4)
|
||||||
|
case "cm":
|
||||||
|
*target = val * (1.0 / 2.54)
|
||||||
|
case "pc":
|
||||||
|
*target = val * (1.0 / 6.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err != nil {
|
||||||
|
form.append(
|
||||||
|
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
*target = val
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
// Custom helps to define a custom binding function for a form field.
|
// Custom helps to define a custom binding function for a form field.
|
||||||
//
|
//
|
||||||
// var foo map[string]string
|
// var foo map[string]string
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -768,6 +769,333 @@ func TestFormData_MandatoryDuration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormData_Inches(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
form *FormData
|
||||||
|
defaultValue float64
|
||||||
|
expect float64
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "key does not exist, fallback to default zero value",
|
||||||
|
form: &FormData{},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does not exist, fallback to default value",
|
||||||
|
form: &FormData{},
|
||||||
|
defaultValue: 2.5,
|
||||||
|
expect: 2.5,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist, but empty value, fallback to default value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist, value has a unit, but the rest is not float64 compatible",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foomm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist, but value has no unit and is invalid",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a pt value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"72pt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a px value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"96px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with an in value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"1in",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a mm value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"25.4mm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a cm value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"2.54cm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a pc value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"6pc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with no unit in the value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 100,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
var actual float64
|
||||||
|
|
||||||
|
tc.form.Inches("foo", &actual, tc.defaultValue)
|
||||||
|
|
||||||
|
if fmt.Sprintf("%.1f", actual) != fmt.Sprintf("%.1f", tc.expect) {
|
||||||
|
t.Errorf("expected %.1f but got %.1f", tc.expect, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && tc.form.errors == nil {
|
||||||
|
t.Fatal("expected error but got none", tc.form.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && tc.form.errors != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", tc.form.errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormData_MandatoryInches(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
form *FormData
|
||||||
|
expect float64
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing mandatory key",
|
||||||
|
form: &FormData{},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "mandatory value is empty",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "mandatory value has a unit, but the rest is not float64 compatible",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foomm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "mandatory value has no unit and is invalid",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a pt mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"72pt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a px mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"96px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "an in mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"1in",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a mm mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"25.4mm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a cm mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"2.54cm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a pc mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"6pc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no unit in the mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 100,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
var actual float64
|
||||||
|
|
||||||
|
tc.form.MandatoryInches("foo", &actual)
|
||||||
|
|
||||||
|
if fmt.Sprintf("%.1f", actual) != fmt.Sprintf("%.1f", tc.expect) {
|
||||||
|
t.Errorf("expected %.1f but got %.1f", tc.expect, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && tc.form.errors == nil {
|
||||||
|
t.Fatal("expected error but got none", tc.form.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && tc.form.errors != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", tc.form.errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormData_Custom(t *testing.T) {
|
func TestFormData_Custom(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,14 +11,21 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrAsyncProcess happens when a handler or middleware handles a request in an
|
var (
|
||||||
// asynchronous fashion.
|
// ErrAsyncProcess happens when a handler or middleware handles a request
|
||||||
var ErrAsyncProcess = errors.New("async process")
|
// in an asynchronous fashion.
|
||||||
|
ErrAsyncProcess = errors.New("async process")
|
||||||
|
|
||||||
|
// ErrNoOutputFile happens when a handler or middleware handles a request
|
||||||
|
// without sending any output file.
|
||||||
|
ErrNoOutputFile = errors.New("no output file")
|
||||||
|
)
|
||||||
|
|
||||||
// ParseError parses an error and returns the corresponding HTTP status and
|
// ParseError parses an error and returns the corresponding HTTP status and
|
||||||
// HTTP message.
|
// HTTP message.
|
||||||
@@ -44,6 +52,10 @@ func ParseError(err error) (int, string) {
|
|||||||
return http.StatusBadRequest, "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues"
|
return http.StatusBadRequest, "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
|
||||||
|
return http.StatusBadRequest, "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues"
|
||||||
|
}
|
||||||
|
|
||||||
var httpErr HttpError
|
var httpErr HttpError
|
||||||
if errors.As(err, &httpErr) {
|
if errors.As(err, &httpErr) {
|
||||||
return httpErr.HttpError()
|
return httpErr.HttpError()
|
||||||
@@ -102,7 +114,6 @@ func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
|
|||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
c.Set("rootPath", rootPath)
|
c.Set("rootPath", rootPath)
|
||||||
|
|
||||||
// Call the next middleware in the chain.
|
// Call the next middleware in the chain.
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
@@ -207,6 +218,17 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// basicAuthMiddleware manages basic authentication.
|
||||||
|
func basicAuthMiddleware(username, password string) echo.MiddlewareFunc {
|
||||||
|
return middleware.BasicAuth(func(u string, p string, e echo.Context) (bool, error) {
|
||||||
|
if subtle.ConstantTimeCompare([]byte(u), []byte(username)) == 1 &&
|
||||||
|
subtle.ConstantTimeCompare([]byte(p), []byte(password)) == 1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
|
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
|
||||||
// [Context] and related context.CancelFunc in the [echo.Context] under
|
// [Context] and related context.CancelFunc in the [echo.Context] under
|
||||||
// "context" and "cancel". If the process is synchronous, it also handles the
|
// "context" and "cancel". If the process is synchronous, it also handles the
|
||||||
@@ -242,6 +264,13 @@ func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration) echo.Mid
|
|||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
if errors.Is(err, ErrNoOutputFile) {
|
||||||
|
// A middleware/handler tells us that it's handling the process
|
||||||
|
// in an asynchronous fashion. Therefore, we must not cancel
|
||||||
|
// the context nor send an output file.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ func TestParseError(t *testing.T) {
|
|||||||
expectStatus: http.StatusBadRequest,
|
expectStatus: http.StatusBadRequest,
|
||||||
expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues",
|
expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
err: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||||
|
expectStatus: http.StatusBadRequest,
|
||||||
|
expectMessage: "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
err: WrapError(
|
err: WrapError(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
@@ -231,6 +236,56 @@ func TestTraceMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBasicAuthMiddleware(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
request *http.Request
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "invalid basic auth",
|
||||||
|
request: func() *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.SetBasicAuth("invalid", "invalid")
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
username: "foo",
|
||||||
|
password: "bar",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "valid basic auth",
|
||||||
|
request: func() *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.SetBasicAuth("foo", "bar")
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
username: "foo",
|
||||||
|
password: "bar",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
srv := echo.New()
|
||||||
|
srv.HideBanner = true
|
||||||
|
srv.HidePort = true
|
||||||
|
c := srv.NewContext(tc.request, recorder)
|
||||||
|
err := basicAuthMiddleware(tc.username, tc.password)(func(c echo.Context) error {
|
||||||
|
return nil
|
||||||
|
})(c)
|
||||||
|
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 TestLoggerMiddleware(t *testing.T) {
|
func TestLoggerMiddleware(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
request *http.Request
|
request *http.Request
|
||||||
@@ -330,6 +385,15 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
expectStatus: http.StatusNoContent,
|
expectStatus: http.StatusNoContent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
request: buildMultipartFormDataRequest(),
|
||||||
|
next: func() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return ErrNoOutputFile
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
request: buildMultipartFormDataRequest(),
|
request: buildMultipartFormDataRequest(),
|
||||||
next: func() echo.HandlerFunc {
|
next: func() echo.HandlerFunc {
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
clearCacheActionFunc(logger, b.arguments.clearCache),
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
setCookiesActionFunc(logger, options.Cookies),
|
||||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||||
@@ -248,6 +249,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
|
|||||||
clearCacheActionFunc(logger, b.arguments.clearCache),
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
setCookiesActionFunc(logger, options.Cookies),
|
||||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||||
|
|||||||
@@ -579,6 +579,41 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
|||||||
"JavaScript disabled, skipping wait expression",
|
"JavaScript disabled, skipping wait expression",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "set cookies",
|
||||||
|
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>Set cookies</h1>"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: PdfOptions{
|
||||||
|
Options: Options{Cookies: []Cookie{{Name: "foo", Value: "bar", Domain: ".foo.bar"}}},
|
||||||
|
},
|
||||||
|
noDeadline: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
expectedLogEntries: []string{
|
||||||
|
"set cookie",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "extra HTTP headers",
|
scenario: "extra HTTP headers",
|
||||||
browser: newChromiumBrowser(
|
browser: newChromiumBrowser(
|
||||||
@@ -1125,6 +1160,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
|||||||
"cache not cleared",
|
"cache not cleared",
|
||||||
"cookies not cleared",
|
"cookies not cleared",
|
||||||
"JavaScript not disabled",
|
"JavaScript not disabled",
|
||||||
|
"no cookies to set",
|
||||||
"no extra HTTP headers",
|
"no extra HTTP headers",
|
||||||
"navigate to",
|
"navigate to",
|
||||||
"default white background not hidden",
|
"default white background not hidden",
|
||||||
@@ -1549,6 +1585,41 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
|||||||
"JavaScript disabled, skipping wait expression",
|
"JavaScript disabled, skipping wait expression",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "set cookies",
|
||||||
|
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>Set cookies</h1>"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: ScreenshotOptions{
|
||||||
|
Options: Options{Cookies: []Cookie{{Name: "fpp", Value: "bar", Domain: ".foo.bar"}}},
|
||||||
|
},
|
||||||
|
noDeadline: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
expectedLogEntries: []string{
|
||||||
|
"set cookie",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "extra HTTP headers",
|
scenario: "extra HTTP headers",
|
||||||
browser: newChromiumBrowser(
|
browser: newChromiumBrowser(
|
||||||
@@ -1964,6 +2035,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
|||||||
"cache not cleared",
|
"cache not cleared",
|
||||||
"cookies not cleared",
|
"cookies not cleared",
|
||||||
"JavaScript not disabled",
|
"JavaScript not disabled",
|
||||||
|
"no cookies to set",
|
||||||
"no extra HTTP headers",
|
"no extra HTTP headers",
|
||||||
"navigate to",
|
"navigate to",
|
||||||
"default white background not hidden",
|
"default white background not hidden",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
|
"github.com/chromedp/cdproto/network"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
@@ -103,6 +104,10 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
WaitForExpression string
|
WaitForExpression string
|
||||||
|
|
||||||
|
// Cookies are the cookies to put in the Chromium cookies' jar.
|
||||||
|
// Optional
|
||||||
|
Cookies []Cookie
|
||||||
|
|
||||||
// ExtraHttpHeaders are the HTTP headers to send by Chromium while loading
|
// ExtraHttpHeaders are the HTTP headers to send by Chromium while loading
|
||||||
// the HTML document.
|
// the HTML document.
|
||||||
// Optional.
|
// Optional.
|
||||||
@@ -128,6 +133,7 @@ func DefaultOptions() Options {
|
|||||||
WaitDelay: 0,
|
WaitDelay: 0,
|
||||||
WaitWindowStatus: "",
|
WaitWindowStatus: "",
|
||||||
WaitForExpression: "",
|
WaitForExpression: "",
|
||||||
|
Cookies: nil,
|
||||||
ExtraHttpHeaders: nil,
|
ExtraHttpHeaders: nil,
|
||||||
EmulatedMediaType: "",
|
EmulatedMediaType: "",
|
||||||
OmitBackground: false,
|
OmitBackground: false,
|
||||||
@@ -258,6 +264,38 @@ func DefaultScreenshotOptions() ScreenshotOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cookie gathers the available entries for setting a cookie in the Chromium
|
||||||
|
// cookies' jar.
|
||||||
|
type Cookie struct {
|
||||||
|
// Name is the cookie name.
|
||||||
|
// Required.
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Value is the cookie value.
|
||||||
|
// Required.
|
||||||
|
Value string `json:"value"`
|
||||||
|
|
||||||
|
// Domain is the cookie domain.
|
||||||
|
// Required.
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
|
||||||
|
// Path is the cookie path.
|
||||||
|
// Optional.
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
|
||||||
|
// Secure sets the cookie secure if true.
|
||||||
|
// Optional.
|
||||||
|
Secure bool `json:"secure,omitempty"`
|
||||||
|
|
||||||
|
// HttpOnly sets the cookie as HTTP-only if true.
|
||||||
|
// Optional.
|
||||||
|
HttpOnly bool `json:"httpOnly,omitempty"`
|
||||||
|
|
||||||
|
// SameSite is cookie 'Same-Site' status.
|
||||||
|
// Optional.
|
||||||
|
SameSite network.CookieSameSite `json:"sameSite,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Api helps to interact with Chromium for converting HTML documents to PDF.
|
// Api helps to interact with Chromium for converting HTML documents to PDF.
|
||||||
type Api interface {
|
type Api interface {
|
||||||
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
waitDelay time.Duration
|
waitDelay time.Duration
|
||||||
waitWindowStatus string
|
waitWindowStatus string
|
||||||
waitForExpression string
|
waitForExpression string
|
||||||
|
cookies []Cookie
|
||||||
extraHttpHeaders map[string]string
|
extraHttpHeaders map[string]string
|
||||||
emulatedMediaType string
|
emulatedMediaType string
|
||||||
omitBackground bool
|
omitBackground bool
|
||||||
@@ -58,6 +59,25 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||||
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||||
|
Custom("cookies", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
cookies = defaultOptions.Cookies
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(value), &cookies)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, cookie := range cookies {
|
||||||
|
if strings.TrimSpace(cookie.Name) == "" || strings.TrimSpace(cookie.Value) == "" || strings.TrimSpace(cookie.Domain) == "" {
|
||||||
|
err = multierr.Append(err, fmt.Errorf("cookie %d must have its name, value and domain set", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}).
|
||||||
Custom("extraHttpHeaders", func(value string) error {
|
Custom("extraHttpHeaders", func(value string) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
|
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
|
||||||
@@ -78,7 +98,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if value != "screen" && value != "print" {
|
if value != "screen" && value != "print" {
|
||||||
return fmt.Errorf("wrong value, expected either 'screen', 'print' or empty")
|
return errors.New("wrong value, expected either 'screen', 'print' or empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
emulatedMediaType = value
|
emulatedMediaType = value
|
||||||
@@ -94,6 +114,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
WaitDelay: waitDelay,
|
WaitDelay: waitDelay,
|
||||||
WaitWindowStatus: waitWindowStatus,
|
WaitWindowStatus: waitWindowStatus,
|
||||||
WaitForExpression: waitForExpression,
|
WaitForExpression: waitForExpression,
|
||||||
|
Cookies: cookies,
|
||||||
ExtraHttpHeaders: extraHttpHeaders,
|
ExtraHttpHeaders: extraHttpHeaders,
|
||||||
EmulatedMediaType: emulatedMediaType,
|
EmulatedMediaType: emulatedMediaType,
|
||||||
OmitBackground: omitBackground,
|
OmitBackground: omitBackground,
|
||||||
@@ -122,12 +143,12 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
|||||||
Bool("printBackground", &printBackground, defaultPdfOptions.PrintBackground).
|
Bool("printBackground", &printBackground, defaultPdfOptions.PrintBackground).
|
||||||
Float64("scale", &scale, defaultPdfOptions.Scale).
|
Float64("scale", &scale, defaultPdfOptions.Scale).
|
||||||
Bool("singlePage", &singlePage, defaultPdfOptions.SinglePage).
|
Bool("singlePage", &singlePage, defaultPdfOptions.SinglePage).
|
||||||
Float64("paperWidth", &paperWidth, defaultPdfOptions.PaperWidth).
|
Inches("paperWidth", &paperWidth, defaultPdfOptions.PaperWidth).
|
||||||
Float64("paperHeight", &paperHeight, defaultPdfOptions.PaperHeight).
|
Inches("paperHeight", &paperHeight, defaultPdfOptions.PaperHeight).
|
||||||
Float64("marginTop", &marginTop, defaultPdfOptions.MarginTop).
|
Inches("marginTop", &marginTop, defaultPdfOptions.MarginTop).
|
||||||
Float64("marginBottom", &marginBottom, defaultPdfOptions.MarginBottom).
|
Inches("marginBottom", &marginBottom, defaultPdfOptions.MarginBottom).
|
||||||
Float64("marginLeft", &marginLeft, defaultPdfOptions.MarginLeft).
|
Inches("marginLeft", &marginLeft, defaultPdfOptions.MarginLeft).
|
||||||
Float64("marginRight", &marginRight, defaultPdfOptions.MarginRight).
|
Inches("marginRight", &marginRight, defaultPdfOptions.MarginRight).
|
||||||
String("nativePageRanges", &pageRanges, defaultPdfOptions.PageRanges).
|
String("nativePageRanges", &pageRanges, defaultPdfOptions.PageRanges).
|
||||||
Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate).
|
Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate).
|
||||||
Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).
|
Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).
|
||||||
@@ -233,6 +254,21 @@ func FormDataChromiumPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormDataPdfMetadata creates metadata object from the form data.
|
||||||
|
func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
|
||||||
|
var metadata map[string]interface{}
|
||||||
|
form.Custom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
|
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
|
||||||
func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
@@ -243,6 +279,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(form)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
metadata := FormDataPdfMetadata(form)
|
||||||
|
|
||||||
var url string
|
var url string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -252,7 +289,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -302,6 +339,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(form)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
metadata := FormDataPdfMetadata(form)
|
||||||
|
|
||||||
var inputPath string
|
var inputPath string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -312,7 +350,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("file://%s", inputPath)
|
url := fmt.Sprintf("file://%s", inputPath)
|
||||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -363,6 +401,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(form)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
metadata := FormDataPdfMetadata(form)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
inputPath string
|
inputPath string
|
||||||
@@ -382,7 +421,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -506,7 +545,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
|
|||||||
return fmt.Sprintf("file://%s", inputPath), nil
|
return fmt.Sprintf("file://%s", inputPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options PdfOptions) error {
|
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}) error {
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
||||||
@@ -562,6 +601,14 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
outputPath = convertOutputPath
|
outputPath = convertOutputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes and potentially overrides metadata entries, if any.
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = ctx.AddOutputPaths(outputPath)
|
err = ctx.AddOutputPaths(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add output path: %w", err)
|
return fmt.Errorf("add output path: %w", err)
|
||||||
|
|||||||
@@ -62,6 +62,59 @@ func TestFormDataChromiumOptions(t *testing.T) {
|
|||||||
return options
|
return options
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid cookies form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"cookies": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectedOptions: DefaultOptions(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid cookies form field (missing required values)",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"cookies": {
|
||||||
|
"[{}]",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectedOptions: func() Options {
|
||||||
|
options := DefaultOptions()
|
||||||
|
// No validation in this method, so it still instantiates
|
||||||
|
// an empty item.
|
||||||
|
options.Cookies = []Cookie{{}}
|
||||||
|
return options
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "valid cookies form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"cookies": {
|
||||||
|
`[{"name":"foo","value":"bar","domain":".foo.bar"}]`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectedOptions: func() Options {
|
||||||
|
options := DefaultOptions()
|
||||||
|
options.Cookies = []Cookie{{
|
||||||
|
Name: "foo",
|
||||||
|
Value: "bar",
|
||||||
|
Domain: ".foo.bar",
|
||||||
|
}}
|
||||||
|
return options
|
||||||
|
}(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "invalid extraHttpHeaders form field",
|
scenario: "invalid extraHttpHeaders form field",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -399,6 +452,57 @@ func TestFormDataChromiumPdfFormats(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormDataPdfMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *api.ContextMock
|
||||||
|
expectedMetadata map[string]interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no metadata form field",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
expectedMetadata: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectedMetadata: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "valid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"foo\":\"bar\"}",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectedMetadata: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
actual := FormDataPdfMetadata(tc.ctx.Context.FormData())
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
|
||||||
|
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConvertUrlRoute(t *testing.T) {
|
func TestConvertUrlRoute(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
@@ -1223,8 +1327,9 @@ func TestConvertUrl(t *testing.T) {
|
|||||||
ctx *api.ContextMock
|
ctx *api.ContextMock
|
||||||
api Api
|
api Api
|
||||||
engine gotenberg.PdfEngine
|
engine gotenberg.PdfEngine
|
||||||
pdfFormats gotenberg.PdfFormats
|
|
||||||
options PdfOptions
|
options PdfOptions
|
||||||
|
pdfFormats gotenberg.PdfFormats
|
||||||
|
metadata map[string]interface{}
|
||||||
expectError bool
|
expectError bool
|
||||||
expectHttpError bool
|
expectHttpError bool
|
||||||
expectHttpStatus int
|
expectHttpStatus int
|
||||||
@@ -1330,7 +1435,7 @@ func TestConvertUrl(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "error from PDF engine",
|
scenario: "PDF engine convert error",
|
||||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return nil
|
return nil
|
||||||
@@ -1338,14 +1443,14 @@ func TestConvertUrl(t *testing.T) {
|
|||||||
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return errors.New("foo")
|
return errors.New("foo")
|
||||||
}},
|
}},
|
||||||
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
|
|
||||||
options: DefaultPdfOptions(),
|
options: DefaultPdfOptions(),
|
||||||
|
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with pdfa form field",
|
scenario: "success with PDF formats",
|
||||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return nil
|
return nil
|
||||||
@@ -1353,12 +1458,29 @@ func TestConvertUrl(t *testing.T) {
|
|||||||
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
|
|
||||||
options: DefaultPdfOptions(),
|
options: DefaultPdfOptions(),
|
||||||
|
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine write metadata error",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
engine: &gotenberg.PdfEngineMock{WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}},
|
||||||
|
options: DefaultPdfOptions(),
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"Creator": "foo",
|
||||||
|
"Producer": "bar",
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot add output paths",
|
scenario: "cannot add output paths",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -1380,7 +1502,20 @@ func TestConvertUrl(t *testing.T) {
|
|||||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
options: DefaultPdfOptions(),
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: DefaultPdfOptions(),
|
||||||
|
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"Creator": "foo",
|
||||||
|
"Producer": "bar",
|
||||||
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
@@ -1388,7 +1523,7 @@ func TestConvertUrl(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
tc.ctx.SetLogger(zap.NewNop())
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.pdfFormats, tc.options)
|
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.options, tc.pdfFormats, tc.metadata)
|
||||||
|
|
||||||
if tc.expectError && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Fatal("expected error but got none", err)
|
t.Fatal("expected error but got none", err)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package chromium
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@@ -210,6 +211,55 @@ func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.Acti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setCookiesActionFunc(logger *zap.Logger, cookies []Cookie) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
if len(cookies) == 0 {
|
||||||
|
logger.Debug("no cookies to set")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline, ok := ctx.Deadline()
|
||||||
|
if !ok {
|
||||||
|
return errors.New("context has no deadline, cannot set cookies")
|
||||||
|
}
|
||||||
|
epochTime := cdp.TimeSinceEpoch(deadline)
|
||||||
|
|
||||||
|
cookiePretty := func(c *network.SetCookieParams) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Name: '%s', Value: '%s', Domain: '%s', Path: '%s', Secure: %t, HTTPOnly: %t, SameSite: '%s', Expires: %s",
|
||||||
|
c.Name,
|
||||||
|
c.Value,
|
||||||
|
c.Domain,
|
||||||
|
c.Path,
|
||||||
|
c.Secure,
|
||||||
|
c.HTTPOnly,
|
||||||
|
c.SameSite.String(),
|
||||||
|
c.Expires.Time().String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
cookieParams := network.
|
||||||
|
SetCookie(cookie.Name, cookie.Value).
|
||||||
|
WithDomain(cookie.Domain).
|
||||||
|
WithPath(cookie.Path).
|
||||||
|
WithSecure(cookie.Secure).
|
||||||
|
WithHTTPOnly(cookie.HttpOnly).
|
||||||
|
WithSameSite(cookie.SameSite).
|
||||||
|
WithExpires(&epochTime)
|
||||||
|
|
||||||
|
err := cookieParams.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("set cookie %s: %w", cookiePretty(cookieParams), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("set cookie %s", cookiePretty(cookieParams)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if len(extraHttpHeaders) == 0 {
|
if len(extraHttpHeaders) == 0 {
|
||||||
|
|||||||
11
pkg/modules/exiftool/doc.go
Normal file
11
pkg/modules/exiftool/doc.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Package exiftool provides an implementation of the gotenberg.PdfEngine
|
||||||
|
// interface using the ExifTool command-line tool. This package allows for:
|
||||||
|
//
|
||||||
|
// 1. The reading of metadata.
|
||||||
|
// 2. The writing of metadata.
|
||||||
|
//
|
||||||
|
// The path to the exiftool binary must be specified using the
|
||||||
|
// EXIFTOOL_BIN_PATH environment variable.
|
||||||
|
//
|
||||||
|
// See: https://exiftool.org.
|
||||||
|
package exiftool
|
||||||
143
pkg/modules/exiftool/exiftool.go
Normal file
143
pkg/modules/exiftool/exiftool.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package exiftool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/barasher/go-exiftool"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gotenberg.MustRegisterModule(new(ExifTool))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExifTool abstracts the CLI tool ExifTool and implements the
|
||||||
|
// [gotenberg.PdfEngine] interface.
|
||||||
|
type ExifTool struct {
|
||||||
|
binPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor returns [ExifTool]'s module descriptor.
|
||||||
|
func (engine *ExifTool) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{
|
||||||
|
ID: "exiftool",
|
||||||
|
New: func() gotenberg.Module { return new(ExifTool) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets the module properties.
|
||||||
|
func (engine *ExifTool) Provision(ctx *gotenberg.Context) error {
|
||||||
|
binPath, ok := os.LookupEnv("EXIFTOOL_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("EXIFTOOL_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.binPath = binPath
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the module properties.
|
||||||
|
func (engine *ExifTool) Validate() error {
|
||||||
|
_, err := os.Stat(engine.binPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("ExifTool binary path does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge is not available in this implementation.
|
||||||
|
func (engine *ExifTool) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return fmt.Errorf("merge PDFs with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert is not available in this implementation.
|
||||||
|
func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMetadata extracts the metadata of a given PDF file.
|
||||||
|
func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new ExifTool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(exifTool *exiftool.Exiftool) {
|
||||||
|
err := exifTool.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close ExifTool: %v", err))
|
||||||
|
}
|
||||||
|
}(exifTool)
|
||||||
|
|
||||||
|
fileMetadata := exifTool.ExtractMetadata(inputPath)
|
||||||
|
if fileMetadata[0].Err != nil {
|
||||||
|
return nil, fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileMetadata[0].Fields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata writes the metadata into a given PDF file.
|
||||||
|
func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new ExifTool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(exifTool *exiftool.Exiftool) {
|
||||||
|
err := exifTool.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close ExifTool: %v", err))
|
||||||
|
}
|
||||||
|
}(exifTool)
|
||||||
|
|
||||||
|
fileMetadata := exifTool.ExtractMetadata(inputPath)
|
||||||
|
if fileMetadata[0].Err != nil {
|
||||||
|
return fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range metadata {
|
||||||
|
switch val := value.(type) {
|
||||||
|
case string:
|
||||||
|
fileMetadata[0].SetString(key, val)
|
||||||
|
case []string:
|
||||||
|
fileMetadata[0].SetStrings(key, val)
|
||||||
|
case bool:
|
||||||
|
fileMetadata[0].SetString(key, fmt.Sprintf("%t", val))
|
||||||
|
case int:
|
||||||
|
fileMetadata[0].SetInt(key, int64(val))
|
||||||
|
case int64:
|
||||||
|
fileMetadata[0].SetInt(key, val)
|
||||||
|
case float32:
|
||||||
|
fileMetadata[0].SetFloat(key, float64(val))
|
||||||
|
case float64:
|
||||||
|
fileMetadata[0].SetFloat(key, val)
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exifTool.WriteMetadata(fileMetadata)
|
||||||
|
if fileMetadata[0].Err != nil {
|
||||||
|
return fmt.Errorf("write PDF metadata with ExifTool: %w", fileMetadata[0].Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*ExifTool)(nil)
|
||||||
|
_ gotenberg.Provisioner = (*ExifTool)(nil)
|
||||||
|
_ gotenberg.Validator = (*ExifTool)(nil)
|
||||||
|
_ gotenberg.PdfEngine = (*ExifTool)(nil)
|
||||||
|
)
|
||||||
322
pkg/modules/exiftool/exiftool_test.go
Normal file
322
pkg/modules/exiftool/exiftool_test.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package exiftool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExifTool_Descriptor(t *testing.T) {
|
||||||
|
descriptor := new(ExifTool).Descriptor()
|
||||||
|
|
||||||
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
|
expect := reflect.TypeOf(new(ExifTool))
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExifTool_Provision(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||||
|
|
||||||
|
err := engine.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExifTool_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("EXIFTOOL_BIN_PATH"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
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 TestExiftool_Merge(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExiftool_Convert(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExiftool_ReadMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
inputPath string
|
||||||
|
expectMetadata map[string]interface{}
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "invalid input path",
|
||||||
|
inputPath: "foo",
|
||||||
|
expectMetadata: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
expectMetadata: map[string]interface{}{
|
||||||
|
"FileName": "sample1.pdf",
|
||||||
|
"FileTypeExtension": "pdf",
|
||||||
|
"MIMEType": "application/pdf",
|
||||||
|
"PDFVersion": 1.4,
|
||||||
|
"PageCount": float64(3),
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Provision(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), tc.inputPath)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectMetadata != nil && err == nil {
|
||||||
|
for k, v := range tc.expectMetadata {
|
||||||
|
if v2, ok := metadata[k]; !ok || v != v2 {
|
||||||
|
t.Errorf("expected entry %s with value %v to exists", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExiftool_WriteMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
createCopy bool
|
||||||
|
inputPath string
|
||||||
|
metadata map[string]interface{}
|
||||||
|
expectMetadata map[string]interface{}
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "invalid input path",
|
||||||
|
createCopy: false,
|
||||||
|
inputPath: "foo",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported",
|
||||||
|
createCopy: true,
|
||||||
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"Unsupported": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
createCopy: true,
|
||||||
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"Author": "Julien Neuhart",
|
||||||
|
"Copyright": "Julien Neuhart",
|
||||||
|
"CreationDate": "2006-09-18T16:27:50-04:00",
|
||||||
|
"Creator": "Gotenberg",
|
||||||
|
"Keywords": []string{
|
||||||
|
"first",
|
||||||
|
"second",
|
||||||
|
},
|
||||||
|
"Marked": true,
|
||||||
|
"ModDate": "2006-09-18T16:27:50-04:00",
|
||||||
|
"PDFVersion": 1.7,
|
||||||
|
"Producer": "Gotenberg",
|
||||||
|
"Subject": "Sample",
|
||||||
|
"Title": "Sample",
|
||||||
|
"Trapped": "Unknown",
|
||||||
|
// Those are not valid PDF metadata.
|
||||||
|
"int": 1,
|
||||||
|
"int64": int64(2),
|
||||||
|
"float32": float32(2.2),
|
||||||
|
"float64": 3.3,
|
||||||
|
},
|
||||||
|
expectMetadata: map[string]interface{}{
|
||||||
|
"Author": "Julien Neuhart",
|
||||||
|
"Copyright": "Julien Neuhart",
|
||||||
|
"CreationDate": "2006:09:18 16:27:50-04:00",
|
||||||
|
"Creator": "Gotenberg",
|
||||||
|
"Keywords": []interface{}{
|
||||||
|
"first",
|
||||||
|
"second",
|
||||||
|
},
|
||||||
|
"Marked": true,
|
||||||
|
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||||
|
"PDFVersion": 1.7,
|
||||||
|
"Producer": "Gotenberg",
|
||||||
|
"Subject": "Sample",
|
||||||
|
"Title": "Sample",
|
||||||
|
"Trapped": "Unknown",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Provision(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var destinationPath string
|
||||||
|
if tc.createCopy {
|
||||||
|
fs := gotenberg.NewFileSystem()
|
||||||
|
outputDir, err := fs.MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected error no but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = os.RemoveAll(fs.WorkingDirPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
destinationPath = fmt.Sprintf("%s/copy_temp.pdf", outputDir)
|
||||||
|
source, err := os.Open(tc.inputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open source file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(source *os.File) {
|
||||||
|
err := source.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close file: %v", err)
|
||||||
|
}
|
||||||
|
}(source)
|
||||||
|
|
||||||
|
destination, err := os.Create(destinationPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create destination file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(destination *os.File) {
|
||||||
|
err := destination.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close file: %v", err)
|
||||||
|
}
|
||||||
|
}(destination)
|
||||||
|
|
||||||
|
_, err = io.Copy(destination, source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("copy source into destination: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
destinationPath = tc.inputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.WriteMetadata(context.Background(), zap.NewNop(), tc.metadata, destinationPath)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), destinationPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectMetadata != nil && err == nil {
|
||||||
|
for k, v := range tc.expectMetadata {
|
||||||
|
v2, ok := metadata[k]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected entry %s with value %v to exists, but got none", k, v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v2.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
for i, entry := range v.([]interface{}) {
|
||||||
|
if entry != v2.([]interface{})[i] {
|
||||||
|
t.Errorf("expected entry %s to contain value %v, but got %v", k, entry, v2.([]interface{})[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if v != v2 {
|
||||||
|
t.Errorf("expected entry %s with value %v to exists, but got %v", k, v, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,14 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
PageRanges string
|
PageRanges string
|
||||||
|
|
||||||
|
// ExportFormFields allows to... export form fields in the resulting PDF.
|
||||||
|
// Optional.
|
||||||
|
ExportFormFields bool
|
||||||
|
|
||||||
|
// SinglePageSheets allows to output each sheet as a single page in the resulting PDF.
|
||||||
|
// Optional
|
||||||
|
SinglePageSheets bool
|
||||||
|
|
||||||
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
|
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
|
||||||
// PDF/A-3b and PDF/UA.
|
// PDF/A-3b and PDF/UA.
|
||||||
// Optional.
|
// Optional.
|
||||||
@@ -266,14 +274,25 @@ func (a *Api) Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath
|
|||||||
// FIXME: don't care, take all on the route level?
|
// FIXME: don't care, take all on the route level?
|
||||||
func (a *Api) Extensions() []string {
|
func (a *Api) Extensions() []string {
|
||||||
return []string{
|
return []string{
|
||||||
|
".123",
|
||||||
|
".602",
|
||||||
|
".abw",
|
||||||
".bib",
|
".bib",
|
||||||
".bmp",
|
".bmp",
|
||||||
|
".cdr",
|
||||||
|
".cgm",
|
||||||
|
".cmx",
|
||||||
".csv",
|
".csv",
|
||||||
|
".cwk",
|
||||||
".dbf",
|
".dbf",
|
||||||
".dif",
|
".dif",
|
||||||
".doc",
|
".doc",
|
||||||
|
".docm",
|
||||||
".docx",
|
".docx",
|
||||||
|
".dot",
|
||||||
|
".dotm",
|
||||||
".dotx",
|
".dotx",
|
||||||
|
".dxf",
|
||||||
".emf",
|
".emf",
|
||||||
".eps",
|
".eps",
|
||||||
".epub",
|
".epub",
|
||||||
@@ -281,35 +300,52 @@ func (a *Api) Extensions() []string {
|
|||||||
".fodp",
|
".fodp",
|
||||||
".fods",
|
".fods",
|
||||||
".fodt",
|
".fodt",
|
||||||
|
".fopd",
|
||||||
".gif",
|
".gif",
|
||||||
|
".htm",
|
||||||
".html",
|
".html",
|
||||||
|
".hwp",
|
||||||
".jpeg",
|
".jpeg",
|
||||||
".jpg",
|
".jpg",
|
||||||
".key",
|
".key",
|
||||||
".ltx",
|
".ltx",
|
||||||
|
".lwp",
|
||||||
|
".mcw",
|
||||||
".met",
|
".met",
|
||||||
|
".mml",
|
||||||
|
".mw",
|
||||||
|
".numbers",
|
||||||
".odd",
|
".odd",
|
||||||
".odg",
|
".odg",
|
||||||
|
".odm",
|
||||||
".odp",
|
".odp",
|
||||||
".ods",
|
".ods",
|
||||||
".odt",
|
".odt",
|
||||||
".otg",
|
".otg",
|
||||||
|
".oth",
|
||||||
|
".otp",
|
||||||
".ots",
|
".ots",
|
||||||
".ott",
|
".ott",
|
||||||
".pages",
|
".pages",
|
||||||
".pbm",
|
".pbm",
|
||||||
|
".pcd",
|
||||||
".pct",
|
".pct",
|
||||||
|
".pcx",
|
||||||
".pdb",
|
".pdb",
|
||||||
".pdf",
|
".pdf",
|
||||||
".pgm",
|
".pgm",
|
||||||
".png",
|
".png",
|
||||||
".pot",
|
".pot",
|
||||||
".potm",
|
".potm",
|
||||||
|
".potx",
|
||||||
".ppm",
|
".ppm",
|
||||||
".pps",
|
".pps",
|
||||||
".ppt",
|
".ppt",
|
||||||
|
".pptm",
|
||||||
".pptx",
|
".pptx",
|
||||||
|
".psd",
|
||||||
".psw",
|
".psw",
|
||||||
|
".pub",
|
||||||
".pwp",
|
".pwp",
|
||||||
".pxl",
|
".pxl",
|
||||||
".ras",
|
".ras",
|
||||||
@@ -317,8 +353,11 @@ func (a *Api) Extensions() []string {
|
|||||||
".sda",
|
".sda",
|
||||||
".sdc",
|
".sdc",
|
||||||
".sdd",
|
".sdd",
|
||||||
|
".sdp",
|
||||||
".sdw",
|
".sdw",
|
||||||
|
".sgl",
|
||||||
".slk",
|
".slk",
|
||||||
|
".smf",
|
||||||
".stc",
|
".stc",
|
||||||
".std",
|
".std",
|
||||||
".sti",
|
".sti",
|
||||||
@@ -328,25 +367,43 @@ func (a *Api) Extensions() []string {
|
|||||||
".swf",
|
".swf",
|
||||||
".sxc",
|
".sxc",
|
||||||
".sxd",
|
".sxd",
|
||||||
|
".sxg",
|
||||||
".sxi",
|
".sxi",
|
||||||
|
".sxm",
|
||||||
".sxw",
|
".sxw",
|
||||||
".sxw",
|
".tga",
|
||||||
".tif",
|
".tif",
|
||||||
".tiff",
|
".tiff",
|
||||||
".txt",
|
".txt",
|
||||||
|
".uof",
|
||||||
".uop",
|
".uop",
|
||||||
".uos",
|
".uos",
|
||||||
".uot",
|
".uot",
|
||||||
|
".vdx",
|
||||||
".vor",
|
".vor",
|
||||||
|
".vsd",
|
||||||
|
".vsdm",
|
||||||
|
".vsdx",
|
||||||
|
".wb2",
|
||||||
|
".wk1",
|
||||||
|
".wks",
|
||||||
".wmf",
|
".wmf",
|
||||||
|
".wpd",
|
||||||
|
".wpg",
|
||||||
".wps",
|
".wps",
|
||||||
|
".xbm",
|
||||||
".xhtml",
|
".xhtml",
|
||||||
".xls",
|
".xls",
|
||||||
|
".xlsb",
|
||||||
|
".xlsm",
|
||||||
".xlsx",
|
".xlsx",
|
||||||
".xlt",
|
".xlt",
|
||||||
|
".xltm",
|
||||||
".xltx",
|
".xltx",
|
||||||
|
".xlw",
|
||||||
".xml",
|
".xml",
|
||||||
".xpm",
|
".xpm",
|
||||||
|
".zabw",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ func TestApi_Extensions(t *testing.T) {
|
|||||||
extensions := a.Extensions()
|
extensions := a.Extensions()
|
||||||
|
|
||||||
actual := len(extensions)
|
actual := len(extensions)
|
||||||
expect := 81
|
expect := 130
|
||||||
|
|
||||||
if actual != expect {
|
if actual != expect {
|
||||||
t.Errorf("expected %d extensions, but got %d", expect, actual)
|
t.Errorf("expected %d extensions, but got %d", expect, actual)
|
||||||
|
|||||||
@@ -273,6 +273,14 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
|||||||
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !options.ExportFormFields {
|
||||||
|
args = append(args, "--export", "ExportFormFields=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.SinglePageSheets {
|
||||||
|
args = append(args, "--export", "SinglePageSheets=true")
|
||||||
|
}
|
||||||
|
|
||||||
switch options.PdfFormats.PdfA {
|
switch options.PdfFormats.PdfA {
|
||||||
case "":
|
case "":
|
||||||
case gotenberg.PdfA1b:
|
case gotenberg.PdfA1b:
|
||||||
|
|||||||
@@ -364,6 +364,64 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
start: true,
|
start: true,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (disable form fields)",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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/document.txt", fs.WorkingDirPath()), []byte("DisableFormFields"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{ExportFormFields: false},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (single page sheets)",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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/document.txt", fs.WorkingDirPath()), []byte("SinglePageSheets"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{SinglePageSheets: true},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (page ranges)",
|
scenario: "success (page ranges)",
|
||||||
libreOffice: newLibreOfficeProcess(
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// Package pdfengine provides a module which interacts with LibreOffice via the
|
// Package pdfengine provides a module which interacts with LibreOffice via the
|
||||||
// UNO (Universal Network Objects) API and implements the gotenberg.PdfEngine
|
// UNO (Universal Network Objects) API and implements the gotenberg.PdfEngine
|
||||||
// interface.
|
// interface. This package allows for:
|
||||||
|
//
|
||||||
|
// 1. The conversion to specific PDF formats.
|
||||||
package pdfengine
|
package pdfengine
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, err)
|
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMetadata is not available in this implementation.
|
||||||
|
func (engine *LibreOfficePdfEngine) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
||||||
|
|||||||
@@ -166,3 +166,21 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(LibreOfficePdfEngine)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(LibreOfficePdfEngine)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package libreoffice
|
package libreoffice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -27,20 +28,34 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
inputPaths []string
|
inputPaths []string
|
||||||
landscape bool
|
landscape bool
|
||||||
nativePageRanges string
|
nativePageRanges string
|
||||||
|
exportFormFields bool
|
||||||
|
singlePageSheets bool
|
||||||
pdfa string
|
pdfa string
|
||||||
pdfua bool
|
pdfua bool
|
||||||
nativePdfFormats bool
|
nativePdfFormats bool
|
||||||
merge bool
|
merge bool
|
||||||
|
metadata map[string]interface{}
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths(libreOffice.Extensions(), &inputPaths).
|
MandatoryPaths(libreOffice.Extensions(), &inputPaths).
|
||||||
Bool("landscape", &landscape, false).
|
Bool("landscape", &landscape, false).
|
||||||
String("nativePageRanges", &nativePageRanges, "").
|
String("nativePageRanges", &nativePageRanges, "").
|
||||||
|
Bool("exportFormFields", &exportFormFields, true).
|
||||||
|
Bool("singlePageSheets", &singlePageSheets, false).
|
||||||
String("pdfa", &pdfa, "").
|
String("pdfa", &pdfa, "").
|
||||||
Bool("pdfua", &pdfua, false).
|
Bool("pdfua", &pdfua, false).
|
||||||
Bool("nativePdfFormats", &nativePdfFormats, true).
|
Bool("nativePdfFormats", &nativePdfFormats, true).
|
||||||
Bool("merge", &merge, false).
|
Bool("merge", &merge, false).
|
||||||
|
Custom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
Validate()
|
Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
@@ -56,8 +71,10 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
for i, inputPath := range inputPaths {
|
for i, inputPath := range inputPaths {
|
||||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||||
options := libreofficeapi.Options{
|
options := libreofficeapi.Options{
|
||||||
Landscape: landscape,
|
Landscape: landscape,
|
||||||
PageRanges: nativePageRanges,
|
PageRanges: nativePageRanges,
|
||||||
|
ExportFormFields: exportFormFields,
|
||||||
|
SinglePageSheets: singlePageSheets,
|
||||||
}
|
}
|
||||||
|
|
||||||
if nativePdfFormats {
|
if nativePdfFormats {
|
||||||
@@ -87,8 +104,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// So far so good, let's check if we have to merge the PDFs. Quick
|
// So far so good, let's check if we have to merge the PDFs.
|
||||||
// win: if there is only one PDF, skip this step.
|
|
||||||
if len(outputPaths) > 1 && merge {
|
if len(outputPaths) > 1 && merge {
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
@@ -97,34 +113,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
return fmt.Errorf("merge PDFs: %w", err)
|
return fmt.Errorf("merge PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, let's check if the client want to convert this
|
// Only one output path.
|
||||||
// resulting PDF to specific PDF formats.
|
outputPaths = []string{outputPath}
|
||||||
zeroValued := gotenberg.PdfFormats{}
|
|
||||||
if !nativePdfFormats && pdfFormats != zeroValued {
|
|
||||||
convertInputPath := outputPath
|
|
||||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("convert PDF: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Important: the output path is now the converted file.
|
|
||||||
outputPath = convertOutputPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last but not least, add the output path to the context so that
|
|
||||||
// the API is able to send it as a response to the client.
|
|
||||||
err = ctx.AddOutputPaths(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("add output path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ok, we don't have to merge the PDFs. Let's check if the client
|
// Let's check if the client want to convert each PDF to a specific
|
||||||
// want to convert each PDF to a specific PDF format.
|
// PDF format.
|
||||||
zeroValued := gotenberg.PdfFormats{}
|
zeroValued := gotenberg.PdfFormats{}
|
||||||
if !nativePdfFormats && pdfFormats != zeroValued {
|
if !nativePdfFormats && pdfFormats != zeroValued {
|
||||||
convertOutputPaths := make([]string, len(outputPaths))
|
convertOutputPaths := make([]string, len(outputPaths))
|
||||||
@@ -143,6 +137,16 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
outputPaths = convertOutputPaths
|
outputPaths = convertOutputPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes and potentially overrides metadata entries, if any.
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
for _, outputPath := range outputPaths {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(outputPaths) > 1 {
|
if len(outputPaths) > 1 {
|
||||||
// If .zip archive, document.docx -> document.docx.pdf.
|
// If .zip archive, document.docx -> document.docx.pdf.
|
||||||
for i, inputPath := range inputPaths {
|
for i, inputPath := range inputPaths {
|
||||||
|
|||||||
@@ -39,6 +39,28 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "ErrPdfFormatNotSupported (nativePdfFormats)",
|
scenario: "ErrPdfFormatNotSupported (nativePdfFormats)",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -110,7 +132,39 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "PDF engine convert error (single file)",
|
scenario: "PDF engine merge error",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
"document2.docx": "/document2.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"merge": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine convert error",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -144,13 +198,17 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot add output paths (single file)",
|
scenario: "PDF engine write metadata error",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
})
|
})
|
||||||
ctx.SetCancelled(true)
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
@@ -161,31 +219,15 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
return []string{".docx"}
|
return []string{".docx"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
scenario: "success (single file)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
scenario: "cannot rename many files",
|
scenario: "cannot rename many files",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -212,6 +254,78 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot add output paths",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetCancelled(true)
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (single file)",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
"document2.docx": "/document2.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"merge": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
"nativePdfFormats": {
|
||||||
|
"false",
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (many files)",
|
scenario: "success (many files)",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -221,6 +335,20 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"document2.docx": "/document2.docx",
|
"document2.docx": "/document2.docx",
|
||||||
"document2.doc": "/document2.doc",
|
"document2.doc": "/document2.doc",
|
||||||
})
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
"nativePdfFormats": {
|
||||||
|
"false",
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
return nil
|
return nil
|
||||||
}})
|
}})
|
||||||
@@ -234,60 +362,28 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
return []string{".docx", ".doc"}
|
return []string{".docx", ".doc"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 3,
|
expectOutputPathsCount: 3,
|
||||||
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
|
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with non-native PDF/A & PDF/UA (many files)",
|
scenario: "success with native PDF/A & PDF/UA",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
|
||||||
return nil
|
|
||||||
}})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 2,
|
|
||||||
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with native PDF/A & PDF/UA (many files)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
@@ -297,9 +393,6 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"true",
|
"true",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
|
||||||
return nil
|
|
||||||
}})
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
@@ -310,238 +403,6 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
return []string{".docx"}
|
return []string{".docx"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 2,
|
|
||||||
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "merge error",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "PDF engine convert error (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "cannot add output paths (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
ctx.SetCancelled(true)
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with non-native PDF/A & PDF/UA (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with non-native PDF/A & PDF/UA (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
|
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
|
||||||
// interface using the pdfcpu library. This package allows for the merging of
|
// interface using the pdfcpu library. This package allows for:
|
||||||
// PDF files but does not support conversion to specific PDF formats.
|
//
|
||||||
|
// 1. The merging of PDF files.
|
||||||
//
|
//
|
||||||
// See: https://github.com/pdfcpu/pdfcpu.
|
// See: https://github.com/pdfcpu/pdfcpu.
|
||||||
package pdfcpu
|
package pdfcpu
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats g
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMetadata is not available in this implementation.
|
||||||
|
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*PdfCpu)(nil)
|
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||||
|
|||||||
@@ -102,3 +102,21 @@ func TestPdfCpu_Convert(t *testing.T) {
|
|||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfCpu)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfCpu)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package pdfengines
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -70,6 +71,65 @@ func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, f
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type readMetadataResult struct {
|
||||||
|
metadata map[string]interface{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
var err error
|
||||||
|
var mu sync.Mutex // to safely append errors.
|
||||||
|
|
||||||
|
resultChan := make(chan readMetadataResult, len(multi.engines))
|
||||||
|
|
||||||
|
for _, engine := range multi.engines {
|
||||||
|
go func(engine gotenberg.PdfEngine) {
|
||||||
|
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
|
||||||
|
resultChan <- readMetadataResult{metadata: metadata, err: err}
|
||||||
|
}(engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range multi.engines {
|
||||||
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
if result.err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
err = multierr.Append(err, result.err)
|
||||||
|
mu.Unlock()
|
||||||
|
} else {
|
||||||
|
return result.metadata, nil
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with multi PDF engines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
var err error
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
for _, engine := range multi.engines {
|
||||||
|
go func(engine gotenberg.PdfEngine) {
|
||||||
|
errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
|
||||||
|
}(engine)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case writeMetadataErr := <-errChan:
|
||||||
|
errored := multierr.AppendInto(&err, writeMetadataErr)
|
||||||
|
if !errored {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||||
|
|||||||
@@ -177,3 +177,169 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
engine *multiPdfEngines
|
||||||
|
ctx context.Context
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
_, err := tc.engine.ReadMetadata(tc.ctx, zap.NewNop(), "")
|
||||||
|
|
||||||
|
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 TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
engine *multiPdfEngines
|
||||||
|
ctx context.Context
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "nominal behavior",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "context expired",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.engine.WriteMetadata(tc.ctx, zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
|||||||
return []api.Route{
|
return []api.Route{
|
||||||
mergeRoute(engine),
|
mergeRoute(engine),
|
||||||
convertRoute(engine),
|
convertRoute(engine),
|
||||||
|
readMetadataRoute(engine),
|
||||||
|
writeMetadataRoute(engine),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 2,
|
expectRoutes: 4,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package pdfengines
|
package pdfengines
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
@@ -25,12 +27,22 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
inputPaths []string
|
inputPaths []string
|
||||||
pdfa string
|
pdfa string
|
||||||
pdfua bool
|
pdfua bool
|
||||||
|
metadata map[string]interface{}
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
String("pdfa", &pdfa, "").
|
String("pdfa", &pdfa, "").
|
||||||
Bool("pdfua", &pdfua, false).
|
Bool("pdfua", &pdfua, false).
|
||||||
|
Custom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
Validate()
|
Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
@@ -66,6 +78,14 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
outputPath = convertOutputPath
|
outputPath = convertOutputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes and potentially overrides metadata entries, if any.
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Last but not least, add the output path to the context so that
|
// Last but not least, add the output path to the context so that
|
||||||
// the API is able to send it as a response to the client.
|
// the API is able to send it as a response to the client.
|
||||||
err = ctx.AddOutputPaths(outputPath)
|
err = ctx.AddOutputPaths(outputPath)
|
||||||
@@ -78,8 +98,8 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertRoute returns an [api.Route] which can convert a PDF to a specific
|
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
||||||
// PDF format.
|
// format.
|
||||||
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
@@ -152,3 +172,98 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readMetadataRoute returns an [api.Route] which returns the metadata of PDFs.
|
||||||
|
func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/pdfengines/metadata/read",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
// Let's get the data from the form and validate them.
|
||||||
|
var inputPaths []string
|
||||||
|
|
||||||
|
err := ctx.FormData().
|
||||||
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alright, let's read the metadata.
|
||||||
|
res := make(map[string]map[string]interface{}, len(inputPaths))
|
||||||
|
for _, inputPath := range inputPaths {
|
||||||
|
metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res[filepath.Base(inputPath)] = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.JSON(http.StatusOK, res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("return JSON response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.ErrNoOutputFile
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMetadataRoute returns an [api.Route] which can write metadata into
|
||||||
|
// PDFs.
|
||||||
|
func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/pdfengines/metadata/write",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
// Let's get the data from the form and validate them.
|
||||||
|
var (
|
||||||
|
inputPaths []string
|
||||||
|
metadata map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
err := ctx.FormData().
|
||||||
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
MandatoryCustom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return errors.New("no metadata")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alright, let's convert the PDFs.
|
||||||
|
for _, inputPath := range inputPaths {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last but not least, add the output paths to the context so that
|
||||||
|
// the API is able to send them as a response to the client.
|
||||||
|
err = ctx.AddOutputPaths(inputPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add output paths: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -33,7 +35,27 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "error from PDF engine",
|
scenario: "invalid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine merge error",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -51,6 +73,60 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine convert error",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine write metadata error",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot add output paths",
|
scenario: "cannot add output paths",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -73,25 +149,6 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success",
|
scenario: "success",
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"file.pdf": "/file.pdf",
|
|
||||||
"file2.pdf": "/file2.pdf",
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "error from PDF engine (convert)",
|
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -102,6 +159,9 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1b,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -110,36 +170,9 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with PDF/A & PDF/UA form fields",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"file.pdf": "/file.pdf",
|
|
||||||
"file2.pdf": "/file2.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -401,3 +434,295 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadMetadataHandler(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *api.ContextMock
|
||||||
|
engine gotenberg.PdfEngine
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
|
expectedJson string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing at least one mandatory file",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "error from PDF engine",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectedError: api.ErrNoOutputFile,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectedJson: `{"file.pdf":{"bar":"foo","foo":"bar"}}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/forms/pdfengines/metadata/read", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c := echo.New().NewContext(req, rec)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
|
err := readMetadataRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpErr api.HttpError
|
||||||
|
isHttpError := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHttpError && !isHttpError {
|
||||||
|
t.Errorf("expected an HTTP error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHttpError && isHttpError {
|
||||||
|
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHttpError && isHttpError {
|
||||||
|
status, _ := httpErr.HttpError()
|
||||||
|
if status != tc.expectHttpStatus {
|
||||||
|
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedJson != "" && tc.expectedJson != strings.TrimSpace(rec.Body.String()) {
|
||||||
|
t.Errorf("expected '%s' as HTTP response but got '%s'", tc.expectedJson, strings.TrimSpace(rec.Body.String()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteMetadataHandler(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *api.ContextMock
|
||||||
|
engine gotenberg.PdfEngine
|
||||||
|
expectError bool
|
||||||
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
|
expectOutputPathsCount int
|
||||||
|
expectOutputPaths []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing at least one mandatory file",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no metadata",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{}",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "error from PDF engine",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot add output paths",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetCancelled(true)
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
c := echo.New().NewContext(nil, nil)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
|
err := writeMetadataRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpErr api.HttpError
|
||||||
|
isHttpError := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHttpError && !isHttpError {
|
||||||
|
t.Errorf("expected an HTTP error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHttpError && isHttpError {
|
||||||
|
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHttpError && isHttpError {
|
||||||
|
status, _ := httpErr.HttpError()
|
||||||
|
if status != tc.expectHttpStatus {
|
||||||
|
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||||
|
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range tc.expectOutputPaths {
|
||||||
|
if !slices.Contains(tc.ctx.OutputPaths(), path) {
|
||||||
|
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Package pdftk provides an implementation of the gotenberg.PdfEngine
|
// Package pdftk provides an implementation of the gotenberg.PdfEngine
|
||||||
// interface using the PDFtk command-line tool. This package allows for the
|
// interface using the PDFtk command-line tool. This package allows for:
|
||||||
// merging of PDF files but does not support conversion to specific PDF
|
//
|
||||||
// formats. The path to the PDFtk binary must be specified using the
|
// 1. The merging of PDF files.
|
||||||
// PDFTK_BIN_PATH environment variable.
|
//
|
||||||
|
// The path to the PDFtk binary must be specified using the PDFTK_BIN_PATH
|
||||||
|
// environment variable.
|
||||||
//
|
//
|
||||||
// See: https://gitlab.com/pdftk-java/pdftk.
|
// See: https://gitlab.com/pdftk-java/pdftk.
|
||||||
package pdftk
|
package pdftk
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats go
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMetadata is not available in this implementation.
|
||||||
|
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*PdfTk)(nil)
|
_ gotenberg.Module = (*PdfTk)(nil)
|
||||||
|
|||||||
@@ -148,3 +148,21 @@ func TestPdfTk_Convert(t *testing.T) {
|
|||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfTk)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfTk)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Package qpdf provides an implementation of the gotenberg.PdfEngine
|
// Package qpdf provides an implementation of the gotenberg.PdfEngine
|
||||||
// interface using the QPDF command-line tool. This package allows for the
|
// interface using the QPDF command-line tool. This package allows for:
|
||||||
// merging of PDF files but does not support conversion to specific PDF
|
//
|
||||||
// formats. The path to the QPDF binary must be specified using the
|
// 1. The merging of PDF files.
|
||||||
// QPDK_BIN_PATH environment variable.
|
//
|
||||||
|
// The path to the QPDF binary must be specified using the QPDK_BIN_PATH
|
||||||
|
// environment variable.
|
||||||
//
|
//
|
||||||
// See: https://github.com/qpdf/qpdf.
|
// See: https://github.com/qpdf/qpdf.
|
||||||
package qpdf
|
package qpdf
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats got
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMetadata is not available in this implementation.
|
||||||
|
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*QPdf)(nil)
|
_ gotenberg.Module = (*QPdf)(nil)
|
||||||
_ gotenberg.Provisioner = (*QPdf)(nil)
|
_ gotenberg.Provisioner = (*QPdf)(nil)
|
||||||
|
|||||||
@@ -148,3 +148,21 @@ func TestQPdf_Convert(t *testing.T) {
|
|||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(QPdf)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(QPdf)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
// Standard Gotenberg modules.
|
// Standard Gotenberg modules.
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/chromium"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/chromium"
|
||||||
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/exiftool"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/pdfengine"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/pdfengine"
|
||||||
|
|||||||
@@ -3,4 +3,8 @@
|
|||||||
set -x
|
set -x
|
||||||
|
|
||||||
go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./...
|
go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./...
|
||||||
go tool cover -html=coverage.txt -o /tests/coverage.html
|
RESULT=$?
|
||||||
|
|
||||||
|
go tool cover -html=coverage.txt -o /tests/coverage.html
|
||||||
|
|
||||||
|
exit $RESULT
|
||||||
Reference in New Issue
Block a user