fix: LibreOffice newer versions stability (#697)

This commit is contained in:
Julien Neuhart
2023-10-23 17:05:10 +02:00
committed by GitHub
parent 9cc8e16d64
commit 258876d13f
59 changed files with 870 additions and 1383 deletions

View File

@@ -1,10 +1,27 @@
linters-settings:
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/gotenberg/gotenberg/v7) # Ensure that this is always at the top and always has a line break.
# Skip generated files.
# Default: true
skip-generated: true
# Skip vendor files.
# Default: true
skip-vendor: true
# Enable custom order of sections.
# If `true`, make the section order the same as the order of `sections`.
# Default: false
custom-order: true
linters: linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- errcheck - errcheck
- gofmt - gci
- goimports - gofumpt
- gosec - gosec
- gosimple - gosimple
- govet - govet

View File

@@ -138,9 +138,12 @@ tests-once: ## Run the tests once (prefer the "tests" command while developing)
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \ $(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \
gotest gotest
# go install mvdan.cc/gofumpt@latest
# go install github.com/daixiang0/gci@latest
.PHONY: fmt .PHONY: fmt
fmt: ## Format the code and "optimize" the dependencies fmt: ## Format the code and "optimize" the dependencies
go fmt ./... gofumpt -l -w .
gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v7)" --skip-generated --skip-vendor --custom-order .
go mod tidy go mod tidy
.PHONY: godoc .PHONY: godoc

View File

@@ -30,7 +30,7 @@ RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v7/cmd.Ve
# ---------------------------------------------- # ----------------------------------------------
# Final stage # Final stage
# ---------------------------------------------- # ----------------------------------------------
FROM debian:11-slim FROM debian:12-slim
ARG GOTENBERG_VERSION ARG GOTENBERG_VERSION
ARG GOTENBERG_USER_GID ARG GOTENBERG_USER_GID
@@ -57,8 +57,7 @@ RUN \
# Install system dependencies required for the next instructions or debugging. # Install system dependencies required for the next instructions or debugging.
# Note: tini is a helper for reaping zombie processes. # Note: tini is a helper for reaping zombie processes.
apt-get update -qq &&\ apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg htop tini python3 default-jre-headless &&\ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg tini python3 default-jre-headless &&\
ln -s /usr/bin/htop /usr/bin/top &&\
# Cleanup. # Cleanup.
# Note: the Debian image does automatically a clean after each install thanks to a hook. # Note: the Debian image does automatically a clean after each install thanks to a hook.
# Therefore, there is no need for apt-get clean. # Therefore, there is no need for apt-get clean.
@@ -140,8 +139,9 @@ RUN \
RUN \ RUN \
# Install LibreOffice & unoconv. # Install LibreOffice & unoconv.
echo "deb http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list &&\
apt-get update -qq &&\ apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends libreoffice &&\ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bookworm-backports libreoffice &&\
curl -Ls https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv -o /usr/bin/unoconv &&\ curl -Ls https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv -o /usr/bin/unoconv &&\
chmod +x /usr/bin/unoconv &&\ chmod +x /usr/bin/unoconv &&\
# unoconv will look for the Python binary, which has to be at version 3. # unoconv will look for the Python binary, which has to be at version 3.
@@ -179,7 +179,6 @@ COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
COPY --from=binary-stage /home/gotenberg /usr/bin/ COPY --from=binary-stage /home/gotenberg /usr/bin/
# Environment variables required by modules or else. # Environment variables required by modules or else.
ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
ENV CHROMIUM_BIN_PATH /usr/bin/chromium ENV CHROMIUM_BIN_PATH /usr/bin/chromium
ENV UNOCONV_BIN_PATH /usr/bin/unoconv ENV UNOCONV_BIN_PATH /usr/bin/unoconv
ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin

View File

@@ -8,9 +8,10 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg. // See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.

View File

@@ -2,7 +2,6 @@ package main
import ( import (
gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd" gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd"
// Gotenberg modules. // Gotenberg modules.
_ "github.com/gotenberg/gotenberg/v7/pkg/standard" _ "github.com/gotenberg/gotenberg/v7/pkg/standard"
) )

20
go.mod
View File

@@ -5,7 +5,7 @@ go 1.21
require ( require (
github.com/alexliesenfeld/health v0.7.0 github.com/alexliesenfeld/health v0.7.0
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/chromedp/cdproto v0.0.0-20230914224007-a15a36ccbc2e github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617
github.com/chromedp/chromedp v0.9.2 github.com/chromedp/chromedp v0.9.2
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.1 github.com/google/uuid v1.3.1
@@ -21,18 +21,18 @@ require (
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.5.0
github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/prometheus/client_golang v1.16.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.13.0 // indirect golang.org/x/crypto v0.14.0 // indirect
golang.org/x/image v0.12.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.15.0 golang.org/x/net v0.16.0
golang.org/x/sync v0.3.0 golang.org/x/sync v0.4.0
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.12.0 golang.org/x/term v0.13.0
golang.org/x/text v0.13.0 golang.org/x/text v0.13.0
) )
@@ -55,9 +55,9 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // 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

70
go.sum
View File

@@ -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-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20230914224007-a15a36ccbc2e h1:BfDqq+EHA0HP037qWakDtYxIg9erpn2aZfZlrtnB35E= github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617 h1:/5dwcyi5WOawM1Iz6MjrYqB90TRIdZv3O0fVHEJb86w=
github.com/chromedp/cdproto v0.0.0-20230914224007-a15a36ccbc2e/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
@@ -104,14 +104,14 @@ 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=
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.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -142,62 +142,32 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.5.0/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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=

View File

@@ -158,7 +158,6 @@ func (cmd Cmd) pipeOutput() error {
for { for {
line, _, err := r.ReadLine() line, _, err := r.ReadLine()
if err != nil { if err != nil {
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") { if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
logger.Error(fmt.Sprintf("pipe unix process output error: %s", err)) logger.Error(fmt.Sprintf("pipe unix process output error: %s", err))

View File

@@ -47,7 +47,6 @@ func (ctx Context) ParsedFlags() ParsedFlags {
// initializes it. Otherwise, returns the already initialized instance. // initializes it. Otherwise, returns the already initialized instance.
func (ctx *Context) Module(kind interface{}) (interface{}, error) { func (ctx *Context) Module(kind interface{}) (interface{}, error) {
mods, err := ctx.Modules(kind) mods, err := ctx.Modules(kind)
if err != nil { if err != nil {
return nil, fmt.Errorf("get module: %w", err) return nil, fmt.Errorf("get module: %w", err)
} }

View File

@@ -7,24 +7,44 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
// TmpPath returns the default directory to use for temporary files and // FileSystem provides utilities for managing temporary directories. It creates
// directories. Most if not all files and directories created by the // unique directory names based on UUIDs to ensure isolation of temporary files
// application and its dependencies must be based on this default directory. // for different modules.
func TmpPath() string { type FileSystem struct {
return os.TempDir() workingDir string
} }
// NewDirPath returns a random absolute path based on the temporary path. // NewFileSystem initializes a new FileSystem instance with a unique working
func NewDirPath() string { // directory.
return fmt.Sprintf("%s/%s", TmpPath(), uuid.New()) func NewFileSystem() *FileSystem {
return &FileSystem{
workingDir: uuid.NewString(),
}
} }
// MkdirAll creates a random directory based on the temporary path and // WorkingDir returns the unique name of the working directory.
// returns its absolute path. func (fs *FileSystem) WorkingDir() string {
func MkdirAll() (string, error) { return fs.workingDir
path := NewDirPath() }
err := os.MkdirAll(path, 0755) // WorkingDirPath constructs and returns the full path to the working directory
// inside the system's temporary directory.
func (fs *FileSystem) WorkingDirPath() string {
return fmt.Sprintf("%s/%s", os.TempDir(), fs.workingDir)
}
// NewDirPath generates a new unique path for a directory inside the working
// directory.
func (fs *FileSystem) NewDirPath() string {
return fmt.Sprintf("%s/%s", fs.WorkingDirPath(), uuid.NewString())
}
// MkdirAll creates a new unique directory inside the working directory and
// returns its path. If the directory creation fails, an error is returned.
func (fs *FileSystem) MkdirAll() (string, error) {
path := fs.NewDirPath()
err := os.MkdirAll(path, 0o755)
if err != nil { if err != nil {
return "", fmt.Errorf("create directory %s: %w", path, err) return "", fmt.Errorf("create directory %s: %w", path, err)
} }

View File

@@ -1,59 +1,55 @@
package gotenberg package gotenberg
import ( import (
"fmt"
"os" "os"
"strings" "strings"
"testing" "testing"
) )
func TestTmpPath(t *testing.T) { func TestFileSystem_WorkingDir(t *testing.T) {
osTempDir := os.TempDir() fs := NewFileSystem()
tmpPath := TmpPath() dirName := fs.WorkingDir()
if tmpPath != osTempDir { if dirName == "" {
t.Errorf("expected path '%s' but got '%s'", osTempDir, tmpPath) t.Error("expected directory name but got empty string")
} }
} }
func TestNewDirPath(t *testing.T) { func TestFileSystem_WorkingDirPath(t *testing.T) {
newDirPath := NewDirPath() fs := NewFileSystem()
tmpPath := TmpPath() expectedPath := fmt.Sprintf("%s/%s", os.TempDir(), fs.WorkingDir())
if !strings.HasPrefix(newDirPath, tmpPath) { if fs.WorkingDirPath() != expectedPath {
t.Fatalf("expected path '%s' to start with '%s'", newDirPath, tmpPath) t.Errorf("expected path '%s' but got '%s'", expectedPath, fs.WorkingDirPath())
}
newDirPaths := make([]string, 1000)
for i := range newDirPaths {
newDirPaths[i] = NewDirPath()
}
for i, newDirPath := range newDirPaths {
for j, comparison := range newDirPaths {
if i == j {
continue
}
if newDirPath == comparison {
t.Fatalf("expected path '%s' (index %d) to be unique, but found an identical path on index %d", newDirPath, i, j)
}
}
} }
} }
func TestMkdirAll(t *testing.T) { func TestFileSystem_NewDirPath(t *testing.T) {
path, err := MkdirAll() fs := NewFileSystem()
newDir := fs.NewDirPath()
expectedPrefix := fs.WorkingDirPath()
if !strings.HasPrefix(newDir, expectedPrefix) {
t.Errorf("expected new directory to start with '%s' but got '%s'", expectedPrefix, newDir)
}
}
func TestFileSystem_MkdirAll(t *testing.T) {
fs := NewFileSystem()
newPath, err := fs.MkdirAll()
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
tmpPath := TmpPath() _, err = os.Stat(newPath)
if !strings.HasPrefix(path, tmpPath) { if os.IsNotExist(err) {
t.Fatalf("expected path '%s' to start with '%s'", path, tmpPath) t.Errorf("expected directory '%s' to exist but it doesn't", newPath)
} }
_, err = os.Stat(path) err = os.RemoveAll(fs.WorkingDirPath())
if os.IsNotExist(err) { if err != nil {
t.Errorf("expected path '%s' to exist but got: %v", path, err) t.Fatalf("expected no error while cleaning up but got: %v", err)
} }
} }

53
pkg/gotenberg/gc.go Normal file
View File

@@ -0,0 +1,53 @@
package gotenberg
import (
"fmt"
"os"
"path/filepath"
"strings"
"go.uber.org/zap"
)
// GarbageCollect scans the root path and deletes files or directories with
// names containing specific substrings.
func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr []string) error {
logger = logger.Named("gc")
// To make sure that the next Walk method stays on
// the root level of the considered path, we have to
// return a filepath.SkipDir error if the current path
// is a directory.
skipDirOrNil := func(info os.FileInfo) error {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
return filepath.Walk(rootPath, func(path string, info os.FileInfo, pathErr error) error {
if pathErr != nil {
return pathErr
}
if path == rootPath {
return nil
}
for _, substr := range includeSubstr {
if strings.Contains(info.Name(), substr) || path == substr {
err := os.RemoveAll(path)
if err != nil {
return fmt.Errorf("garbage collect '%s': %w", path, err)
}
logger.Debug(fmt.Sprintf("'%s' removed", path))
return skipDirOrNil(info)
}
}
return skipDirOrNil(info)
})
}

97
pkg/gotenberg/gc_test.go Normal file
View File

@@ -0,0 +1,97 @@
package gotenberg
import (
"fmt"
"os"
"testing"
"github.com/google/uuid"
"go.uber.org/zap"
)
func TestGarbageCollect(t *testing.T) {
for _, tc := range []struct {
scenario string
rootPath string
includeSubstr []string
expectErr bool
expectNotExists []string
expectExists []string
}{
{
scenario: "root path does not exist",
rootPath: uuid.NewString(),
expectErr: true,
},
{
scenario: "remove include substrings",
rootPath: func() string {
path := fmt.Sprintf("%s/a_directory", os.TempDir())
err := os.MkdirAll(path, 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file", path), []byte{1}, 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
err = os.WriteFile(fmt.Sprintf("%s/a_bar_file", path), []byte{1}, 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
err = os.WriteFile(fmt.Sprintf("%s/a_baz_file", path), []byte{1}, 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return path
}(),
includeSubstr: []string{"foo", fmt.Sprintf("%s/a_directory/a_bar_file", os.TempDir())},
expectExists: []string{"a_baz_file"},
expectNotExists: []string{"a_foo_file", "a_bar_file"},
},
} {
func() {
defer func() {
err := os.RemoveAll(tc.rootPath)
if err != nil {
t.Fatalf("%s: expected no error while cleaning up but got: %v", tc.scenario, err)
}
}()
err := GarbageCollect(zap.NewNop(), tc.rootPath, tc.includeSubstr)
if !tc.expectErr && err != nil {
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err)
}
if tc.expectErr && err == nil {
t.Fatalf("%s: expected error but got: %v", tc.scenario, err)
}
if tc.expectErr && err != nil {
return
}
for _, name := range tc.expectNotExists {
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
_, err = os.Stat(path)
if !os.IsNotExist(err) {
t.Errorf("%s: expected '%s' not to exist but it does: %v", tc.scenario, path, err)
}
}
for _, name := range tc.expectExists {
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
_, err = os.Stat(path)
if os.IsNotExist(err) {
t.Errorf("%s: expected '%s' to exist but it does not: %v", tc.scenario, path, err)
}
}
}()
}
}

View File

@@ -12,13 +12,13 @@ import (
"time" "time"
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"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"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func init() { func init() {
@@ -39,7 +39,7 @@ type API struct {
routes []Route routes []Route
externalMiddlewares []Middleware externalMiddlewares []Middleware
healthChecks []health.CheckerOption healthChecks []health.CheckerOption
gcGraceDuration time.Duration fs *gotenberg.FileSystem
logger *zap.Logger logger *zap.Logger
srv *echo.Echo srv *echo.Echo
} }
@@ -149,12 +149,6 @@ type HealthChecker interface {
Checks() ([]health.CheckerOption, error) Checks() ([]health.CheckerOption, error)
} }
// GarbageCollectorGraceDurationIncrementer is a module interface for
// increasing the grace duration provided by the API for the garbage collector.
type GarbageCollectorGraceDurationIncrementer interface {
AddGraceDuration() time.Duration
}
// Descriptor returns an API's module descriptor. // Descriptor returns an API's module descriptor.
func (API) Descriptor() gotenberg.ModuleDescriptor { func (API) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ return gotenberg.ModuleDescriptor{
@@ -283,18 +277,7 @@ func (a *API) Provision(ctx *gotenberg.Context) error {
a.healthChecks = append(a.healthChecks, checks...) a.healthChecks = append(a.healthChecks, checks...)
} }
// Grace duration. // Logger.
a.gcGraceDuration = a.timeout
mods, err = ctx.Modules(new(GarbageCollectorGraceDurationIncrementer))
if err != nil {
return fmt.Errorf("get garbage collector grace duration increments: %w", err)
}
for _, incrementer := range mods {
a.gcGraceDuration += incrementer.(GarbageCollectorGraceDurationIncrementer).AddGraceDuration()
}
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider)) loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
if err != nil { if err != nil {
return fmt.Errorf("get logger provider: %w", err) return fmt.Errorf("get logger provider: %w", err)
@@ -307,6 +290,9 @@ func (a *API) Provision(ctx *gotenberg.Context) error {
a.logger = logger a.logger = logger
// File system.
a.fs = gotenberg.NewFileSystem()
return nil return nil
} }
@@ -437,7 +423,7 @@ func (a *API) Start() error {
var middlewares []echo.MiddlewareFunc var middlewares []echo.MiddlewareFunc
if route.IsMultipart { if route.IsMultipart {
middlewares = append(middlewares, contextMiddleware(a.timeout)) middlewares = append(middlewares, contextMiddleware(a.fs, a.timeout))
for _, externalMultipartMiddleware := range externalMultipartMiddlewares { for _, externalMultipartMiddleware := range externalMultipartMiddlewares {
middlewares = append(middlewares, externalMultipartMiddleware.Handler) middlewares = append(middlewares, externalMultipartMiddleware.Handler)
@@ -488,17 +474,10 @@ func (a API) Stop(ctx context.Context) error {
return a.srv.Shutdown(ctx) return a.srv.Shutdown(ctx)
} }
// GraceDuration updates the expiration time of files and directories parsed by
// the gc.GarbageCollector.
func (a API) GraceDuration() time.Duration {
return a.gcGraceDuration
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*API)(nil) _ gotenberg.Module = (*API)(nil)
_ gotenberg.Provisioner = (*API)(nil) _ gotenberg.Provisioner = (*API)(nil)
_ gotenberg.Validator = (*API)(nil) _ gotenberg.Validator = (*API)(nil)
_ gotenberg.App = (*API)(nil) _ gotenberg.App = (*API)(nil)
_ gc.GarbageCollectorGraceDurationModifier = (*API)(nil)
) )

View File

@@ -10,12 +10,12 @@ import (
"os" "os"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
type ProtoModule struct { type ProtoModule struct {
@@ -62,15 +62,6 @@ func (mod ProtoHealthChecker) Checks() ([]health.CheckerOption, error) {
return mod.checks() return mod.checks()
} }
type ProtoGarbageCollectorGraceDurationIncrementer struct {
ProtoValidator
addGraceDuration func() time.Duration
}
func (mod ProtoGarbageCollectorGraceDurationIncrementer) AddGraceDuration() time.Duration {
return mod.addGraceDuration()
}
type ProtoLoggerProvider struct { type ProtoLoggerProvider struct {
ProtoModule ProtoModule
logger func(mod gotenberg.Module) (*zap.Logger, error) logger func(mod gotenberg.Module) (*zap.Logger, error)
@@ -93,18 +84,16 @@ func TestAPI_Descriptor(t *testing.T) {
func TestAPI_Provision(t *testing.T) { func TestAPI_Provision(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
ctx *gotenberg.Context ctx *gotenberg.Context
setEnv func(i int) setEnv func(i int)
expectPort int expectPort int
expectMiddlewares []Middleware expectMiddlewares []Middleware
expectGraceDuration time.Duration expectErr bool
expectErr bool
}{ }{
{ {
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=FOO"}) err := fs.Parse([]string{"--api-port-from-env=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)
} }
@@ -122,7 +111,6 @@ func TestAPI_Provision(t *testing.T) {
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-port-from-env=PORT"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -146,7 +134,6 @@ func TestAPI_Provision(t *testing.T) {
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-port-from-env=PORT"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -170,7 +157,6 @@ func TestAPI_Provision(t *testing.T) {
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-port-from-env=PORT"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -335,60 +321,6 @@ func TestAPI_Provision(t *testing.T) {
}(), }(),
expectErr: true, expectErr: true,
}, },
{
ctx: func() *gotenberg.Context {
mod := struct {
ProtoGarbageCollectorGraceDurationIncrementer
}{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
}
mod.validate = func() error {
return errors.New("foo")
}
mod.addGraceDuration = func() time.Duration {
return 0
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
mod.Descriptor(),
},
)
}(),
expectErr: true,
},
{
ctx: func() *gotenberg.Context {
mod := struct {
ProtoGarbageCollectorGraceDurationIncrementer
}{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
}
mod.validate = func() error {
return nil
}
mod.addGraceDuration = func() time.Duration {
return time.Duration(3) * time.Second
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(API).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
mod.Descriptor(),
},
)
}(),
expectGraceDuration: time.Duration(33) * time.Second,
expectErr: true,
},
{ {
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -526,10 +458,6 @@ func TestAPI_Provision(t *testing.T) {
t.Errorf("test %d: expected %+v, but got: %+v", i, tc.expectMiddlewares, mod.externalMiddlewares) t.Errorf("test %d: expected %+v, but got: %+v", i, tc.expectMiddlewares, mod.externalMiddlewares)
} }
if tc.expectGraceDuration != 0 && mod.gcGraceDuration != tc.expectGraceDuration {
t.Errorf("test %d: expected gc grace duration '%s' but got '%s'", i, tc.expectGraceDuration, mod.gcGraceDuration)
}
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("test %d: expected error but got: %v", i, err)
} }
@@ -759,6 +687,7 @@ func TestAPI_Start(t *testing.T) {
}(), }(),
}, },
} }
mod.fs = gotenberg.NewFileSystem()
mod.logger = zap.NewNop() mod.logger = zap.NewNop()
err := mod.Start() err := mod.Start()
@@ -866,36 +795,20 @@ func TestAPI_Stop(t *testing.T) {
} }
} }
func TestAPI_GraceDuration(t *testing.T) {
mod := API{
gcGraceDuration: time.Duration(3) * time.Second,
}
expect := time.Duration(3) * time.Second
actual := mod.GraceDuration()
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*ProtoModule)(nil) _ gotenberg.Module = (*ProtoModule)(nil)
_ gotenberg.Validator = (*ProtoValidator)(nil) _ gotenberg.Validator = (*ProtoValidator)(nil)
_ gotenberg.Module = (*ProtoValidator)(nil) _ gotenberg.Module = (*ProtoValidator)(nil)
_ Router = (*ProtoRouter)(nil) _ Router = (*ProtoRouter)(nil)
_ gotenberg.Module = (*ProtoRouter)(nil) _ gotenberg.Module = (*ProtoRouter)(nil)
_ gotenberg.Validator = (*ProtoRouter)(nil) _ gotenberg.Validator = (*ProtoRouter)(nil)
_ MiddlewareProvider = (*ProtoMiddlewareProvider)(nil) _ MiddlewareProvider = (*ProtoMiddlewareProvider)(nil)
_ gotenberg.Module = (*ProtoMiddlewareProvider)(nil) _ gotenberg.Module = (*ProtoMiddlewareProvider)(nil)
_ gotenberg.Validator = (*ProtoMiddlewareProvider)(nil) _ gotenberg.Validator = (*ProtoMiddlewareProvider)(nil)
_ HealthChecker = (*ProtoHealthChecker)(nil) _ HealthChecker = (*ProtoHealthChecker)(nil)
_ gotenberg.Module = (*ProtoHealthChecker)(nil) _ gotenberg.Module = (*ProtoHealthChecker)(nil)
_ gotenberg.Validator = (*ProtoHealthChecker)(nil) _ gotenberg.Validator = (*ProtoHealthChecker)(nil)
_ GarbageCollectorGraceDurationIncrementer = (*ProtoGarbageCollectorGraceDurationIncrementer)(nil) _ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
_ gotenberg.Module = (*ProtoGarbageCollectorGraceDurationIncrementer)(nil) _ gotenberg.Module = (*ProtoLoggerProvider)(nil)
_ gotenberg.Validator = (*ProtoGarbageCollectorGraceDurationIncrementer)(nil)
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
) )

View File

@@ -15,13 +15,14 @@ import (
"unicode" "unicode"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"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/runes"
"golang.org/x/text/transform" "golang.org/x/text/transform"
"golang.org/x/text/unicode/norm" "golang.org/x/text/unicode/norm"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
var ( var (
@@ -49,7 +50,7 @@ type Context struct {
} }
// newContext returns a Context by parsing a "multipart/form-data" request. // newContext returns a Context by parsing a "multipart/form-data" request.
func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration) (*Context, context.CancelFunc, error) { func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSystem, timeout time.Duration) (*Context, context.CancelFunc, error) {
processCtx, processCancel := context.WithTimeout(context.Background(), timeout) processCtx, processCancel := context.WithTimeout(context.Background(), timeout)
ctx := &Context{ ctx := &Context{
@@ -81,7 +82,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
return return
} }
ctx.logger.Debug(fmt.Sprintf("'%s' removed", ctx.dirPath)) ctx.logger.Debug(fmt.Sprintf("'%s' context's working directory removed", ctx.dirPath))
ctx.cancelled = true ctx.cancelled = true
} }
}() }()
@@ -113,7 +114,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
return nil, cancel, fmt.Errorf("get multipart form: %w", err) return nil, cancel, fmt.Errorf("get multipart form: %w", err)
} }
dirPath, err := gotenberg.MkdirAll() dirPath, err := fs.MkdirAll()
if err != nil { if err != nil {
return nil, cancel, fmt.Errorf("create working directory: %w", err) return nil, cancel, fmt.Errorf("create working directory: %w", err)
} }

View File

@@ -12,9 +12,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestNewContext(t *testing.T) { func TestNewContext(t *testing.T) {
@@ -104,7 +105,7 @@ func TestNewContext(t *testing.T) {
}, },
} { } {
handler := func(c echo.Context) error { handler := func(c echo.Context) error {
_, cancel, err := newContext(c, zap.NewNop(), time.Duration(10)*time.Second) _, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(), time.Duration(10)*time.Second)
defer cancel() defer cancel()
// Context already cancelled. // Context already cancelled.
defer cancel() defer cancel()
@@ -277,28 +278,33 @@ func TestContext_BuildOutputFile(t *testing.T) {
}, },
}, },
} { } {
dirPath, err := gotenberg.MkdirAll() func() {
if err != nil { fs := gotenberg.NewFileSystem()
t.Fatalf("%d: expected no erro but got: %v", i, err) dirPath, err := fs.MkdirAll()
} if err != nil {
t.Fatalf("%d: expected no erro but got: %v", i, err)
}
tc.ctx.dirPath = dirPath defer func() {
tc.ctx.logger = zap.NewNop() err := os.RemoveAll(fs.WorkingDirPath())
if err != nil {
t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
}
}()
_, err = tc.ctx.BuildOutputFile() tc.ctx.dirPath = dirPath
tc.ctx.logger = zap.NewNop()
if tc.expectErr && err == nil { _, err = tc.ctx.BuildOutputFile()
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("test %d: expected error but got: %v", i, err)
} }
err = os.RemoveAll(dirPath) if !tc.expectErr && err != nil {
if err != nil { t.Errorf("test %d: expected no error but got: %v", i, err)
t.Fatalf("%d: expected no erro but got: %v", i, err) }
} }()
} }
} }

View File

@@ -11,6 +11,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
// ErrAsyncProcess happens when a handler or middleware handles a request in an // ErrAsyncProcess happens when a handler or middleware handles a request in an
@@ -20,7 +22,8 @@ var ErrAsyncProcess = errors.New("async process")
// 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.
func ParseError(err error) (int, string) { func ParseError(err error) (int, string) {
echoErr, ok := err.(*echo.HTTPError) var echoErr *echo.HTTPError
ok := errors.As(err, &echoErr)
if ok { if ok {
return echoErr.Code, http.StatusText(echoErr.Code) return echoErr.Code, http.StatusText(echoErr.Code)
} }
@@ -199,14 +202,14 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
// //
// ctx := c.Get("context").(*api.Context) // ctx := c.Get("context").(*api.Context)
// cancel := c.Get("cancel").(context.CancelFunc) // cancel := c.Get("cancel").(context.CancelFunc)
func contextMiddleware(timeout time.Duration) echo.MiddlewareFunc { func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration) 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 {
logger := c.Get("logger").(*zap.Logger) logger := c.Get("logger").(*zap.Logger)
// We create a context with a timeout so that underlying processes are // We create a context with a timeout so that underlying processes are
// able to stop early and handle correctly a timeout scenario. // able to stop early and handle correctly a timeout scenario.
ctx, cancel, err := newContext(c, logger, timeout) ctx, cancel, err := newContext(c, logger, fs, timeout)
if err != nil { if err != nil {
cancel() cancel()

View File

@@ -13,6 +13,8 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestParseError(t *testing.T) { func TestParseError(t *testing.T) {
@@ -122,7 +124,6 @@ func TestLatencyMiddleware(t *testing.T) {
return nil return nil
}, },
)(c) )(c)
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -150,7 +151,6 @@ func TestRootPathMiddleware(t *testing.T) {
return nil return nil
}, },
)(c) )(c)
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -191,7 +191,6 @@ func TestTraceMiddleware(t *testing.T) {
return nil return nil
}, },
)(c) )(c)
if err != nil { if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err) t.Fatalf("test %d: expected no error but got: %v", i, err)
} }
@@ -271,7 +270,6 @@ func TestLoggerMiddleware(t *testing.T) {
} }
err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c) err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c)
if err != nil { if err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("test %d: expected no error but got: %v", i, err)
} }
@@ -389,7 +387,7 @@ func TestContextMiddleware(t *testing.T) {
c.Set("trace", "foo") c.Set("trace", "foo")
c.Set("startTime", time.Now()) c.Set("startTime", time.Now())
err := contextMiddleware(time.Duration(10) * time.Second)(tc.next)(c) err := contextMiddleware(gotenberg.NewFileSystem(), time.Duration(10)*time.Second)(tc.next)(c)
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("test %d: expected error but got: %v", i, err)

View File

@@ -19,11 +19,12 @@ import (
"github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime" "github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func init() { func init() {
@@ -68,8 +69,11 @@ var (
// Chromium is a module which provides both an API and routes for converting // Chromium is a module which provides both an API and routes for converting
// HTML document to PDF. // HTML document to PDF.
type Chromium struct { type Chromium struct {
binPath string binPath string
engine gotenberg.PDFEngine engine gotenberg.PDFEngine
fs *gotenberg.FileSystem
logger *zap.Logger
failedStartsThreshold int failedStartsThreshold int
userAgent string userAgent string
incognito bool incognito bool
@@ -311,6 +315,20 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
mod.binPath = binPath mod.binPath = binPath
// Logger.
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
if err != nil {
return fmt.Errorf("get logger provider: %w", err)
}
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(mod)
if err != nil {
return fmt.Errorf("get logger: %w", err)
}
mod.logger = logger
// PDF engine.
provider, err := ctx.Module(new(gotenberg.PDFEngineProvider)) provider, err := ctx.Module(new(gotenberg.PDFEngineProvider))
if err != nil { if err != nil {
return fmt.Errorf("get PDF engine provider: %w", err) return fmt.Errorf("get PDF engine provider: %w", err)
@@ -323,6 +341,9 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
mod.engine = engine mod.engine = engine
// File system.
mod.fs = gotenberg.NewFileSystem()
return nil return nil
} }
@@ -412,7 +433,7 @@ func (mod Chromium) Routes() ([]api.Route, error) {
// the end of the conversion. // the end of the conversion.
func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error { func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
debug := debugLogger{logger: logger.Named("browser")} debug := debugLogger{logger: logger.Named("browser")}
userProfileDirPath := gotenberg.NewDirPath() userProfileDirPath := mod.fs.NewDirPath()
args := append(chromedp.DefaultExecAllocatorOptions[:], args := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.CombinedOutput(debug), chromedp.CombinedOutput(debug),
@@ -799,7 +820,6 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
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)
} }
@@ -886,7 +906,7 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
} }
}() }()
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0600) file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil { if err != nil {
return fmt.Errorf("open output path: %w", err) return fmt.Errorf("open output path: %w", err)
} }
@@ -921,13 +941,20 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
activeInstancesCountMu.Unlock() activeInstancesCountMu.Unlock()
// Always remove the user profile directory created by Chromium. // Always remove the user profile directory created by Chromium.
go func() { defer func() {
logger.Debug(fmt.Sprintf("remove user profile directory '%s'", userProfileDirPath)) go func() {
// FIXME: Chromium seems to recreate the user profile directory
// right after its deletion if we do not wait a certain amount
// of time.
time.Sleep(10 * time.Second)
err := os.RemoveAll(userProfileDirPath) err := os.RemoveAll(userProfileDirPath)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("remove user profile directory: %s", err)) logger.Error(fmt.Sprintf("remove Chromium's user profile directory: %s", err))
} }
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
}()
}() }()
if err != nil { if err != nil {

View File

@@ -10,18 +10,11 @@ import (
"time" "time"
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
type ProtoModule struct {
descriptor func() gotenberg.ModuleDescriptor
}
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
return mod.descriptor()
}
type ProtoAPI struct { type ProtoAPI struct {
pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error
} }
@@ -30,28 +23,6 @@ func (mod ProtoAPI) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
return mod.pdf(ctx, logger, URL, outputPath, options) return mod.pdf(ctx, logger, URL, outputPath, options)
} }
type ProtoPDFEngineProvider struct {
ProtoModule
pdfEngine func() (gotenberg.PDFEngine, error)
}
func (mod ProtoPDFEngineProvider) PDFEngine() (gotenberg.PDFEngine, error) {
return mod.pdfEngine()
}
type ProtoPDFEngine struct {
merge func(_ context.Context, _ *zap.Logger, _ []string, _ string) error
convert func(_ context.Context, _ *zap.Logger, _, _, _ string) error
}
func (mod ProtoPDFEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return mod.merge(ctx, logger, inputPaths, outputPath)
}
func (mod ProtoPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return mod.convert(ctx, logger, format, inputPath, outputPath)
}
func TestDefaultOptions(t *testing.T) { func TestDefaultOptions(t *testing.T) {
actual := DefaultOptions() actual := DefaultOptions()
notExpect := Options{} notExpect := Options{}
@@ -73,11 +44,13 @@ func TestChromium_Descriptor(t *testing.T) {
} }
func TestChromium_Provision(t *testing.T) { func TestChromium_Provision(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
ctx *gotenberg.Context ctx *gotenberg.Context
expectErr bool expectErr bool
}{ }{
{ {
scenario: "no logger provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
@@ -89,12 +62,16 @@ func TestChromium_Provision(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
scenario: "no logger from logger provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoPDFEngineProvider }{} mod := struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
} }
mod.pdfEngine = func() (gotenberg.PDFEngine, error) { mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return nil, errors.New("foo") return nil, errors.New("foo")
} }
@@ -110,13 +87,75 @@ func TestChromium_Provision(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
scenario: "no PDF engine provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoPDFEngineProvider }{} mod := struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
} }
mod.pdfEngine = func() (gotenberg.PDFEngine, error) { mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return struct{ ProtoPDFEngine }{}, nil return zap.NewNop(), nil
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(Chromium).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
mod.Descriptor(),
},
)
}(),
expectErr: true,
},
{
scenario: "no PDF engine from PDF engine provider",
ctx: func() *gotenberg.Context {
mod := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
}
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return nil, errors.New("foo")
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(Chromium).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
mod.Descriptor(),
},
)
}(),
expectErr: true,
},
{
scenario: "provision success",
ctx: func() *gotenberg.Context {
mod := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
}
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -134,11 +173,11 @@ func TestChromium_Provision(t *testing.T) {
err := mod.Provision(tc.ctx) err := mod.Provision(tc.ctx)
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("test %s: expected error but got: %v", tc.scenario, err)
} }
if !tc.expectErr && err != nil { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("test %s: expected no error but got: %v", tc.scenario, err)
} }
} }
} }
@@ -643,16 +682,18 @@ func TestChromium_PDF(t *testing.T) {
mod.allowList = tc.allowList mod.allowList = tc.allowList
mod.denyList = tc.denyList mod.denyList = tc.denyList
mod.disableJavaScript = tc.disableJavaScript mod.disableJavaScript = tc.disableJavaScript
mod.fs = gotenberg.NewFileSystem()
outputDir, err := gotenberg.MkdirAll() ctxFs := gotenberg.NewFileSystem()
outputDir, err := ctxFs.MkdirAll()
if err != nil { if err != nil {
t.Fatalf("test %s: expected error but got: %v", tc.name, err) t.Fatalf("test %s: expected error but got: %v", tc.name, err)
} }
defer func() { defer func() {
err := os.RemoveAll(outputDir) err := os.RemoveAll(ctxFs.WorkingDirPath())
if err != nil { if err != nil {
t.Fatalf("test %s: expected no error but got: %v", tc.name, err) t.Fatalf("test %s: expected no error while cleaning up but got: %v", tc.name, err)
} }
}() }()
@@ -678,9 +719,5 @@ func TestChromium_PDF(t *testing.T) {
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*ProtoModule)(nil) _ API = (*ProtoAPI)(nil)
_ API = (*ProtoAPI)(nil)
_ gotenberg.PDFEngineProvider = (*ProtoPDFEngineProvider)(nil)
_ gotenberg.Module = (*ProtoPDFEngineProvider)(nil)
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
) )

View File

@@ -43,7 +43,6 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
if allow { if allow {
req := fetch.ContinueRequest(e.RequestID) req := fetch.ContinueRequest(e.RequestID)
err := req.Do(executorCtx) err := req.Do(executorCtx)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("continue request: %s", err)) logger.Error(fmt.Sprintf("continue request: %s", err))
} }
@@ -53,7 +52,6 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied) req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
err := req.Do(executorCtx) err := req.Do(executorCtx)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("fail request: %s", err)) logger.Error(fmt.Sprintf("fail request: %s", err))
} }

View File

@@ -12,12 +12,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2" "github.com/russross/blackfriday/v2"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
// FormDataChromiumPDFOptions creates Options form the form data. Fallback to // FormDataChromiumPDFOptions creates Options form the form data. Fallback to
@@ -163,7 +164,6 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
return nil 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)
} }
@@ -197,7 +197,6 @@ func convertHTMLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
MandatoryPath("index.html", &inputPath). MandatoryPath("index.html", &inputPath).
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &PDFformat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
@@ -236,7 +235,6 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
MandatoryPaths([]string{".md"}, &markdownPaths). MandatoryPaths([]string{".md"}, &markdownPaths).
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &PDFformat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
@@ -284,7 +282,6 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
return template.HTML(sanitized), nil return template.HTML(sanitized), nil
}, },
}).ParseFiles(inputPath) }).ParseFiles(inputPath)
if err != nil { if err != nil {
return fmt.Errorf("parse template file: %w", err) return fmt.Errorf("parse template file: %w", err)
} }
@@ -308,7 +305,7 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
inputPath = ctx.GeneratePath(".html") inputPath = ctx.GeneratePath(".html")
err = os.WriteFile(inputPath, buffer.Bytes(), 0600) err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
if err != nil { if err != nil {
return fmt.Errorf("write template result: %w", err) return fmt.Errorf("write template result: %w", err)
} }

View File

@@ -8,10 +8,11 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func TestFormDataChromiumPDFOptions(t *testing.T) { func TestFormDataChromiumPDFOptions(t *testing.T) {
@@ -588,8 +589,7 @@ func TestConvertMarkdownHandler(t *testing.T) {
} { } {
func() { func() {
if tc.outputDir != "" { if tc.outputDir != "" {
err := os.MkdirAll(tc.outputDir, 0755) err := os.MkdirAll(tc.outputDir, 0o755)
if err != nil { if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %d: expected error but got: %v", i, err)
} }
@@ -784,8 +784,8 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI return chromiumAPI
}(), }(),
engine: func() gotenberg.PDFEngine { engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{ return &gotenberg.PDFEngineMock{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { ConvertMock: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return gotenberg.ErrPDFFormatNotAvailable return gotenberg.ErrPDFFormatNotAvailable
}, },
} }
@@ -807,8 +807,8 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI return chromiumAPI
}(), }(),
engine: func() gotenberg.PDFEngine { engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{ return &gotenberg.PDFEngineMock{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { ConvertMock: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return errors.New("foo") return errors.New("foo")
}, },
} }
@@ -828,8 +828,8 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI return chromiumAPI
}(), }(),
engine: func() gotenberg.PDFEngine { engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{ return &gotenberg.PDFEngineMock{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { ConvertMock: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return nil return nil
}, },
} }

View File

@@ -1,3 +0,0 @@
// Package gc provides a module for removing files and directories that have
// expired.
package gc

View File

@@ -1,225 +0,0 @@
package gc
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
)
func init() {
gotenberg.MustRegisterModule(GarbageCollector{})
}
// GarbageCollector is a module for removing files and directories that have
// expired. It allows us to make sure that the application does not leak files
// or directories when running.
type GarbageCollector struct {
rootPath string
graceDuration time.Duration
excludeSubstr []string
ticker *time.Ticker
done chan bool
logger *zap.Logger
}
// GarbageCollectorGraceDurationModifier is a module interface which allows to
// update the expiration time of files and directories parsed by the garbage
// collector. For instance, if the grace duration is 30s, the garbage collector
// will remove paths that have a modification time older than 30s. If there are
// many GarbageCollectorGraceDurationModifier, only the longest grace duration
// is selected.
type GarbageCollectorGraceDurationModifier interface {
GraceDuration() time.Duration
}
// GarbageCollectorExcludeSubstrModifier is a module interface which adds the
// given substrings to the exclude list of the garbage collector. If a path
// contains one of those substrings, the garbage collector ignores it.
type GarbageCollectorExcludeSubstrModifier interface {
ExcludeSubstr() []string
}
// Descriptor returns a GarbageCollector's module descriptor.
func (gc GarbageCollector) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "gc",
New: func() gotenberg.Module { return new(GarbageCollector) },
}
}
// Provision sets the module properties.
func (gc *GarbageCollector) Provision(ctx *gotenberg.Context) error {
gc.rootPath = gotenberg.TmpPath()
graceDurationModifiers, err := ctx.Modules(new(GarbageCollectorGraceDurationModifier))
if err != nil {
return fmt.Errorf("get grace duration modifiers: %w", err)
}
for _, graceDurationModifier := range graceDurationModifiers {
modifier := graceDurationModifier.(GarbageCollectorGraceDurationModifier)
if gc.graceDuration < modifier.GraceDuration() {
gc.graceDuration = modifier.GraceDuration()
}
}
excludeSubstrModifiers, err := ctx.Modules(new(GarbageCollectorExcludeSubstrModifier))
if err != nil {
return fmt.Errorf("get exclude substr modifiers: %w", err)
}
gc.excludeSubstr = strings.Split(os.Getenv("GC_EXCLUDE_SUBSTR"), ",")
for _, excludeSubstrModifier := range excludeSubstrModifiers {
modifier := excludeSubstrModifier.(GarbageCollectorExcludeSubstrModifier)
gc.excludeSubstr = append(gc.excludeSubstr, modifier.ExcludeSubstr()...)
}
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
if err != nil {
return fmt.Errorf("get logger provider: %w", err)
}
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(gc)
if err != nil {
return fmt.Errorf("get logger: %w", err)
}
gc.logger = logger
return nil
}
// Start starts the garbage collector.
func (gc *GarbageCollector) Start() error {
gc.ticker = time.NewTicker(gc.graceDuration + time.Duration(1)*time.Second)
gc.done = make(chan bool, 1)
go func() {
for {
func() {
gcMu.RLock()
defer gcMu.RUnlock()
select {
case <-gc.done:
return
case <-gc.ticker.C:
gc.collect(false)
}
}()
}
}()
return nil
}
// collect parses the root path of the garbage collector and removes files or
// directories that have expired. It ignores the expiration date if the "force"
// argument is set to true.
func (gc GarbageCollector) collect(force bool) {
expirationTime := time.Now().Add(-gc.graceDuration)
// To make sure that the next Walk method stays on
// the root level of the considered path, we have to
// return a filepath.SkipDir error if the current path
// is a directory.
skipDirOrNil := func(info os.FileInfo) error {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
removePath := func(path string) {
err := os.RemoveAll(path)
if err != nil {
gc.logger.Error(fmt.Sprintf("remove '%s': %s", path, err))
}
gc.logger.Debug(fmt.Sprintf("'%s' removed", path))
}
err := filepath.Walk(gc.rootPath, func(path string, info os.FileInfo, pathErr error) error {
if pathErr != nil {
// For whatever reasons, the Walk method failed
// to process the current path.
return pathErr
}
if path == gc.rootPath {
return nil
}
for _, substr := range gc.excludeSubstr {
if strings.Contains(info.Name(), substr) {
return skipDirOrNil(info)
}
}
if force {
removePath(path)
return skipDirOrNil(info)
}
if info.ModTime().Before(expirationTime) {
removePath(path)
}
return skipDirOrNil(info)
})
if err != nil {
gc.logger.Error(err.Error())
}
}
// StartupMessage returns an empty string.
func (gc GarbageCollector) StartupMessage() string {
return ""
}
// Stop stops the garbage collector.
func (gc *GarbageCollector) Stop(ctx context.Context) error {
_, ok := ctx.Deadline()
if !ok {
return errors.New("no context dead line")
}
// Block until the context is done so that other module may gracefully stop
// before we do a shutdown cleanup.
gc.logger.Debug("wait for the end of grace duration")
<-ctx.Done()
gc.ticker.Stop()
gc.done <- true
gc.logger.Debug("shutdown cleanup...")
gc.collect(true)
return nil
}
var gcMu sync.RWMutex
// Interface guards.
var (
_ gotenberg.Module = (*GarbageCollector)(nil)
_ gotenberg.Provisioner = (*GarbageCollector)(nil)
_ gotenberg.App = (*GarbageCollector)(nil)
)

View File

@@ -1,468 +0,0 @@
package gc
import (
"context"
"errors"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
)
type ProtoModule struct {
descriptor func() gotenberg.ModuleDescriptor
}
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
return mod.descriptor()
}
type ProtoValidator struct {
ProtoModule
validate func() error
}
func (mod ProtoValidator) Validate() error {
return mod.validate()
}
type ProtoGarbageCollectorGraceDurationModifier struct {
ProtoValidator
graceDuration func() time.Duration
}
func (mod ProtoGarbageCollectorGraceDurationModifier) GraceDuration() time.Duration {
return mod.graceDuration()
}
type ProtoGarbageCollectorExcludeSubstrModifier struct {
ProtoValidator
excludeSubstr func() []string
}
func (mod ProtoGarbageCollectorExcludeSubstrModifier) ExcludeSubstr() []string {
return mod.excludeSubstr()
}
type ProtoLoggerProvider struct {
ProtoModule
logger func(mod gotenberg.Module) (*zap.Logger, error)
}
func (factory ProtoLoggerProvider) Logger(mod gotenberg.Module) (*zap.Logger, error) {
return factory.logger(mod)
}
func TestGarbageCollector_Descriptor(t *testing.T) {
descriptor := GarbageCollector{}.Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(GarbageCollector))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestGarbageCollector_Provision(t *testing.T) {
for i, tc := range []struct {
ctx *gotenberg.Context
expectGraceDuration time.Duration
expectExcludeSubstr []string
expectErr bool
}{
{
ctx: func() *gotenberg.Context {
mod := struct {
ProtoGarbageCollectorGraceDurationModifier
}{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
}
mod.validate = func() error { return errors.New("foo") }
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
mod.Descriptor(),
})
}(),
expectErr: true,
},
{
ctx: func() *gotenberg.Context {
mod := struct {
ProtoGarbageCollectorExcludeSubstrModifier
}{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
}
mod.validate = func() error { return errors.New("foo") }
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
mod.Descriptor(),
})
}(),
expectErr: true,
},
{
ctx: gotenberg.NewContext(gotenberg.ParsedFlags{}, make([]gotenberg.ModuleDescriptor, 0)),
expectErr: true,
},
{
ctx: func() *gotenberg.Context {
mod := struct {
ProtoLoggerProvider
}{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
}
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return nil, errors.New("foo") }
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
mod.Descriptor(),
})
}(),
expectErr: true,
},
{
ctx: func() *gotenberg.Context {
mod := struct {
ProtoLoggerProvider
}{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
}
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
mod.Descriptor(),
})
}(),
},
{
ctx: func() *gotenberg.Context {
mod1 := struct {
ProtoGarbageCollectorGraceDurationModifier
}{}
mod1.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
}
mod1.graceDuration = func() time.Duration { return time.Duration(10) * time.Second }
mod1.validate = func() error { return nil }
mod2 := struct {
ProtoGarbageCollectorGraceDurationModifier
}{}
mod2.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
}
mod2.graceDuration = func() time.Duration { return time.Duration(20) * time.Second }
mod2.validate = func() error { return nil }
mod3 := struct {
ProtoLoggerProvider
}{}
mod3.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }}
}
mod3.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
mod1.Descriptor(),
mod2.Descriptor(),
mod3.Descriptor(),
})
}(),
expectGraceDuration: time.Duration(20) * time.Second,
},
{
ctx: func() *gotenberg.Context {
mod1 := struct {
ProtoGarbageCollectorExcludeSubstrModifier
}{}
mod1.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
}
mod1.excludeSubstr = func() []string { return []string{"foo"} }
mod1.validate = func() error { return nil }
mod2 := struct {
ProtoGarbageCollectorExcludeSubstrModifier
}{}
mod2.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
}
mod2.excludeSubstr = func() []string { return []string{"bar"} }
mod2.validate = func() error { return nil }
mod3 := struct {
ProtoLoggerProvider
}{}
mod3.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }}
}
mod3.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
mod1.Descriptor(),
mod2.Descriptor(),
mod3.Descriptor(),
})
}(),
expectExcludeSubstr: func() []string {
expect := strings.Split(os.Getenv("GC_EXCLUDE_SUBSTR"), ",")
return append(expect, "foo", "bar")
}(),
},
} {
mod := new(GarbageCollector)
err := mod.Provision(tc.ctx)
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if tc.expectGraceDuration != 0 && tc.expectGraceDuration != mod.graceDuration {
t.Errorf("test %d: expected grace duration of '%s' but got '%s'", i, tc.expectGraceDuration, mod.graceDuration)
}
if tc.expectExcludeSubstr != nil && !reflect.DeepEqual(tc.expectExcludeSubstr, mod.excludeSubstr) {
t.Errorf("test %d: expected exclude substr '%s' but got '%s'", i, tc.expectExcludeSubstr, mod.excludeSubstr)
}
}
}
func TestGarbageCollector_Start(t *testing.T) {
mod := new(GarbageCollector)
mod.logger = zap.NewNop()
path, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
mod.rootPath = path
err = mod.Start()
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
time.Sleep(time.Duration(2) * time.Second)
mod.ticker.Stop()
mod.done <- true
}
func TestGarbageCollector_collect(t *testing.T) {
for i, tc := range []struct {
gc *GarbageCollector
expectNotExists []string
expectExists []string
force bool
}{
{
gc: func() *GarbageCollector {
mod := new(GarbageCollector)
mod.logger = zap.NewNop()
path, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
mod.rootPath = path
err = os.WriteFile(path+"/foo", []byte{1}, 0755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
mod.excludeSubstr = []string{
"foo",
}
return mod
}(),
expectExists: []string{
"/foo",
},
},
{
gc: func() *GarbageCollector {
mod := new(GarbageCollector)
mod.logger = zap.NewNop()
path, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
mod.rootPath = path
err = os.WriteFile(path+"/foo", []byte{1}, 0755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
err = os.MkdirAll(path+"/bar", 0755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return mod
}(),
expectNotExists: []string{
"/foo",
"/bar",
},
force: true,
},
{
gc: func() *GarbageCollector {
mod := new(GarbageCollector)
mod.logger = zap.NewNop()
mod.graceDuration = time.Duration(10) * time.Second
path, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
mod.rootPath = path
err = os.WriteFile(path+"/foo", []byte{1}, 0755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
newTime := time.Now().Add(-time.Duration(20) * time.Second)
err = os.Chtimes(path+"/foo", newTime, newTime)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
err = os.WriteFile(path+"/bar", []byte{1}, 0755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
newTime = time.Now().Add(time.Duration(10) * time.Second)
err = os.Chtimes(path+"/bar", newTime, newTime)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return mod
}(),
expectNotExists: []string{
"/foo",
},
expectExists: []string{
"/bar",
},
},
} {
tc.gc.collect(tc.force)
for _, name := range tc.expectNotExists {
path := tc.gc.rootPath + name
_, err := os.Stat(path)
if !os.IsNotExist(err) {
t.Errorf("test %d: expected '%s' not to exist but got: %v", i, path, err)
}
}
for _, name := range tc.expectExists {
path := tc.gc.rootPath + name
_, err := os.Stat(path)
if os.IsNotExist(err) {
t.Errorf("test %d: expected '%s' to exist but got: %v", i, path, err)
}
}
err := os.RemoveAll(tc.gc.rootPath)
if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestGarbageCollector_StartupMessage(t *testing.T) {
actual := new(GarbageCollector).StartupMessage()
expect := ""
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestGarbageCollector_Stop(t *testing.T) {
for i, tc := range []struct {
timeout time.Duration
expectErr bool
}{
{
expectErr: true,
},
{
timeout: time.Duration(1) * time.Nanosecond,
},
} {
func() {
mod := new(GarbageCollector)
mod.logger = zap.NewNop()
path, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
mod.rootPath = path
err = mod.Start()
if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
if tc.timeout == 0 {
err = mod.Stop(context.TODO())
} else {
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
defer cancel()
err = mod.Stop(ctx)
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}()
}
}
// Interface guards.
var (
_ gotenberg.Module = (*ProtoModule)(nil)
_ gotenberg.Validator = (*ProtoValidator)(nil)
_ GarbageCollectorGraceDurationModifier = (*ProtoGarbageCollectorGraceDurationModifier)(nil)
_ gotenberg.Module = (*ProtoGarbageCollectorGraceDurationModifier)(nil)
_ gotenberg.Validator = (*ProtoGarbageCollectorGraceDurationModifier)(nil)
_ GarbageCollectorExcludeSubstrModifier = (*ProtoGarbageCollectorExcludeSubstrModifier)(nil)
_ gotenberg.Module = (*ProtoGarbageCollectorExcludeSubstrModifier)(nil)
_ gotenberg.Validator = (*ProtoGarbageCollectorExcludeSubstrModifier)(nil)
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
)

View File

@@ -3,10 +3,11 @@ package libreoffice
import ( import (
"fmt" "fmt"
flag "github.com/spf13/pflag"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
flag "github.com/spf13/pflag"
) )
func init() { func init() {

View File

@@ -5,9 +5,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"go.uber.org/zap"
) )
func init() { func init() {

View File

@@ -6,9 +6,10 @@ import (
"reflect" "reflect"
"testing" "testing"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"go.uber.org/zap"
) )
func TestUNO_Descriptor(t *testing.T) { func TestUNO_Descriptor(t *testing.T) {

View File

@@ -5,10 +5,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"github.com/labstack/echo/v4"
) )
// convertRoute returns an api.Route which can convert LibreOffice documents // convertRoute returns an api.Route which can convert LibreOffice documents
@@ -41,7 +42,6 @@ func convertRoute(unoAPI uno.API, engine gotenberg.PDFEngine) api.Route {
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &PDFformat, "").
Bool("merge", &merge, false). Bool("merge", &merge, false).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }

View File

@@ -6,11 +6,12 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
) )
func TestConvertHandler(t *testing.T) { func TestConvertHandler(t *testing.T) {

View File

@@ -8,8 +8,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
type listener interface { type listener interface {
@@ -42,15 +43,18 @@ type libreOfficeListener struct {
queueLength int queueLength int
queueLengthMu sync.RWMutex queueLengthMu sync.RWMutex
lockChan chan struct{} lockChan chan struct{}
logger *zap.Logger
fs *gotenberg.FileSystem
logger *zap.Logger
} }
func newLibreOfficeListener(logger *zap.Logger, binPath string, startTimeout time.Duration, threshold int) listener { func newLibreOfficeListener(logger *zap.Logger, fs *gotenberg.FileSystem, binPath string, startTimeout time.Duration, threshold int) listener {
return &libreOfficeListener{ return &libreOfficeListener{
binPath: binPath, binPath: binPath,
startTimeout: startTimeout, startTimeout: startTimeout,
threshold: threshold, threshold: threshold,
lockChan: make(chan struct{}, 1), lockChan: make(chan struct{}, 1),
fs: fs,
logger: logger.Named("listener"), logger: logger.Named("listener"),
} }
} }
@@ -65,10 +69,7 @@ func (listener *libreOfficeListener) start(logger *zap.Logger) error {
return fmt.Errorf("get free port: %w", err) return fmt.Errorf("get free port: %w", err)
} }
// Good to know: the garbage collector might delete the next directory userProfileDirPath := listener.fs.NewDirPath()
// while it is still running. It does seem to cause any issue though.
userProfileDirPath := gotenberg.NewDirPath()
args := []string{ args := []string{
"--headless", "--headless",
"--invisible", "--invisible",
@@ -158,6 +159,12 @@ func (listener *libreOfficeListener) start(logger *zap.Logger) error {
if err != nil { if err != nil {
logger.Debug(fmt.Sprintf("kill LibreOffice listener process: %v", err)) logger.Debug(fmt.Sprintf("kill LibreOffice listener process: %v", err))
} }
// And the user profile directory is deleted.
err = os.RemoveAll(userProfileDirPath)
if err != nil {
logger.Debug(fmt.Sprintf("remove user profile directory: %v", err))
}
}() }()
logger.Debug("waiting for the LibreOffice listener socket to be available...") logger.Debug("waiting for the LibreOffice listener socket to be available...")
@@ -185,9 +192,21 @@ func (listener *libreOfficeListener) stop(logger *zap.Logger) error {
defer func() { defer func() {
defer listener.cfgMu.RUnlock() defer listener.cfgMu.RUnlock()
if listener.userProfileDirPath == "" {
return
}
err := os.RemoveAll(listener.userProfileDirPath) err := os.RemoveAll(listener.userProfileDirPath)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("remove LibreOffice listener user profile directory: %v", err)) logger.Error(fmt.Sprintf("remove LibreOffice listener's user profile directory: %v", err))
}
logger.Debug(fmt.Sprintf("'%s' LibreOffice listener's user profile directory removed", listener.userProfileDirPath))
// Also remove listener specific files in the temporary directory.
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{"OSL_PIPE", ".tmp"})
if err != nil {
logger.Error(err.Error())
} }
}() }()
@@ -317,7 +336,7 @@ func (listener *libreOfficeListener) unlock(logger *zap.Logger) error {
} }
listener.usage += 1 listener.usage += 1
if listener.usage < listener.threshold { if listener.threshold > 0 && listener.usage < listener.threshold {
return nil return nil
} }

View File

@@ -7,6 +7,8 @@ import (
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestListener_start(t *testing.T) { func TestListener_start(t *testing.T) {
@@ -17,11 +19,11 @@ func TestListener_start(t *testing.T) {
}{ }{
{ {
name: "nominal behavior", name: "nominal behavior",
listener: newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10), listener: newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10),
}, },
{ {
name: "non-exit code 81 on first start", name: "non-exit code 81 on first start",
listener: newLibreOfficeListener(zap.NewNop(), "foo", time.Duration(10)*time.Second, 10), listener: newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), "foo", time.Duration(10)*time.Second, 10),
expectStartErr: true, expectStartErr: true,
}, },
} }
@@ -57,6 +59,7 @@ func TestListener_start(t *testing.T) {
func TestListener_stop(t *testing.T) { func TestListener_stop(t *testing.T) {
listener := newLibreOfficeListener( listener := newLibreOfficeListener(
zap.NewNop(), zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"), os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second, time.Duration(10)*time.Second,
10, 10,
@@ -76,6 +79,7 @@ func TestListener_stop(t *testing.T) {
func TestListener_restart(t *testing.T) { func TestListener_restart(t *testing.T) {
listener := newLibreOfficeListener( listener := newLibreOfficeListener(
zap.NewNop(), zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"), os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second, time.Duration(10)*time.Second,
10, 10,
@@ -107,7 +111,7 @@ func TestListener_lock(t *testing.T) {
{ {
name: "nominal behavior", name: "nominal behavior",
listener: func() listener { listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10) listener := newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop()) err := listener.start(zap.NewNop())
if err != nil { if err != nil {
@@ -123,7 +127,7 @@ func TestListener_lock(t *testing.T) {
}, },
{ {
name: "first start", name: "first start",
listener: newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10), listener: newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10),
ctx: context.Background(), ctx: context.Background(),
teardown: func(listener listener) error { teardown: func(listener listener) error {
return listener.stop(zap.NewNop()) return listener.stop(zap.NewNop())
@@ -132,7 +136,7 @@ func TestListener_lock(t *testing.T) {
{ {
name: "unhealthy listener", name: "unhealthy listener",
listener: func() listener { listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10) listener := newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop()) err := listener.start(zap.NewNop())
if err != nil { if err != nil {
@@ -154,7 +158,7 @@ func TestListener_lock(t *testing.T) {
{ {
name: "context done", name: "context done",
listener: func() listener { listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10) listener := newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop()) err := listener.start(zap.NewNop())
if err != nil { if err != nil {
@@ -212,7 +216,7 @@ func TestListener_unlock(t *testing.T) {
{ {
name: "nominal behavior", name: "nominal behavior",
listener: func() listener { listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10) listener := newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop()) err := listener.start(zap.NewNop())
if err != nil { if err != nil {
@@ -233,7 +237,7 @@ func TestListener_unlock(t *testing.T) {
{ {
name: "unhealthy listener", name: "unhealthy listener",
listener: func() listener { listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10) listener := newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop()) err := listener.start(zap.NewNop())
if err != nil { if err != nil {
@@ -259,7 +263,7 @@ func TestListener_unlock(t *testing.T) {
{ {
name: "threshold reached", name: "threshold reached",
listener: func() listener { listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 1) listener := newLibreOfficeListener(zap.NewNop(), gotenberg.NewFileSystem(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 1)
err := listener.start(zap.NewNop()) err := listener.start(zap.NewNop())
if err != nil { if err != nil {
@@ -299,6 +303,7 @@ func TestListener_unlock(t *testing.T) {
func TestListener_port(t *testing.T) { func TestListener_port(t *testing.T) {
listener := newLibreOfficeListener( listener := newLibreOfficeListener(
zap.NewNop(), zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"), os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second, time.Duration(10)*time.Second,
10, 10,
@@ -323,6 +328,7 @@ func TestListener_port(t *testing.T) {
func TestListener_queue(t *testing.T) { func TestListener_queue(t *testing.T) {
listener := newLibreOfficeListener( listener := newLibreOfficeListener(
zap.NewNop(), zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"), os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second, time.Duration(10)*time.Second,
10, 10,
@@ -395,6 +401,7 @@ func TestListener_healthy(t *testing.T) {
startTimeout: time.Duration(10) * time.Second, startTimeout: time.Duration(10) * time.Second,
threshold: 10, threshold: 10,
lockChan: make(chan struct{}, 1), lockChan: make(chan struct{}, 1),
fs: gotenberg.NewFileSystem(),
logger: zap.NewNop(), logger: zap.NewNop(),
} }

View File

@@ -9,11 +9,12 @@ import (
"time" "time"
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.uber.org/multierr" "go.uber.org/multierr"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func init() { func init() {
@@ -82,10 +83,10 @@ func (UNO) Descriptor() gotenberg.ModuleDescriptor {
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("uno", flag.ExitOnError) fs := flag.NewFlagSet("uno", flag.ExitOnError)
fs.Duration("uno-listener-start-timeout", time.Duration(10)*time.Second, "Time limit for restarting the LibreOffice listener") fs.Duration("uno-listener-start-timeout", time.Duration(10)*time.Second, "Time limit for restarting the LibreOffice listener")
fs.Int("uno-listener-restart-threshold", 10, "Conversions limit after which the LibreOffice listener is restarted - 0 means no long-running LibreOffice listener") fs.Int("uno-listener-restart-threshold", 10, "Conversions limit after which the LibreOffice listener is restarted - 0 means no restart")
fs.Bool("unoconv-disable-listener", false, "Do not start a long-running listener - save resources in detriment of unitary performance") fs.Bool("unoconv-disable-listener", false, "Do not start a long-running listener - save resources in detriment of unitary performance")
err := fs.MarkDeprecated("unoconv-disable-listener", "use uno-listener-restart-threshold with 0 instead") err := fs.MarkDeprecated("unoconv-disable-listener", "listener cannot be disabled")
if err != nil { if err != nil {
panic(fmt.Errorf("create deprecated flags for the uno module: %v", err)) panic(fmt.Errorf("create deprecated flags for the uno module: %v", err))
} }
@@ -134,8 +135,10 @@ func (mod *UNO) Provision(ctx *gotenberg.Context) error {
mod.logger = logger mod.logger = logger
// Listener.
mod.listener = newLibreOfficeListener( mod.listener = newLibreOfficeListener(
mod.logger, mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath, mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout, mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold, mod.libreOfficeRestartThreshold,
@@ -170,19 +173,11 @@ func (mod UNO) Start() error {
// StartupMessage returns a custom startup message. // StartupMessage returns a custom startup message.
func (mod UNO) StartupMessage() string { func (mod UNO) StartupMessage() string {
if mod.libreOfficeRestartThreshold == 0 {
return "long-running LibreOffice listener disabled"
}
return "long-running LibreOffice listener ready to start" return "long-running LibreOffice listener ready to start"
} }
// Stop stops the long-running LibreOffice Listener if it exists. // Stop stops the long-running LibreOffice Listener.
func (mod UNO) Stop(ctx context.Context) error { func (mod UNO) Stop(ctx context.Context) error {
if mod.libreOfficeRestartThreshold == 0 {
return nil
}
// Block until the context is done so that other module may gracefully stop // Block until the context is done so that other module may gracefully stop
// before we do a shutdown cleanup. // before we do a shutdown cleanup.
mod.logger.Debug("wait for the end of grace duration") mod.logger.Debug("wait for the end of grace duration")
@@ -194,7 +189,7 @@ func (mod UNO) Stop(ctx context.Context) error {
return nil return nil
} }
return fmt.Errorf("stop long-running LibreOffice listener") return fmt.Errorf("stop long-running LibreOffice listener: %w", err)
} }
// Metrics returns the metrics. // Metrics returns the metrics.
@@ -284,14 +279,8 @@ func (mod UNO) Checks() ([]health.CheckerOption, error) {
}, nil }, nil
} }
// PDF converts a document to PDF. // PDF converts a document to PDF. Be cautious when making multiple concurrent
// // calls, as it might lead to reaching the context's deadline.
// If there is no long-running LibreOffice listener, it creates a dedicated
// LibreOffice instance for the conversion. Substantial calls to this method
// may increase CPU and memory usage drastically
//
// If there is a long-running LibreOffice listener, the conversion performance
// improves substantially. However, it cannot perform parallel operations.
func (mod UNO) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error { func (mod UNO) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
args := []string{ args := []string{
"--no-launch", "--no-launch",
@@ -299,45 +288,26 @@ func (mod UNO) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPat
"pdf", "pdf",
} }
switch mod.libreOfficeRestartThreshold { err := mod.listener.lock(ctx, logger)
case 0: if err != nil {
listener := newLibreOfficeListener(logger, mod.libreOfficeBinPath, mod.libreOfficeStartTimeout, 0) return fmt.Errorf("lock long-running LibreOffice listener: %w", err)
}
err := listener.start(logger) defer func() {
if err != nil { go func() {
return fmt.Errorf("start LibreOffice listener: %w", err) err := mod.listener.unlock(logger)
}
defer func() {
err := listener.stop(logger)
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("stop LibreOffice listener: %v", err)) mod.logger.Error(fmt.Sprintf("unlock long-running LibreOffice listener: %v", err))
return
} }
}() }()
}()
args = append(args, "--port", fmt.Sprintf("%d", listener.port())) // If the LibreOffice listener is restarting while acquiring the lock,
default: // the port will change. It's therefore important to add the port args
err := mod.listener.lock(ctx, logger) // after we acquire the lock.
if err != nil { args = append(args, "--port", fmt.Sprintf("%d", mod.listener.port()))
return fmt.Errorf("lock long-running LibreOffice listener: %w", err)
}
defer func() {
go func() {
err := mod.listener.unlock(logger)
if err != nil {
mod.logger.Error(fmt.Sprintf("unlock long-running LibreOffice listener: %v", err))
return
}
}()
}()
// If the LibreOffice listener is restarting while acquiring the lock,
// the port will change. It's therefore important to add the port args
// after we acquire the lock.
args = append(args, "--port", fmt.Sprintf("%d", mod.listener.port()))
}
checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity") checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
if checkedEntry != nil { if checkedEntry != nil {

View File

@@ -9,9 +9,10 @@ import (
"time" "time"
"github.com/alexliesenfeld/health" "github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestUNO_Descriptor(t *testing.T) { func TestUNO_Descriptor(t *testing.T) {
@@ -78,7 +79,6 @@ func TestUNO_Provision(t *testing.T) {
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := new(UNO).Descriptor().FlagSet fs := new(UNO).Descriptor().FlagSet
err := fs.Parse([]string{"--unoconv-disable-listener=true"}) err := fs.Parse([]string{"--unoconv-disable-listener=true"})
if err != nil { if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err) t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
} }
@@ -205,35 +205,11 @@ func TestUNO_Start(t *testing.T) {
} }
func TestUNO_StartupMessage(t *testing.T) { func TestUNO_StartupMessage(t *testing.T) {
tests := []struct { actual := new(UNO).StartupMessage()
name string expect := "long-running LibreOffice listener ready to start"
mod UNO
expectMessage string
}{
{
name: "long-running LibreOffice listener ready to start",
mod: UNO{
libreOfficeRestartThreshold: 10,
},
expectMessage: "long-running LibreOffice listener ready to start",
},
{
name: "long-running LibreOffice listener disabled",
mod: UNO{
libreOfficeRestartThreshold: 0,
},
expectMessage: "long-running LibreOffice listener disabled",
},
}
for _, tc := range tests { if actual != expect {
t.Run(tc.name, func(t *testing.T) { t.Errorf("expected '%s' but got '%s'", expect, actual)
actual := tc.mod.StartupMessage()
if tc.expectMessage != actual {
t.Errorf("expected '%s' from mod.StartupMessage(), but got '%s'", tc.expectMessage, actual)
}
})
} }
} }
@@ -255,13 +231,6 @@ func TestUNO_Stop(t *testing.T) {
logger: zap.NewNop(), logger: zap.NewNop(),
}, },
}, },
{
name: "no long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 0,
logger: zap.NewNop(),
},
},
{ {
name: "stop error", name: "stop error",
mod: UNO{ mod: UNO{
@@ -468,7 +437,7 @@ func TestUNO_Checks(t *testing.T) {
func TestUNO_PDF(t *testing.T) { func TestUNO_PDF(t *testing.T) {
tests := []struct { tests := []struct {
name string scenario string
mod UNO mod UNO
ctx context.Context ctx context.Context
logger *zap.Logger logger *zap.Logger
@@ -478,19 +447,7 @@ func TestUNO_PDF(t *testing.T) {
teardown func(mod UNO) error teardown func(mod UNO) error
}{ }{
{ {
name: "nominal behavior with no long-running LibreOffice listener", scenario: "nominal behavior with a long-running LibreOffice listener",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
},
{
name: "nominal behavior with a long-running LibreOffice listener",
mod: func() UNO { mod: func() UNO {
mod := UNO{ mod := UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
@@ -501,6 +458,7 @@ func TestUNO_PDF(t *testing.T) {
} }
mod.listener = newLibreOfficeListener( mod.listener = newLibreOfficeListener(
mod.logger, mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath, mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout, mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold, mod.libreOfficeRestartThreshold,
@@ -518,56 +476,122 @@ func TestUNO_PDF(t *testing.T) {
return mod.Stop(ctx) return mod.Stop(ctx)
}, },
}, },
//{
// scenario: "convert with a debug logger",
// mod: func() UNO {
// mod := UNO{
// unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
// libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
// libreOfficeStartTimeout: time.Duration(10) * time.Second,
// libreOfficeRestartThreshold: 10,
// logger: zap.NewNop(),
// }
// mod.listener = newLibreOfficeListener(
// mod.logger,
// gotenberg.NewFileSystem(),
// mod.libreOfficeBinPath,
// mod.libreOfficeStartTimeout,
// mod.libreOfficeRestartThreshold,
// )
//
// return mod
// }(),
// ctx: context.Background(),
// logger: zap.NewExample(),
// inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
// teardown: func(mod UNO) error {
// ctx, cancel := context.WithCancel(context.Background())
// cancel()
//
// return mod.Stop(ctx)
// },
//},
{ {
name: "convert with a debug logger", scenario: "convert with landscape",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
ctx: context.Background(), logger: zap.NewNop(),
logger: zap.NewExample(), }
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", mod.listener = newLibreOfficeListener(
}, mod.logger,
{ gotenberg.NewFileSystem(),
name: "convert with landscape", mod.libreOfficeBinPath,
mod: UNO{ mod.libreOfficeStartTimeout,
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod.libreOfficeRestartThreshold,
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), )
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0, return mod
}, }(),
ctx: context.Background(), ctx: context.Background(),
logger: zap.NewNop(), logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{ options: Options{
Landscape: true, Landscape: true,
}, },
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
}, },
{ {
name: "convert with page ranges", scenario: "convert with page ranges",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
logger: zap.NewNop(),
}
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return mod
}(),
ctx: context.Background(), ctx: context.Background(),
logger: zap.NewNop(), logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{ options: Options{
PageRanges: "1-2", PageRanges: "1-2",
}, },
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
}, },
{ {
name: "convert with invalid page ranges", scenario: "convert with invalid page ranges",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
logger: zap.NewNop(),
}
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return mod
}(),
ctx: context.Background(), ctx: context.Background(),
logger: zap.NewNop(), logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
@@ -575,60 +599,132 @@ func TestUNO_PDF(t *testing.T) {
PageRanges: "foo", PageRanges: "foo",
}, },
expectPDFErr: true, expectPDFErr: true,
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
}, },
{ {
name: "convert to PDF/A-1a", scenario: "convert to PDF/A-1a",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
logger: zap.NewNop(),
}
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return mod
}(),
ctx: context.Background(), ctx: context.Background(),
logger: zap.NewNop(), logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{ options: Options{
PDFformat: gotenberg.FormatPDFA1a, PDFformat: gotenberg.FormatPDFA1a,
}, },
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
}, },
{ {
name: "convert to PDF/A-2b", scenario: "convert to PDF/A-2b",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
logger: zap.NewNop(),
}
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return mod
}(),
ctx: context.Background(), ctx: context.Background(),
logger: zap.NewNop(), logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{ options: Options{
PDFformat: gotenberg.FormatPDFA2b, PDFformat: gotenberg.FormatPDFA2b,
}, },
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
}, },
{ {
name: "convert to PDF/A-3b", scenario: "convert to PDF/A-3b",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
logger: zap.NewNop(),
}
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return mod
}(),
ctx: context.Background(), ctx: context.Background(),
logger: zap.NewNop(), logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{ options: Options{
PDFformat: gotenberg.FormatPDFA3b, PDFformat: gotenberg.FormatPDFA3b,
}, },
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
}, },
{ {
name: "convert to invalid PDF format", scenario: "convert to invalid PDF format",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
logger: zap.NewNop(),
}
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return mod
}(),
ctx: context.Background(), ctx: context.Background(),
logger: zap.NewNop(), logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
@@ -636,28 +732,33 @@ func TestUNO_PDF(t *testing.T) {
PDFformat: "foo", PDFformat: "foo",
}, },
expectPDFErr: true, expectPDFErr: true,
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
}, },
{ {
name: "nil context", scenario: "expired context",
mod: UNO{ mod: func() UNO {
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), mod := UNO{
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeRestartThreshold: 0, libreOfficeStartTimeout: time.Duration(10) * time.Second,
}, libreOfficeRestartThreshold: 10,
ctx: nil, logger: zap.NewNop(),
logger: zap.NewNop(), }
inputPath: "/tests/test/testdata/libreoffice/sample1.docx", mod.listener = newLibreOfficeListener(
expectPDFErr: true, mod.logger,
}, gotenberg.NewFileSystem(),
{ mod.libreOfficeBinPath,
name: "expired context", mod.libreOfficeStartTimeout,
mod: UNO{ mod.libreOfficeRestartThreshold,
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), )
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second, return mod
libreOfficeRestartThreshold: 0, }(),
},
ctx: func() context.Context { ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
@@ -669,7 +770,7 @@ func TestUNO_PDF(t *testing.T) {
expectPDFErr: true, expectPDFErr: true,
}, },
{ {
name: "cannot lock long-running LibreOffice listener", scenario: "cannot lock long-running LibreOffice listener",
mod: UNO{ mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
@@ -688,7 +789,7 @@ func TestUNO_PDF(t *testing.T) {
expectPDFErr: true, expectPDFErr: true,
}, },
{ {
name: "cannot unlock long-running LibreOffice listener", scenario: "cannot unlock long-running LibreOffice listener",
mod: UNO{ mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"), unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"), libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
@@ -715,7 +816,7 @@ func TestUNO_PDF(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
defer func() { defer func() {
if tc.teardown == nil { if tc.teardown == nil {
return return
@@ -727,15 +828,16 @@ func TestUNO_PDF(t *testing.T) {
} }
}() }()
outputDir, err := gotenberg.MkdirAll() fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil { if err != nil {
t.Fatalf("expected no error from gotenberg.MkdirAll(), but got: %v", err) t.Fatalf("test %s: expected error but got: %v", tc.scenario, err)
} }
defer func() { defer func() {
err := os.RemoveAll(outputDir) err := os.RemoveAll(fs.WorkingDirPath())
if err != nil { if err != nil {
t.Errorf("expected no error from os.RemoveAll(), but got: %v", err) t.Fatalf("test %s: expected no error while cleaning up but got: %v", tc.scenario, err)
} }
}() }()

View File

@@ -5,12 +5,13 @@ import (
"os" "os"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.uber.org/multierr" "go.uber.org/multierr"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"golang.org/x/term" "golang.org/x/term"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func init() { func init() {

View File

@@ -5,10 +5,11 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer" "go.uber.org/zap/zaptest/observer"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestLogging_Descriptor(t *testing.T) { func TestLogging_Descriptor(t *testing.T) {

View File

@@ -4,11 +4,12 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
pdfcpuAPI "github.com/pdfcpu/pdfcpu/pkg/api" pdfcpuAPI "github.com/pdfcpu/pdfcpu/pkg/api"
pdfcpuLog "github.com/pdfcpu/pdfcpu/pkg/log" pdfcpuLog "github.com/pdfcpu/pdfcpu/pkg/log"
pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func init() { func init() {

View File

@@ -7,8 +7,9 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestPDFcpu_Descriptor(t *testing.T) { func TestPDFcpu_Descriptor(t *testing.T) {
@@ -63,15 +64,16 @@ func TestPDFcpu_Merge(t *testing.T) {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %d: expected error but got: %v", i, err)
} }
outputDir, err := gotenberg.MkdirAll() fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil { if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %d: expected error but got: %v", i, err)
} }
defer func() { defer func() {
err := os.RemoveAll(outputDir) err := os.RemoveAll(fs.WorkingDirPath())
if err != nil { if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err) t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
} }
}() }()

View File

@@ -4,9 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/multierr" "go.uber.org/multierr"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
// multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one // multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one

View File

@@ -5,8 +5,9 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestMultiPDFEngines_Merge(t *testing.T) { func TestMultiPDFEngines_Merge(t *testing.T) {

View File

@@ -5,9 +5,10 @@ import (
"fmt" "fmt"
"strings" "strings"
flag "github.com/spf13/pflag"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag"
) )
func init() { func init() {

View File

@@ -6,8 +6,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestPDFEngines_Descriptor(t *testing.T) { func TestPDFEngines_Descriptor(t *testing.T) {
@@ -110,7 +111,6 @@ func TestPDFEngines_Provision(t *testing.T) {
fs := new(PDFEngines).Descriptor().FlagSet fs := new(PDFEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"}) err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
if err != nil { if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err) t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
} }
@@ -158,7 +158,6 @@ func TestPDFEngines_Provision(t *testing.T) {
fs := new(PDFEngines).Descriptor().FlagSet fs := new(PDFEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"}) err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
if err != nil { if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err) t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
} }

View File

@@ -5,9 +5,10 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
) )
// mergeRoute returns an api.Route which can merge PDFs. // mergeRoute returns an api.Route which can merge PDFs.
@@ -29,7 +30,6 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
MandatoryPaths([]string{".pdf"}, &inputPaths). MandatoryPaths([]string{".pdf"}, &inputPaths).
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &PDFformat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
@@ -104,7 +104,6 @@ func convertRoute(engine gotenberg.PDFEngine) api.Route {
MandatoryPaths([]string{".pdf"}, &inputPaths). MandatoryPaths([]string{".pdf"}, &inputPaths).
MandatoryString("pdfFormat", &PDFformat). MandatoryString("pdfFormat", &PDFformat).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }

View File

@@ -6,10 +6,11 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func TestMergeHandler(t *testing.T) { func TestMergeHandler(t *testing.T) {

View File

@@ -7,8 +7,9 @@ import (
"os" "os"
"sync" "sync"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func init() { func init() {

View File

@@ -7,8 +7,9 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestPDFtk_Descriptor(t *testing.T) { func TestPDFtk_Descriptor(t *testing.T) {
@@ -117,15 +118,16 @@ func TestPDFtk_Merge(t *testing.T) {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %d: expected error but got: %v", i, err)
} }
outputDir, err := gotenberg.MkdirAll() fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil { if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %d: expected error but got: %v", i, err)
} }
defer func() { defer func() {
err := os.RemoveAll(outputDir) err := os.RemoveAll(fs.WorkingDirPath())
if err != nil { if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err) t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
} }
}() }()

View File

@@ -7,12 +7,13 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func init() { func init() {

View File

@@ -6,8 +6,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
type ProtoModule struct { type ProtoModule struct {
@@ -57,7 +58,6 @@ func TestPrometheus_Provision(t *testing.T) {
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
fs := new(Prometheus).Descriptor().FlagSet fs := new(Prometheus).Descriptor().FlagSet
err := fs.Parse([]string{"--prometheus-disable-collect=true"}) err := fs.Parse([]string{"--prometheus-disable-collect=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)
} }

View File

@@ -7,8 +7,9 @@ import (
"os" "os"
"sync" "sync"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func init() { func init() {

View File

@@ -7,8 +7,9 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
func TestQPDF_Descriptor(t *testing.T) { func TestQPDF_Descriptor(t *testing.T) {
@@ -117,15 +118,16 @@ func TestQPDF_Merge(t *testing.T) {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %d: expected error but got: %v", i, err)
} }
outputDir, err := gotenberg.MkdirAll() fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil { if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %d: expected error but got: %v", i, err)
} }
defer func() { defer func() {
err := os.RemoveAll(outputDir) err := os.RemoveAll(fs.WorkingDirPath())
if err != nil { if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err) t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
} }
}() }()

View File

@@ -62,7 +62,6 @@ func (c client) send(body io.Reader, headers map[string]string, erroed bool) err
// least it works. // least it works.
bodySize, err := strconv.ParseInt(contentLength, 10, 64) bodySize, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("parse content length entry: %w", err) return fmt.Errorf("parse content length entry: %w", err)
} }

View File

@@ -14,9 +14,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func webhookMiddleware(w Webhook) api.Middleware { func webhookMiddleware(w Webhook) api.Middleware {
@@ -196,7 +197,6 @@ func webhookMiddleware(w Webhook) api.Middleware {
// Call the next middleware in the chain. // Call the next middleware in the chain.
err := next(c) err := next(c)
if err != nil { if err != nil {
// The process failed for whatever reason. Let's send the // The process failed for whatever reason. Let's send the
// details to the webhook. // details to the webhook.

View File

@@ -16,9 +16,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func TestWebhookMiddlewareGuards(t *testing.T) { func TestWebhookMiddlewareGuards(t *testing.T) {
@@ -558,5 +559,4 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
} }
}() }()
} }
} }

View File

@@ -5,10 +5,11 @@ import (
"regexp" "regexp"
"time" "time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
func init() { func init() {
@@ -103,27 +104,9 @@ func (w Webhook) Middlewares() ([]api.Middleware, error) {
}, nil }, nil
} }
// AddGraceDuration increases the grace duration provided by the API for the
// garbage collector.
func (w Webhook) AddGraceDuration() time.Duration {
var duration time.Duration
if w.disable {
return duration
}
for i := 0; i < w.maxRetry; i++ {
// Yep... Golang does not allow int * time.Duration.
duration += w.retryMaxWait
}
return duration
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*Webhook)(nil) _ gotenberg.Module = (*Webhook)(nil)
_ gotenberg.Provisioner = (*Webhook)(nil) _ gotenberg.Provisioner = (*Webhook)(nil)
_ api.MiddlewareProvider = (*Webhook)(nil) _ api.MiddlewareProvider = (*Webhook)(nil)
_ api.GarbageCollectorGraceDurationIncrementer = (*Webhook)(nil)
) )

View File

@@ -3,7 +3,6 @@ package webhook
import ( import (
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
@@ -59,31 +58,3 @@ func TestWebhook_Middlewares(t *testing.T) {
} }
} }
} }
func TestWebhook_AddGraceDuration(t *testing.T) {
for i, tc := range []struct {
maxRetry int
retryMaxWait time.Duration
expectDuration time.Duration
disable bool
}{
{
maxRetry: 3,
retryMaxWait: time.Duration(1) * time.Second,
expectDuration: time.Duration(3) * time.Second,
},
{
disable: true,
},
} {
mod := new(Webhook)
mod.maxRetry = tc.maxRetry
mod.retryMaxWait = tc.retryMaxWait
mod.disable = tc.disable
actual := mod.AddGraceDuration()
if actual != tc.expectDuration {
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectDuration, actual)
}
}
}

View File

@@ -4,7 +4,6 @@ import (
// Standard Gotenberg modules. // Standard Gotenberg modules.
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/api" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/chromium" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/chromium"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"

View File

@@ -3,7 +3,7 @@ ARG DOCKER_REPOSITORY
ARG GOTENBERG_VERSION ARG GOTENBERG_VERSION
ARG GOLANGCI_LINT_VERSION ARG GOLANGCI_LINT_VERSION
FROM golang:$GOLANG_VERSION-bullseye as golang FROM golang:$GOLANG_VERSION-bookworm as golang
# We're extending the Gotenberg's Docker image because our code relies on external # We're extending the Gotenberg's Docker image because our code relies on external
# dependencies like Google Chrome, LibreOffice, etc. # dependencies like Google Chrome, LibreOffice, etc.
@@ -15,6 +15,8 @@ ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
ENV CGO_ENABLED 1 ENV CGO_ENABLED 1
COPY --from=golang /usr/local/go /usr/local/go
RUN apt-get update -qq &&\ RUN apt-get update -qq &&\
apt-get install -y -qq --no-install-recommends \ apt-get install -y -qq --no-install-recommends \
sudo \ sudo \
@@ -31,9 +33,10 @@ RUN apt-get update -qq &&\
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers &&\ echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers &&\
# We cannot use $PATH in the next command (print $PATH instead of the environment variable value). # We cannot use $PATH in the next command (print $PATH instead of the environment variable value).
sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/go/bin:/usr/local/go/bin#g' /etc/sudoers &&\ sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/go/bin:/usr/local/go/bin#g' /etc/sudoers &&\
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VERSION curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VERSION &&\
go install mvdan.cc/gofumpt@latest &&\
go install github.com/daixiang0/gci@latest
COPY --from=golang /usr/local/go /usr/local/go
COPY ./test/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh COPY ./test/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
COPY ./test/golint.sh /usr/bin/golint COPY ./test/golint.sh /usr/bin/golint
COPY ./test/gotest.sh /usr/bin/gotest COPY ./test/gotest.sh /usr/bin/gotest