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
+19 -2
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:
disable-all: true
enable:
- bodyclose
- errcheck
- gofmt
- goimports
- gci
- gofumpt
- gosec
- gosimple
- govet
+4 -1
View File
@@ -138,9 +138,12 @@ tests-once: ## Run the tests once (prefer the "tests" command while developing)
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \
gotest
# go install mvdan.cc/gofumpt@latest
# go install github.com/daixiang0/gci@latest
.PHONY: fmt
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
.PHONY: godoc
+4 -5
View File
@@ -30,7 +30,7 @@ RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v7/cmd.Ve
# ----------------------------------------------
# Final stage
# ----------------------------------------------
FROM debian:11-slim
FROM debian:12-slim
ARG GOTENBERG_VERSION
ARG GOTENBERG_USER_GID
@@ -57,8 +57,7 @@ RUN \
# Install system dependencies required for the next instructions or debugging.
# Note: tini is a helper for reaping zombie processes.
apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg htop tini python3 default-jre-headless &&\
ln -s /usr/bin/htop /usr/bin/top &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg tini python3 default-jre-headless &&\
# Cleanup.
# Note: the Debian image does automatically a clean after each install thanks to a hook.
# Therefore, there is no need for apt-get clean.
@@ -140,8 +139,9 @@ RUN \
RUN \
# Install LibreOffice & unoconv.
echo "deb http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list &&\
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 &&\
chmod +x /usr/bin/unoconv &&\
# 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/
# Environment variables required by modules or else.
ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
ENV UNOCONV_BIN_PATH /usr/bin/unoconv
ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin
+2 -1
View File
@@ -8,9 +8,10 @@ import (
"syscall"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag"
"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.
-1
View File
@@ -2,7 +2,6 @@ package main
import (
gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd"
// Gotenberg modules.
_ "github.com/gotenberg/gotenberg/v7/pkg/standard"
)
+10 -10
View File
@@ -5,7 +5,7 @@ go 1.21
require (
github.com/alexliesenfeld/health v0.7.0
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/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.1
@@ -21,18 +21,18 @@ require (
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pdfcpu/pdfcpu v0.5.0
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/spf13/pflag v1.0.5
github.com/ulikunitz/xz v0.5.11 // indirect
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/image v0.12.0 // indirect
golang.org/x/net v0.15.0
golang.org/x/sync v0.3.0
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.12.0
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.16.0
golang.org/x/sync v0.4.0
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term 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/matttproud/golang_protobuf_extensions v1.0.4 // 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/procfs v0.11.1 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
+20 -50
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/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-20230914224007-a15a36ccbc2e h1:BfDqq+EHA0HP037qWakDtYxIg9erpn2aZfZlrtnB35E=
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 h1:/5dwcyi5WOawM1Iz6MjrYqB90TRIdZv3O0fVHEJb86w=
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/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
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/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
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.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
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/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
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/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
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/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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-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.5.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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/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=
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=
-1
View File
@@ -158,7 +158,6 @@ func (cmd Cmd) pipeOutput() error {
for {
line, _, err := r.ReadLine()
if err != nil {
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
logger.Error(fmt.Sprintf("pipe unix process output error: %s", err))
-1
View File
@@ -47,7 +47,6 @@ func (ctx Context) ParsedFlags() ParsedFlags {
// initializes it. Otherwise, returns the already initialized instance.
func (ctx *Context) Module(kind interface{}) (interface{}, error) {
mods, err := ctx.Modules(kind)
if err != nil {
return nil, fmt.Errorf("get module: %w", err)
}
+33 -13
View File
@@ -7,24 +7,44 @@ import (
"github.com/google/uuid"
)
// TmpPath returns the default directory to use for temporary files and
// directories. Most if not all files and directories created by the
// application and its dependencies must be based on this default directory.
func TmpPath() string {
return os.TempDir()
// FileSystem provides utilities for managing temporary directories. It creates
// unique directory names based on UUIDs to ensure isolation of temporary files
// for different modules.
type FileSystem struct {
workingDir string
}
// NewDirPath returns a random absolute path based on the temporary path.
func NewDirPath() string {
return fmt.Sprintf("%s/%s", TmpPath(), uuid.New())
// NewFileSystem initializes a new FileSystem instance with a unique working
// directory.
func NewFileSystem() *FileSystem {
return &FileSystem{
workingDir: uuid.NewString(),
}
}
// MkdirAll creates a random directory based on the temporary path and
// returns its absolute path.
func MkdirAll() (string, error) {
path := NewDirPath()
// WorkingDir returns the unique name of the working directory.
func (fs *FileSystem) WorkingDir() string {
return fs.workingDir
}
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 {
return "", fmt.Errorf("create directory %s: %w", path, err)
}
+31 -35
View File
@@ -1,59 +1,55 @@
package gotenberg
import (
"fmt"
"os"
"strings"
"testing"
)
func TestTmpPath(t *testing.T) {
osTempDir := os.TempDir()
tmpPath := TmpPath()
func TestFileSystem_WorkingDir(t *testing.T) {
fs := NewFileSystem()
dirName := fs.WorkingDir()
if tmpPath != osTempDir {
t.Errorf("expected path '%s' but got '%s'", osTempDir, tmpPath)
if dirName == "" {
t.Error("expected directory name but got empty string")
}
}
func TestNewDirPath(t *testing.T) {
newDirPath := NewDirPath()
tmpPath := TmpPath()
func TestFileSystem_WorkingDirPath(t *testing.T) {
fs := NewFileSystem()
expectedPath := fmt.Sprintf("%s/%s", os.TempDir(), fs.WorkingDir())
if !strings.HasPrefix(newDirPath, tmpPath) {
t.Fatalf("expected path '%s' to start with '%s'", newDirPath, tmpPath)
}
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)
}
}
if fs.WorkingDirPath() != expectedPath {
t.Errorf("expected path '%s' but got '%s'", expectedPath, fs.WorkingDirPath())
}
}
func TestMkdirAll(t *testing.T) {
path, err := MkdirAll()
func TestFileSystem_NewDirPath(t *testing.T) {
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 {
t.Fatalf("expected no error but got: %v", err)
}
tmpPath := TmpPath()
if !strings.HasPrefix(path, tmpPath) {
t.Fatalf("expected path '%s' to start with '%s'", path, tmpPath)
_, err = os.Stat(newPath)
if os.IsNotExist(err) {
t.Errorf("expected directory '%s' to exist but it doesn't", newPath)
}
_, err = os.Stat(path)
if os.IsNotExist(err) {
t.Errorf("expected path '%s' to exist but got: %v", path, err)
err = os.RemoveAll(fs.WorkingDirPath())
if err != nil {
t.Fatalf("expected no error while cleaning up but got: %v", err)
}
}
+53
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
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)
}
}
}()
}
}
+12 -33
View File
@@ -12,13 +12,13 @@ import (
"time"
"github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
"github.com/labstack/echo/v4"
flag "github.com/spf13/pflag"
"go.uber.org/multierr"
"go.uber.org/zap"
"golang.org/x/net/http2"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func init() {
@@ -39,7 +39,7 @@ type API struct {
routes []Route
externalMiddlewares []Middleware
healthChecks []health.CheckerOption
gcGraceDuration time.Duration
fs *gotenberg.FileSystem
logger *zap.Logger
srv *echo.Echo
}
@@ -149,12 +149,6 @@ type HealthChecker interface {
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.
func (API) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
@@ -283,18 +277,7 @@ func (a *API) Provision(ctx *gotenberg.Context) error {
a.healthChecks = append(a.healthChecks, checks...)
}
// Grace duration.
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()
}
// Logger.
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
if err != nil {
return fmt.Errorf("get logger provider: %w", err)
@@ -307,6 +290,9 @@ func (a *API) Provision(ctx *gotenberg.Context) error {
a.logger = logger
// File system.
a.fs = gotenberg.NewFileSystem()
return nil
}
@@ -437,7 +423,7 @@ func (a *API) Start() error {
var middlewares []echo.MiddlewareFunc
if route.IsMultipart {
middlewares = append(middlewares, contextMiddleware(a.timeout))
middlewares = append(middlewares, contextMiddleware(a.fs, a.timeout))
for _, externalMultipartMiddleware := range externalMultipartMiddlewares {
middlewares = append(middlewares, externalMultipartMiddleware.Handler)
@@ -488,17 +474,10 @@ func (a API) Stop(ctx context.Context) error {
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.
var (
_ gotenberg.Module = (*API)(nil)
_ gotenberg.Provisioner = (*API)(nil)
_ gotenberg.Validator = (*API)(nil)
_ gotenberg.App = (*API)(nil)
_ gc.GarbageCollectorGraceDurationModifier = (*API)(nil)
_ gotenberg.Module = (*API)(nil)
_ gotenberg.Provisioner = (*API)(nil)
_ gotenberg.Validator = (*API)(nil)
_ gotenberg.App = (*API)(nil)
)
+22 -109
View File
@@ -10,12 +10,12 @@ import (
"os"
"reflect"
"testing"
"time"
"github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
type ProtoModule struct {
@@ -62,15 +62,6 @@ func (mod ProtoHealthChecker) Checks() ([]health.CheckerOption, error) {
return mod.checks()
}
type ProtoGarbageCollectorGraceDurationIncrementer struct {
ProtoValidator
addGraceDuration func() time.Duration
}
func (mod ProtoGarbageCollectorGraceDurationIncrementer) AddGraceDuration() time.Duration {
return mod.addGraceDuration()
}
type ProtoLoggerProvider struct {
ProtoModule
logger func(mod gotenberg.Module) (*zap.Logger, error)
@@ -93,18 +84,16 @@ func TestAPI_Descriptor(t *testing.T) {
func TestAPI_Provision(t *testing.T) {
for i, tc := range []struct {
ctx *gotenberg.Context
setEnv func(i int)
expectPort int
expectMiddlewares []Middleware
expectGraceDuration time.Duration
expectErr bool
ctx *gotenberg.Context
setEnv func(i int)
expectPort int
expectMiddlewares []Middleware
expectErr bool
}{
{
ctx: func() *gotenberg.Context {
fs := new(API).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=FOO"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
@@ -122,7 +111,6 @@ func TestAPI_Provision(t *testing.T) {
ctx: func() *gotenberg.Context {
fs := new(API).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
@@ -146,7 +134,6 @@ func TestAPI_Provision(t *testing.T) {
ctx: func() *gotenberg.Context {
fs := new(API).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
@@ -170,7 +157,6 @@ func TestAPI_Provision(t *testing.T) {
ctx: func() *gotenberg.Context {
fs := new(API).Descriptor().FlagSet
err := fs.Parse([]string{"--api-port-from-env=PORT"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
@@ -335,60 +321,6 @@ func TestAPI_Provision(t *testing.T) {
}(),
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 {
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)
}
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 {
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()
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.
var (
_ gotenberg.Module = (*ProtoModule)(nil)
_ gotenberg.Validator = (*ProtoValidator)(nil)
_ gotenberg.Module = (*ProtoValidator)(nil)
_ Router = (*ProtoRouter)(nil)
_ gotenberg.Module = (*ProtoRouter)(nil)
_ gotenberg.Validator = (*ProtoRouter)(nil)
_ MiddlewareProvider = (*ProtoMiddlewareProvider)(nil)
_ gotenberg.Module = (*ProtoMiddlewareProvider)(nil)
_ gotenberg.Validator = (*ProtoMiddlewareProvider)(nil)
_ HealthChecker = (*ProtoHealthChecker)(nil)
_ gotenberg.Module = (*ProtoHealthChecker)(nil)
_ gotenberg.Validator = (*ProtoHealthChecker)(nil)
_ GarbageCollectorGraceDurationIncrementer = (*ProtoGarbageCollectorGraceDurationIncrementer)(nil)
_ gotenberg.Module = (*ProtoGarbageCollectorGraceDurationIncrementer)(nil)
_ gotenberg.Validator = (*ProtoGarbageCollectorGraceDurationIncrementer)(nil)
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
_ gotenberg.Module = (*ProtoModule)(nil)
_ gotenberg.Validator = (*ProtoValidator)(nil)
_ gotenberg.Module = (*ProtoValidator)(nil)
_ Router = (*ProtoRouter)(nil)
_ gotenberg.Module = (*ProtoRouter)(nil)
_ gotenberg.Validator = (*ProtoRouter)(nil)
_ MiddlewareProvider = (*ProtoMiddlewareProvider)(nil)
_ gotenberg.Module = (*ProtoMiddlewareProvider)(nil)
_ gotenberg.Validator = (*ProtoMiddlewareProvider)(nil)
_ HealthChecker = (*ProtoHealthChecker)(nil)
_ gotenberg.Module = (*ProtoHealthChecker)(nil)
_ gotenberg.Validator = (*ProtoHealthChecker)(nil)
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
)
+5 -4
View File
@@ -15,13 +15,14 @@ import (
"unicode"
"github.com/google/uuid"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/labstack/echo/v4"
"github.com/mholt/archiver/v3"
"go.uber.org/zap"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
var (
@@ -49,7 +50,7 @@ type Context struct {
}
// 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)
ctx := &Context{
@@ -81,7 +82,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
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
}
}()
@@ -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)
}
dirPath, err := gotenberg.MkdirAll()
dirPath, err := fs.MkdirAll()
if err != nil {
return nil, cancel, fmt.Errorf("create working directory: %w", err)
}
+25 -19
View File
@@ -12,9 +12,10 @@ import (
"testing"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestNewContext(t *testing.T) {
@@ -104,7 +105,7 @@ func TestNewContext(t *testing.T) {
},
} {
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()
// Context already cancelled.
defer cancel()
@@ -277,28 +278,33 @@ func TestContext_BuildOutputFile(t *testing.T) {
},
},
} {
dirPath, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("%d: expected no erro but got: %v", i, err)
}
func() {
fs := gotenberg.NewFileSystem()
dirPath, err := fs.MkdirAll()
if err != nil {
t.Fatalf("%d: expected no erro but got: %v", i, err)
}
tc.ctx.dirPath = dirPath
tc.ctx.logger = zap.NewNop()
defer func() {
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 {
t.Errorf("test %d: expected error but got: %v", i, err)
}
_, err = tc.ctx.BuildOutputFile()
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
err = os.RemoveAll(dirPath)
if err != nil {
t.Fatalf("%d: expected no erro but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}()
}
}
+6 -3
View File
@@ -11,6 +11,8 @@ import (
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
// 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
// HTTP message.
func ParseError(err error) (int, string) {
echoErr, ok := err.(*echo.HTTPError)
var echoErr *echo.HTTPError
ok := errors.As(err, &echoErr)
if ok {
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)
// 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(c echo.Context) error {
logger := c.Get("logger").(*zap.Logger)
// We create a context with a timeout so that underlying processes are
// 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 {
cancel()
+3 -5
View File
@@ -13,6 +13,8 @@ import (
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestParseError(t *testing.T) {
@@ -122,7 +124,6 @@ func TestLatencyMiddleware(t *testing.T) {
return nil
},
)(c)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
@@ -150,7 +151,6 @@ func TestRootPathMiddleware(t *testing.T) {
return nil
},
)(c)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
@@ -191,7 +191,6 @@ func TestTraceMiddleware(t *testing.T) {
return nil
},
)(c)
if err != nil {
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)
if err != nil {
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("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 {
t.Errorf("test %d: expected error but got: %v", i, err)
+40 -13
View File
@@ -19,11 +19,12 @@ import (
"github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func init() {
@@ -68,8 +69,11 @@ var (
// Chromium is a module which provides both an API and routes for converting
// HTML document to PDF.
type Chromium struct {
binPath string
engine gotenberg.PDFEngine
binPath string
engine gotenberg.PDFEngine
fs *gotenberg.FileSystem
logger *zap.Logger
failedStartsThreshold int
userAgent string
incognito bool
@@ -311,6 +315,20 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
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))
if err != nil {
return fmt.Errorf("get PDF engine provider: %w", err)
@@ -323,6 +341,9 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
mod.engine = engine
// File system.
mod.fs = gotenberg.NewFileSystem()
return nil
}
@@ -412,7 +433,7 @@ func (mod Chromium) Routes() ([]api.Route, error) {
// the end of the conversion.
func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
debug := debugLogger{logger: logger.Named("browser")}
userProfileDirPath := gotenberg.NewDirPath()
userProfileDirPath := mod.fs.NewDirPath()
args := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.CombinedOutput(debug),
@@ -799,7 +820,6 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
evaluate := chromedp.Evaluate(expression, &ok)
err := evaluate.Do(ctx)
if err != nil {
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 {
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()
// Always remove the user profile directory created by Chromium.
go func() {
logger.Debug(fmt.Sprintf("remove user profile directory '%s'", userProfileDirPath))
defer func() {
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)
if err != nil {
logger.Error(fmt.Sprintf("remove user profile directory: %s", err))
}
err := os.RemoveAll(userProfileDirPath)
if err != nil {
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 {
+86 -49
View File
@@ -10,18 +10,11 @@ import (
"time"
"github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"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 {
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)
}
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) {
actual := DefaultOptions()
notExpect := Options{}
@@ -73,11 +44,13 @@ func TestChromium_Descriptor(t *testing.T) {
}
func TestChromium_Provision(t *testing.T) {
for i, tc := range []struct {
for _, tc := range []struct {
scenario string
ctx *gotenberg.Context
expectErr bool
}{
{
scenario: "no logger provider",
ctx: func() *gotenberg.Context {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
@@ -89,12 +62,16 @@ func TestChromium_Provision(t *testing.T) {
expectErr: true,
},
{
scenario: "no logger from logger provider",
ctx: func() *gotenberg.Context {
mod := struct{ ProtoPDFEngineProvider }{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
mod := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
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")
}
@@ -110,13 +87,75 @@ func TestChromium_Provision(t *testing.T) {
expectErr: true,
},
{
scenario: "no PDF engine provider",
ctx: func() *gotenberg.Context {
mod := struct{ ProtoPDFEngineProvider }{}
mod.descriptor = func() gotenberg.ModuleDescriptor {
mod := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
}
mod.pdfEngine = func() (gotenberg.PDFEngine, error) {
return struct{ ProtoPDFEngine }{}, nil
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
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(
@@ -134,11 +173,11 @@ func TestChromium_Provision(t *testing.T) {
err := mod.Provision(tc.ctx)
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 {
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.denyList = tc.denyList
mod.disableJavaScript = tc.disableJavaScript
mod.fs = gotenberg.NewFileSystem()
outputDir, err := gotenberg.MkdirAll()
ctxFs := gotenberg.NewFileSystem()
outputDir, err := ctxFs.MkdirAll()
if err != nil {
t.Fatalf("test %s: expected error but got: %v", tc.name, err)
}
defer func() {
err := os.RemoveAll(outputDir)
err := os.RemoveAll(ctxFs.WorkingDirPath())
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.
var (
_ gotenberg.Module = (*ProtoModule)(nil)
_ API = (*ProtoAPI)(nil)
_ gotenberg.PDFEngineProvider = (*ProtoPDFEngineProvider)(nil)
_ gotenberg.Module = (*ProtoPDFEngineProvider)(nil)
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
_ API = (*ProtoAPI)(nil)
)
-2
View File
@@ -43,7 +43,6 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
if allow {
req := fetch.ContinueRequest(e.RequestID)
err := req.Do(executorCtx)
if err != nil {
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)
err := req.Do(executorCtx)
if err != nil {
logger.Error(fmt.Sprintf("fail request: %s", err))
}
+4 -7
View File
@@ -12,12 +12,13 @@ import (
"strings"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"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
@@ -163,7 +164,6 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
return nil
}).
Validate()
if err != nil {
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).
String("pdfFormat", &PDFformat, "").
Validate()
if err != nil {
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).
String("pdfFormat", &PDFformat, "").
Validate()
if err != nil {
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
},
}).ParseFiles(inputPath)
if err != nil {
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")
err = os.WriteFile(inputPath, buffer.Bytes(), 0600)
err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
if err != nil {
return fmt.Errorf("write template result: %w", err)
}
+10 -10
View File
@@ -8,10 +8,11 @@ import (
"reflect"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func TestFormDataChromiumPDFOptions(t *testing.T) {
@@ -588,8 +589,7 @@ func TestConvertMarkdownHandler(t *testing.T) {
} {
func() {
if tc.outputDir != "" {
err := os.MkdirAll(tc.outputDir, 0755)
err := os.MkdirAll(tc.outputDir, 0o755)
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
@@ -784,8 +784,8 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return &gotenberg.PDFEngineMock{
ConvertMock: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return gotenberg.ErrPDFFormatNotAvailable
},
}
@@ -807,8 +807,8 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return &gotenberg.PDFEngineMock{
ConvertMock: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return errors.New("foo")
},
}
@@ -828,8 +828,8 @@ func TestConvertURL(t *testing.T) {
return chromiumAPI
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return &gotenberg.PDFEngineMock{
ConvertMock: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return nil
},
}
-3
View File
@@ -1,3 +0,0 @@
// Package gc provides a module for removing files and directories that have
// expired.
package gc
-225
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)
)
-468
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)
)
+2 -1
View File
@@ -3,10 +3,11 @@ package libreoffice
import (
"fmt"
flag "github.com/spf13/pflag"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
flag "github.com/spf13/pflag"
)
func init() {
@@ -5,9 +5,10 @@ import (
"errors"
"fmt"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"go.uber.org/zap"
)
func init() {
@@ -6,9 +6,10 @@ import (
"reflect"
"testing"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"go.uber.org/zap"
)
func TestUNO_Descriptor(t *testing.T) {
+2 -2
View File
@@ -5,10 +5,11 @@ import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"github.com/labstack/echo/v4"
)
// 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, "").
Bool("merge", &merge, false).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
+3 -2
View File
@@ -6,11 +6,12 @@ import (
"net/http"
"testing"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
)
func TestConvertHandler(t *testing.T) {
+28 -9
View File
@@ -8,8 +8,9 @@ import (
"sync"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
type listener interface {
@@ -42,15 +43,18 @@ type libreOfficeListener struct {
queueLength int
queueLengthMu sync.RWMutex
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{
binPath: binPath,
startTimeout: startTimeout,
threshold: threshold,
lockChan: make(chan struct{}, 1),
fs: fs,
logger: logger.Named("listener"),
}
}
@@ -65,10 +69,7 @@ func (listener *libreOfficeListener) start(logger *zap.Logger) error {
return fmt.Errorf("get free port: %w", err)
}
// Good to know: the garbage collector might delete the next directory
// while it is still running. It does seem to cause any issue though.
userProfileDirPath := gotenberg.NewDirPath()
userProfileDirPath := listener.fs.NewDirPath()
args := []string{
"--headless",
"--invisible",
@@ -158,6 +159,12 @@ func (listener *libreOfficeListener) start(logger *zap.Logger) error {
if err != nil {
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...")
@@ -185,9 +192,21 @@ func (listener *libreOfficeListener) stop(logger *zap.Logger) error {
defer func() {
defer listener.cfgMu.RUnlock()
if listener.userProfileDirPath == "" {
return
}
err := os.RemoveAll(listener.userProfileDirPath)
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
if listener.usage < listener.threshold {
if listener.threshold > 0 && listener.usage < listener.threshold {
return nil
}
+16 -9
View File
@@ -7,6 +7,8 @@ import (
"time"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestListener_start(t *testing.T) {
@@ -17,11 +19,11 @@ func TestListener_start(t *testing.T) {
}{
{
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",
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,
},
}
@@ -57,6 +59,7 @@ func TestListener_start(t *testing.T) {
func TestListener_stop(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
@@ -76,6 +79,7 @@ func TestListener_stop(t *testing.T) {
func TestListener_restart(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
@@ -107,7 +111,7 @@ func TestListener_lock(t *testing.T) {
{
name: "nominal behavior",
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())
if err != nil {
@@ -123,7 +127,7 @@ func TestListener_lock(t *testing.T) {
},
{
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(),
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
@@ -132,7 +136,7 @@ func TestListener_lock(t *testing.T) {
{
name: "unhealthy 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())
if err != nil {
@@ -154,7 +158,7 @@ func TestListener_lock(t *testing.T) {
{
name: "context done",
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())
if err != nil {
@@ -212,7 +216,7 @@ func TestListener_unlock(t *testing.T) {
{
name: "nominal behavior",
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())
if err != nil {
@@ -233,7 +237,7 @@ func TestListener_unlock(t *testing.T) {
{
name: "unhealthy 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())
if err != nil {
@@ -259,7 +263,7 @@ func TestListener_unlock(t *testing.T) {
{
name: "threshold reached",
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())
if err != nil {
@@ -299,6 +303,7 @@ func TestListener_unlock(t *testing.T) {
func TestListener_port(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
@@ -323,6 +328,7 @@ func TestListener_port(t *testing.T) {
func TestListener_queue(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
gotenberg.NewFileSystem(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
@@ -395,6 +401,7 @@ func TestListener_healthy(t *testing.T) {
startTimeout: time.Duration(10) * time.Second,
threshold: 10,
lockChan: make(chan struct{}, 1),
fs: gotenberg.NewFileSystem(),
logger: zap.NewNop(),
}
+26 -56
View File
@@ -9,11 +9,12 @@ import (
"time"
"github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag"
"go.uber.org/multierr"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func init() {
@@ -82,10 +83,10 @@ func (UNO) Descriptor() gotenberg.ModuleDescriptor {
FlagSet: func() *flag.FlagSet {
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.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")
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 {
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
// Listener.
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
@@ -170,19 +173,11 @@ func (mod UNO) Start() error {
// StartupMessage returns a custom startup message.
func (mod UNO) StartupMessage() string {
if mod.libreOfficeRestartThreshold == 0 {
return "long-running LibreOffice listener disabled"
}
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 {
if mod.libreOfficeRestartThreshold == 0 {
return nil
}
// Block until the context is done so that other module may gracefully stop
// before we do a shutdown cleanup.
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 fmt.Errorf("stop long-running LibreOffice listener")
return fmt.Errorf("stop long-running LibreOffice listener: %w", err)
}
// Metrics returns the metrics.
@@ -284,14 +279,8 @@ func (mod UNO) Checks() ([]health.CheckerOption, error) {
}, nil
}
// PDF converts a document to PDF.
//
// 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.
// PDF converts a document to PDF. Be cautious when making multiple concurrent
// calls, as it might lead to reaching the context's deadline.
func (mod UNO) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
args := []string{
"--no-launch",
@@ -299,45 +288,26 @@ func (mod UNO) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPat
"pdf",
}
switch mod.libreOfficeRestartThreshold {
case 0:
listener := newLibreOfficeListener(logger, mod.libreOfficeBinPath, mod.libreOfficeStartTimeout, 0)
err := mod.listener.lock(ctx, logger)
if err != nil {
return fmt.Errorf("lock long-running LibreOffice listener: %w", err)
}
err := listener.start(logger)
if err != nil {
return fmt.Errorf("start LibreOffice listener: %w", err)
}
defer func() {
err := listener.stop(logger)
defer func() {
go func() {
err := mod.listener.unlock(logger)
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()))
default:
err := mod.listener.lock(ctx, logger)
if err != nil {
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()))
}
// 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")
if checkedEntry != nil {
+241 -139
View File
@@ -9,9 +9,10 @@ import (
"time"
"github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestUNO_Descriptor(t *testing.T) {
@@ -78,7 +79,6 @@ func TestUNO_Provision(t *testing.T) {
FlagSet: func() *flag.FlagSet {
fs := new(UNO).Descriptor().FlagSet
err := fs.Parse([]string{"--unoconv-disable-listener=true"})
if err != nil {
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) {
tests := []struct {
name string
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",
},
}
actual := new(UNO).StartupMessage()
expect := "long-running LibreOffice listener ready to start"
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual := tc.mod.StartupMessage()
if tc.expectMessage != actual {
t.Errorf("expected '%s' from mod.StartupMessage(), but got '%s'", tc.expectMessage, actual)
}
})
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
@@ -255,13 +231,6 @@ func TestUNO_Stop(t *testing.T) {
logger: zap.NewNop(),
},
},
{
name: "no long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 0,
logger: zap.NewNop(),
},
},
{
name: "stop error",
mod: UNO{
@@ -468,7 +437,7 @@ func TestUNO_Checks(t *testing.T) {
func TestUNO_PDF(t *testing.T) {
tests := []struct {
name string
scenario string
mod UNO
ctx context.Context
logger *zap.Logger
@@ -478,19 +447,7 @@ func TestUNO_PDF(t *testing.T) {
teardown func(mod UNO) error
}{
{
name: "nominal behavior with no 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",
scenario: "nominal behavior with a long-running LibreOffice listener",
mod: func() UNO {
mod := UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
@@ -501,6 +458,7 @@ func TestUNO_PDF(t *testing.T) {
}
mod.listener = newLibreOfficeListener(
mod.logger,
gotenberg.NewFileSystem(),
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
@@ -518,56 +476,122 @@ func TestUNO_PDF(t *testing.T) {
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",
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.NewExample(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
},
{
name: "convert with landscape",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "convert with landscape",
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.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
Landscape: true,
},
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
},
{
name: "convert with page ranges",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "convert with page ranges",
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.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
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",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "convert with invalid page ranges",
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.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
@@ -575,60 +599,132 @@ func TestUNO_PDF(t *testing.T) {
PageRanges: "foo",
},
expectPDFErr: true,
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
},
{
name: "convert to PDF/A-1a",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "convert to PDF/A-1a",
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.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
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",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "convert to PDF/A-2b",
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.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
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",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "convert to PDF/A-3b",
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.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
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",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "convert to invalid PDF format",
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.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
@@ -636,28 +732,33 @@ func TestUNO_PDF(t *testing.T) {
PDFformat: "foo",
},
expectPDFErr: true,
teardown: func(mod UNO) error {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return mod.Stop(ctx)
},
},
{
name: "nil context",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
ctx: nil,
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
expectPDFErr: true,
},
{
name: "expired context",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
libreOfficeStartTimeout: time.Duration(10) * time.Second,
libreOfficeRestartThreshold: 0,
},
scenario: "expired context",
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: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
@@ -669,7 +770,7 @@ func TestUNO_PDF(t *testing.T) {
expectPDFErr: true,
},
{
name: "cannot lock long-running LibreOffice listener",
scenario: "cannot lock long-running LibreOffice listener",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
@@ -688,7 +789,7 @@ func TestUNO_PDF(t *testing.T) {
expectPDFErr: true,
},
{
name: "cannot unlock long-running LibreOffice listener",
scenario: "cannot unlock long-running LibreOffice listener",
mod: UNO{
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
@@ -715,7 +816,7 @@ func TestUNO_PDF(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Run(tc.scenario, func(t *testing.T) {
defer func() {
if tc.teardown == nil {
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 {
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() {
err := os.RemoveAll(outputDir)
err := os.RemoveAll(fs.WorkingDirPath())
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)
}
}()
+2 -1
View File
@@ -5,12 +5,13 @@ import (
"os"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag"
"go.uber.org/multierr"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/term"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func init() {
+2 -1
View File
@@ -5,10 +5,11 @@ import (
"reflect"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestLogging_Descriptor(t *testing.T) {
+2 -1
View File
@@ -4,11 +4,12 @@ import (
"context"
"fmt"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
pdfcpuAPI "github.com/pdfcpu/pdfcpu/pkg/api"
pdfcpuLog "github.com/pdfcpu/pdfcpu/pkg/log"
pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func init() {
+6 -4
View File
@@ -7,8 +7,9 @@ import (
"reflect"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
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)
}
outputDir, err := gotenberg.MkdirAll()
fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
defer func() {
err := os.RemoveAll(outputDir)
err := os.RemoveAll(fs.WorkingDirPath())
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)
}
}()
+2 -1
View File
@@ -4,9 +4,10 @@ import (
"context"
"fmt"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/multierr"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
// multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one
+2 -1
View File
@@ -5,8 +5,9 @@ import (
"errors"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestMultiPDFEngines_Merge(t *testing.T) {
+2 -1
View File
@@ -5,9 +5,10 @@ import (
"fmt"
"strings"
flag "github.com/spf13/pflag"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag"
)
func init() {
+2 -3
View File
@@ -6,8 +6,9 @@ import (
"strings"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestPDFEngines_Descriptor(t *testing.T) {
@@ -110,7 +111,6 @@ func TestPDFEngines_Provision(t *testing.T) {
fs := new(PDFEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
if err != nil {
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
err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
}
+2 -3
View File
@@ -5,9 +5,10 @@ import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
)
// mergeRoute returns an api.Route which can merge PDFs.
@@ -29,7 +30,6 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
MandatoryPaths([]string{".pdf"}, &inputPaths).
String("pdfFormat", &PDFformat, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
@@ -104,7 +104,6 @@ func convertRoute(engine gotenberg.PDFEngine) api.Route {
MandatoryPaths([]string{".pdf"}, &inputPaths).
MandatoryString("pdfFormat", &PDFformat).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
+3 -2
View File
@@ -6,10 +6,11 @@ import (
"net/http"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func TestMergeHandler(t *testing.T) {
+2 -1
View File
@@ -7,8 +7,9 @@ import (
"os"
"sync"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func init() {
+6 -4
View File
@@ -7,8 +7,9 @@ import (
"reflect"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
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)
}
outputDir, err := gotenberg.MkdirAll()
fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
defer func() {
err := os.RemoveAll(outputDir)
err := os.RemoveAll(fs.WorkingDirPath())
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)
}
}()
+3 -2
View File
@@ -7,12 +7,13 @@ import (
"net/http"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
flag "github.com/spf13/pflag"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func init() {
+2 -2
View File
@@ -6,8 +6,9 @@ import (
"testing"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/prometheus/client_golang/prometheus"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
type ProtoModule struct {
@@ -57,7 +58,6 @@ func TestPrometheus_Provision(t *testing.T) {
ctx: func() *gotenberg.Context {
fs := new(Prometheus).Descriptor().FlagSet
err := fs.Parse([]string{"--prometheus-disable-collect=true"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
+2 -1
View File
@@ -7,8 +7,9 @@ import (
"os"
"sync"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func init() {
+6 -4
View File
@@ -7,8 +7,9 @@ import (
"reflect"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
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)
}
outputDir, err := gotenberg.MkdirAll()
fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
defer func() {
err := os.RemoveAll(outputDir)
err := os.RemoveAll(fs.WorkingDirPath())
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)
}
}()
-1
View File
@@ -62,7 +62,6 @@ func (c client) send(body io.Reader, headers map[string]string, erroed bool) err
// least it works.
bodySize, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil {
return fmt.Errorf("parse content length entry: %w", err)
}
+2 -2
View File
@@ -14,9 +14,10 @@ import (
"strings"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/hashicorp/go-retryablehttp"
"github.com/labstack/echo/v4"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func webhookMiddleware(w Webhook) api.Middleware {
@@ -196,7 +197,6 @@ func webhookMiddleware(w Webhook) api.Middleware {
// Call the next middleware in the chain.
err := next(c)
if err != nil {
// The process failed for whatever reason. Let's send the
// details to the webhook.
+2 -2
View File
@@ -16,9 +16,10 @@ import (
"testing"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func TestWebhookMiddlewareGuards(t *testing.T) {
@@ -558,5 +559,4 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
}
}()
}
}
+6 -23
View File
@@ -5,10 +5,11 @@ import (
"regexp"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
flag "github.com/spf13/pflag"
"go.uber.org/multierr"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
func init() {
@@ -103,27 +104,9 @@ func (w Webhook) Middlewares() ([]api.Middleware, error) {
}, 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.
var (
_ gotenberg.Module = (*Webhook)(nil)
_ gotenberg.Provisioner = (*Webhook)(nil)
_ api.MiddlewareProvider = (*Webhook)(nil)
_ api.GarbageCollectorGraceDurationIncrementer = (*Webhook)(nil)
_ gotenberg.Module = (*Webhook)(nil)
_ gotenberg.Provisioner = (*Webhook)(nil)
_ api.MiddlewareProvider = (*Webhook)(nil)
)
-29
View File
@@ -3,7 +3,6 @@ package webhook
import (
"reflect"
"testing"
"time"
"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)
}
}
}
-1
View File
@@ -4,7 +4,6 @@ import (
// Standard Gotenberg modules.
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
_ "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/pdfengine"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
+6 -3
View File
@@ -3,7 +3,7 @@ ARG DOCKER_REPOSITORY
ARG GOTENBERG_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
# dependencies like Google Chrome, LibreOffice, etc.
@@ -15,6 +15,8 @@ ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
ENV CGO_ENABLED 1
COPY --from=golang /usr/local/go /usr/local/go
RUN apt-get update -qq &&\
apt-get install -y -qq --no-install-recommends \
sudo \
@@ -31,9 +33,10 @@ RUN apt-get update -qq &&\
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers &&\
# 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 &&\
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/golint.sh /usr/bin/golint
COPY ./test/gotest.sh /usr/bin/gotest