From 258876d13f81a5189ab3288c42b72af3612e4318 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Mon, 23 Oct 2023 17:05:10 +0200 Subject: [PATCH] fix: LibreOffice newer versions stability (#697) --- .golangci.yml | 21 +- Makefile | 5 +- build/Dockerfile | 9 +- cmd/gotenberg.go | 3 +- cmd/gotenberg/main.go | 1 - go.mod | 20 +- go.sum | 70 +-- pkg/gotenberg/cmd.go | 1 - pkg/gotenberg/context.go | 1 - pkg/gotenberg/fs.go | 46 +- pkg/gotenberg/fs_test.go | 66 ++- pkg/gotenberg/gc.go | 53 ++ pkg/gotenberg/gc_test.go | 97 ++++ pkg/modules/api/api.go | 45 +- pkg/modules/api/api_test.go | 131 +---- pkg/modules/api/context.go | 9 +- pkg/modules/api/context_test.go | 44 +- pkg/modules/api/middlewares.go | 9 +- pkg/modules/api/middlewares_test.go | 8 +- pkg/modules/chromium/chromium.go | 53 +- pkg/modules/chromium/chromium_test.go | 135 +++-- pkg/modules/chromium/events.go | 2 - pkg/modules/chromium/routes.go | 11 +- pkg/modules/chromium/routes_test.go | 20 +- pkg/modules/gc/doc.go | 3 - pkg/modules/gc/gc.go | 225 --------- pkg/modules/gc/gc_test.go | 468 ------------------ pkg/modules/libreoffice/libreoffice.go | 3 +- .../libreoffice/pdfengine/pdfengine.go | 3 +- .../libreoffice/pdfengine/pdfengine_test.go | 3 +- pkg/modules/libreoffice/routes.go | 4 +- pkg/modules/libreoffice/routes_test.go | 5 +- pkg/modules/libreoffice/uno/listener.go | 37 +- pkg/modules/libreoffice/uno/listener_test.go | 25 +- pkg/modules/libreoffice/uno/uno.go | 82 +-- pkg/modules/libreoffice/uno/uno_test.go | 380 ++++++++------ pkg/modules/logging/logging.go | 3 +- pkg/modules/logging/logging_test.go | 3 +- pkg/modules/pdfcpu/pdfcpu.go | 3 +- pkg/modules/pdfcpu/pdfcpu_test.go | 10 +- pkg/modules/pdfengines/multi.go | 3 +- pkg/modules/pdfengines/multi_test.go | 3 +- pkg/modules/pdfengines/pdfengines.go | 3 +- pkg/modules/pdfengines/pdfengines_test.go | 5 +- pkg/modules/pdfengines/routes.go | 5 +- pkg/modules/pdfengines/routes_test.go | 5 +- pkg/modules/pdftk/pdftk.go | 3 +- pkg/modules/pdftk/pdftk_test.go | 10 +- pkg/modules/prometheus/prometheus.go | 5 +- pkg/modules/prometheus/prometheus_test.go | 4 +- pkg/modules/qpdf/qpdf.go | 3 +- pkg/modules/qpdf/qpdf_test.go | 10 +- pkg/modules/webhook/client.go | 1 - pkg/modules/webhook/middleware.go | 4 +- pkg/modules/webhook/middleware_test.go | 4 +- pkg/modules/webhook/webhook.go | 29 +- pkg/modules/webhook/webhook_test.go | 29 -- pkg/standard/imports.go | 1 - test/Dockerfile | 9 +- 59 files changed, 870 insertions(+), 1383 deletions(-) create mode 100644 pkg/gotenberg/gc.go create mode 100644 pkg/gotenberg/gc_test.go delete mode 100644 pkg/modules/gc/doc.go delete mode 100644 pkg/modules/gc/gc.go delete mode 100644 pkg/modules/gc/gc_test.go diff --git a/.golangci.yml b/.golangci.yml index 902b6cec..7612ff17 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/Makefile b/Makefile index 7d4a7269..73269960 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/build/Dockerfile b/build/Dockerfile index 0a98d0d4..ca2f6925 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -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 diff --git a/cmd/gotenberg.go b/cmd/gotenberg.go index c1f62991..2b60df9a 100644 --- a/cmd/gotenberg.go +++ b/cmd/gotenberg.go @@ -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. diff --git a/cmd/gotenberg/main.go b/cmd/gotenberg/main.go index 84bb818f..86d5d11d 100644 --- a/cmd/gotenberg/main.go +++ b/cmd/gotenberg/main.go @@ -2,7 +2,6 @@ package main import ( gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd" - // Gotenberg modules. _ "github.com/gotenberg/gotenberg/v7/pkg/standard" ) diff --git a/go.mod b/go.mod index 2ed59fc1..11875b2c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 19e8d2fb..67a4f492 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/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= diff --git a/pkg/gotenberg/cmd.go b/pkg/gotenberg/cmd.go index 2e3e18d2..622c0d1a 100644 --- a/pkg/gotenberg/cmd.go +++ b/pkg/gotenberg/cmd.go @@ -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)) diff --git a/pkg/gotenberg/context.go b/pkg/gotenberg/context.go index 729d5bb5..ce0d7899 100644 --- a/pkg/gotenberg/context.go +++ b/pkg/gotenberg/context.go @@ -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) } diff --git a/pkg/gotenberg/fs.go b/pkg/gotenberg/fs.go index b54fc229..901e50f8 100644 --- a/pkg/gotenberg/fs.go +++ b/pkg/gotenberg/fs.go @@ -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) } diff --git a/pkg/gotenberg/fs_test.go b/pkg/gotenberg/fs_test.go index 5c9acb90..f074acb6 100644 --- a/pkg/gotenberg/fs_test.go +++ b/pkg/gotenberg/fs_test.go @@ -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) } } diff --git a/pkg/gotenberg/gc.go b/pkg/gotenberg/gc.go new file mode 100644 index 00000000..3de80a4c --- /dev/null +++ b/pkg/gotenberg/gc.go @@ -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) + }) +} diff --git a/pkg/gotenberg/gc_test.go b/pkg/gotenberg/gc_test.go new file mode 100644 index 00000000..0d897868 --- /dev/null +++ b/pkg/gotenberg/gc_test.go @@ -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) + } + } + }() + } +} diff --git a/pkg/modules/api/api.go b/pkg/modules/api/api.go index 35cb279b..8608aaf4 100644 --- a/pkg/modules/api/api.go +++ b/pkg/modules/api/api.go @@ -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) ) diff --git a/pkg/modules/api/api_test.go b/pkg/modules/api/api_test.go index 3900cc77..7b7427e1 100644 --- a/pkg/modules/api/api_test.go +++ b/pkg/modules/api/api_test.go @@ -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) ) diff --git a/pkg/modules/api/context.go b/pkg/modules/api/context.go index 54a6051f..9cb1303a 100644 --- a/pkg/modules/api/context.go +++ b/pkg/modules/api/context.go @@ -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) } diff --git a/pkg/modules/api/context_test.go b/pkg/modules/api/context_test.go index 21cacac3..a363159e 100644 --- a/pkg/modules/api/context_test.go +++ b/pkg/modules/api/context_test.go @@ -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) + } + }() } } diff --git a/pkg/modules/api/middlewares.go b/pkg/modules/api/middlewares.go index 6796de86..2a02ae5d 100644 --- a/pkg/modules/api/middlewares.go +++ b/pkg/modules/api/middlewares.go @@ -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() diff --git a/pkg/modules/api/middlewares_test.go b/pkg/modules/api/middlewares_test.go index 486f72d1..a59a3c19 100644 --- a/pkg/modules/api/middlewares_test.go +++ b/pkg/modules/api/middlewares_test.go @@ -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) diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index 1258a468..2b83f06b 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -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 { diff --git a/pkg/modules/chromium/chromium_test.go b/pkg/modules/chromium/chromium_test.go index a5190e1c..c47b9ab4 100644 --- a/pkg/modules/chromium/chromium_test.go +++ b/pkg/modules/chromium/chromium_test.go @@ -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) ) diff --git a/pkg/modules/chromium/events.go b/pkg/modules/chromium/events.go index e61733c2..09fc2ee1 100644 --- a/pkg/modules/chromium/events.go +++ b/pkg/modules/chromium/events.go @@ -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)) } diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 897550fd..2a9c7833 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -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) } diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index 281d0d3f..e24dbe01 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -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 }, } diff --git a/pkg/modules/gc/doc.go b/pkg/modules/gc/doc.go deleted file mode 100644 index cc923b9e..00000000 --- a/pkg/modules/gc/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package gc provides a module for removing files and directories that have -// expired. -package gc diff --git a/pkg/modules/gc/gc.go b/pkg/modules/gc/gc.go deleted file mode 100644 index 75f641e0..00000000 --- a/pkg/modules/gc/gc.go +++ /dev/null @@ -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) -) diff --git a/pkg/modules/gc/gc_test.go b/pkg/modules/gc/gc_test.go deleted file mode 100644 index adcba889..00000000 --- a/pkg/modules/gc/gc_test.go +++ /dev/null @@ -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) -) diff --git a/pkg/modules/libreoffice/libreoffice.go b/pkg/modules/libreoffice/libreoffice.go index adc70179..711987e5 100644 --- a/pkg/modules/libreoffice/libreoffice.go +++ b/pkg/modules/libreoffice/libreoffice.go @@ -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() { diff --git a/pkg/modules/libreoffice/pdfengine/pdfengine.go b/pkg/modules/libreoffice/pdfengine/pdfengine.go index c6a39ac4..8eed1f01 100644 --- a/pkg/modules/libreoffice/pdfengine/pdfengine.go +++ b/pkg/modules/libreoffice/pdfengine/pdfengine.go @@ -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() { diff --git a/pkg/modules/libreoffice/pdfengine/pdfengine_test.go b/pkg/modules/libreoffice/pdfengine/pdfengine_test.go index 46a5f636..000bc332 100644 --- a/pkg/modules/libreoffice/pdfengine/pdfengine_test.go +++ b/pkg/modules/libreoffice/pdfengine/pdfengine_test.go @@ -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) { diff --git a/pkg/modules/libreoffice/routes.go b/pkg/modules/libreoffice/routes.go index 12915761..471c0c40 100644 --- a/pkg/modules/libreoffice/routes.go +++ b/pkg/modules/libreoffice/routes.go @@ -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) } diff --git a/pkg/modules/libreoffice/routes_test.go b/pkg/modules/libreoffice/routes_test.go index db3949c2..e65b278a 100644 --- a/pkg/modules/libreoffice/routes_test.go +++ b/pkg/modules/libreoffice/routes_test.go @@ -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) { diff --git a/pkg/modules/libreoffice/uno/listener.go b/pkg/modules/libreoffice/uno/listener.go index 96a34415..d659ad50 100644 --- a/pkg/modules/libreoffice/uno/listener.go +++ b/pkg/modules/libreoffice/uno/listener.go @@ -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 } diff --git a/pkg/modules/libreoffice/uno/listener_test.go b/pkg/modules/libreoffice/uno/listener_test.go index 0cc2458e..435f2e28 100644 --- a/pkg/modules/libreoffice/uno/listener_test.go +++ b/pkg/modules/libreoffice/uno/listener_test.go @@ -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(), } diff --git a/pkg/modules/libreoffice/uno/uno.go b/pkg/modules/libreoffice/uno/uno.go index 6b7cf306..9b32c8e7 100644 --- a/pkg/modules/libreoffice/uno/uno.go +++ b/pkg/modules/libreoffice/uno/uno.go @@ -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 { diff --git a/pkg/modules/libreoffice/uno/uno_test.go b/pkg/modules/libreoffice/uno/uno_test.go index b5f3c804..5f094905 100644 --- a/pkg/modules/libreoffice/uno/uno_test.go +++ b/pkg/modules/libreoffice/uno/uno_test.go @@ -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) } }() diff --git a/pkg/modules/logging/logging.go b/pkg/modules/logging/logging.go index dda41c4b..00a030e6 100644 --- a/pkg/modules/logging/logging.go +++ b/pkg/modules/logging/logging.go @@ -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() { diff --git a/pkg/modules/logging/logging_test.go b/pkg/modules/logging/logging_test.go index b6d4fa8a..15b694fa 100644 --- a/pkg/modules/logging/logging_test.go +++ b/pkg/modules/logging/logging_test.go @@ -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) { diff --git a/pkg/modules/pdfcpu/pdfcpu.go b/pkg/modules/pdfcpu/pdfcpu.go index 2bf8c496..2700c4bb 100644 --- a/pkg/modules/pdfcpu/pdfcpu.go +++ b/pkg/modules/pdfcpu/pdfcpu.go @@ -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() { diff --git a/pkg/modules/pdfcpu/pdfcpu_test.go b/pkg/modules/pdfcpu/pdfcpu_test.go index 924eac55..af94d720 100644 --- a/pkg/modules/pdfcpu/pdfcpu_test.go +++ b/pkg/modules/pdfcpu/pdfcpu_test.go @@ -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) } }() diff --git a/pkg/modules/pdfengines/multi.go b/pkg/modules/pdfengines/multi.go index 7e163c82..8b025fb4 100644 --- a/pkg/modules/pdfengines/multi.go +++ b/pkg/modules/pdfengines/multi.go @@ -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 diff --git a/pkg/modules/pdfengines/multi_test.go b/pkg/modules/pdfengines/multi_test.go index 014d2baf..f67940e1 100644 --- a/pkg/modules/pdfengines/multi_test.go +++ b/pkg/modules/pdfengines/multi_test.go @@ -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) { diff --git a/pkg/modules/pdfengines/pdfengines.go b/pkg/modules/pdfengines/pdfengines.go index 761c0b3c..4c3780ac 100644 --- a/pkg/modules/pdfengines/pdfengines.go +++ b/pkg/modules/pdfengines/pdfengines.go @@ -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() { diff --git a/pkg/modules/pdfengines/pdfengines_test.go b/pkg/modules/pdfengines/pdfengines_test.go index b10e3345..35435264 100644 --- a/pkg/modules/pdfengines/pdfengines_test.go +++ b/pkg/modules/pdfengines/pdfengines_test.go @@ -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) } diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index c27cbf52..336feaeb 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -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) } diff --git a/pkg/modules/pdfengines/routes_test.go b/pkg/modules/pdfengines/routes_test.go index 8006b822..e552e13e 100644 --- a/pkg/modules/pdfengines/routes_test.go +++ b/pkg/modules/pdfengines/routes_test.go @@ -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) { diff --git a/pkg/modules/pdftk/pdftk.go b/pkg/modules/pdftk/pdftk.go index 064b5548..65a99aa8 100644 --- a/pkg/modules/pdftk/pdftk.go +++ b/pkg/modules/pdftk/pdftk.go @@ -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() { diff --git a/pkg/modules/pdftk/pdftk_test.go b/pkg/modules/pdftk/pdftk_test.go index f3cf8279..cd43b89e 100644 --- a/pkg/modules/pdftk/pdftk_test.go +++ b/pkg/modules/pdftk/pdftk_test.go @@ -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) } }() diff --git a/pkg/modules/prometheus/prometheus.go b/pkg/modules/prometheus/prometheus.go index ba6b268e..be0e336d 100644 --- a/pkg/modules/prometheus/prometheus.go +++ b/pkg/modules/prometheus/prometheus.go @@ -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() { diff --git a/pkg/modules/prometheus/prometheus_test.go b/pkg/modules/prometheus/prometheus_test.go index 383e12f2..021b37f6 100644 --- a/pkg/modules/prometheus/prometheus_test.go +++ b/pkg/modules/prometheus/prometheus_test.go @@ -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) } diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index b04b0685..856b444b 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -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() { diff --git a/pkg/modules/qpdf/qpdf_test.go b/pkg/modules/qpdf/qpdf_test.go index 2238b7b0..b3ab45b3 100644 --- a/pkg/modules/qpdf/qpdf_test.go +++ b/pkg/modules/qpdf/qpdf_test.go @@ -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) } }() diff --git a/pkg/modules/webhook/client.go b/pkg/modules/webhook/client.go index d1eb7d46..65e03214 100644 --- a/pkg/modules/webhook/client.go +++ b/pkg/modules/webhook/client.go @@ -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) } diff --git a/pkg/modules/webhook/middleware.go b/pkg/modules/webhook/middleware.go index 6898f649..b1d0d7f7 100644 --- a/pkg/modules/webhook/middleware.go +++ b/pkg/modules/webhook/middleware.go @@ -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. diff --git a/pkg/modules/webhook/middleware_test.go b/pkg/modules/webhook/middleware_test.go index 9089f00e..bed78c32 100644 --- a/pkg/modules/webhook/middleware_test.go +++ b/pkg/modules/webhook/middleware_test.go @@ -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) { } }() } - } diff --git a/pkg/modules/webhook/webhook.go b/pkg/modules/webhook/webhook.go index d5b271ab..c973810f 100644 --- a/pkg/modules/webhook/webhook.go +++ b/pkg/modules/webhook/webhook.go @@ -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) ) diff --git a/pkg/modules/webhook/webhook_test.go b/pkg/modules/webhook/webhook_test.go index 32fe9306..15705c1f 100644 --- a/pkg/modules/webhook/webhook_test.go +++ b/pkg/modules/webhook/webhook_test.go @@ -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) - } - } -} diff --git a/pkg/standard/imports.go b/pkg/standard/imports.go index 40bf3020..db5701ea 100644 --- a/pkg/standard/imports.go +++ b/pkg/standard/imports.go @@ -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" diff --git a/test/Dockerfile b/test/Dockerfile index 5dfc82da..0f0d710f 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -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