mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b4d435c3 | ||
|
|
a8bde9d396 | ||
|
|
5c6f29095f | ||
|
|
0d3942848c | ||
|
|
9256a203ef | ||
|
|
f13e045e7f | ||
|
|
a60cb33ff1 | ||
|
|
dec32d981f | ||
|
|
3046615c3c | ||
|
|
130b94aa8c |
2
.github/workflows/continuous_integration.yml
vendored
2
.github/workflows/continuous_integration.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
cache: false
|
cache: false
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -29,13 +29,14 @@ build: ## Build the Gotenberg's Docker image
|
|||||||
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
||||||
API_PORT=3000
|
API_PORT=3000
|
||||||
API_PORT_FROM_ENV=
|
API_PORT_FROM_ENV=
|
||||||
|
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_DISABLE_HEALTH_CHECK_LOGGING=false
|
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||||
CHROMIUM_RESTART_AFTER=0
|
CHROMIUM_RESTART_AFTER=0
|
||||||
CHROMIUM_AUTO_START=false
|
CHROMIUM_AUTO_START=false
|
||||||
CHROMIUM_START_TIMEOUT=10s
|
CHROMIUM_START_TIMEOUT=20s
|
||||||
CHROMIUM_INCOGNITO=false
|
CHROMIUM_INCOGNITO=false
|
||||||
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
|
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
|
||||||
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
||||||
@@ -49,7 +50,7 @@ CHROMIUM_DISABLE_JAVASCRIPT=false
|
|||||||
CHROMIUM_DISABLE_ROUTES=false
|
CHROMIUM_DISABLE_ROUTES=false
|
||||||
LIBREOFFICE_RESTART_AFTER=10
|
LIBREOFFICE_RESTART_AFTER=10
|
||||||
LIBREOFFICE_AUTO_START=false
|
LIBREOFFICE_AUTO_START=false
|
||||||
LIBREOFFICE_START_TIMEOUT=10s
|
LIBREOFFICE_START_TIMEOUT=20s
|
||||||
LIBREOFFICE_DISABLE_ROUTES=false
|
LIBREOFFICE_DISABLE_ROUTES=false
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
LOG_FORMAT=auto
|
LOG_FORMAT=auto
|
||||||
@@ -79,6 +80,7 @@ run: ## Start a Gotenberg container
|
|||||||
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||||
--api-port=$(API_PORT) \
|
--api-port=$(API_PORT) \
|
||||||
--api-port-from-env=$(API_PORT_FROM_ENV) \
|
--api-port-from-env=$(API_PORT_FROM_ENV) \
|
||||||
|
--api-start-timeout=$(API_START_TIMEOUT) \
|
||||||
--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) \
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
# concatenate them. Also, we have to repeat ARG instructions in each build
|
# concatenate them. Also, we have to repeat ARG instructions in each build
|
||||||
# stage that uses them.
|
# stage that uses them.
|
||||||
ARG GOLANG_VERSION
|
ARG GOLANG_VERSION
|
||||||
ARG GOTENBERG_VERSION
|
|
||||||
|
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
# Gotenberg binary build stage
|
# Gotenberg binary build stage
|
||||||
@@ -134,7 +133,12 @@ RUN \
|
|||||||
mv /usr/bin/google-chrome-stable /usr/bin/chromium; \
|
mv /usr/bin/google-chrome-stable /usr/bin/chromium; \
|
||||||
elif [[ "$(dpkg --print-architecture)" == "armhf" ]]; then \
|
elif [[ "$(dpkg --print-architecture)" == "armhf" ]]; then \
|
||||||
apt-get update -qq &&\
|
apt-get update -qq &&\
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium-common="$TMP_CHOMIUM_VERSION_ARMHF" chromium="$TMP_CHOMIUM_VERSION_ARMHF"; \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends devscripts &&\
|
||||||
|
debsnap chromium-common "$TMP_CHOMIUM_VERSION_ARMHF" -v --force --binary --architecture armhf &&\
|
||||||
|
debsnap chromium "$TMP_CHOMIUM_VERSION_ARMHF" -v --force --binary --architecture armhf &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install --fix-broken -y -qq --no-install-recommends "./binary-chromium-common/chromium-common_${TMP_CHOMIUM_VERSION_ARMHF}_armhf.deb" "./binary-chromium/chromium_${TMP_CHOMIUM_VERSION_ARMHF}_armhf.deb" &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq devscripts &&\
|
||||||
|
rm -rf ./binary-chromium-common/* ./binary-chromium/*; \
|
||||||
else \
|
else \
|
||||||
apt-get update -qq &&\
|
apt-get update -qq &&\
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium; \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium; \
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ func Run() {
|
|||||||
startupMessage := app.StartupMessage()
|
startupMessage := app.StartupMessage()
|
||||||
if startupMessage == "" {
|
if startupMessage == "" {
|
||||||
fmt.Printf("[SYSTEM] %s: application started\n", id)
|
fmt.Printf("[SYSTEM] %s: application started\n", id)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +143,6 @@ func Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
|
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}(a.(gotenberg.App)))
|
}(a.(gotenberg.App)))
|
||||||
|
|||||||
18
go.mod
18
go.mod
@@ -5,13 +5,13 @@ go 1.21
|
|||||||
require (
|
require (
|
||||||
github.com/alexliesenfeld/health v0.8.0
|
github.com/alexliesenfeld/health v0.8.0
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20231114014204-3e458d5176f9
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61
|
||||||
github.com/chromedp/chromedp v0.9.3
|
github.com/chromedp/chromedp v0.9.3
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/uuid v1.4.0
|
github.com/google/uuid v1.5.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.3 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/labstack/echo/v4 v4.11.3
|
github.com/labstack/echo/v4 v4.11.3
|
||||||
github.com/labstack/gommon v0.4.1
|
github.com/labstack/gommon v0.4.1
|
||||||
@@ -19,20 +19,20 @@ require (
|
|||||||
github.com/mholt/archiver/v3 v3.5.1
|
github.com/mholt/archiver/v3 v3.5.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||||
github.com/pdfcpu/pdfcpu v0.5.0
|
github.com/pdfcpu/pdfcpu v0.6.0
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
||||||
github.com/prometheus/client_golang v1.17.0
|
github.com/prometheus/client_golang v1.17.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.11 // indirect
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.11.0
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.15.0 // indirect
|
golang.org/x/crypto v0.16.0 // indirect
|
||||||
golang.org/x/image v0.14.0 // indirect
|
golang.org/x/image v0.14.0 // indirect
|
||||||
golang.org/x/net v0.18.0
|
golang.org/x/net v0.19.0
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.5.0
|
||||||
golang.org/x/sys v0.14.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
golang.org/x/term v0.14.0
|
golang.org/x/term v0.15.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
36
go.sum
36
go.sum
@@ -10,8 +10,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20231114014204-3e458d5176f9 h1:e3tMnG8i9SfKOilykpprojNk3a49O4dn+wqZsam1qYQ=
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61 h1:XD280QPATe9jaz20dylKe3vBsNcH1w3mkssGY0lidn8=
|
||||||
github.com/chromedp/cdproto v0.0.0-20231114014204-3e458d5176f9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
||||||
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
||||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
@@ -35,8 +35,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
|||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
@@ -53,8 +53,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.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
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=
|
||||||
@@ -89,11 +89,11 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l
|
|||||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/pdfcpu/pdfcpu v0.5.0 h1:F3wC4bwPbaJM+RPgm1D0Q4SAUwxElw7BhwNvL3iPgDo=
|
github.com/pdfcpu/pdfcpu v0.6.0 h1:z4kARP5bcWa39TTYMcN/kjBnm7MvhTWjXgeYmkdAGMI=
|
||||||
github.com/pdfcpu/pdfcpu v0.5.0/go.mod h1:UPcHdWcMw1V6Bo5tcWHd3jZfkG8cwUwrJkQOlB6o+7g=
|
github.com/pdfcpu/pdfcpu v0.6.0/go.mod h1:kmpD0rk8YnZj0l3qSeGBlAB+XszHUgNv//ORH/E7EYo=
|
||||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -136,20 +136,20 @@ 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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.5.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.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
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/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=
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package gotenberg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrProcessAlreadyRestarting happens if the [ProcessSupervisor] is trying
|
||||||
|
// to restart an already restarting [Process].
|
||||||
|
var ErrProcessAlreadyRestarting = errors.New("process already restarting")
|
||||||
|
|
||||||
// Process is an interface that represents an abstract process
|
// Process is an interface that represents an abstract process
|
||||||
// and provides methods for starting, stopping, and checking the health of the
|
// and provides methods for starting, stopping, and checking the health of the
|
||||||
// process.
|
// process.
|
||||||
@@ -122,7 +127,7 @@ func (s *processSupervisor) restart() error {
|
|||||||
if s.isRestarting.Load() {
|
if s.isRestarting.Load() {
|
||||||
s.logger.Debug("process already restarting, skip restart")
|
s.logger.Debug("process already restarting, skip restart")
|
||||||
|
|
||||||
return nil
|
return ErrProcessAlreadyRestarting
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Debug("restart process")
|
s.logger.Debug("restart process")
|
||||||
@@ -164,53 +169,66 @@ func (s *processSupervisor) Healthy() bool {
|
|||||||
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
s.reqQueueSize.Add(1)
|
s.reqQueueSize.Add(1)
|
||||||
|
|
||||||
select {
|
for {
|
||||||
case s.mutexChan <- struct{}{}:
|
err := func() error {
|
||||||
logger.Debug("process lock acquired")
|
select {
|
||||||
s.reqQueueSize.Add(-1)
|
case s.mutexChan <- struct{}{}:
|
||||||
s.reqCounter.Add(1)
|
logger.Debug("process lock acquired")
|
||||||
|
s.reqQueueSize.Add(-1)
|
||||||
|
s.reqCounter.Add(1)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Debug("process lock released")
|
logger.Debug("process lock released")
|
||||||
<-s.mutexChan
|
<-s.mutexChan
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !s.firstStart.Load() {
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.Launch()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process first start: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.Healthy() {
|
||||||
|
s.logger.Debug("process is unhealthy, cannot handle task, restarting...")
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.restart()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process restart before task: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.maxReqLimit > 0 && s.reqCounter.Load() >= s.maxReqLimit {
|
||||||
|
s.logger.Debug("max request limit reached, restarting...")
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.restart()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process restart before task: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: no error wrapping because it leaks on Chromium console exceptions output.
|
||||||
|
return s.runWithDeadline(ctx, task)
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Debug("failed to acquire process lock before deadline")
|
||||||
|
s.reqQueueSize.Add(-1)
|
||||||
|
|
||||||
|
return fmt.Errorf("acquire process lock: %w", ctx.Err())
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !s.firstStart.Load() {
|
if errors.Is(err, ErrProcessAlreadyRestarting) {
|
||||||
err := s.runWithDeadline(ctx, func() error {
|
logger.Debug("process is already restarting, trying to acquire process lock again...")
|
||||||
return s.Launch()
|
s.reqQueueSize.Add(1)
|
||||||
})
|
continue
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("process first start: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.Healthy() {
|
// Note: no error wrapping because it leaks on Chromium console exceptions output.
|
||||||
s.logger.Debug("process is unhealthy, cannot handle task, restarting...")
|
return err
|
||||||
err := s.runWithDeadline(ctx, func() error {
|
|
||||||
return s.restart()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("process restart before task: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.maxReqLimit > 0 && s.reqCounter.Load() >= s.maxReqLimit {
|
|
||||||
s.logger.Debug("max request limit reached, restarting...")
|
|
||||||
err := s.runWithDeadline(ctx, func() error {
|
|
||||||
return s.restart()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("process restart before task: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: no error wrapping because it leaks on Chromium console exceptions output.
|
|
||||||
return s.runWithDeadline(ctx, task)
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.Debug("failed to acquire process lock before deadline")
|
|
||||||
s.reqQueueSize.Add(-1)
|
|
||||||
|
|
||||||
return fmt.Errorf("acquire process lock: %w", ctx.Err())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,11 +115,13 @@ func TestProcessSupervisor_restart(t *testing.T) {
|
|||||||
startError error
|
startError error
|
||||||
stopError error
|
stopError error
|
||||||
expectError bool
|
expectError bool
|
||||||
|
expectedError error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "already restarting",
|
scenario: "already restarting",
|
||||||
initiallyRestarting: true,
|
initiallyRestarting: true,
|
||||||
expectError: false,
|
expectError: true,
|
||||||
|
expectedError: ErrProcessAlreadyRestarting,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "successful restart",
|
scenario: "successful restart",
|
||||||
@@ -166,6 +168,10 @@ func TestProcessSupervisor_restart(t *testing.T) {
|
|||||||
if tc.expectError && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Fatal("expected error but got none")
|
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,12 +238,14 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
initiallyStarted bool
|
initiallyStarted bool
|
||||||
|
isRestarting bool
|
||||||
startError error
|
startError error
|
||||||
processHealthy bool
|
processHealthy bool
|
||||||
maxReqLimit int64
|
maxReqLimit int64
|
||||||
tasksToRun int
|
tasksToRun int
|
||||||
taskError error
|
taskError error
|
||||||
expectError bool
|
expectError bool
|
||||||
|
skipCallsCheck bool
|
||||||
expectedStartCalls int64
|
expectedStartCalls int64
|
||||||
expectedHealthyCalls int64
|
expectedHealthyCalls int64
|
||||||
expectedStopCalls int64
|
expectedStopCalls int64
|
||||||
@@ -245,6 +253,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "successfully run task on non-started process",
|
scenario: "successfully run task on non-started process",
|
||||||
initiallyStarted: false,
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -256,6 +265,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "cannot launch non-started process",
|
scenario: "cannot launch non-started process",
|
||||||
initiallyStarted: false,
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
startError: errors.New("launch error"),
|
startError: errors.New("launch error"),
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
@@ -268,6 +278,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "run task with unhealthy process causing restart",
|
scenario: "run task with unhealthy process causing restart",
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: false,
|
processHealthy: false,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -280,6 +291,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
scenario: "cannot restart unhealthy process",
|
scenario: "cannot restart unhealthy process",
|
||||||
startError: errors.New("start error"),
|
startError: errors.New("start error"),
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: false,
|
processHealthy: false,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -288,9 +300,20 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
expectedHealthyCalls: 1,
|
expectedHealthyCalls: 1,
|
||||||
expectedStopCalls: 1,
|
expectedStopCalls: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrProcessAlreadyRestarting",
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: true,
|
||||||
|
processHealthy: false,
|
||||||
|
maxReqLimit: 1,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: true,
|
||||||
|
skipCallsCheck: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "run tasks reaching max request limit causing restart",
|
scenario: "run tasks reaching max request limit causing restart",
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 3,
|
tasksToRun: 3,
|
||||||
@@ -303,6 +326,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
scenario: "cannot restart after reaching max request limit",
|
scenario: "cannot restart after reaching max request limit",
|
||||||
startError: errors.New("start error"),
|
startError: errors.New("start error"),
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 2,
|
tasksToRun: 2,
|
||||||
@@ -314,6 +338,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "task error",
|
scenario: "task error",
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 0,
|
maxReqLimit: 0,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -351,6 +376,9 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
if tc.initiallyStarted {
|
if tc.initiallyStarted {
|
||||||
ps.firstStart.Store(true)
|
ps.firstStart.Store(true)
|
||||||
}
|
}
|
||||||
|
if tc.isRestarting {
|
||||||
|
ps.isRestarting.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
task := func() error {
|
task := func() error {
|
||||||
return tc.taskError
|
return tc.taskError
|
||||||
@@ -386,6 +414,10 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.skipCallsCheck {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if startCalls.Load() != tc.expectedStartCalls {
|
if startCalls.Load() != tc.expectedStartCalls {
|
||||||
t.Errorf("expected %d process.Start calls, got %d", tc.expectedStartCalls, startCalls.Load())
|
t.Errorf("expected %d process.Start calls, got %d", tc.expectedStartCalls, startCalls.Load())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
@@ -31,6 +32,7 @@ type Api struct {
|
|||||||
port int
|
port int
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
writeTimeout time.Duration
|
writeTimeout time.Duration
|
||||||
|
startTimeout time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
rootPath string
|
rootPath string
|
||||||
traceHeader string
|
traceHeader string
|
||||||
@@ -39,6 +41,7 @@ type Api struct {
|
|||||||
routes []Route
|
routes []Route
|
||||||
externalMiddlewares []Middleware
|
externalMiddlewares []Middleware
|
||||||
healthChecks []health.CheckerOption
|
healthChecks []health.CheckerOption
|
||||||
|
readyFn []func() error
|
||||||
fs *gotenberg.FileSystem
|
fs *gotenberg.FileSystem
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
srv *echo.Echo
|
srv *echo.Echo
|
||||||
@@ -147,6 +150,7 @@ type Middleware struct {
|
|||||||
// See https://github.com/alexliesenfeld/health for more details.
|
// See https://github.com/alexliesenfeld/health for more details.
|
||||||
type HealthChecker interface {
|
type HealthChecker interface {
|
||||||
Checks() ([]health.CheckerOption, error)
|
Checks() ([]health.CheckerOption, error)
|
||||||
|
Ready() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns an [Api]'s module descriptor.
|
// Descriptor returns an [Api]'s module descriptor.
|
||||||
@@ -157,6 +161,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
||||||
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
||||||
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
||||||
|
fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
|
||||||
fs.Duration("api-read-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to read a complete request, including the body")
|
fs.Duration("api-read-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to read a complete request, including the body")
|
||||||
fs.Duration("api-process-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to process a request")
|
fs.Duration("api-process-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to process a request")
|
||||||
fs.Duration("api-write-timeout", time.Duration(30)*time.Second, "Set the maximum duration before timing out writes of the response")
|
fs.Duration("api-write-timeout", time.Duration(30)*time.Second, "Set the maximum duration before timing out writes of the response")
|
||||||
@@ -184,6 +189,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
a.port = flags.MustInt("api-port")
|
a.port = flags.MustInt("api-port")
|
||||||
|
a.startTimeout = flags.MustDuration("api-start-timeout")
|
||||||
a.readTimeout = flags.MustDeprecatedDuration("api-read-timeout", "api-timeout")
|
a.readTimeout = flags.MustDeprecatedDuration("api-read-timeout", "api-timeout")
|
||||||
a.writeTimeout = flags.MustDeprecatedDuration("api-write-timeout", "api-timeout")
|
a.writeTimeout = flags.MustDeprecatedDuration("api-write-timeout", "api-timeout")
|
||||||
a.timeout = flags.MustDeprecatedDuration("api-process-timeout", "api-timeout")
|
a.timeout = flags.MustDeprecatedDuration("api-process-timeout", "api-timeout")
|
||||||
@@ -275,6 +281,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.healthChecks = append(a.healthChecks, checks...)
|
a.healthChecks = append(a.healthChecks, checks...)
|
||||||
|
a.readyFn = append(a.readyFn, healthChecker.Ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger.
|
// Logger.
|
||||||
@@ -446,12 +453,25 @@ func (a *Api) Start() error {
|
|||||||
func() echo.HandlerFunc {
|
func() echo.HandlerFunc {
|
||||||
checks := append(a.healthChecks, health.WithTimeout(a.timeout))
|
checks := append(a.healthChecks, health.WithTimeout(a.timeout))
|
||||||
checker := health.NewChecker(checks...)
|
checker := health.NewChecker(checks...)
|
||||||
|
|
||||||
return echo.WrapHandler(health.NewHandler(checker))
|
return echo.WrapHandler(health.NewHandler(checker))
|
||||||
}(),
|
}(),
|
||||||
hardTimeoutMiddleware(hardTimeout),
|
hardTimeoutMiddleware(hardTimeout),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for all modules to be ready.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), a.startTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
for _, f := range a.readyFn {
|
||||||
|
eg.Go(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("waiting for modules readiness: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// As the following code is blocking, run it in a goroutine.
|
// As the following code is blocking, run it in a goroutine.
|
||||||
go func() {
|
go func() {
|
||||||
server := &http2.Server{}
|
server := &http2.Server{}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -222,9 +223,6 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
mod.ValidateMock = func() error {
|
mod.ValidateMock = func() error {
|
||||||
return errors.New("foo")
|
return errors.New("foo")
|
||||||
}
|
}
|
||||||
mod.ChecksMock = func() ([]health.CheckerOption, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: new(Api).Descriptor().FlagSet,
|
FlagSet: new(Api).Descriptor().FlagSet,
|
||||||
@@ -347,6 +345,9 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
mod3.ChecksMock = func() ([]health.CheckerOption, error) {
|
mod3.ChecksMock = func() ([]health.CheckerOption, error) {
|
||||||
return []health.CheckerOption{health.WithDisabledAutostart()}, nil
|
return []health.CheckerOption{health.WithDisabledAutostart()}, nil
|
||||||
}
|
}
|
||||||
|
mod3.ReadyMock = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
mod4 := &struct {
|
mod4 := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
@@ -643,141 +644,176 @@ func TestApi_Validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestApi_Start(t *testing.T) {
|
func TestApi_Start(t *testing.T) {
|
||||||
mod := new(Api)
|
for _, tc := range []struct {
|
||||||
mod.port = 3000
|
scenario string
|
||||||
mod.rootPath = "/"
|
readyFn []func() error
|
||||||
mod.disableHealthCheckLogging = true
|
expectError bool
|
||||||
mod.routes = []Route{
|
}{
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
scenario: "at least one module not ready",
|
||||||
Path: "/forms/foo",
|
readyFn: []func() error{
|
||||||
IsMultipart: true,
|
func() error { return nil },
|
||||||
DisableLogging: true,
|
func() error { return errors.New("not ready") },
|
||||||
Handler: func(c echo.Context) error {
|
|
||||||
ctx := c.Get("context").(*Context)
|
|
||||||
ctx.outputPaths = []string{
|
|
||||||
"/tests/test/testdata/api/sample1.txt",
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
scenario: "success",
|
||||||
Path: "/forms/bar",
|
readyFn: []func() error{
|
||||||
IsMultipart: true,
|
func() error { return nil },
|
||||||
Handler: func(_ echo.Context) error { return errors.New("foo") },
|
func() error { return nil },
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
}
|
} {
|
||||||
mod.externalMiddlewares = []Middleware{
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
{
|
mod := new(Api)
|
||||||
Stack: PreRouterStack,
|
mod.port = 3000
|
||||||
Handler: func() echo.MiddlewareFunc {
|
mod.startTimeout = time.Duration(30) * time.Second
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
mod.rootPath = "/"
|
||||||
return func(c echo.Context) error {
|
mod.disableHealthCheckLogging = true
|
||||||
return next(c)
|
mod.routes = []Route{
|
||||||
}
|
{
|
||||||
}
|
Method: http.MethodPost,
|
||||||
}(),
|
Path: "/forms/foo",
|
||||||
},
|
IsMultipart: true,
|
||||||
{
|
DisableLogging: true,
|
||||||
Stack: MultipartStack,
|
Handler: func(c echo.Context) error {
|
||||||
Handler: func() echo.MiddlewareFunc {
|
ctx := c.Get("context").(*Context)
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
ctx.outputPaths = []string{
|
||||||
return func(c echo.Context) error {
|
"/tests/test/testdata/api/sample1.txt",
|
||||||
return next(c)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Stack: DefaultStack,
|
|
||||||
Handler: func() echo.MiddlewareFunc {
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Handler: func() echo.MiddlewareFunc {
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mod.fs = gotenberg.NewFileSystem()
|
|
||||||
mod.logger = zap.NewNop()
|
|
||||||
|
|
||||||
err := mod.Start()
|
return nil
|
||||||
if err != nil {
|
},
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
},
|
||||||
}
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/bar",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(_ echo.Context) error { return errors.New("foo") },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mod.externalMiddlewares = []Middleware{
|
||||||
|
{
|
||||||
|
Stack: PreRouterStack,
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Stack: MultipartStack,
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Stack: DefaultStack,
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Handler: func() echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mod.readyFn = tc.readyFn
|
||||||
|
mod.fs = gotenberg.NewFileSystem()
|
||||||
|
mod.logger = zap.NewNop()
|
||||||
|
|
||||||
// health request.
|
err := mod.Start()
|
||||||
recorder := httptest.NewRecorder()
|
if !tc.expectError && err != nil {
|
||||||
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
||||||
|
|
||||||
mod.srv.ServeHTTP(recorder, healthRequest)
|
|
||||||
if recorder.Code != http.StatusOK {
|
|
||||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// "multipart/form-data" request.
|
|
||||||
multipartRequest := func(url string) *http.Request {
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err := writer.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
err := writer.WriteField("foo", "foo")
|
if tc.expectError && err == nil {
|
||||||
if err != nil {
|
t.Fatal("expected error but got none")
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
|
if tc.expectError {
|
||||||
if err != nil {
|
return
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_, err = part.Write([]byte("foo"))
|
// health request.
|
||||||
if err != nil {
|
recorder := httptest.NewRecorder()
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, url, body)
|
mod.srv.ServeHTTP(recorder, healthRequest)
|
||||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
return req
|
// "multipart/form-data" request.
|
||||||
}
|
multipartRequest := func(url string) *http.Request {
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
|
||||||
recorder = httptest.NewRecorder()
|
writer := multipart.NewWriter(body)
|
||||||
mod.srv.ServeHTTP(recorder, multipartRequest("/forms/foo"))
|
|
||||||
|
|
||||||
if recorder.Code != http.StatusOK {
|
defer func() {
|
||||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
err := writer.Close()
|
||||||
}
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
recorder = httptest.NewRecorder()
|
err := writer.WriteField("foo", "foo")
|
||||||
mod.srv.ServeHTTP(recorder, multipartRequest("/forms/bar"))
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if recorder.Code != http.StatusInternalServerError {
|
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
|
||||||
t.Errorf("expected %d status code but got %d", http.StatusInternalServerError, recorder.Code)
|
if err != nil {
|
||||||
}
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = mod.srv.Shutdown(context.TODO())
|
_, err = part.Write([]byte("foo"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, url, body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
mod.srv.ServeHTTP(recorder, multipartRequest("/forms/foo"))
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
mod.srv.ServeHTTP(recorder, multipartRequest("/forms/bar"))
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("expected %d status code but got %d", http.StatusInternalServerError, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mod.srv.Shutdown(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/mholt/archiver/v3"
|
"github.com/mholt/archiver/v3"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/text/runes"
|
|
||||||
"golang.org/x/text/transform"
|
|
||||||
"golang.org/x/text/unicode/norm"
|
"golang.org/x/text/unicode/norm"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
@@ -124,15 +121,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
|||||||
ctx.files = make(map[string]string)
|
ctx.files = make(map[string]string)
|
||||||
|
|
||||||
copyToDisk := func(fh *multipart.FileHeader) error {
|
copyToDisk := func(fh *multipart.FileHeader) error {
|
||||||
// Avoid directory traversal and normalize filename.
|
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/104.
|
|
||||||
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
|
||||||
|
|
||||||
filename, _, err := transform.String(t, filepath.Base(fh.Filename))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("transform filename: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
in, err := fh.Open()
|
in, err := fh.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open multipart file: %w", err)
|
return fmt.Errorf("open multipart file: %w", err)
|
||||||
@@ -145,6 +133,10 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Avoid directory traversal and make sure filename characters are
|
||||||
|
// normalized.
|
||||||
|
// See: https://github.com/gotenberg/gotenberg/issues/662.
|
||||||
|
filename := norm.NFC.String(filepath.Base(fh.Filename))
|
||||||
path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
|
path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
|
||||||
|
|
||||||
out, err := os.Create(path)
|
out, err := os.Create(path)
|
||||||
|
|||||||
@@ -105,12 +105,17 @@ func (provider *MiddlewareProviderMock) Middlewares() ([]Middleware, error) {
|
|||||||
// HealthCheckerMock is mock for the [HealthChecker] interface.
|
// HealthCheckerMock is mock for the [HealthChecker] interface.
|
||||||
type HealthCheckerMock struct {
|
type HealthCheckerMock struct {
|
||||||
ChecksMock func() ([]health.CheckerOption, error)
|
ChecksMock func() ([]health.CheckerOption, error)
|
||||||
|
ReadyMock func() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mod *HealthCheckerMock) Checks() ([]health.CheckerOption, error) {
|
func (mod *HealthCheckerMock) Checks() ([]health.CheckerOption, error) {
|
||||||
return mod.ChecksMock()
|
return mod.ChecksMock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mod *HealthCheckerMock) Ready() error {
|
||||||
|
return mod.ReadyMock()
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ Router = (*RouterMock)(nil)
|
_ Router = (*RouterMock)(nil)
|
||||||
|
|||||||
@@ -148,10 +148,18 @@ func TestHealthCheckerMock(t *testing.T) {
|
|||||||
ChecksMock: func() ([]health.CheckerOption, error) {
|
ChecksMock: func() ([]health.CheckerOption, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
|
ReadyMock: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := mock.Checks()
|
_, err := mock.Checks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from HealthCheckerMock.Checks, but got: %v", err)
|
t.Errorf("expected no error from HealthCheckerMock.Checks, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mock.Ready()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from HealthCheckerMock.Ready, but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func (b *chromiumBrowser) Start(logger *zap.Logger) error {
|
|||||||
b.ctxMu.Lock()
|
b.ctxMu.Lock()
|
||||||
defer b.ctxMu.Unlock()
|
defer b.ctxMu.Unlock()
|
||||||
|
|
||||||
// We have to keep the context around, as we need it to create a new tabs
|
// We have to keep the context around, as we need it to create new tabs
|
||||||
// later.
|
// later.
|
||||||
b.ctx = ctx
|
b.ctx = ctx
|
||||||
b.cancelFunc = func() {
|
b.cancelFunc = func() {
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
|
|
||||||
fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
||||||
fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion")
|
fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion")
|
||||||
fs.Duration("chromium-start-timeout", time.Duration(10)*time.Second, "Maximum duration to wait for Chromium to start or restart")
|
fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart")
|
||||||
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
|
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
|
||||||
fs.Bool("chromium-allow-insecure-localhost", false, "Ignore TLS/SSL errors on localhost")
|
fs.Bool("chromium-allow-insecure-localhost", false, "Ignore TLS/SSL errors on localhost")
|
||||||
fs.Bool("chromium-ignore-certificate-errors", false, "Ignore the certificate errors")
|
fs.Bool("chromium-ignore-certificate-errors", false, "Ignore the certificate errors")
|
||||||
@@ -408,6 +408,34 @@ func (mod *Chromium) Checks() ([]health.CheckerOption, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ready returns no error if the module is ready.
|
||||||
|
func (mod *Chromium) Ready() error {
|
||||||
|
if !mod.autoStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), mod.args.wsUrlReadTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return fmt.Errorf("context done while waiting for Chromium to be ready: %w", ctx.Err())
|
||||||
|
case <-ticker.C:
|
||||||
|
ok := mod.browser.Healthy(mod.logger)
|
||||||
|
if ok {
|
||||||
|
ticker.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Chromium returns an [Api] for interacting with Chromium for converting HTML
|
// Chromium returns an [Api] for interacting with Chromium for converting HTML
|
||||||
// documents to PDF.
|
// documents to PDF.
|
||||||
func (mod *Chromium) Chromium() (Api, error) {
|
func (mod *Chromium) Chromium() (Api, error) {
|
||||||
|
|||||||
@@ -399,6 +399,61 @@ func TestChromium_Checks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChromium_Ready(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
autoStart bool
|
||||||
|
startTimeout time.Duration
|
||||||
|
browser browser
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no auto-start",
|
||||||
|
autoStart: false,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start: context done",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(200) * time.Millisecond,
|
||||||
|
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start success",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return true
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
mod.autoStart = tc.autoStart
|
||||||
|
mod.args = browserArguments{wsUrlReadTimeout: tc.startTimeout}
|
||||||
|
mod.browser = tc.browser
|
||||||
|
|
||||||
|
err := mod.Ready()
|
||||||
|
|
||||||
|
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 TestChromium_Chromium(t *testing.T) {
|
func TestChromium_Chromium(t *testing.T) {
|
||||||
mod := new(Chromium)
|
mod := new(Chromium)
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ func waitForEventLoadingFinished(ctx context.Context, logger *zap.Logger) func()
|
|||||||
// completed or an error is encountered.
|
// completed or an error is encountered.
|
||||||
func runBatch(ctx context.Context, fn ...func() error) error {
|
func runBatch(ctx context.Context, fn ...func() error) error {
|
||||||
eg, _ := errgroup.WithContext(ctx)
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
for _, f := range fn {
|
for _, f := range fn {
|
||||||
eg.Go(f)
|
eg.Go(f)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ 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.PdfA1a},
|
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
|
||||||
options: DefaultOptions(),
|
options: DefaultOptions(),
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
WithFooterTemplate(options.FooterTemplate)
|
WithFooterTemplate(options.FooterTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug(fmt.Sprintf("print to Pdf with: %+v", printToPdf))
|
logger.Debug(fmt.Sprintf("print to PDF with: %+v", printToPdf))
|
||||||
|
|
||||||
_, stream, err := printToPdf.Do(ctx)
|
_, stream, err := printToPdf.Do(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("print to Pdf: %w", err)
|
return fmt.Errorf("print to PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := &streamReader{
|
reader := &streamReader{
|
||||||
@@ -295,12 +295,11 @@ func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScrip
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
|
|
||||||
return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err())
|
return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err())
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
evaluate := chromedp.Evaluate(expression, &ok)
|
evaluate := chromedp.Evaluate(expression, &ok)
|
||||||
|
|
||||||
err := evaluate.Do(ctx)
|
err := evaluate.Do(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression)
|
return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression)
|
||||||
@@ -308,7 +307,6 @@ func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScrip
|
|||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
PageRanges string
|
PageRanges string
|
||||||
|
|
||||||
// PdfFormats allows to convert the resulting PDF to PDF/A-1a, 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.
|
||||||
PdfFormats gotenberg.PdfFormats
|
PdfFormats gotenberg.PdfFormats
|
||||||
@@ -97,7 +97,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
|
|
||||||
fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature")
|
fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature")
|
||||||
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
|
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
|
||||||
fs.Duration("libreoffice-start-timeout", time.Duration(10)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
|
fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
@@ -277,6 +277,34 @@ func (a *Api) Checks() ([]health.CheckerOption, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ready returns no error if the module is ready.
|
||||||
|
func (a *Api) Ready() error {
|
||||||
|
if !a.autoStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), a.args.startTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return fmt.Errorf("context done while waiting for LibreOffice to be ready: %w", ctx.Err())
|
||||||
|
case <-ticker.C:
|
||||||
|
ok := a.libreOffice.Healthy(a.logger)
|
||||||
|
if ok {
|
||||||
|
ticker.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LibreOffice returns a [Uno] for interacting with LibreOffice.
|
// LibreOffice returns a [Uno] for interacting with LibreOffice.
|
||||||
func (a *Api) LibreOffice() (Uno, error) {
|
func (a *Api) LibreOffice() (Uno, error) {
|
||||||
return a, nil
|
return a, nil
|
||||||
|
|||||||
@@ -364,6 +364,61 @@ func TestApi_Checks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChromium_Ready(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
autoStart bool
|
||||||
|
startTimeout time.Duration
|
||||||
|
libreOffice libreOffice
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no auto-start",
|
||||||
|
autoStart: false,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start: context done",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(200) * time.Millisecond,
|
||||||
|
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start success",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return true
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.autoStart = tc.autoStart
|
||||||
|
a.args = libreOfficeArguments{startTimeout: tc.startTimeout}
|
||||||
|
a.libreOffice = tc.libreOffice
|
||||||
|
|
||||||
|
err := a.Ready()
|
||||||
|
|
||||||
|
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 TestApi_LibreOffice(t *testing.T) {
|
func TestApi_LibreOffice(t *testing.T) {
|
||||||
a := new(Api)
|
a := new(Api)
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
@@ -273,6 +276,9 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
|||||||
switch options.PdfFormats.PdfA {
|
switch options.PdfFormats.PdfA {
|
||||||
case "":
|
case "":
|
||||||
case gotenberg.PdfA1a:
|
case gotenberg.PdfA1a:
|
||||||
|
logger.Warn("PDF/A-1a is no more supported by LibreOffice (use PDF/A-1b instead)")
|
||||||
|
args = append(args, "--export", "SelectPdfVersion=1")
|
||||||
|
case gotenberg.PdfA1b:
|
||||||
args = append(args, "--export", "SelectPdfVersion=1")
|
args = append(args, "--export", "SelectPdfVersion=1")
|
||||||
case gotenberg.PdfA2b:
|
case gotenberg.PdfA2b:
|
||||||
args = append(args, "--export", "SelectPdfVersion=2")
|
args = append(args, "--export", "SelectPdfVersion=2")
|
||||||
@@ -290,6 +296,11 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputPath, err := nonBasicLatinCharactersGuard(logger, inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("non-basic latin characters guard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
args = append(args, "--output", outputPath, inputPath)
|
args = append(args, "--output", outputPath, inputPath)
|
||||||
|
|
||||||
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.unoBinPath, args...)
|
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.unoBinPath, args...)
|
||||||
@@ -321,6 +332,65 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
|||||||
return fmt.Errorf("convert to PDF: %w", err)
|
return fmt.Errorf("convert to PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LibreOffice cannot convert a file with a name containing non-basic Latin
|
||||||
|
// characters.
|
||||||
|
// See:
|
||||||
|
// https://github.com/gotenberg/gotenberg/issues/104
|
||||||
|
// https://github.com/gotenberg/gotenberg/issues/730
|
||||||
|
func nonBasicLatinCharactersGuard(logger *zap.Logger, inputPath string) (string, error) {
|
||||||
|
hasNonBasicLatinChars := func(str string) bool {
|
||||||
|
for _, r := range str {
|
||||||
|
// Check if the character is outside basic Latin.
|
||||||
|
if r != '.' && (r < ' ' || r > '~') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := filepath.Base(inputPath)
|
||||||
|
if !hasNonBasicLatinChars(filename) {
|
||||||
|
logger.Debug("no non-basic latin characters in filename, skip copy")
|
||||||
|
return inputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warn("non-basic latin characters in filename, copy to a file with a valid filename")
|
||||||
|
basePath := filepath.Dir(inputPath)
|
||||||
|
ext := filepath.Ext(inputPath)
|
||||||
|
newInputPath := filepath.Join(basePath, fmt.Sprintf("%s%s", uuid.NewString(), ext))
|
||||||
|
|
||||||
|
in, err := os.Open(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("open file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := in.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close file: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := os.Create(newInputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create new file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := out.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close new file: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("copy file to new file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newInputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Process = (*libreOfficeProcess)(nil)
|
_ gotenberg.Process = (*libreOfficeProcess)(nil)
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (PDF/A-1a)",
|
scenario: "success (PDF/A-1b)",
|
||||||
libreOffice: newLibreOfficeProcess(
|
libreOffice: newLibreOfficeProcess(
|
||||||
libreOfficeArguments{
|
libreOfficeArguments{
|
||||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
@@ -417,7 +417,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1a}},
|
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b}},
|
||||||
cancelledCtx: false,
|
cancelledCtx: false,
|
||||||
start: true,
|
start: true,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
@@ -545,7 +545,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
err := tc.libreOffice.pdf(
|
err := tc.libreOffice.pdf(
|
||||||
ctx,
|
ctx,
|
||||||
logger,
|
logger,
|
||||||
fmt.Sprintf("file://%s/document.txt", tc.fs.WorkingDirPath()),
|
fmt.Sprintf("%s/document.txt", tc.fs.WorkingDirPath()),
|
||||||
fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()),
|
fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()),
|
||||||
tc.options,
|
tc.options,
|
||||||
)
|
)
|
||||||
@@ -564,3 +564,87 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNonBasicLatinCharactersGuard(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
fs *gotenberg.FileSystem
|
||||||
|
filename string
|
||||||
|
expectSameInputPath bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "basic latin characters",
|
||||||
|
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("Basic latin characters"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
filename: "document.txt",
|
||||||
|
expectSameInputPath: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "non-basic latin characters",
|
||||||
|
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/éèßàùä.txt", fs.WorkingDirPath()), []byte("Non-basic latin characters"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
filename: "éèßàùä.txt",
|
||||||
|
expectSameInputPath: false,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(tc.fs.WorkingDirPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
inputPath := fmt.Sprintf("%s/%s", tc.fs.WorkingDirPath(), tc.filename)
|
||||||
|
newInputPath, err := nonBasicLatinCharactersGuard(
|
||||||
|
zap.NewNop(),
|
||||||
|
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.expectSameInputPath && newInputPath != inputPath {
|
||||||
|
t.Fatalf("expected same input path, but got '%s'", newInputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectSameInputPath && newInputPath == inputPath {
|
||||||
|
t.Fatalf("expected different input path, but got same '%s'", newInputPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func init() {
|
|||||||
// LibreOfficePdfEngine interacts with the LibreOffice (Universal Network Objects) API
|
// LibreOfficePdfEngine interacts with the LibreOffice (Universal Network Objects) API
|
||||||
// and implements the [gotenberg.PdfEngine] interface.
|
// and implements the [gotenberg.PdfEngine] interface.
|
||||||
type LibreOfficePdfEngine struct {
|
type LibreOfficePdfEngine struct {
|
||||||
unoAPI api.Uno
|
unoApi api.Uno
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a [LibreOfficePdfEngine]'s module descriptor.
|
// Descriptor returns a [LibreOfficePdfEngine]'s module descriptor.
|
||||||
@@ -36,12 +36,12 @@ func (engine *LibreOfficePdfEngine) Provision(ctx *gotenberg.Context) error {
|
|||||||
return fmt.Errorf("get LibreOffice Uno provider: %w", err)
|
return fmt.Errorf("get LibreOffice Uno provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
unoAPI, err := provider.(api.Provider).LibreOffice()
|
unoApi, err := provider.(api.Provider).LibreOffice()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get LibreOffice Uno: %w", err)
|
return fmt.Errorf("get LibreOffice Uno: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.unoAPI = unoAPI
|
engine.unoApi = unoApi
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -52,11 +52,11 @@ func (engine *LibreOfficePdfEngine) Merge(ctx context.Context, logger *zap.Logge
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
||||||
// PDF/A-1a, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
|
// PDF/A-1b, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
|
||||||
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
|
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
|
||||||
// error.
|
// error.
|
||||||
func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
err := engine.unoAPI.Pdf(ctx, logger, inputPath, outputPath, api.Options{
|
err := engine.unoApi.Pdf(ctx, logger, inputPath, outputPath, api.Options{
|
||||||
PdfFormats: formats,
|
PdfFormats: formats,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
engine := &LibreOfficePdfEngine{unoAPI: tc.api}
|
engine := &LibreOfficePdfEngine{unoApi: tc.api}
|
||||||
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|
||||||
if !tc.expectError && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
|
|||||||
@@ -88,9 +88,12 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
"pdfa": {
|
||||||
"foo",
|
"foo",
|
||||||
},
|
},
|
||||||
|
"nativePdfFormats": {
|
||||||
|
"false",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -120,8 +123,11 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"nativePdfFormats": {
|
||||||
|
"false",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -209,7 +215,7 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 2,
|
expectOutputPathsCount: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with non-native PDF/A (single file)",
|
scenario: "success with non-native PDF/A & PDF/UA (single file)",
|
||||||
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{
|
||||||
@@ -217,7 +223,10 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
},
|
},
|
||||||
"nativePdfFormats": {
|
"nativePdfFormats": {
|
||||||
"false",
|
"false",
|
||||||
@@ -254,13 +263,13 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"true",
|
"true",
|
||||||
},
|
},
|
||||||
"pdfFormat": {
|
"pdfFormat": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"nativePdfFormat": {
|
"nativePdfFormat": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"pdfua": {
|
"pdfua": {
|
||||||
"true",
|
"true",
|
||||||
@@ -375,7 +384,7 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"true",
|
"true",
|
||||||
},
|
},
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"nativePdfFormats": {
|
"nativePdfFormats": {
|
||||||
"false",
|
"false",
|
||||||
@@ -469,7 +478,7 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with non-native PDF/A (merge)",
|
scenario: "success with non-native PDF/A & PDF/UA (merge)",
|
||||||
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{
|
||||||
@@ -481,7 +490,54 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"true",
|
"true",
|
||||||
},
|
},
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
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": {
|
"nativePdfFormats": {
|
||||||
"false",
|
"false",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (engine *PdfCpu) Provision(ctx *gotenberg.Context) error {
|
|||||||
|
|
||||||
// Merge combines multiple PDFs into a single PDF.
|
// Merge combines multiple PDFs into a single PDF.
|
||||||
func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf)
|
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, false, engine.conf)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -127,7 +127,7 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -157,7 +157,7 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1a,
|
||||||
},
|
},
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"pdfua": {
|
"pdfua": {
|
||||||
"true",
|
"true",
|
||||||
@@ -259,7 +259,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -283,7 +283,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -306,7 +306,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
ctx.SetCancelled(true)
|
ctx.SetCancelled(true)
|
||||||
@@ -333,7 +333,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1a,
|
||||||
},
|
},
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"pdfua": {
|
"pdfua": {
|
||||||
"true",
|
"true",
|
||||||
@@ -363,7 +363,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1a,
|
||||||
},
|
},
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1a,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"pdfua": {
|
"pdfua": {
|
||||||
"true",
|
"true",
|
||||||
|
|||||||
Reference in New Issue
Block a user