Compare commits

...

56 Commits

Author SHA1 Message Date
Julien Neuhart
b73207f29a fix: websocket timeout reached (fixes #524) 2022-10-17 10:50:55 +02:00
Julien Neuhart
8d8f0b55ea chore: update Go dependencies 2022-10-17 10:50:55 +02:00
Julien Neuhart
0e1e9b9cf3 fix: only linux/amd64 for Cloud Run 2022-10-03 10:25:32 +02:00
Julien Neuhart
ab4eed78af chore: update PDFtk from v3.3.2 to v3.3.3 2022-10-03 10:23:18 +02:00
Julien Neuhart
8a36c15d5c chore: update Noto emoji from v2.034 to v2.038 2022-10-03 10:22:31 +02:00
Julien Neuhart
bd895286bc chore: update Go dependencies 2022-10-03 10:21:49 +02:00
Julien Neuhart
b76990ec16 feat(chromium): new flags chromium-allow-insecure-localhost and chromium-host-resolver-rules 2022-09-01 18:04:00 +02:00
Julien Neuhart
9bc93c5cf5 chore: remove ioutil package 2022-09-01 18:04:00 +02:00
Julien Neuhart
1d8ae79f84 feat(chromium): add ReturnAsStream support 2022-09-01 18:04:00 +02:00
Julien Neuhart
4269aa2422 chore: update Go dependencies 2022-09-01 18:04:00 +02:00
Julien Neuhart
c887dc1ec8 chore: upgrade golangci-lint to version from version 1.48 to 1.49 2022-09-01 18:04:00 +02:00
Julien Neuhart
8bbacd691d chore: update Golang versionin go.mod file 2022-08-19 15:24:09 +02:00
Julien Neuhart
d8f3f85fb7 fix: replace depracted io/ioutil package with os package 2022-08-19 15:01:31 +02:00
Julien Neuhart
159c0b3350 fix: Golang Docker image for tests version 2022-08-19 14:52:36 +02:00
Julien Neuhart
45788f3235 chore: upgrade Golang dependencies 2022-08-19 14:11:36 +02:00
Julien Neuhart
4c85c6ccdf chore: upgrade from Golang 1.18 to 1.19, golangci-lint from 1.46 to 1.48 2022-08-19 14:03:32 +02:00
Julien Neuhart
08408ae005 chore: update Go dependencies 2022-06-07 15:08:04 +02:00
Julien Neuhart
d470279fb7 chore: update golangci-lint version 2022-06-07 14:43:33 +02:00
Julien Neuhart
8260458f8f Revert "fix(chromium): regex only accepts tmp folder by default (#429)"
This reverts commit 27018fa8e2.
2022-03-22 10:48:01 +01:00
Julien Neuhart
0512dd1e86 fix(tests): add -buildvcs=false as a temporary fix for go 1.18 bug (see https://github.com/golang/go/issues/51723) 2022-03-21 17:13:35 +01:00
Julien Neuhart
4972019cb9 chore: update golang-ci-lint 2022-03-21 16:46:46 +01:00
Julien Neuhart
5e615cab52 chore: upgrade go to 1.18 and update Go dependencies 2022-03-21 16:46:15 +01:00
Julien Neuhart
64ea2b6d3b fix(libreoffice): start the long-running listener on the first conversion (fixes #420) 2022-03-21 16:39:52 +01:00
Gabor Szabad
27018fa8e2 fix(chromium): regex only accepts tmp folder by default (#429) 2022-02-25 13:52:27 +01:00
Julien Neuhart
263d634801 chore: rename MockContext to ContextMock and move it to a dedicated go file 2022-02-09 16:09:48 +01:00
Julien Neuhart
c61c33f71c chore: update LICENSE 2022-02-09 10:41:50 +01:00
Julien Neuhart
bfb30b8fed chore: remove Go Report Card from README (not working) 2022-02-08 19:46:53 +01:00
Julien Neuhart
ca3e27937e chore(uno): update uno-listener-restart-threshold flag description 2022-02-08 19:46:31 +01:00
Julien Neuhart
50c34a4186 fix(uno): repare not working test 2022-02-08 19:45:39 +01:00
Julien Neuhart
84875aaf82 fix(chromium): remove local files from extra ling tags & extra script tags (closes #418) 2022-02-08 19:45:10 +01:00
Julien Neuhart
b7a8b95820 fix(uno): close tcp connexions to the LibreOffice listener, better health check that take the restart into account 2022-02-08 16:12:41 +01:00
Julien Neuhart
5cad5ca11a fix: PDFtk new binary URL 2022-02-08 11:33:55 +01:00
Julien Neuhart
7598de29dd chore: upgrade golangci-lint ot v1.44.0 2022-02-08 11:33:55 +01:00
Julien Neuhart
af6ffe67c8 chore: upgrade PDFtk to v3.3.2 2022-02-08 11:33:55 +01:00
Julien Neuhart
6944c4b3bf chore: upgrade Noto Color Emoji font to v2.034 2022-02-08 11:33:55 +01:00
Julien Neuhart
24817d5707 feat: add more PDF formats, drastically improve LibreOffice long-running instance management 2022-02-08 11:33:55 +01:00
Julien Neuhart
3d855712ee chore: update Go dependencies 2022-02-08 11:33:55 +01:00
Julien Neuhart
872263d253 feat: switch from top to htop 2022-01-05 15:46:20 +01:00
Julien Neuhart
1c491fb1c5 fix: use bullseye-backports distribution instead of sid for installing LibreOffice (fix #403) 2022-01-05 14:59:38 +01:00
Julien Neuhart
29374c579a chore: update Go dependencies 2021-12-30 17:45:16 +01:00
Julien Neuhart
206d7b6ca9 fix(chromium): typo in godoc <link> -> <script> 2021-12-27 17:49:06 +01:00
Julien Neuhart
ab6ca8c894 chore(README): improve Gotenberg description 2021-12-27 17:34:10 +01:00
Kaijia Feng
e7f5fde051 feat: add Noto CJK fonts 2021-12-27 17:21:20 +01:00
Julien Neuhart
030b358b0e fix: rework api and webhook module timeout properties (fixes #396) 2021-12-17 19:13:14 +01:00
Julien Neuhart
4b1791d35c fix(release): add noto-emoji arg 2021-12-16 17:49:48 +01:00
Julien Neuhart
b788cf823d chore: update Go dependencies 2021-12-08 17:22:24 +01:00
Julien Neuhart
11cbc0288f chore: update Go dependencies 2021-12-08 17:17:40 +01:00
Julien Neuhart
2269c80b02 feat: start a unoconv listener by default, but allow stateless mode as before with the --unoconv-disable-listener flag. Also improve the shutdown process 2021-12-08 17:17:40 +01:00
Julien Neuhart
c922cc042e chore: improve some tests 2021-12-08 17:17:40 +01:00
Julien Neuhart
9cb6f44f72 feat: improve prometheus startup message 2021-12-08 17:17:40 +01:00
Julien Neuhart
f376cd5ab1 feat: improve 'logger' field from logger 2021-12-08 15:41:08 +01:00
Julien Neuhart
8b8a02b68c fix(qpdf): typo in godoc 2021-12-08 14:39:20 +01:00
Rodrigo Delduca
5759b1336a feat: add qpdf engine (#380)
Co-authored-by: Andrei Regiani <andrei.cpp@gmail.com>
2021-12-07 19:48:41 +01:00
Julien Neuhart
76f17cb6d7 fix(pdfengines): selection of PDF engines and add a new module interface for modules that want to print system message on startup 2021-12-07 17:20:25 +01:00
Julien Neuhart
7149c421cf fix(pdfengines): improve --pdfengines-engines flag description 2021-11-28 17:05:25 +01:00
Julien Neuhart
353c599767 chore: align <p> tags from README 2021-11-28 15:49:49 +01:00
71 changed files with 5331 additions and 2928 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Run linters - name: Run linters
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
with: with:
version: v1.43 version: v1.49.0
tests: tests:
needs: needs:

View File

@@ -2,7 +2,6 @@ linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- errcheck - errcheck
- gofmt - gofmt
- goimports - goimports
@@ -13,11 +12,9 @@ linters:
- misspell - misspell
- prealloc - prealloc
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- varcheck
run: run:
deadline: 5m deadline: 5m

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021 Julien Neuhart Copyright (c) 2022 Julien Neuhart
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -5,14 +5,14 @@ help: ## Show the help
.PHONY: it .PHONY: it
it: build build-tests ## Initialize the development environment it: build build-tests ## Initialize the development environment
GOLANG_VERSION=1.17 GOLANG_VERSION=1.19
DOCKER_REPOSITORY=gotenberg DOCKER_REPOSITORY=gotenberg
GOTENBERG_VERSION=snapshot GOTENBERG_VERSION=snapshot
GOTENBERG_USER_GID=1001 GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001 GOTENBERG_USER_UID=1001
NOTO_COLOR_EMOJI_VERSION=v2.028 # See https://github.com/googlefonts/noto-emoji/releases. NOTO_COLOR_EMOJI_VERSION=v2.038 # See https://github.com/googlefonts/noto-emoji/releases.
PDFTK_VERSION=1527259628 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package. PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
GOLANGCI_LINT_VERSION=v1.43.0 # See https://github.com/golangci/golangci-lint/releases. GOLANGCI_LINT_VERSION=v1.49.0 # See https://github.com/golangci/golangci-lint/releases.
.PHONY: build .PHONY: build
build: ## Build the Gotenberg's Docker image build: ## Build the Gotenberg's Docker image
@@ -29,16 +29,16 @@ build: ## Build the Gotenberg's Docker image
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
API_PORT=3000 API_PORT=3000
API_PORT_FROM_ENV= API_PORT_FROM_ENV=
API_READ_TIMEOUT=30s API_TIMEOUT=30s
API_PROCESS_TIMEOUT=30s
API_WRITE_TIMEOUT=30s
API_ROOT_PATH=/ API_ROOT_PATH=/
API_TRACE_HEADER=Gotenberg-Trace API_TRACE_HEADER=Gotenberg-Trace
API_DISABLE_HEALTH_CHECK_LOGGING=false API_DISABLE_HEALTH_CHECK_LOGGING=false
CHROMIUM_INCOGNITO=false CHROMIUM_INCOGNITO=false
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
CHROMIUM_DISABLE_WEB_SECURITY=false CHROMIUM_DISABLE_WEB_SECURITY=false
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
CHROMIUM_HOST_RESOLVER_RULES=
CHROMIUM_PROXY_SERVER= CHROMIUM_PROXY_SERVER=
CHROMIUM_ALLOW_LIST= CHROMIUM_ALLOW_LIST=
CHROMIUM_DENY_LIST="^file:///[^tmp].*" CHROMIUM_DENY_LIST="^file:///[^tmp].*"
@@ -53,6 +53,8 @@ PROMETHEUS_NAMESPACE=gotenberg
PROMETHEUS_COLLECT_INTERVAL=1s PROMETHEUS_COLLECT_INTERVAL=1s
PROMETHEUS_DISABLE_ROUTE_LOGGING=false PROMETHEUS_DISABLE_ROUTE_LOGGING=false
PROMETHEUS_DISABLE_COLLECT=false PROMETHEUS_DISABLE_COLLECT=false
UNO_LISTENER_START_TIMEOUT=10s
UNO_LISTENER_RESTART_THRESHOLD=10
WEBHOOK_ALLOW_LIST= WEBHOOK_ALLOW_LIST=
WEBHOOK_DENY_LIST= WEBHOOK_DENY_LIST=
WEBHOOK_ERROR_ALLOW_LIST= WEBHOOK_ERROR_ALLOW_LIST=
@@ -60,6 +62,7 @@ WEBHOOK_ERROR_DENY_LIST=
WEBHOOK_MAX_RETRY=4 WEBHOOK_MAX_RETRY=4
WEBHOOK_RETRY_MIN_WAIT=1s WEBHOOK_RETRY_MIN_WAIT=1s
WEBHOOK_RETRY_MAX_WAIT=30s WEBHOOK_RETRY_MAX_WAIT=30s
WEBHOOK_CLIENT_TIMEOUT=30s
WEBHOOK_DISABLE=false WEBHOOK_DISABLE=false
.PHONY: run .PHONY: run
@@ -71,16 +74,16 @@ run: ## Start a Gotenberg container
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \ --gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
--api-port=$(API_PORT) \ --api-port=$(API_PORT) \
--api-port-from-env=$(API_PORT_FROM_ENV) \ --api-port-from-env=$(API_PORT_FROM_ENV) \
--api-read-timeout=$(API_READ_TIMEOUT) \ --api-timeout=$(API_TIMEOUT) \
--api-process-timeout=$(API_PROCESS_TIMEOUT) \
--api-write-timeout=$(API_WRITE_TIMEOUT) \
--api-root-path=$(API_ROOT_PATH) \ --api-root-path=$(API_ROOT_PATH) \
--api-trace-header=$(API_TRACE_HEADER) \ --api-trace-header=$(API_TRACE_HEADER) \
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \ --api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
--chromium-incognito=$(CHROMIUM_INCOGNITO) \ --chromium-incognito=$(CHROMIUM_INCOGNITO) \
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \ --chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
--chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \ --chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \ --chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \ --chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \ --chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \ --chromium-deny-list=$(CHROMIUM_DENY_LIST) \
@@ -95,6 +98,8 @@ run: ## Start a Gotenberg container
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \ --prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \ --prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \ --prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
--uno-listener-start-timeout=$(UNO_LISTENER_START_TIMEOUT) \
--uno-listener-restart-threshold=$(UNO_LISTENER_RESTART_THRESHOLD) \
--webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \ --webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \
--webhook-deny-list=$(WEBHOOK_DENY_LIST) \ --webhook-deny-list=$(WEBHOOK_DENY_LIST) \
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \ --webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
@@ -102,6 +107,7 @@ run: ## Start a Gotenberg container
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \ --webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
--webhook-retry-min-wait=$(WEBHOOK_RETRY_MIN_WAIT) \ --webhook-retry-min-wait=$(WEBHOOK_RETRY_MIN_WAIT) \
--webhook-retry-max-wait=$(WEBHOOK_RETRY_MAX_WAIT) \ --webhook-retry-max-wait=$(WEBHOOK_RETRY_MAX_WAIT) \
--webhook-client-timeout=$(WEBHOOK_CLIENT_TIMEOUT) \
--webhook-disable=$(WEBHOOK_DISABLE) --webhook-disable=$(WEBHOOK_DISABLE)
.PHONY: build-tests .PHONY: build-tests
@@ -145,5 +151,6 @@ release: ## Build the Gotenberg's Docker image for many platforms, then push it
$(GOTENBERG_VERSION) \ $(GOTENBERG_VERSION) \
$(GOTENBERG_USER_GID) \ $(GOTENBERG_USER_GID) \
$(GOTENBERG_USER_UID) \ $(GOTENBERG_USER_UID) \
$(NOTO_COLOR_EMOJI_VERSION) \
$(PDFTK_VERSION) \ $(PDFTK_VERSION) \
$(DOCKER_REPOSITORY) $(DOCKER_REPOSITORY)

View File

@@ -1,14 +1,14 @@
<p align="center"> <p align="center">
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" /> <img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
<h3 align="center">Gotenberg</h3> <h3 align="center">Gotenberg</h3>
<p align="center">A Docker-powered stateless API for PDF files</p> <p align="center">A Docker-powered stateless API for PDF files</p>
<p align="center"><a href="https://gotenberg.dev/docs/about">Documentation</a> &#183; 🔥 <a href="https://gotenberg.dev/docs/get-started/live-demo">Live Demo</a></p> <p align="center"><a href="https://gotenberg.dev/docs/about">Documentation</a> &#183; 🔥 <a href="https://gotenberg.dev/docs/get-started/live-demo">Live Demo</a></p>
</p> </p>
--- ---
Gotenberg provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice to convert many Gotenberg provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice for converting
documents (HTML, Markdown, Word, Excel, etc.) to PDF, transform them, merge them, and more! numerous document formats (HTML, Markdown, Word, Excel, etc.) into PDF files, and more!
## Quick Start ## Quick Start
@@ -43,4 +43,3 @@ Head to the [documentation](https://gotenberg.dev/docs/about) to learn how to in
[![Continuous Integration](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml) [![Continuous Integration](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/gotenberg/gotenberg.svg)](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7) [![Go Reference](https://pkg.go.dev/badge/github.com/gotenberg/gotenberg.svg)](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
[![Codecov](https://codecov.io/gh/gotenberg/gotenberg/branch/main/graph/badge.svg)](https://codecov.io/gh/gotenberg/gotenberg) [![Codecov](https://codecov.io/gh/gotenberg/gotenberg/branch/main/graph/badge.svg)](https://codecov.io/gh/gotenberg/gotenberg)
[![Go Report Card](https://goreportcard.com/badge/github.com/gotenberg/gotenberg)](https://goreportcard.com/report/gotenberg/gotenberg)

View File

@@ -64,10 +64,10 @@ RUN \
mkdir /home/gotenberg &&\ mkdir /home/gotenberg &&\
chown gotenberg: /home/gotenberg &&\ chown gotenberg: /home/gotenberg &&\
# Install dependencies required for the next instructions or debugging. # Install dependencies required for the next instructions or debugging.
# Note: procps for "top" command (useful when debugging processes).
# Note: tini is a helper for reaping zombie processes. # Note: tini is a helper for reaping zombie processes.
apt-get update -qq &&\ apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg procps tini python3 default-jre-headless &&\ 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 &&\
# Install fonts. # Install fonts.
# Credits: # Credits:
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile. # https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile.
@@ -103,6 +103,7 @@ RUN \
fonts-liberation \ fonts-liberation \
fonts-liberation2 \ fonts-liberation2 \
fonts-linuxlibertine \ fonts-linuxlibertine \
fonts-noto-cjk \
fonts-noto-core \ fonts-noto-core \
fonts-noto-mono \ fonts-noto-mono \
fonts-noto-ui-core \ fonts-noto-ui-core \
@@ -117,20 +118,24 @@ RUN \
# Install Google Chrome / Chromium. # Install Google Chrome / Chromium.
/tmp/install-chromium.sh &&\ /tmp/install-chromium.sh &&\
# Install LibreOffice. # Install LibreOffice.
# Note: we use the sid distribution to get the latest LibreOffice version. # Note: we use the bullseye-backports distribution to get the latest LibreOffice version.
# See https://github.com/gotenberg/gotenberg/pull/322. # See:
echo "deb https://httpredir.debian.org/debian/ sid main contrib non-free" >> /etc/apt/sources.list &&\ # https://github.com/gotenberg/gotenberg/pull/322.
# https://github.com/gotenberg/gotenberg/issues/403.
echo "deb https://httpredir.debian.org/debian/ bullseye-backports main contrib non-free" >> /etc/apt/sources.list &&\
apt-get update -qq &&\ apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t sid libreoffice &&\ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bullseye-backports libreoffice &&\
# Download unoconv (Python script). # Download unoconv (Python script).
curl -Ls https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv -o /usr/bin/unoconv &&\ curl -Ls https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv -o /usr/bin/unoconv &&\
chmod +x /usr/bin/unoconv &&\ chmod +x /usr/bin/unoconv &&\
# unoconv will look for the Python binary, which has to be at version 3. # unoconv will look for the Python binary, which has to be at version 3.
ln -s /usr/bin/python3 /usr/bin/python &&\ ln -s /usr/bin/python3 /usr/bin/python &&\
# Download PDFtk. # Download PDFtk.
# See https://github.com/gotenberg/gotenberg/pull/273. # See https://github.com/gotenberg/gotenberg/pull/273. \
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/pdftk-java/pdftk/-/jobs/$PDFTK_VERSION/artifacts/raw/build/libs/pdftk-all.jar" &&\ curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
chmod a+x /usr/bin/pdftk-all.jar &&\ chmod a+x /usr/bin/pdftk-all.jar &&\
# Download QPDF.
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
# See https://github.com/nextcloud/docker/issues/380. # See https://github.com/nextcloud/docker/issues/380.
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\ mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
# Cleanup. # Cleanup.
@@ -142,7 +147,8 @@ RUN \
chromium --version &&\ chromium --version &&\
libreoffice --version &&\ libreoffice --version &&\
unoconv --version &&\ unoconv --version &&\
pdftk --version pdftk --version &&\
qpdf --version
# Copy the Gotenberg binary from the builder stage. # Copy the Gotenberg binary from the builder stage.
COPY --from=builder /home/gotenberg /usr/bin/ COPY --from=builder /home/gotenberg /usr/bin/
@@ -151,7 +157,9 @@ COPY --from=builder /home/gotenberg /usr/bin/
ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg" ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
ENV CHROMIUM_BIN_PATH /usr/bin/chromium ENV CHROMIUM_BIN_PATH /usr/bin/chromium
ENV UNOCONV_BIN_PATH /usr/bin/unoconv ENV UNOCONV_BIN_PATH /usr/bin/unoconv
ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin
ENV PDFTK_BIN_PATH /usr/bin/pdftk ENV PDFTK_BIN_PATH /usr/bin/pdftk
ENV QPDF_BIN_PATH /usr/bin/qpdf
USER gotenberg USER gotenberg
WORKDIR /home/gotenberg WORKDIR /home/gotenberg
@@ -160,4 +168,4 @@ WORKDIR /home/gotenberg
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT [ "/usr/bin/tini", "--" ] ENTRYPOINT [ "/usr/bin/tini", "--" ]
CMD [ "gotenberg" ] CMD [ "gotenberg" ]

View File

@@ -9,6 +9,7 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
) )
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg. // See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.
@@ -33,7 +34,7 @@ var Version = "snapshot"
func Run() { func Run() {
fmt.Printf(banner, Version) fmt.Printf(banner, Version)
// Creates the roo` FlagSet and adds the modules flags to it. // Create the root FlagSet and adds the modules flags to it.
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError) fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration") fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
@@ -46,14 +47,14 @@ func Run() {
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo) fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
// Parses the flags... // Parse the flags...
err := fs.Parse(os.Args[1:]) err := fs.Parse(os.Args[1:])
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
// ...and creates a wrapper around those. // ...and create a wrapper around those.
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs} parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
// Get the graceful shutdown duration. // Get the graceful shutdown duration.
@@ -61,7 +62,7 @@ func Run() {
ctx := gotenberg.NewContext(parsedFlags, descriptors) ctx := gotenberg.NewContext(parsedFlags, descriptors)
// Starts application modules. // Start application modules.
apps, err := ctx.Modules(new(gotenberg.App)) apps, err := ctx.Modules(new(gotenberg.App))
if err != nil { if err != nil {
fmt.Printf("[FATAL] %s\n", err) fmt.Printf("[FATAL] %s\n", err)
@@ -87,7 +88,23 @@ func Run() {
fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage) fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage)
}(a.(gotenberg.App)) }(a.(gotenberg.App))
}
// Get modules that want to print system messages.
sysLoggers, err := ctx.Modules(new(gotenberg.SystemLogger))
if err != nil {
fmt.Printf("[FATAL] %s\n", err)
os.Exit(1)
}
for _, l := range sysLoggers {
go func(logger gotenberg.SystemLogger) {
id := logger.(gotenberg.Module).Descriptor().ID
for _, message := range logger.SystemMessages() {
fmt.Printf("[SYSTEM] %s: %s\n", id, message)
}
}(l.(gotenberg.SystemLogger))
} }
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
@@ -101,18 +118,40 @@ func Run() {
gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration) gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration)
defer cancel() defer cancel()
forceQuit := make(chan os.Signal, 1)
signal.Notify(forceQuit, os.Interrupt)
go func() {
// In case of force quit, cancel the context.
<-forceQuit
cancel()
}()
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration) fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
eg, _ := errgroup.WithContext(gracefulShutdownCtx)
for _, a := range apps { for _, a := range apps {
id := a.(gotenberg.Module).Descriptor().ID eg.Go(func(app gotenberg.App) func() error {
app := a.(gotenberg.App) return func() error {
id := app.(gotenberg.Module).Descriptor().ID
err = app.Stop(gracefulShutdownCtx) err = app.Stop(gracefulShutdownCtx)
if err != nil { if err != nil {
fmt.Printf("[ERROR] stopping %s: %s\n", id, err) return fmt.Errorf("stopping %s: %w", id, err)
} }
fmt.Printf("[SYSTEM] %s: application stopped\n", id) fmt.Printf("[SYSTEM] %s: application stopped\n", id)
return nil
}
}(a.(gotenberg.App)))
}
err = eg.Wait()
if err != nil {
fmt.Printf("[FATAL] %v\n", err)
os.Exit(1)
} }
os.Exit(0) os.Exit(0)

56
go.mod
View File

@@ -1,40 +1,40 @@
module github.com/gotenberg/gotenberg/v7 module github.com/gotenberg/gotenberg/v7
go 1.17 go 1.19
require ( require (
github.com/alexliesenfeld/health v0.6.0 github.com/alexliesenfeld/health v0.6.0
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/chromedp/cdproto v0.0.0-20211112223831-e508c7bf6446 github.com/chromedp/cdproto v0.0.0-20221011223153-490dc4d81f7c
github.com/chromedp/chromedp v0.7.4 github.com/chromedp/chromedp v0.8.6
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-retryablehttp v0.7.1
github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/compress v1.15.11 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect
github.com/labstack/echo/v4 v4.6.1 github.com/labstack/echo/v4 v4.9.1
github.com/labstack/gommon v0.3.1 github.com/labstack/gommon v0.4.0
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.16 github.com/microcosm-cc/bluemonday v1.0.21
github.com/nwaples/rardecode v1.1.2 // indirect github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pdfcpu/pdfcpu v0.3.12 github.com/pdfcpu/pdfcpu v0.3.13
github.com/pierrec/lz4/v4 v4.1.11 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.13.0
github.com/russross/blackfriday/v2 v2.1.0 github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/ulikunitz/xz v0.5.10 // indirect github.com/ulikunitz/xz v0.5.10 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.7.0 go.uber.org/multierr v1.8.0
go.uber.org/zap v1.19.1 go.uber.org/zap v1.23.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
golang.org/x/net v0.0.0-20210913180222-943fd674d43e golang.org/x/net v0.0.0-20221014081412-f15817d10f9b
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b golang.org/x/term v0.0.0-20220919170432-7a66f970e087
golang.org/x/text v0.3.7 golang.org/x/text v0.3.8
) )
require ( require (
@@ -52,15 +52,15 @@ require (
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

154
go.sum
View File

@@ -46,7 +46,6 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -55,11 +54,10 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20210713064928-7d28b402946a/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= github.com/chromedp/cdproto v0.0.0-20221011223153-490dc4d81f7c h1:JQvTh3Lqw0aGKFu/roG/DTMgqVCmSE6aVOegf+mMNrY=
github.com/chromedp/cdproto v0.0.0-20211112223831-e508c7bf6446 h1:uG7ssld4pEDC5O+1UoKqMb0djjJb8qjuKZVnCskGY+s= github.com/chromedp/cdproto v0.0.0-20221011223153-490dc4d81f7c/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0=
github.com/chromedp/cdproto v0.0.0-20211112223831-e508c7bf6446/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= github.com/chromedp/chromedp v0.8.6 h1:KobeeqR2dpfKSG1prS3Y6+FbffMmGC6xmAobRXA9QEQ=
github.com/chromedp/chromedp v0.7.4 h1:U+0d3WbB/Oj4mDuBOI0P7S3PJEued5UZIl5AJ3QulwU= github.com/chromedp/chromedp v0.8.6/go.mod h1:nBYHoD6YSNzrr82cIeuOzhw1Jo/s2o0QQ+ifTeoCZ+c=
github.com/chromedp/chromedp v0.7.4/go.mod h1:dBj+SXuQHznp6ZPwZeDDEBZKwclUwDLbZ0hjMialMYs=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -83,9 +81,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
@@ -94,7 +94,6 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -136,8 +135,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -160,8 +159,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
@@ -176,6 +175,7 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@@ -183,8 +183,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -196,45 +196,42 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M= github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y=
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k= github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM=
github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc= github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pdfcpu/pdfcpu v0.3.13 h1:VFon2Yo1PJt+sA57vPAeXWGLSZ7Ux3Jl4h02M0+s3dg=
github.com/pdfcpu/pdfcpu v0.3.12 h1:B+MdKisilWNSk5OCO58Z9U6H93usH73xqk6hMOaZCls= github.com/pdfcpu/pdfcpu v0.3.13/go.mod h1:UJc5xsXg0fpmjp1zOPdyYcAQArc/Zf3V0nv5URe+9fg=
github.com/pdfcpu/pdfcpu v0.3.12/go.mod h1:8XVBtVxuuIuSZL4Ez15Q4QoC+H8zeAaGnuiOEwAk8jA=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.11 h1:LVs17FAZJFOjgmJXl9Tf13WfLUvZq7/RjfEJrnwZ9OE= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.11/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -244,8 +241,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -254,14 +253,16 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -276,15 +277,14 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
@@ -292,30 +292,27 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -329,9 +326,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -350,7 +346,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -379,18 +374,18 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -400,13 +395,12 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -415,13 +409,11 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -439,32 +431,33 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -505,11 +498,9 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -586,8 +577,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -598,13 +589,12 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -31,7 +31,7 @@ func Command(logger *zap.Logger, binPath string, args ...string) Cmd {
return Cmd{ return Cmd{
ctx: nil, ctx: nil,
logger: logger.Named("cmd"), logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
process: cmd, process: cmd,
} }
} }
@@ -51,7 +51,7 @@ func CommandContext(ctx context.Context, logger *zap.Logger, binPath string, arg
return Cmd{ return Cmd{
ctx: ctx, ctx: ctx,
logger: logger.Named("cmd"), logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
process: cmd, process: cmd,
}, nil }, nil
} }
@@ -73,22 +73,37 @@ func (cmd Cmd) Start() error {
return nil return nil
} }
// Wait waits for the command to complete. It should be called when using the
// Start method, so that the command does not leak zombies.
func (cmd Cmd) Wait() error {
err := cmd.process.Wait()
if err != nil {
return fmt.Errorf("wait for unix process: %w", err)
}
return nil
}
// Exec executes the command and wait for its completion or until the context // Exec executes the command and wait for its completion or until the context
// is done. In any case, it kills the unix process and all its children. // is done. In any case, it kills the unix process and all its children.
func (cmd Cmd) Exec() error { func (cmd Cmd) Exec() (int, error) {
if cmd.ctx == nil { if cmd.ctx == nil {
return errors.New("nil context") return 10, errors.New("nil context")
} }
err := cmd.Start() err := cmd.Start()
if err != nil { if err != nil {
return fmt.Errorf("start command: %w", err) if cmd.process.ProcessState == nil {
return 131, fmt.Errorf("start command: %w", err)
}
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("start command: %w", err)
} }
errChan := make(chan error, 1) errChan := make(chan error, 1)
go func() { go func() {
errChan <- cmd.process.Wait() errChan <- cmd.Wait()
}() }()
select { select {
@@ -99,17 +114,21 @@ func (cmd Cmd) Exec() error {
} }
if err == nil { if err == nil {
return nil return 0, nil
} }
return fmt.Errorf("unix process error: %w", err) if cmd.process.ProcessState == nil {
return 131, fmt.Errorf("unix process error: %w", err)
}
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("unix process error: %w", err)
case <-cmd.ctx.Done(): case <-cmd.ctx.Done():
errProc := cmd.Kill() errProc := cmd.Kill()
if errProc != nil { if errProc != nil {
cmd.logger.Error(errProc.Error()) cmd.logger.Error(errProc.Error())
} }
return fmt.Errorf("context done: %w", cmd.ctx.Err()) return 62, fmt.Errorf("context done: %w", cmd.ctx.Err())
} }
} }

View File

@@ -12,209 +12,313 @@ func TestCommand(t *testing.T) {
cmd := Command(zap.NewNop(), "foo") cmd := Command(zap.NewNop(), "foo")
if !cmd.process.SysProcAttr.Setpgid { if !cmd.process.SysProcAttr.Setpgid {
t.Error("expected Setpgid to be true") t.Error("expected cmd.process.SysProcAttr.Setpgid to be true")
} }
} }
func TestCommandContext(t *testing.T) { func TestCommandContext(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx context.Context name string
expectErr bool ctx context.Context
expectCommandContextErr bool
}{ }{
{ {
ctx: nil, name: "nominal behavior",
expectErr: true, ctx: context.Background(),
}, },
{ {
ctx: context.TODO(), name: "nil context",
expectCommandContextErr: true,
}, },
} { }
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
if err == nil && !cmd.process.SysProcAttr.Setpgid { for _, tc := range tests {
t.Fatalf("test %d: expected Setpgid to be true", i) t.Run(tc.name, func(t *testing.T) {
} cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
if tc.expectErr && err == nil { if err == nil && !cmd.process.SysProcAttr.Setpgid {
t.Errorf("test %d: expected error but got: %v", i, err) t.Fatal("expected cmd.process.SysProcAttr.Setpgid to be true")
} }
if !tc.expectErr && err != nil { if tc.expectCommandContextErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Error("expected error from CommandContext(), but got none")
} }
if !tc.expectCommandContextErr && err != nil {
t.Errorf("expected no error from CommandContext(), but got: %v", err)
}
})
} }
} }
func TestCmd_Start(t *testing.T) { func TestCmd_Start(t *testing.T) {
for i, tc := range []struct { tests := []struct {
cmd Cmd name string
expectErr bool cmd Cmd
expectStartErr bool
}{ }{
{ {
cmd: Command(zap.NewNop(), "foo"), name: "nominal behavior",
expectErr: true, cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
}, },
{ {
cmd: Command(zap.NewNop(), "echo", "Hello", "World"), name: "start error",
cmd: Command(zap.NewNop(), "foo"),
expectStartErr: true,
}, },
} { }
err := tc.cmd.Start()
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} err := tc.cmd.Start()
if !tc.expectErr && err != nil { if tc.expectStartErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Error("expected error from cmd.Start(), but got none")
} }
if !tc.expectStartErr && err != nil {
t.Errorf("expected no error from cmd.Start(), but got: %v", err)
}
})
}
}
func TestCmd_Wait(t *testing.T) {
tests := []struct {
name string
cmd Cmd
expectWaitErr bool
}{
{
name: "nominal behavior",
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
err := cmd.Start()
if err != nil {
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
}
return cmd
}(),
},
{
name: "wait error",
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
err := cmd.Start()
if err != nil {
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
}
err = cmd.Kill()
if err != nil {
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
}
return cmd
}(),
expectWaitErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.Wait()
if tc.expectWaitErr && err == nil {
t.Error("expected error from cmd.Wait(), but got none")
}
if !tc.expectWaitErr && err != nil {
t.Errorf("expected no error from cmd.Wait(), but got: %v", err)
}
})
} }
} }
func TestCmd_Exec(t *testing.T) { func TestCmd_Exec(t *testing.T) {
for i, tc := range []struct { tests := []struct {
cmd Cmd name string
timeout time.Duration cmd Cmd
expectErr bool timeout time.Duration
expectExecErr bool
}{ }{
{ {
cmd: Command(zap.NewNop(), "foo"), name: "nominal behavior",
expectErr: true, cmd: func() Cmd {
cmd, err := CommandContext(context.Background(), zap.NewNop(), "echo", "Hello", "World")
if err != nil {
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
}
return cmd
}(),
}, },
{ {
cmd: Command(zap.NewNop(), "foo"), name: "nil context",
timeout: time.Duration(5) * time.Second, cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
expectErr: true, expectExecErr: true,
}, },
{ {
cmd: Command(zap.NewNop(), "echo", "Hello", "World"), name: "start error",
timeout: time.Duration(5) * time.Second, cmd: func() Cmd {
cmd, err := CommandContext(context.Background(), zap.NewNop(), "foo")
if err != nil {
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
}
return cmd
}(),
expectExecErr: true,
}, },
{ {
cmd: Command(zap.NewNop(), "sleep", "3"), name: "context done",
timeout: time.Duration(2) * time.Second, cmd: Command(zap.NewNop(), "sleep", "2"),
expectErr: true, timeout: time.Duration(1) * time.Second,
expectExecErr: true,
}, },
} { }
if tc.timeout > 0 {
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
defer cancel()
tc.cmd.ctx = ctx for _, tc := range tests {
} t.Run(tc.name, func(t *testing.T) {
if tc.timeout > 0 {
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
defer cancel()
err := tc.cmd.Exec() tc.cmd.ctx = ctx
}
if tc.expectErr && err == nil { _, err := tc.cmd.Exec()
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil { if tc.expectExecErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Error("expected error from cmd.Exec(), but got none")
} }
if !tc.expectExecErr && err != nil {
t.Errorf("expected no error from cmd.Exec(), but got: %v", err)
}
})
} }
} }
func TestCmd_pipeOutput(t *testing.T) { func TestCmd_pipeOutput(t *testing.T) {
for i, tc := range []struct { tests := []struct {
cmd Cmd name string
run bool cmd Cmd
expectErr bool run bool
expectPipeOutputErr bool
}{ }{
{ {
cmd: Command(zap.NewNop(), "echo", "Hello", "World"), name: "nominal behavior",
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
run: true,
}, },
{ {
name: "no debug, no pipe",
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
},
{
name: "stdout already piped",
cmd: func() Cmd { cmd: func() Cmd {
cmd := Command(zap.NewExample(), "echo", "Hello", "World") cmd := Command(zap.NewExample(), "echo", "Hello", "World")
_, err := cmd.process.StdoutPipe() _, err := cmd.process.StdoutPipe()
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error from cmd.process.StdoutPipe(), but got: %v", err)
} }
return cmd return cmd
}(), }(),
expectErr: true, expectPipeOutputErr: true,
}, },
{ {
name: "stderr already piped",
cmd: func() Cmd { cmd: func() Cmd {
cmd := Command(zap.NewExample(), "echo", "Hello", "World") cmd := Command(zap.NewExample(), "echo", "Hello", "World")
_, err := cmd.process.StderrPipe()
_, err := cmd.process.StderrPipe()
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error from cmd.process.StderrPipe(), but got: %v", err)
} }
return cmd return cmd
}(), }(),
expectErr: true, expectPipeOutputErr: true,
}, },
{ }
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
run: true,
},
} {
err := tc.cmd.pipeOutput()
if tc.run { for _, tc := range tests {
errStart := tc.cmd.process.Start() t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.pipeOutput()
if errStart != nil { if tc.run {
t.Fatalf("test %d: expected no error but got: %v", i, err) errStart := tc.cmd.process.Start()
if errStart != nil {
t.Fatalf("expected no error from tc.cmd.process.Start(), but got: %v", errStart)
}
} }
}
if tc.expectErr && err == nil { if tc.expectPipeOutputErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Error("expected error from cmd.pipeOutput(), but got none")
} }
if !tc.expectErr && err != nil { if !tc.expectPipeOutputErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("expected no error from cmd.pipeOutput(), but got: %v", err)
} }
})
} }
} }
func TestCmd_Kill(t *testing.T) { func TestCmd_Kill(t *testing.T) {
for i, tc := range []struct { tests := []struct {
cmd Cmd name string
expectErr bool cmd Cmd
}{ }{
{ {
cmd: Cmd{logger: zap.NewNop()}, name: "nominal behavior",
},
{
cmd: func() Cmd { cmd: func() Cmd {
cmd := Command(zap.NewNop(), "sleep", "60") cmd := Command(zap.NewNop(), "sleep", "60")
err := cmd.process.Start()
err := cmd.process.Start()
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
} }
return cmd return cmd
}(), }(),
}, },
{ {
name: "no process",
cmd: Cmd{logger: zap.NewNop()},
},
{
name: "process already killed",
cmd: func() Cmd { cmd: func() Cmd {
cmd := Command(zap.NewNop(), "echo", "Hello", "World") cmd := Command(zap.NewNop(), "sleep", "60")
err := cmd.process.Run()
err := cmd.process.Start()
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
}
err = cmd.Kill()
if err != nil {
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
} }
return cmd return cmd
}(), }(),
}, },
} { }
err := tc.cmd.Kill()
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} err := tc.cmd.Kill()
if err != nil {
if !tc.expectErr && err != nil { t.Errorf("expected no error from cmd.Kill(), but got: %v", err)
t.Errorf("test %d: expected no error but got: %v", i, err) }
} })
} }
} }

66
pkg/gotenberg/mocks.go Normal file
View File

@@ -0,0 +1,66 @@
package gotenberg
import (
"context"
"go.uber.org/zap"
)
// ModuleMock is a mock for the Module interface.
type ModuleMock struct {
DescriptorMock func() ModuleDescriptor
}
func (mod ModuleMock) Descriptor() ModuleDescriptor {
return mod.DescriptorMock()
}
// ValidatorMock is a mock for the Validator interface.
type ValidatorMock struct {
ValidateMock func() error
}
func (mod ValidatorMock) Validate() error {
return mod.ValidateMock()
}
// PDFEngineMock is a mock for the PDFEngine interface.
type PDFEngineMock struct {
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
ConvertMock func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
}
func (engine PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
}
func (engine PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return engine.ConvertMock(ctx, logger, format, inputPath, outputPath)
}
// PDFEngineProviderMock is a mock for the PDFEngineProvider interface.
type PDFEngineProviderMock struct {
PDFEngineMock func() (PDFEngine, error)
}
func (provider PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
return provider.PDFEngineMock()
}
// LoggerProviderMock is a mock for the LoggerProvider interface.
type LoggerProviderMock struct {
LoggerMock func(mod Module) (*zap.Logger, error)
}
func (provider LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
return provider.LoggerMock(mod)
}
// Interface guards.
var (
_ Module = (*ModuleMock)(nil)
_ Validator = (*ValidatorMock)(nil)
_ PDFEngine = (*PDFEngineMock)(nil)
_ PDFEngineProvider = (*PDFEngineProviderMock)(nil)
_ LoggerProvider = (*LoggerProviderMock)(nil)
)

View File

@@ -0,0 +1,82 @@
package gotenberg
import (
"context"
"testing"
"go.uber.org/zap"
)
func TestModuleMock(t *testing.T) {
mock := ModuleMock{
DescriptorMock: func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module {
return nil
}}
},
}
if mock.Descriptor().ID != "foo" {
t.Errorf("expected ID '%s' from mock.Descriptor(), but got '%s'", "foo", mock.Descriptor().ID)
}
}
func TestValidatorMock(t *testing.T) {
mock := ValidatorMock{
ValidateMock: func() error {
return nil
},
}
err := mock.Validate()
if err != nil {
t.Errorf("expected no error from mock.Validate(), but got: %v", err)
}
}
func TestPDFEngineMock(t *testing.T) {
mock := PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
}
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
if err != nil {
t.Errorf("expected no error from mock.Merge(), but got: %v", err)
}
err = mock.Convert(context.Background(), zap.NewNop(), "", "", "")
if err != nil {
t.Errorf("expected no error from mock.Convert(), but got: %v", err)
}
}
func TestPDFEngineProvider(t *testing.T) {
mock := PDFEngineProviderMock{
PDFEngineMock: func() (PDFEngine, error) {
return PDFEngineMock{}, nil
},
}
_, err := mock.PDFEngine()
if err != nil {
t.Errorf("expected no error from mock.PDFEngine(), but got: %v", err)
}
}
func TestLoggerProviderMock(t *testing.T) {
mock := LoggerProviderMock{
LoggerMock: func(mod Module) (*zap.Logger, error) {
return nil, nil
},
}
_, err := mock.Logger(ModuleMock{})
if err != nil {
t.Errorf("expected no error from mock.Logger(), but got: %v", err)
}
}

View File

@@ -69,6 +69,12 @@ type App interface {
Stop(ctx context.Context) error Stop(ctx context.Context) error
} }
// SystemLogger is a module interface for modules which want to display
// messages on startup.
type SystemLogger interface {
SystemMessages() []string
}
// MustRegisterModule registers a module. // MustRegisterModule registers a module.
// //
// To register a module, create an init() method in the module main go file: // To register a module, create an init() method in the module main go file:

View File

@@ -30,8 +30,8 @@ func init() {
type API struct { type API struct {
port int port int
readTimeout time.Duration readTimeout time.Duration
processTimeout time.Duration
writeTimeout time.Duration writeTimeout time.Duration
timeout time.Duration
rootPath string rootPath string
traceHeader string traceHeader string
disableHealthCheckLogging bool disableHealthCheckLogging bool
@@ -102,28 +102,28 @@ const (
// Middleware is a middleware which can be added to the API's middlewares // Middleware is a middleware which can be added to the API's middlewares
// chain. // chain.
// //
// middleware := Middleware{ // middleware := Middleware{
// Handler: func() echo.MiddlewareFunc { // Handler: func() echo.MiddlewareFunc {
// return func(next echo.HandlerFunc) echo.HandlerFunc { // return func(next echo.HandlerFunc) echo.HandlerFunc {
// return func(c echo.Context) error { // return func(c echo.Context) error {
// rootPath := c.Get("rootPath").(string) // rootPath := c.Get("rootPath").(string)
// healthURI := fmt.Sprintf("%shealth", rootPath) // healthURI := fmt.Sprintf("%shealth", rootPath)
// //
// // Skip the middleware if health check URI. // // Skip the middleware if health check URI.
// if c.Request().RequestURI == healthURI { // if c.Request().RequestURI == healthURI {
// // Call the next middleware in the chain. // // Call the next middleware in the chain.
// return next(c) // return next(c)
// } // }
// //
// // Your middleware process. // // Your middleware process.
// // ... // // ...
// //
// // Call the next middleware in the chain. // // Call the next middleware in the chain.
// return next(c) // return next(c)
// } // }
// } // }
// }(), // }(),
// } // }
type Middleware struct { type Middleware struct {
// Stack tells in which stack the middleware should be located. // Stack tells in which stack the middleware should be located.
// Default to DefaultStack. // Default to DefaultStack.
@@ -166,10 +166,20 @@ func (API) Descriptor() gotenberg.ModuleDescriptor {
fs.Duration("api-read-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to read a complete request, including the body") fs.Duration("api-read-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to read a complete request, including the body")
fs.Duration("api-process-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to process a request") fs.Duration("api-process-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to process a request")
fs.Duration("api-write-timeout", time.Duration(30)*time.Second, "Set the maximum duration before timing out writes of the response") fs.Duration("api-write-timeout", time.Duration(30)*time.Second, "Set the maximum duration before timing out writes of the response")
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths") fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests") fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging") fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
var err error
err = multierr.Append(err, fs.MarkDeprecated("api-read-timeout", "use api-timeout instead"))
err = multierr.Append(err, fs.MarkDeprecated("api-process-timeout", "use api-timeout instead"))
err = multierr.Append(err, fs.MarkDeprecated("api-write-timeout", "use api-timeout instead"))
if err != nil {
panic(fmt.Errorf("create deprecated flags for the api module: %v", err))
}
return fs return fs
}(), }(),
New: func() gotenberg.Module { return new(API) }, New: func() gotenberg.Module { return new(API) },
@@ -180,9 +190,9 @@ func (API) Descriptor() gotenberg.ModuleDescriptor {
func (a *API) Provision(ctx *gotenberg.Context) error { func (a *API) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags() flags := ctx.ParsedFlags()
a.port = flags.MustInt("api-port") a.port = flags.MustInt("api-port")
a.readTimeout = flags.MustDuration("api-read-timeout") a.readTimeout = flags.MustDeprecatedDuration("api-read-timeout", "api-timeout")
a.processTimeout = flags.MustDuration("api-process-timeout") a.writeTimeout = flags.MustDeprecatedDuration("api-write-timeout", "api-timeout")
a.writeTimeout = flags.MustDuration("api-write-timeout") a.timeout = flags.MustDeprecatedDuration("api-process-timeout", "api-timeout")
a.rootPath = flags.MustString("api-root-path") a.rootPath = flags.MustString("api-root-path")
a.traceHeader = flags.MustString("api-trace-header") a.traceHeader = flags.MustString("api-trace-header")
a.disableHealthCheckLogging = flags.MustBool("api-disable-health-check-logging") a.disableHealthCheckLogging = flags.MustBool("api-disable-health-check-logging")
@@ -274,7 +284,7 @@ func (a *API) Provision(ctx *gotenberg.Context) error {
} }
// Grace duration. // Grace duration.
a.gcGraceDuration = a.readTimeout + a.processTimeout + a.writeTimeout a.gcGraceDuration = a.timeout
mods, err = ctx.Modules(new(GarbageCollectorGraceDurationIncrementer)) mods, err = ctx.Modules(new(GarbageCollectorGraceDurationIncrementer))
if err != nil { if err != nil {
@@ -378,7 +388,9 @@ func (a *API) Start() error {
a.srv.HideBanner = true a.srv.HideBanner = true
a.srv.HidePort = true a.srv.HidePort = true
a.srv.Server.ReadTimeout = a.readTimeout a.srv.Server.ReadTimeout = a.readTimeout
a.srv.Server.WriteTimeout = a.writeTimeout a.srv.Server.IdleTimeout = a.timeout
// See https://github.com/gotenberg/gotenberg/issues/396.
a.srv.Server.WriteTimeout = a.writeTimeout + a.writeTimeout
a.srv.HTTPErrorHandler = httpErrorHandler() a.srv.HTTPErrorHandler = httpErrorHandler()
// Let's prepare the modules' routes. // Let's prepare the modules' routes.
@@ -402,7 +414,6 @@ func (a *API) Start() error {
latencyMiddleware(), latencyMiddleware(),
rootPathMiddleware(a.rootPath), rootPathMiddleware(a.rootPath),
traceMiddleware(a.traceHeader), traceMiddleware(a.traceHeader),
timeoutsMiddleware(a.readTimeout, a.processTimeout, a.writeTimeout),
loggerMiddleware(a.logger, disableLoggingForPaths), loggerMiddleware(a.logger, disableLoggingForPaths),
) )
@@ -419,14 +430,14 @@ func (a *API) Start() error {
} }
} }
hardTimeout := a.processTimeout + (time.Duration(5) * time.Second) hardTimeout := a.timeout + (time.Duration(5) * time.Second)
// Add the modules' routes and their specific middlewares. // Add the modules' routes and their specific middlewares.
for _, route := range a.routes { for _, route := range a.routes {
var middlewares []echo.MiddlewareFunc var middlewares []echo.MiddlewareFunc
if route.IsMultipart { if route.IsMultipart {
middlewares = append(middlewares, contextMiddleware(a.processTimeout)) middlewares = append(middlewares, contextMiddleware(a.timeout))
for _, externalMultipartMiddleware := range externalMultipartMiddlewares { for _, externalMultipartMiddleware := range externalMultipartMiddlewares {
middlewares = append(middlewares, externalMultipartMiddleware.Handler) middlewares = append(middlewares, externalMultipartMiddleware.Handler)
@@ -447,7 +458,7 @@ func (a *API) Start() error {
a.srv.GET( a.srv.GET(
fmt.Sprintf("%s%s", a.rootPath, "health"), fmt.Sprintf("%s%s", a.rootPath, "health"),
func() echo.HandlerFunc { func() echo.HandlerFunc {
checks := append(a.healthChecks, health.WithTimeout(a.processTimeout)) checks := append(a.healthChecks, health.WithTimeout(a.timeout))
checker := health.NewChecker(checks...) checker := health.NewChecker(checks...)
return echo.WrapHandler(health.NewHandler(checker)) return echo.WrapHandler(health.NewHandler(checker))

View File

@@ -386,7 +386,7 @@ func TestAPI_Provision(t *testing.T) {
}, },
) )
}(), }(),
expectGraceDuration: time.Duration(93) * time.Second, expectGraceDuration: time.Duration(33) * time.Second,
expectErr: true, expectErr: true,
}, },
{ {

View File

@@ -276,71 +276,3 @@ func (ctx Context) OutputFilename(outputPath string) string {
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath)) return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
} }
// MockContext is a helper for tests.
//
// ctx := &api.MockContext{Context: &api.Context{}}
type MockContext struct {
*Context
}
// SetDirPath sets the context's working directory path.
//
// ctx := &api.MockContext{Context: &api.Context{}}
// ctx.SetDirPath("/foo")
func (ctx *MockContext) SetDirPath(path string) {
ctx.dirPath = path
}
// SetValues sets the values.
//
// ctx := &api.MockContext{Context: &api.Context{}}
// ctx.SetValues(map[string][]string{
// "url": {
// "foo",
// },
// })
func (ctx *MockContext) SetValues(values map[string][]string) {
ctx.values = values
}
// SetFiles sets the files.
//
// ctx := &api.MockContext{Context: &api.Context{}}
// ctx.SetFiles(map[string]string{
// "foo": "/foo",
// })
func (ctx *MockContext) SetFiles(files map[string]string) {
ctx.files = files
}
// SetCancelled sets if the context is cancelled or not.
//
// ctx := &api.MockContext{Context: &api.Context{}}
// ctx.SetCancelled(true)
func (ctx *MockContext) SetCancelled(cancelled bool) {
ctx.cancelled = cancelled
}
// OutputPaths returns the registered output paths.
// ctx := &api.MockContext{Context: &api.Context{}}
// outputPaths := ctx.OutputPaths()
func (ctx MockContext) OutputPaths() []string {
return ctx.outputPaths
}
// SetLogger sets the logger.
//
// ctx := &api.MockContext{Context: &api.Context{}}
// ctx.SetLogger(zap.NewNop())
func (ctx *MockContext) SetLogger(logger *zap.Logger) {
ctx.logger = logger
}
// SetEchoContext sets the echo.Context.
//
// ctx := &api.MockContext{Context: &api.Context{}}
// ctx.setEchoContext(c)
func (ctx *MockContext) SetEchoContext(c echo.Context) {
ctx.Context.echoCtx = c
}

View File

@@ -334,99 +334,3 @@ func TestContext_OutputFilename(t *testing.T) {
} }
} }
} }
func TestMockContext_SetDirPath(t *testing.T) {
mock := &MockContext{&Context{}}
mock.SetDirPath("/foo")
actual := mock.dirPath
expect := "/foo"
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestMockContext_SetValues(t *testing.T) {
mock := &MockContext{&Context{}}
mock.SetValues(map[string][]string{
"foo": {"foo"},
})
actual := mock.values
expect := map[string][]string{
"foo": {"foo"},
}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestMockContext_SetFiles(t *testing.T) {
mock := &MockContext{&Context{}}
mock.SetFiles(map[string]string{
"foo": "/foo",
})
actual := mock.files
expect := map[string]string{
"foo": "/foo",
}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestMockContext_SetCancelled(t *testing.T) {
mock := &MockContext{&Context{}}
mock.SetCancelled(true)
actual := mock.cancelled
if !actual {
t.Errorf("expected %t but got %t", true, actual)
}
}
func TestMockContext_OutputPaths(t *testing.T) {
mock := MockContext{
&Context{
outputPaths: []string{"/foo"},
},
}
actual := mock.OutputPaths()
expect := []string{"/foo"}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestMockContext_SetLogger(t *testing.T) {
mock := MockContext{&Context{}}
expect := zap.NewNop()
mock.SetLogger(expect)
actual := mock.logger
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestMockContext_SetEchoContext(t *testing.T) {
mock := MockContext{&Context{}}
expect := echo.New().NewContext(nil, nil)
mock.SetEchoContext(expect)
actual := mock.echoCtx
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}

View File

@@ -52,15 +52,15 @@ func (w sentinelWrappedError) HTTPError() (int, string) {
// will be displayed in a log, while the SentinelHTTPError will be sent in the // will be displayed in a log, while the SentinelHTTPError will be sent in the
// response. // response.
// //
// return api.WrapError( // return api.WrapError(
// // This first error will be logged. // // This first error will be logged.
// fmt.Errorf("my action: %w", err), // fmt.Errorf("my action: %w", err),
// // The HTTP error will be sent as a response. // // The HTTP error will be sent as a response.
// api.NewSentinelHTTPError( // api.NewSentinelHTTPError(
// http.StatusForbidden, // http.StatusForbidden,
// "Hey, you did something wrong!" // "Hey, you did something wrong!"
// ), // ),
// ) // )
func WrapError(err error, sentinel SentinelHTTPError) error { func WrapError(err error, sentinel SentinelHTTPError) error {
return sentinelWrappedError{ return sentinelWrappedError{
error: err, error: err,

View File

@@ -2,8 +2,8 @@ package api
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv" "strconv"
@@ -16,7 +16,7 @@ import (
// FormData is a helper for validating and hydrating values from a // FormData is a helper for validating and hydrating values from a
// "multipart/form-data" request. // "multipart/form-data" request.
// //
// form := ctx.FormData() // form := ctx.FormData()
type FormData struct { type FormData struct {
values map[string][]string values map[string][]string
files map[string]string files map[string]string
@@ -27,11 +27,11 @@ type FormData struct {
// SentinelHTTPError (status code 400, errors' details as message) wrapped // SentinelHTTPError (status code 400, errors' details as message) wrapped
// inside. // inside.
// //
// var foo string // var foo string
// //
// err := ctx.FormData(). // err := ctx.FormData().
// MandatoryString("foo", &foo, "bar"). // MandatoryString("foo", &foo, "bar").
// Validate() // Validate()
func (form FormData) Validate() error { func (form FormData) Validate() error {
if form.errors == nil { if form.errors == nil {
return nil return nil
@@ -45,9 +45,9 @@ func (form FormData) Validate() error {
// String binds a form data value to a string variable. // String binds a form data value to a string variable.
// //
// var foo string // var foo string
// //
// ctx.FormData().String("foo", &foo, "bar") // ctx.FormData().String("foo", &foo, "bar")
func (form *FormData) String(key string, target *string, defaultValue string) *FormData { func (form *FormData) String(key string, target *string, defaultValue string) *FormData {
return form.mustValue(key, target, defaultValue) return form.mustValue(key, target, defaultValue)
} }
@@ -55,9 +55,9 @@ func (form *FormData) String(key string, target *string, defaultValue string) *F
// MandatoryString binds a form data value to a string variable. It populates // MandatoryString binds a form data value to a string variable. It populates
// an error if the value is empty or the "key" does not exist. // an error if the value is empty or the "key" does not exist.
// //
// var foo string // var foo string
// //
// ctx.FormData().MandatoryString("foo", &foo) // ctx.FormData().MandatoryString("foo", &foo)
func (form *FormData) MandatoryString(key string, target *string) *FormData { func (form *FormData) MandatoryString(key string, target *string) *FormData {
return form.mustMandatoryValue(key, target) return form.mustMandatoryValue(key, target)
} }
@@ -65,9 +65,9 @@ func (form *FormData) MandatoryString(key string, target *string) *FormData {
// Bool binds a form data value to a bool variable. It populates an error if // Bool binds a form data value to a bool variable. It populates an error if
// the value is not bool. // the value is not bool.
// //
// var foo bool // var foo bool
// //
// ctx.FormData().Bool("foo", &foo, true) // ctx.FormData().Bool("foo", &foo, true)
func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormData { func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormData {
return form.mustValue(key, target, defaultValue) return form.mustValue(key, target, defaultValue)
} }
@@ -75,9 +75,9 @@ func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormDat
// MandatoryBool binds a form data value to a bool variable. It populates an // MandatoryBool binds a form data value to a bool variable. It populates an
// error if the value is not bool, is empty, or the "key" does not exist. // error if the value is not bool, is empty, or the "key" does not exist.
// //
// var foo bool // var foo bool
// //
// ctx.FormData().MandatoryBool("foo", &foo) // ctx.FormData().MandatoryBool("foo", &foo)
func (form *FormData) MandatoryBool(key string, target *bool) *FormData { func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
return form.mustMandatoryValue(key, target) return form.mustMandatoryValue(key, target)
} }
@@ -85,9 +85,9 @@ func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
// Int binds a form data value to an int variable. It populates an error if the // Int binds a form data value to an int variable. It populates an error if the
// value is not int. // value is not int.
// //
// var foo int // var foo int
// //
// ctx.FormData().Int("foo", &foo, 2) // ctx.FormData().Int("foo", &foo, 2)
func (form *FormData) Int(key string, target *int, defaultValue int) *FormData { func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
return form.mustValue(key, target, defaultValue) return form.mustValue(key, target, defaultValue)
} }
@@ -95,9 +95,9 @@ func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
// MandatoryInt binds a form data value to an int variable. It populates an // MandatoryInt binds a form data value to an int variable. It populates an
// error if the value is not int, is empty, or the "key" does not exist. // error if the value is not int, is empty, or the "key" does not exist.
// //
// var foo int // var foo int
// //
// ctx.FormData().MandatoryInt("foo", &foo) // ctx.FormData().MandatoryInt("foo", &foo)
func (form *FormData) MandatoryInt(key string, target *int) *FormData { func (form *FormData) MandatoryInt(key string, target *int) *FormData {
return form.mustMandatoryValue(key, target) return form.mustMandatoryValue(key, target)
} }
@@ -105,9 +105,9 @@ func (form *FormData) MandatoryInt(key string, target *int) *FormData {
// Float64 binds a form data value to a float64 variable. It populates an error // Float64 binds a form data value to a float64 variable. It populates an error
// if the value is not float64. // if the value is not float64.
// //
// var foo float64 // var foo float64
// //
// ctx.FormData().Float64("foo", &foo, 2.0) // ctx.FormData().Float64("foo", &foo, 2.0)
func (form *FormData) Float64(key string, target *float64, defaultValue float64) *FormData { func (form *FormData) Float64(key string, target *float64, defaultValue float64) *FormData {
return form.mustValue(key, target, defaultValue) return form.mustValue(key, target, defaultValue)
} }
@@ -115,9 +115,9 @@ func (form *FormData) Float64(key string, target *float64, defaultValue float64)
// MandatoryFloat64 binds a form data value to a float64 variable. It populates // MandatoryFloat64 binds a form data value to a float64 variable. It populates
// an error if the is not float64, is empty, or the "key" does not exist. // an error if the is not float64, is empty, or the "key" does not exist.
// //
// var foo float64 // var foo float64
// //
// ctx.FormData().MandatoryFloat64("foo", &foo) // ctx.FormData().MandatoryFloat64("foo", &foo)
func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData { func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData {
return form.mustMandatoryValue(key, target) return form.mustMandatoryValue(key, target)
} }
@@ -125,9 +125,9 @@ func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData {
// Duration binds a form data value to a time.Duration variable. It populates // Duration binds a form data value to a time.Duration variable. It populates
// an error if the form data value is not time.Duration. // an error if the form data value is not time.Duration.
// //
// var foo time.Duration // var foo time.Duration
// //
// ctx.FormData().Duration("foo", &foo, time.Duration(2) * time.Second) // ctx.FormData().Duration("foo", &foo, time.Duration(2) * time.Second)
func (form *FormData) Duration(key string, target *time.Duration, defaultValue time.Duration) *FormData { func (form *FormData) Duration(key string, target *time.Duration, defaultValue time.Duration) *FormData {
return form.mustValue(key, target, defaultValue) return form.mustValue(key, target, defaultValue)
} }
@@ -136,31 +136,31 @@ func (form *FormData) Duration(key string, target *time.Duration, defaultValue t
// populates an error if the value is not time.Duration, is empty, or the "key" // populates an error if the value is not time.Duration, is empty, or the "key"
// does not exist. // does not exist.
// //
// var foo time.Duration // var foo time.Duration
// //
// ctx.FormData().MandatoryDuration("foo", &foo) // ctx.FormData().MandatoryDuration("foo", &foo)
func (form *FormData) MandatoryDuration(key string, target *time.Duration) *FormData { func (form *FormData) MandatoryDuration(key string, target *time.Duration) *FormData {
return form.mustMandatoryValue(key, target) return form.mustMandatoryValue(key, target)
} }
// Custom helps to define a custom binding function for a form data value. // Custom helps to define a custom binding function for a form data value.
// //
// var foo map[string]string // var foo map[string]string
// //
// ctx.FormData().Custom("foo", func(value string) error { // ctx.FormData().Custom("foo", func(value string) error {
// if value == "" { // if value == "" {
// foo = "bar" // foo = "bar"
// //
// return nil // return nil
// } // }
// //
// err := json.Unmarshal([]byte(value), &foo) // err := json.Unmarshal([]byte(value), &foo)
// if err != nil { // if err != nil {
// return fmt.Errorf("unmarshal foo: %w", err) // return fmt.Errorf("unmarshal foo: %w", err)
// } // }
// //
// return nil // return nil
// }) // })
func (form *FormData) Custom(key string, assign func(value string) error) *FormData { func (form *FormData) Custom(key string, assign func(value string) error) *FormData {
var value string var value string
form.mustValue(key, &value, "") form.mustValue(key, &value, "")
@@ -179,16 +179,16 @@ func (form *FormData) Custom(key string, assign func(value string) error) *FormD
// value. It populates an error if the value is empty or the "key" does not // value. It populates an error if the value is empty or the "key" does not
// exist. // exist.
// //
// var foo map[string]string // var foo map[string]string
// //
// ctx.FormData().MandatoryCustom("foo", func(value string) error { // ctx.FormData().MandatoryCustom("foo", func(value string) error {
// err := json.Unmarshal([]byte(value), &foo) // err := json.Unmarshal([]byte(value), &foo)
// if err != nil { // if err != nil {
// return fmt.Errorf("unmarshal foo: %w", err) // return fmt.Errorf("unmarshal foo: %w", err)
// } // }
// //
// return nil // return nil
// }) // })
func (form *FormData) MandatoryCustom(key string, assign func(value string) error) *FormData { func (form *FormData) MandatoryCustom(key string, assign func(value string) error) *FormData {
var value string var value string
form.mustMandatoryValue(key, &value) form.mustMandatoryValue(key, &value)
@@ -209,9 +209,9 @@ func (form *FormData) MandatoryCustom(key string, assign func(value string) erro
// Path binds the absolute path of a form data file to a string variable. // Path binds the absolute path of a form data file to a string variable.
// //
// var path string // var path string
// //
// ctx.FormData().Path("foo.txt", &path) // ctx.FormData().Path("foo.txt", &path)
func (form *FormData) Path(filename string, target *string) *FormData { func (form *FormData) Path(filename string, target *string) *FormData {
return form.path(filename, target) return form.path(filename, target)
} }
@@ -219,18 +219,18 @@ func (form *FormData) Path(filename string, target *string) *FormData {
// MandatoryPath binds the absolute path ofa form data file to a string // MandatoryPath binds the absolute path ofa form data file to a string
// variable. It populates an error if the file does not exist. // variable. It populates an error if the file does not exist.
// //
// var path string // var path string
// //
// ctx.FormData().MandatoryPath("foo.txt", &path) // ctx.FormData().MandatoryPath("foo.txt", &path)
func (form *FormData) MandatoryPath(filename string, target *string) *FormData { func (form *FormData) MandatoryPath(filename string, target *string) *FormData {
return form.mandatoryPath(filename, target) return form.mandatoryPath(filename, target)
} }
// Content binds the content of a form data file to a string variable. // Content binds the content of a form data file to a string variable.
// //
// var content string // var content string
// //
// ctx.FormData().Content("foo.txt", &content, "bar") // ctx.FormData().Content("foo.txt", &content, "bar")
func (form *FormData) Content(filename string, target *string, defaultValue string) *FormData { func (form *FormData) Content(filename string, target *string, defaultValue string) *FormData {
var path string var path string
form.path(filename, &path) form.path(filename, &path)
@@ -247,9 +247,9 @@ func (form *FormData) Content(filename string, target *string, defaultValue stri
// MandatoryContent binds the content of a form data file to a string variable. // MandatoryContent binds the content of a form data file to a string variable.
// It populates an error if the file does not exist. // It populates an error if the file does not exist.
// //
// var content string // var content string
// //
// ctx.FormData().MandatoryContent("foo.txt", &content) // ctx.FormData().MandatoryContent("foo.txt", &content)
func (form *FormData) MandatoryContent(filename string, target *string) *FormData { func (form *FormData) MandatoryContent(filename string, target *string) *FormData {
var path string var path string
form.mandatoryPath(filename, &path) form.mandatoryPath(filename, &path)
@@ -264,9 +264,9 @@ func (form *FormData) MandatoryContent(filename string, target *string) *FormDat
// Paths binds the absolute paths of form data files, according to a list of // Paths binds the absolute paths of form data files, according to a list of
// file extensions, to a string slice variable. // file extensions, to a string slice variable.
// //
// var paths []string // var paths []string
// //
// ctx.FormData().Paths([]string{".txt"}, &paths) // ctx.FormData().Paths([]string{".txt"}, &paths)
func (form *FormData) Paths(extensions []string, target *[]string) *FormData { func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
return form.paths(extensions, target) return form.paths(extensions, target)
} }
@@ -275,9 +275,9 @@ func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
// list of file extensions, to a string slice variable. It populates an error // list of file extensions, to a string slice variable. It populates an error
// if there is no file for given file extensions. // if there is no file for given file extensions.
// //
// var paths []string // var paths []string
// //
// ctx.FormData().MandatoryPaths([]string{".txt"}, &paths) // ctx.FormData().MandatoryPaths([]string{".txt"}, &paths)
func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *FormData { func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *FormData {
form.paths(extensions, target) form.paths(extensions, target)
@@ -426,7 +426,7 @@ func (form *FormData) mandatoryPath(filename string, target *string) *FormData {
// readFile binds the content of a file to a string variable. It populates an // readFile binds the content of a file to a string variable. It populates an
// error if it fails to read the file content. // error if it fails to read the file content.
func (form *FormData) readFile(path, filename string, target *string) *FormData { func (form *FormData) readFile(path, filename string, target *string) *FormData {
b, err := ioutil.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {
form.append( form.append(
fmt.Errorf("form file '%s' is invalid (%w)", filename, err), fmt.Errorf("form file '%s' is invalid (%w)", filename, err),

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@@ -56,7 +57,7 @@ func httpErrorHandler() echo.HTTPErrorHandler {
// latencyMiddleware sets the start time in the echo.Context under "startTime". // latencyMiddleware sets the start time in the echo.Context under "startTime".
// Its value will be used later to calculate a request latency. // Its value will be used later to calculate a request latency.
// //
// startTime := c.Get("startTime").(time.Time) // startTime := c.Get("startTime").(time.Time)
func latencyMiddleware() echo.MiddlewareFunc { func latencyMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@@ -74,14 +75,14 @@ func latencyMiddleware() echo.MiddlewareFunc {
// Its value may be used to skip a middleware execution based on a request // Its value may be used to skip a middleware execution based on a request
// URI. // URI.
// //
// rootPath := c.Get("rootPath").(string) // rootPath := c.Get("rootPath").(string)
// healthURI := fmt.Sprintf("%s/health", rootPath) // healthURI := fmt.Sprintf("%s/health", rootPath)
// //
// // Skip the middleware if health check URI. // // Skip the middleware if health check URI.
// if c.Request().RequestURI == healthURI { // if c.Request().RequestURI == healthURI {
// // Call the next middleware in the chain. // // Call the next middleware in the chain.
// return next(c) // return next(c)
// } // }
func rootPathMiddleware(rootPath string) echo.MiddlewareFunc { func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@@ -97,8 +98,8 @@ func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
// "trace". Its value is either retrieved from the trace header or generated if // "trace". Its value is either retrieved from the trace header or generated if
// the header is not present / its value is empty. // the header is not present / its value is empty.
// //
// trace := c.Get("trace").(string) // trace := c.Get("trace").(string)
// traceHeader := c.Get("traceHeader").(string). // traceHeader := c.Get("traceHeader").(string).
func traceMiddleware(header string) echo.MiddlewareFunc { func traceMiddleware(header string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@@ -119,38 +120,26 @@ func traceMiddleware(header string) echo.MiddlewareFunc {
} }
} }
// timeoutsMiddleware sets the read, process and write timeouts in the
// echo.Context under "readTimeout", "processTimeout" and "writeTimeout".
//
// readTimeout := c.Get("readTimeout").(time.Duration)
// processTimeout := c.Get("processTimeout").(time.Duration)
// writeTimeout := c.Get("writeTimeout").(time.Duration)
func timeoutsMiddleware(readTimeout, processTimeout, writeTimeout time.Duration) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set("readTimeout", readTimeout)
c.Set("processTimeout", processTimeout)
c.Set("writeTimeout", writeTimeout)
// Call the next middleware in the chain.
return next(c)
}
}
}
// loggerMiddleware sets the logger in the echo.Context under "logger" and logs // loggerMiddleware sets the logger in the echo.Context under "logger" and logs
// a synchronous request result. // a synchronous request result.
// //
// logger := c.Get("logger").(*zap.Logger) // logger := c.Get("logger").(*zap.Logger)
func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.MiddlewareFunc { func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
startTime := c.Get("startTime").(time.Time) startTime := c.Get("startTime").(time.Time)
trace := c.Get("trace").(string) trace := c.Get("trace").(string)
rootPath := c.Get("rootPath").(string)
// Create the request logger and add it to our locals. // Create the request logger and add it to our locals.
reqLogger := logger.With(zap.String("trace", trace)) reqLogger := logger.With(zap.String("trace", trace))
c.Set("logger", reqLogger) c.Set("logger", reqLogger.Named(func() string {
return strings.ReplaceAll(
strings.ReplaceAll(c.Request().URL.Path, rootPath, ""),
"/",
"",
)
}()))
// Call the next middleware in the chain. // Call the next middleware in the chain.
err := next(c) err := next(c)
@@ -159,7 +148,6 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
} }
for _, path := range disableLoggingForPaths { for _, path := range disableLoggingForPaths {
rootPath := c.Get("rootPath").(string)
URI := fmt.Sprintf("%s%s", rootPath, path) URI := fmt.Sprintf("%s%s", rootPath, path)
if c.Request().RequestURI == URI { if c.Request().RequestURI == URI {
@@ -209,16 +197,16 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
// and "cancel". If the process is synchronous, it also handles the result of a // and "cancel". If the process is synchronous, it also handles the result of a
// "multipart/form-data" request. // "multipart/form-data" request.
// //
// ctx := c.Get("context").(*api.Context) // ctx := c.Get("context").(*api.Context)
// cancel := c.Get("cancel").(context.CancelFunc) // cancel := c.Get("cancel").(context.CancelFunc)
func contextMiddleware(processTimeout time.Duration) echo.MiddlewareFunc { func contextMiddleware(timeout time.Duration) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
logger := c.Get("logger").(*zap.Logger) logger := c.Get("logger").(*zap.Logger)
// We create a context with a timeout so that underlying processes are // We create a context with a timeout so that underlying processes are
// able to stop early and handle correctly a timeout scenario. // able to stop early and handle correctly a timeout scenario.
ctx, cancel, err := newContext(c, logger, processTimeout) ctx, cancel, err := newContext(c, logger, timeout)
if err != nil { if err != nil {
cancel() cancel()

View File

@@ -222,47 +222,6 @@ func TestTraceMiddleware(t *testing.T) {
} }
} }
func TestTimeoutsMiddleware(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
c := srv.NewContext(request, recorder)
expectReadTimeout := time.Duration(1) * time.Second
expectProcessTimeout := time.Duration(2) * time.Second
expectWriteTimeout := time.Duration(3) * time.Second
err := timeoutsMiddleware(expectReadTimeout, expectProcessTimeout, expectWriteTimeout)(
func(c echo.Context) error {
return nil
},
)(c)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
actualReadTimeout := c.Get("readTimeout").(time.Duration)
actualProcessTimeout := c.Get("processTimeout").(time.Duration)
actualWriteTimeout := c.Get("writeTimeout").(time.Duration)
if actualReadTimeout != expectReadTimeout {
t.Errorf("expected '%s' but got '%s", expectReadTimeout, actualReadTimeout)
}
if actualProcessTimeout != expectProcessTimeout {
t.Errorf("expected '%s' but got '%s", expectProcessTimeout, actualProcessTimeout)
}
if actualWriteTimeout != expectWriteTimeout {
t.Errorf("expected '%s' but got '%s", actualWriteTimeout, expectWriteTimeout)
}
}
func TestLoggerMiddleware(t *testing.T) { func TestLoggerMiddleware(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
request *http.Request request *http.Request

75
pkg/modules/api/mocks.go Normal file
View File

@@ -0,0 +1,75 @@
package api
import (
"github.com/labstack/echo/v4"
"go.uber.org/zap"
)
// ContextMock is a helper for tests.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
type ContextMock struct {
*Context
}
// SetDirPath sets the context's working directory path.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.SetDirPath("/foo")
func (ctx *ContextMock) SetDirPath(path string) {
ctx.dirPath = path
}
// SetValues sets the values.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.SetValues(map[string][]string{
// "url": {
// "foo",
// },
// })
func (ctx *ContextMock) SetValues(values map[string][]string) {
ctx.values = values
}
// SetFiles sets the files.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.SetFiles(map[string]string{
// "foo": "/foo",
// })
func (ctx *ContextMock) SetFiles(files map[string]string) {
ctx.files = files
}
// SetCancelled sets if the context is cancelled or not.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.SetCancelled(true)
func (ctx *ContextMock) SetCancelled(cancelled bool) {
ctx.cancelled = cancelled
}
// OutputPaths returns the registered output paths.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// outputPaths := ctx.OutputPaths()
func (ctx ContextMock) OutputPaths() []string {
return ctx.outputPaths
}
// SetLogger sets the logger.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.SetLogger(zap.NewNop())
func (ctx *ContextMock) SetLogger(logger *zap.Logger) {
ctx.logger = logger
}
// SetEchoContext sets the echo.Context.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.setEchoContext(c)
func (ctx *ContextMock) SetEchoContext(c echo.Context) {
ctx.Context.echoCtx = c
}

View File

@@ -0,0 +1,105 @@
package api
import (
"reflect"
"testing"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
)
func TestContextMock_SetDirPath(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetDirPath("/foo")
actual := mock.dirPath
expect := "/foo"
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestContextMock_SetValues(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetValues(map[string][]string{
"foo": {"foo"},
})
actual := mock.values
expect := map[string][]string{
"foo": {"foo"},
}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestContextMock_SetFiles(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetFiles(map[string]string{
"foo": "/foo",
})
actual := mock.files
expect := map[string]string{
"foo": "/foo",
}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestContextMock_SetCancelled(t *testing.T) {
mock := &ContextMock{&Context{}}
mock.SetCancelled(true)
actual := mock.cancelled
if !actual {
t.Errorf("expected %t but got %t", true, actual)
}
}
func TestContextMock_OutputPaths(t *testing.T) {
mock := ContextMock{
&Context{
outputPaths: []string{"/foo"},
},
}
actual := mock.OutputPaths()
expect := []string{"/foo"}
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %+v but got: %+v", expect, actual)
}
}
func TestContextMock_SetLogger(t *testing.T) {
mock := ContextMock{&Context{}}
expect := zap.NewNop()
mock.SetLogger(expect)
actual := mock.logger
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestContextMock_SetEchoContext(t *testing.T) {
mock := ContextMock{&Context{}}
expect := echo.New().NewContext(nil, nil)
mock.SetEchoContext(expect)
actual := mock.echoCtx
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}

View File

@@ -1,10 +1,10 @@
package chromium package chromium
import ( import (
"bufio"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@@ -66,9 +66,11 @@ type Chromium struct {
engine gotenberg.PDFEngine engine gotenberg.PDFEngine
userAgent string userAgent string
incognito bool incognito bool
allowInsecureLocalhost bool
ignoreCertificateErrors bool ignoreCertificateErrors bool
disableWebSecurity bool disableWebSecurity bool
allowFileAccessFromFiles bool allowFileAccessFromFiles bool
hostResolverRules string
proxyServer string proxyServer string
allowList *regexp.Regexp allowList *regexp.Regexp
denyList *regexp.Regexp denyList *regexp.Regexp
@@ -85,7 +87,7 @@ type LinkTag struct {
// ScriptTag represents an HTML <script> element. // ScriptTag represents an HTML <script> element.
type ScriptTag struct { type ScriptTag struct {
// Src is the "src" attribute of the HTML <link> element. // Src is the "src" attribute of the HTML <script> element.
// Required. // Required.
Src string `json:"src"` Src string `json:"src"`
} }
@@ -250,9 +252,11 @@ func (mod Chromium) Descriptor() gotenberg.ModuleDescriptor {
fs := flag.NewFlagSet("chromium", flag.ExitOnError) fs := flag.NewFlagSet("chromium", flag.ExitOnError)
fs.String("chromium-user-agent", "", "Override the default User-Agent header") fs.String("chromium-user-agent", "", "Override the default User-Agent header")
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode") fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
fs.Bool("chromium-allow-insecure-localhost", false, "Ignore TLS/SSL errors on localhost")
fs.Bool("chromium-ignore-certificate-errors", false, "Ignore the certificate errors") fs.Bool("chromium-ignore-certificate-errors", false, "Ignore the certificate errors")
fs.Bool("chromium-disable-web-security", false, "Don't enforce the same-origin policy") fs.Bool("chromium-disable-web-security", false, "Don't enforce the same-origin policy")
fs.Bool("chromium-allow-file-access-from-files", false, "Allow file:// URIs to read other file:// URIs") fs.Bool("chromium-allow-file-access-from-files", false, "Allow file:// URIs to read other file:// URIs")
fs.String("chromium-host-resolver-rules", "", "Set custom mappings to the host resolver")
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests") fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
fs.String("chromium-allow-list", "", "Set the allowed URLs for Chromium using a regular expression") fs.String("chromium-allow-list", "", "Set the allowed URLs for Chromium using a regular expression")
fs.String("chromium-deny-list", "^file:///[^tmp].*", "Set the denied URLs for Chromium using a regular expression") fs.String("chromium-deny-list", "^file:///[^tmp].*", "Set the denied URLs for Chromium using a regular expression")
@@ -274,9 +278,11 @@ func (mod Chromium) Descriptor() gotenberg.ModuleDescriptor {
func (mod *Chromium) Provision(ctx *gotenberg.Context) error { func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags() flags := ctx.ParsedFlags()
mod.userAgent = flags.MustString("chromium-user-agent") mod.userAgent = flags.MustString("chromium-user-agent")
mod.allowInsecureLocalhost = flags.MustBool("chromium-allow-insecure-localhost")
mod.ignoreCertificateErrors = flags.MustBool("chromium-ignore-certificate-errors") mod.ignoreCertificateErrors = flags.MustBool("chromium-ignore-certificate-errors")
mod.disableWebSecurity = flags.MustBool("chromium-disable-web-security") mod.disableWebSecurity = flags.MustBool("chromium-disable-web-security")
mod.allowFileAccessFromFiles = flags.MustBool("chromium-allow-file-access-from-files") mod.allowFileAccessFromFiles = flags.MustBool("chromium-allow-file-access-from-files")
mod.hostResolverRules = flags.MustString("chromium-host-resolver-rules")
mod.proxyServer = flags.MustString("chromium-proxy-server") mod.proxyServer = flags.MustString("chromium-proxy-server")
mod.allowList = flags.MustRegexp("chromium-allow-list") mod.allowList = flags.MustRegexp("chromium-allow-list")
mod.denyList = flags.MustRegexp("chromium-deny-list") mod.denyList = flags.MustRegexp("chromium-deny-list")
@@ -355,7 +361,7 @@ func (mod Chromium) Routes() ([]api.Route, error) {
// drastically. In such a scenario, the given context may also be done before // drastically. In such a scenario, the given context may also be done before
// the end of the conversion. // the end of the conversion.
func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error { func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
debug := debugLogger{logger: logger.Named("chromium.debug")} debug := debugLogger{logger: logger.Named("browser")}
userProfileDirPath := gotenberg.NewDirPath() userProfileDirPath := gotenberg.NewDirPath()
args := append(chromedp.DefaultExecAllocatorOptions[:], args := append(chromedp.DefaultExecAllocatorOptions[:],
@@ -382,6 +388,11 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
args = append(args, chromedp.Flag("incognito", mod.incognito)) args = append(args, chromedp.Flag("incognito", mod.incognito))
} }
if mod.allowInsecureLocalhost {
// See https://github.com/gotenberg/gotenberg/issues/488.
args = append(args, chromedp.Flag("allow-insecure-localhost", true))
}
if mod.ignoreCertificateErrors { if mod.ignoreCertificateErrors {
args = append(args, chromedp.IgnoreCertErrors) args = append(args, chromedp.IgnoreCertErrors)
} }
@@ -395,6 +406,11 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
args = append(args, chromedp.Flag("allow-file-access-from-files", true)) args = append(args, chromedp.Flag("allow-file-access-from-files", true))
} }
if mod.hostResolverRules != "" {
// See https://github.com/gotenberg/gotenberg/issues/488.
args = append(args, chromedp.Flag("host-resolver-rules", mod.hostResolverRules))
}
if mod.proxyServer != "" { if mod.proxyServer != "" {
// See https://github.com/gotenberg/gotenberg/issues/376. // See https://github.com/gotenberg/gotenberg/issues/376.
args = append(args, chromedp.ProxyServer(mod.proxyServer)) args = append(args, chromedp.ProxyServer(mod.proxyServer))
@@ -404,6 +420,14 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
args = append(args, chromedp.UserAgent(options.UserAgent)) args = append(args, chromedp.UserAgent(options.UserAgent))
} }
// See https://github.com/gotenberg/gotenberg/issues/524.
deadline, ok := ctx.Deadline()
if !ok {
return errors.New("context has no deadline")
}
args = append(args, chromedp.WSURLReadTimeout(time.Until(deadline)))
allocatorCtx, cancel := chromedp.NewExecAllocator(ctx, args...) allocatorCtx, cancel := chromedp.NewExecAllocator(ctx, args...)
defer cancel() defer cancel()
@@ -426,7 +450,7 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
consoleExceptionsMu sync.RWMutex consoleExceptionsMu sync.RWMutex
) )
printToPDF := func(URL string, options Options, result *[]byte) chromedp.Tasks { printToPDF := func(URL string, options Options, outputPath string) chromedp.Tasks {
// We validate the underlying requests against our allow / deny lists. // We validate the underlying requests against our allow / deny lists.
// If a request does not pass the validation, we make it fail. // If a request does not pass the validation, we make it fail.
listenForEventRequestPaused(taskCtx, logger, mod.allowList, mod.denyList) listenForEventRequestPaused(taskCtx, logger, mod.allowList, mod.denyList)
@@ -732,6 +756,7 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
}), }),
chromedp.ActionFunc(func(ctx context.Context) error { chromedp.ActionFunc(func(ctx context.Context) error {
printToPDF := page.PrintToPDF(). printToPDF := page.PrintToPDF().
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
WithLandscape(options.Landscape). WithLandscape(options.Landscape).
WithPrintBackground(options.PrintBackground). WithPrintBackground(options.PrintBackground).
WithScale(options.Scale). WithScale(options.Scale).
@@ -741,7 +766,6 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
WithMarginBottom(options.MarginBottom). WithMarginBottom(options.MarginBottom).
WithMarginLeft(options.MarginLeft). WithMarginLeft(options.MarginLeft).
WithMarginRight(options.MarginRight). WithMarginRight(options.MarginRight).
WithIgnoreInvalidPageRanges(false).
WithPageRanges(options.PageRanges). WithPageRanges(options.PageRanges).
WithDisplayHeaderFooter(true). WithDisplayHeaderFooter(true).
WithHeaderTemplate(options.HeaderTemplate). WithHeaderTemplate(options.HeaderTemplate).
@@ -750,12 +774,44 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
logger.Debug(fmt.Sprintf("print to PDF with: %+v", printToPDF)) logger.Debug(fmt.Sprintf("print to PDF with: %+v", printToPDF))
data, _, err := printToPDF.Do(ctx) _, stream, err := printToPDF.Do(ctx)
if err != nil { if err != nil {
return fmt.Errorf("print to PDF: %w", err) return fmt.Errorf("print to PDF: %w", err)
} }
*result = data reader := &streamReader{
ctx: ctx,
handle: stream,
r: nil,
pos: 0,
eof: false,
}
defer func() {
err := reader.Close()
if err != nil {
logger.Error(fmt.Sprintf("close reader: %s", err))
}
}()
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("open output path: %w", err)
}
defer func() {
err := file.Close()
if err != nil {
logger.Error(fmt.Sprintf("close output path: %s", err))
}
}()
buffer := bufio.NewReader(reader)
_, err = buffer.WriteTo(file)
if err != nil {
return fmt.Errorf("write result to output path: %w", err)
}
return nil return nil
}), }),
@@ -766,8 +822,7 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
activeInstancesCount += 1 activeInstancesCount += 1
activeInstancesCountMu.Unlock() activeInstancesCountMu.Unlock()
var buffer []byte err := chromedp.Run(taskCtx, printToPDF(URL, options, outputPath))
err := chromedp.Run(taskCtx, printToPDF(URL, options, &buffer))
activeInstancesCountMu.Lock() activeInstancesCountMu.Lock()
activeInstancesCount -= 1 activeInstancesCount -= 1
@@ -809,11 +864,6 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions) return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions)
} }
err = ioutil.WriteFile(outputPath, buffer, 0600)
if err != nil {
return fmt.Errorf("write result to output path: %w", err)
}
return nil return nil
} }

View File

@@ -3,7 +3,6 @@ package chromium
import ( import (
"context" "context"
"errors" "errors"
"io/ioutil"
"os" "os"
"reflect" "reflect"
"regexp" "regexp"
@@ -176,11 +175,11 @@ func TestChromium_Validate(t *testing.T) {
func TestChromium_Metrics(t *testing.T) { func TestChromium_Metrics(t *testing.T) {
metrics, err := new(Chromium).Metrics() metrics, err := new(Chromium).Metrics()
if err != nil { if err != nil {
t.Errorf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
if len(metrics) != 1 { if len(metrics) != 1 {
t.Errorf("expected %d metrics, but got %d", 1, len(metrics)) t.Fatalf("expected %d metrics, but got %d", 1, len(metrics))
} }
actual := metrics[0].Read() actual := metrics[0].Read()
@@ -225,16 +224,19 @@ func TestChromium_Routes(t *testing.T) {
} }
func TestChromium_PDF(t *testing.T) { func TestChromium_PDF(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
name string
timeout time.Duration timeout time.Duration
cancel context.CancelFunc cancel context.CancelFunc
URL string URL string
options Options options Options
userAgent string userAgent string
incognito bool incognito bool
allowInsecureLocalhost bool
ignoreCertificateErrors bool ignoreCertificateErrors bool
disableWebSecurity bool disableWebSecurity bool
allowFileAccessFromFiles bool allowFileAccessFromFiles bool
hostResolverRules string
proxyServer string proxyServer string
allowList *regexp.Regexp allowList *regexp.Regexp
denyList *regexp.Regexp denyList *regexp.Regexp
@@ -242,34 +244,51 @@ func TestChromium_PDF(t *testing.T) {
expectErr bool expectErr bool
}{ }{
{ {
name: "context has no deadline",
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html",
expectErr: true,
},
{
name: "URL does not match the expression from the allowed list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
allowList: regexp.MustCompile("file:///tmp/*"), allowList: regexp.MustCompile("file:///tmp/*"),
expectErr: true, expectErr: true,
}, },
{ {
name: "URL does not match the expression from the denied list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
denyList: regexp.MustCompile("file:///tests/*"), denyList: regexp.MustCompile("file:///tests/*"),
expectErr: true, expectErr: true,
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", name: "with user agent",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{ options: Options{
UserAgent: "foo", UserAgent: "foo",
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample10/index.html", name: "fail on console exceptions",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample10/index.html",
options: Options{ options: Options{
FailOnConsoleExceptions: true, FailOnConsoleExceptions: true,
}, },
expectErr: true, expectErr: true,
}, },
{ {
name: "disable JavaScript",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample9/index.html", URL: "file:///tests/test/testdata/chromium/html/sample9/index.html",
disableJavaScript: true, disableJavaScript: true,
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", name: "with extra HTTP headers",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{ options: Options{
ExtraHTTPHeaders: map[string]string{ ExtraHTTPHeaders: map[string]string{
"foo": "bar", "foo": "bar",
@@ -277,7 +296,9 @@ func TestChromium_PDF(t *testing.T) {
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html", name: "with extra link tags",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
options: Options{ options: Options{
ExtraLinkTags: []LinkTag{ ExtraLinkTags: []LinkTag{
{ {
@@ -290,26 +311,34 @@ func TestChromium_PDF(t *testing.T) {
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html", name: "with invalid emulated media type",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
options: Options{ options: Options{
EmulatedMediaType: "foo", EmulatedMediaType: "foo",
}, },
expectErr: true, expectErr: true,
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html", name: "with screen emulated media type",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
options: Options{ options: Options{
EmulatedMediaType: "screen", EmulatedMediaType: "screen",
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html", name: "with print emulated media type",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
options: Options{ options: Options{
EmulatedMediaType: "print", EmulatedMediaType: "print",
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html", name: "with extra script tags",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
options: Options{ options: Options{
ExtraScriptTags: []ScriptTag{ ExtraScriptTags: []ScriptTag{
{ {
@@ -319,12 +348,15 @@ func TestChromium_PDF(t *testing.T) {
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", name: "with wait delay",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{ options: Options{
WaitDelay: time.Duration(1) * time.Nanosecond, WaitDelay: time.Duration(1) * time.Nanosecond,
}, },
}, },
{ {
name: "with invalid wait window status",
timeout: time.Duration(3) * time.Second, timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html", URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{ options: Options{
@@ -333,6 +365,7 @@ func TestChromium_PDF(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
name: "with wait window status",
timeout: time.Duration(3) * time.Second, timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html", URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{ options: Options{
@@ -340,6 +373,7 @@ func TestChromium_PDF(t *testing.T) {
}, },
}, },
{ {
name: "with wait for expression that should not happen",
timeout: time.Duration(3) * time.Second, timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html", URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{ options: Options{
@@ -348,6 +382,7 @@ func TestChromium_PDF(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
name: "with valid wait for expression",
timeout: time.Duration(3) * time.Second, timeout: time.Duration(3) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html", URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
options: Options{ options: Options{
@@ -355,51 +390,69 @@ func TestChromium_PDF(t *testing.T) {
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", name: "with invalid wait for expression",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{ options: Options{
WaitForExpression: "return undefined", WaitForExpression: "return undefined",
}, },
expectErr: true, expectErr: true,
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", name: "with too big margin bottom",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{ options: Options{
MarginBottom: 100, MarginBottom: 100,
}, },
expectErr: true, expectErr: true,
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", name: "with invalid page ranges",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{ options: Options{
PageRanges: "foo", PageRanges: "foo",
}, },
expectErr: true, expectErr: true,
}, },
{ {
name: "with a lot of options",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
userAgent: "foo", userAgent: "foo",
incognito: true, incognito: true,
ignoreCertificateErrors: true, ignoreCertificateErrors: true,
allowInsecureLocalhost: true,
disableWebSecurity: true, disableWebSecurity: true,
allowFileAccessFromFiles: true, allowFileAccessFromFiles: true,
hostResolverRules: "foo",
proxyServer: "foo", proxyServer: "foo",
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html", name: "with file using local and remote assets",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html",
}, },
{ {
name: "URL does match the expression from the allowed list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html", URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
allowList: regexp.MustCompile("file:///tests/*"), allowList: regexp.MustCompile("file:///tests/*"),
}, },
{ {
name: "URL does match the expression from the denied list",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html", URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
denyList: regexp.MustCompile("file:///etc/*"), denyList: regexp.MustCompile("file:///etc/*"),
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html", name: "with custom header and footer templates",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
options: Options{ options: Options{
HeaderTemplate: func() string { HeaderTemplate: func() string {
b, err := ioutil.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html") b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html")
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -407,7 +460,7 @@ func TestChromium_PDF(t *testing.T) {
return string(b) return string(b)
}(), }(),
FooterTemplate: func() string { FooterTemplate: func() string {
b, err := ioutil.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html") b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html")
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -417,14 +470,20 @@ func TestChromium_PDF(t *testing.T) {
}, },
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample5/index.html", name: "with file using a .gif",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample5/index.html",
}, },
{ {
name: "with allow file access from files",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample6/index.html", URL: "file:///tests/test/testdata/chromium/html/sample6/index.html",
allowFileAccessFromFiles: true, allowFileAccessFromFiles: true,
}, },
{ {
URL: "file:///tests/test/testdata/chromium/html/sample7/index.html", name: "with file using a style attribute",
timeout: time.Duration(60) * time.Second,
URL: "file:///tests/test/testdata/chromium/html/sample7/index.html",
}, },
} { } {
func() { func() {
@@ -432,9 +491,11 @@ func TestChromium_PDF(t *testing.T) {
mod.binPath = os.Getenv("CHROMIUM_BIN_PATH") mod.binPath = os.Getenv("CHROMIUM_BIN_PATH")
mod.userAgent = tc.userAgent mod.userAgent = tc.userAgent
mod.incognito = tc.incognito mod.incognito = tc.incognito
mod.allowInsecureLocalhost = tc.allowInsecureLocalhost
mod.ignoreCertificateErrors = tc.ignoreCertificateErrors mod.ignoreCertificateErrors = tc.ignoreCertificateErrors
mod.disableWebSecurity = tc.disableWebSecurity mod.disableWebSecurity = tc.disableWebSecurity
mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles
mod.hostResolverRules = tc.hostResolverRules
mod.proxyServer = tc.proxyServer mod.proxyServer = tc.proxyServer
if tc.allowList == nil { if tc.allowList == nil {
@@ -451,13 +512,13 @@ func TestChromium_PDF(t *testing.T) {
outputDir, err := gotenberg.MkdirAll() outputDir, err := gotenberg.MkdirAll()
if err != nil { if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err) t.Fatalf("test %s: expected error but got: %v", tc.name, err)
} }
defer func() { defer func() {
err := os.RemoveAll(outputDir) err := os.RemoveAll(outputDir)
if err != nil { if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err) t.Fatalf("test %s: expected no error but got: %v", tc.name, err)
} }
}() }()
@@ -471,11 +532,11 @@ func TestChromium_PDF(t *testing.T) {
} }
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("test %s: expected error but got: %v", tc.name, err)
} }
if !tc.expectErr && err != nil { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("test %s: expected no error but got: %v", tc.name, err)
} }
}() }()
} }

View File

@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@@ -130,10 +129,8 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
form, options := FormDataChromiumPDFOptions(ctx) form, options := FormDataChromiumPDFOptions(ctx)
var ( var (
URL string URL string
PDFformat string PDFformat string
linkPaths []string
scriptPaths []string
) )
err := form. err := form.
@@ -163,176 +160,12 @@ func convertURLRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
return nil return nil
}). }).
Paths([]string{".woff2", ".woff", ".ttf", ".css"}, &linkPaths).
Paths([]string{".js"}, &scriptPaths).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
// Thanks to Options.LinkTags and Options.ScriptTags, one may
// "hijack" the content of a remote HTML document (for instance, a
// website) by loading external assets like scripts or CSS
// stylesheets.
//
// There are two possibilities:
//
// 1. We auto-detect all files sent in the request that match the
// following file extensions: ".woff2", ".woff", ".ttf", ".css",
// ".css", and ".js".
//
// 2. The user has sent both files and a JSON mapping via the
// "extraLinkTags" and/or "extraScriptTags" form fields. In such a
// scenario, the JSON mapping has the priority for ordering, and
// files which are a not mapped are added at the end. The user may
// also have sent remote URLs in the JSON mapping.
// First, let's handle the HTML <link> elements.
hasExtraLinkTags := len(options.ExtraLinkTags) > 0
hasLinkPaths := len(linkPaths) > 0
if !hasExtraLinkTags && hasLinkPaths {
// First scenario: there is no JSON mapping, we simply add the
// paths.
options.ExtraLinkTags = make([]LinkTag, len(linkPaths))
for i, path := range linkPaths {
options.ExtraLinkTags[i] = LinkTag{
Href: filepath.Base(path),
}
}
} else if hasExtraLinkTags && hasLinkPaths {
// Second scenario: there are both files and a JSON mapping.
// First, find the filenames of the files.
filenames := make([]string, len(linkPaths))
for i, path := range linkPaths {
filenames[i] = filepath.Base(path)
}
var extraLinkTags []LinkTag
// Then, let's find the filenames that exist in the JSON
// mapping, plus the entries that do only exist in the JSON
// mapping.
for _, linkTagFromMapping := range options.ExtraLinkTags {
found := false
for _, filename := range filenames {
if linkTagFromMapping.Href == filename {
extraLinkTags = append(extraLinkTags, linkTagFromMapping)
found = true
break
}
}
if !found {
// This entry only exist in the JSON mapping.
extraLinkTags = append(extraLinkTags, linkTagFromMapping)
}
}
// Then, add the remaining filenames.
for _, filename := range filenames {
found := false
for _, linkTag := range extraLinkTags {
if linkTag.Href == filename {
found = true
break
}
}
if !found {
extraLinkTags = append(extraLinkTags, LinkTag{
Href: filename,
})
}
}
// Last but not least, update the options.
options.ExtraLinkTags = extraLinkTags
}
// Next, let's handle the HTML <script> elements.
hasExtraScriptTags := len(options.ExtraScriptTags) > 0
hasScriptPaths := len(scriptPaths) > 0
if !hasExtraScriptTags && hasScriptPaths {
// First scenario: there is no JSON mapping, we simply add the
// paths.
options.ExtraScriptTags = make([]ScriptTag, len(scriptPaths))
for i, path := range scriptPaths {
options.ExtraScriptTags[i] = ScriptTag{
Src: filepath.Base(path),
}
}
} else if hasExtraScriptTags && hasScriptPaths {
// Second scenario: there are both files and a JSON mapping.
// First, find the filenames of the files.
filenames := make([]string, len(scriptPaths))
for i, path := range scriptPaths {
filenames[i] = filepath.Base(path)
}
var extraScriptTags []ScriptTag
// Then, let's find the filenames that exist in the JSON
// mapping, plus the entries that do only exist in the JSON
// mapping.
for _, scriptTagFromMapping := range options.ExtraScriptTags {
found := false
for _, filename := range filenames {
if scriptTagFromMapping.Src == filename {
extraScriptTags = append(extraScriptTags, scriptTagFromMapping)
found = true
break
}
}
if !found {
// This entry only exist in the JSON mapping.
extraScriptTags = append(extraScriptTags, scriptTagFromMapping)
}
}
// Then, add the remaining filenames.
for _, filename := range filenames {
found := false
for _, scriptTag := range extraScriptTags {
if scriptTag.Src == filename {
found = true
break
}
}
if !found {
extraScriptTags = append(extraScriptTags, ScriptTag{
Src: filename,
})
}
}
// Last but not least, update the options.
options.ExtraScriptTags = extraScriptTags
}
// For both <link> and <script> HTML elements, other scenarios are:
//
// 1. No files and no JSON mapping.
// 2. No files but JSON mapping.
//
// Nothing to do in such cases.
err = convertURL(ctx, chromium, engine, URL, PDFformat, options) err = convertURL(ctx, chromium, engine, URL, PDFformat, options)
if err != nil { if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err) return fmt.Errorf("convert URL to PDF: %w", err)
@@ -437,7 +270,7 @@ func convertMarkdownRoute(chromium API, engine gotenberg.PDFEngine) api.Route {
return "", nil return "", nil
} }
b, err := ioutil.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {
return "", fmt.Errorf("read markdown file '%s': %w", filename, err) return "", fmt.Errorf("read markdown file '%s': %w", filename, err)
} }

View File

@@ -3,7 +3,6 @@ package chromium
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"os" "os"
"reflect" "reflect"
@@ -17,16 +16,16 @@ import (
func TestFormDataChromiumPDFOptions(t *testing.T) { func TestFormDataChromiumPDFOptions(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
ctx *api.MockContext ctx *api.ContextMock
options Options options Options
}{ }{
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
options: DefaultOptions(), options: DefaultOptions(),
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"extraHttpHeaders": { "extraHttpHeaders": {
"foo", "foo",
@@ -38,8 +37,8 @@ func TestFormDataChromiumPDFOptions(t *testing.T) {
options: DefaultOptions(), options: DefaultOptions(),
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"extraHttpHeaders": { "extraHttpHeaders": {
`{"foo":"bar"}`, `{"foo":"bar"}`,
@@ -58,8 +57,8 @@ func TestFormDataChromiumPDFOptions(t *testing.T) {
}(), }(),
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"extraHttpHeaders": { "extraHttpHeaders": {
"foo", "foo",
@@ -71,8 +70,8 @@ func TestFormDataChromiumPDFOptions(t *testing.T) {
options: DefaultOptions(), options: DefaultOptions(),
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"emulatedMediaType": { "emulatedMediaType": {
"foo", "foo",
@@ -84,8 +83,8 @@ func TestFormDataChromiumPDFOptions(t *testing.T) {
options: DefaultOptions(), options: DefaultOptions(),
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"emulatedMediaType": { "emulatedMediaType": {
"screen", "screen",
@@ -112,7 +111,7 @@ func TestFormDataChromiumPDFOptions(t *testing.T) {
func TestConvertURLHandler(t *testing.T) { func TestConvertURLHandler(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
ctx *api.MockContext ctx *api.ContextMock
api API api API
expectErr bool expectErr bool
expectHTTPErr bool expectHTTPErr bool
@@ -120,14 +119,14 @@ func TestConvertURLHandler(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
expectErr: true, expectErr: true,
expectHTTPErr: true, expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"url": { "url": {
"", "",
@@ -141,8 +140,8 @@ func TestConvertURLHandler(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"url": { "url": {
"foo", "foo",
@@ -162,8 +161,8 @@ func TestConvertURLHandler(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"url": { "url": {
"foo", "foo",
@@ -183,8 +182,8 @@ func TestConvertURLHandler(t *testing.T) {
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"url": { "url": {
"foo", "foo",
@@ -209,92 +208,8 @@ func TestConvertURLHandler(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"url": {
"foo",
},
})
ctx.SetFiles(map[string]string{
"b.css": "/b.css",
"a.woff": "/a.woff",
})
return ctx
}(),
api: func() API {
chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, options Options) error {
expectOptions := DefaultOptions()
expectOptions.ExtraLinkTags = []LinkTag{
{
Href: "a.woff",
},
{
Href: "b.css",
},
}
if !reflect.DeepEqual(options, expectOptions) {
return fmt.Errorf("expected options %+v, but got: %+v", expectOptions, options)
}
return nil
}
return chromiumAPI
}(),
expectOutputPathsCount: 1,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"url": {
"foo",
},
"extraLinkTags": {
`[{"href":"https://cdn.foo"},{"href":"b.css"}]`,
},
})
ctx.SetFiles(map[string]string{
"b.css": "/b.css",
"a.woff": "/a.woff",
})
return ctx
}(),
api: func() API {
chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, options Options) error {
expectOptions := DefaultOptions()
expectOptions.ExtraLinkTags = []LinkTag{
{
Href: "https://cdn.foo",
},
{
Href: "b.css",
},
{
Href: "a.woff",
},
}
if !reflect.DeepEqual(options, expectOptions) {
return fmt.Errorf("expected options %+v, but got: %+v", expectOptions, options)
}
return nil
}
return chromiumAPI
}(),
expectOutputPathsCount: 1,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"url": { "url": {
"foo", "foo",
@@ -319,82 +234,25 @@ func TestConvertURLHandler(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"url": { "url": {
"foo", "foo",
}, },
}) "extraLinkTags": {
ctx.SetFiles(map[string]string{ `[{"href":"https://cdn.foo/foo.css"},{"href":"https://cdn.bar/bar.css"}]`,
"b.js": "/b.js",
"a.js": "/a.js",
})
return ctx
}(),
api: func() API {
chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, options Options) error {
expectOptions := DefaultOptions()
expectOptions.ExtraScriptTags = []ScriptTag{
{
Src: "a.js",
},
{
Src: "b.js",
},
}
if !reflect.DeepEqual(options, expectOptions) {
return fmt.Errorf("expected options %+v, but got: %+v", expectOptions, options)
}
return nil
}
return chromiumAPI
}(),
expectOutputPathsCount: 1,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"url": {
"foo",
}, },
"extraScriptTags": { "extraScriptTags": {
`[{"src":"https://cdn.foo"},{"src":"b.js"}]`, `[{"src":"https://cdn.foo/foo.js"},{"src":"https://cdn.bar/bar.js"}]`,
}, },
}) })
ctx.SetFiles(map[string]string{
"b.js": "/b.js",
"a.js": "/a.js",
})
return ctx return ctx
}(), }(),
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, options Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
expectOptions := DefaultOptions()
expectOptions.ExtraScriptTags = []ScriptTag{
{
Src: "https://cdn.foo",
},
{
Src: "b.js",
},
{
Src: "a.js",
},
}
if !reflect.DeepEqual(options, expectOptions) {
return fmt.Errorf("expected options %+v, but got: %+v", expectOptions, options)
}
return nil return nil
} }
@@ -442,7 +300,7 @@ func TestConvertURLHandler(t *testing.T) {
func TestConvertHTMLHandler(t *testing.T) { func TestConvertHTMLHandler(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
ctx *api.MockContext ctx *api.ContextMock
api API api API
expectErr bool expectErr bool
expectHTTPErr bool expectHTTPErr bool
@@ -450,14 +308,14 @@ func TestConvertHTMLHandler(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
expectErr: true, expectErr: true,
expectHTTPErr: true, expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"foo.html": "/foo/foo.html", "foo.html": "/foo/foo.html",
}) })
@@ -469,8 +327,8 @@ func TestConvertHTMLHandler(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"index.html": "/foo/foo.html", "index.html": "/foo/foo.html",
}) })
@@ -488,8 +346,8 @@ func TestConvertHTMLHandler(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"index.html": "/foo/foo.html", "index.html": "/foo/foo.html",
}) })
@@ -546,7 +404,7 @@ func TestConvertHTMLHandler(t *testing.T) {
func TestConvertMarkdownHandler(t *testing.T) { func TestConvertMarkdownHandler(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
ctx *api.MockContext ctx *api.ContextMock
api API api API
outputDir string outputDir string
expectErr bool expectErr bool
@@ -555,14 +413,14 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
expectErr: true, expectErr: true,
expectHTTPErr: true, expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"foo.html": "/foo/foo.html", "foo.html": "/foo/foo.html",
}) })
@@ -574,8 +432,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"index.html": "/foo/foo.html", "index.html": "/foo/foo.html",
}) })
@@ -587,8 +445,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"index.html": "/foo/foo.html", "index.html": "/foo/foo.html",
"markdown.md": "/foo/markdown.md", "markdown.md": "/foo/markdown.md",
@@ -599,8 +457,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"index.html": "/tests/test/testdata/chromium/markdown/sample2/index.html", "index.html": "/tests/test/testdata/chromium/markdown/sample2/index.html",
"markdown1.md": "/foo/markdown1.md", "markdown1.md": "/foo/markdown1.md",
@@ -613,8 +471,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"index.html": "/tests/test/testdata/chromium/markdown/sample1/index.html", "index.html": "/tests/test/testdata/chromium/markdown/sample1/index.html",
"markdown1.md": "/foo/markdown1.md", "markdown1.md": "/foo/markdown1.md",
@@ -633,8 +491,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"index.html": "/tests/test/testdata/chromium/markdown/sample1/index.html", "index.html": "/tests/test/testdata/chromium/markdown/sample1/index.html",
"markdown1.md": "/tests/test/testdata/chromium/markdown/sample1/markdown1.md", "markdown1.md": "/tests/test/testdata/chromium/markdown/sample1/markdown1.md",
@@ -655,8 +513,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetDirPath("/tmp/foo") ctx.SetDirPath("/tmp/foo")
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
@@ -680,8 +538,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetDirPath("/tmp/foo") ctx.SetDirPath("/tmp/foo")
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
@@ -705,8 +563,8 @@ func TestConvertMarkdownHandler(t *testing.T) {
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetDirPath("/tmp/foo") ctx.SetDirPath("/tmp/foo")
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
@@ -784,7 +642,7 @@ func TestConvertMarkdownHandler(t *testing.T) {
func TestConvertURL(t *testing.T) { func TestConvertURL(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
ctx *api.MockContext ctx *api.ContextMock
api API api API
engine gotenberg.PDFEngine engine gotenberg.PDFEngine
PDFformat string PDFformat string
@@ -795,7 +653,7 @@ func TestConvertURL(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -810,7 +668,7 @@ func TestConvertURL(t *testing.T) {
expectHTTPStatus: http.StatusForbidden, expectHTTPStatus: http.StatusForbidden,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -823,7 +681,7 @@ func TestConvertURL(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -843,7 +701,7 @@ func TestConvertURL(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -858,7 +716,7 @@ func TestConvertURL(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -873,7 +731,7 @@ func TestConvertURL(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -888,7 +746,7 @@ func TestConvertURL(t *testing.T) {
expectHTTPStatus: http.StatusConflict, expectHTTPStatus: http.StatusConflict,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -901,7 +759,7 @@ func TestConvertURL(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -924,7 +782,7 @@ func TestConvertURL(t *testing.T) {
expectHTTPStatus: http.StatusBadRequest, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -945,7 +803,7 @@ func TestConvertURL(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
@@ -966,8 +824,8 @@ func TestConvertURL(t *testing.T) {
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
}, },
{ {
ctx: func() *api.MockContext { ctx: func() *api.ContextMock {
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetCancelled(true) ctx.SetCancelled(true)
return ctx return ctx
@@ -984,7 +842,7 @@ func TestConvertURL(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
ctx: &api.MockContext{Context: &api.Context{}}, ctx: &api.ContextMock{Context: &api.Context{}},
api: func() API { api: func() API {
chromiumAPI := struct{ ProtoAPI }{} chromiumAPI := struct{ ProtoAPI }{}
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error { chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {

View File

@@ -0,0 +1,117 @@
package chromium
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"strings"
"github.com/chromedp/cdproto/cdp"
cdprotoio "github.com/chromedp/cdproto/io"
)
// Credits: https://raw.githubusercontent.com/mafredri/cdp/3c5eab7ffc5cbee667b0a813ce470ac423792811/protocol/io/stream_reader.go.
type streamReader struct {
ctx context.Context
handle cdprotoio.StreamHandle
r io.Reader
pos int
eof bool
}
// Read a chunk of the stream.
func (reader *streamReader) Read(p []byte) (n int, err error) {
if reader.r != nil {
// Continue reading from buffer.
return reader.read(p)
}
if reader.eof {
return 0, io.EOF
}
if len(p) == 0 {
return 0, nil
}
// Chromium might have an off-by-one when deciding the maximum size (at
// least for base64 encoded data), usually it will overflow. We subtract
// one to make sure it fits into p.
size := len(p) - 1
if size < 1 {
// Safety-check to avoid crashing Chrome (e.g. via SetSize(-1)).
size = 1
}
reply, err := reader.next(reader.pos, size)
if err != nil {
return 0, err
}
reader.eof = reply.EOF
switch {
case reply.Base64encoded:
b := []byte(reply.Data)
size := base64.StdEncoding.DecodedLen(len(b))
// Safety-check for fast-path to avoid panics.
if len(p) >= size {
n, err = base64.StdEncoding.Decode(p, b)
reader.pos += n
return n, err
}
reader.r = base64.NewDecoder(base64.StdEncoding, bytes.NewReader(b))
default:
reader.r = strings.NewReader(reply.Data)
}
return reader.read(p)
}
// Close closes the stream, discard any temporary backing storage.
func (reader *streamReader) Close() error {
err := cdprotoio.Close(reader.handle).Do(reader.ctx)
if err == nil {
return nil
}
return fmt.Errorf("close Chromium stream: %w", err)
}
func (reader *streamReader) next(pos, size int) (cdprotoio.ReadReturns, error) {
params := cdprotoio.
Read(reader.handle).
WithOffset(int64(pos)).
WithSize(int64(size))
var res cdprotoio.ReadReturns
err := cdp.Execute(reader.ctx, cdprotoio.CommandRead, params, &res)
if err == nil {
return res, nil
}
return res, fmt.Errorf("execute IO.read command: %w", err)
}
func (reader *streamReader) read(p []byte) (n int, err error) {
n, err = reader.r.Read(p)
reader.pos += n
if !reader.eof && err == io.EOF {
reader.r = nil
err = nil
}
return n, err
}
// Interface guards.
var (
_ io.Reader = (*streamReader)(nil)
)

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
@@ -202,19 +201,10 @@ func (gc *GarbageCollector) Stop(ctx context.Context) error {
} }
// Block until the context is done so that other module may gracefully stop // Block until the context is done so that other module may gracefully stop
// before we do a shutdown cleanup. We skip this step if we receive a // before we do a shutdown cleanup.
// SIGINT in the meantime.
gc.logger.Debug("wait for the end of grace duration") gc.logger.Debug("wait for the end of grace duration")
quit := make(chan os.Signal, 1) <-ctx.Done()
signal.Notify(quit, os.Interrupt)
select {
case <-quit:
return nil
case <-ctx.Done():
break
}
gc.ticker.Stop() gc.ticker.Stop()
gc.done <- true gc.done <- true

View File

@@ -1,3 +1,3 @@
// Package libreoffice provides a module which adds a route for converting // Package libreoffice provides a module which adds a route for converting
// document to PDF with LibreOffice. // documents to PDF with LibreOffice.
package libreoffice package libreoffice

View File

@@ -5,7 +5,7 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
) )
@@ -16,7 +16,7 @@ func init() {
// LibreOffice is a module which provides a route for converting documents to // LibreOffice is a module which provides a route for converting documents to
// PDF with LibreOffice. // PDF with LibreOffice.
type LibreOffice struct { type LibreOffice struct {
unoconv unoconv.API unoAPI uno.API
engine gotenberg.PDFEngine engine gotenberg.PDFEngine
disableRoutes bool disableRoutes bool
} }
@@ -40,17 +40,17 @@ func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags() flags := ctx.ParsedFlags()
mod.disableRoutes = flags.MustBool("libreoffice-disable-routes") mod.disableRoutes = flags.MustBool("libreoffice-disable-routes")
provider, err := ctx.Module(new(unoconv.Provider)) provider, err := ctx.Module(new(uno.Provider))
if err != nil { if err != nil {
return fmt.Errorf("get unoconv provider: %w", err) return fmt.Errorf("get unoAPI provider: %w", err)
} }
uno, err := provider.(unoconv.Provider).Unoconv() unoAPI, err := provider.(uno.Provider).UNO()
if err != nil { if err != nil {
return fmt.Errorf("get unoconv API: %w", err) return fmt.Errorf("get unoAPI API: %w", err)
} }
mod.unoconv = uno mod.unoAPI = unoAPI
provider, err = ctx.Module(new(gotenberg.PDFEngineProvider)) provider, err = ctx.Module(new(gotenberg.PDFEngineProvider))
if err != nil { if err != nil {
@@ -74,7 +74,7 @@ func (mod LibreOffice) Routes() ([]api.Route, error) {
} }
return []api.Route{ return []api.Route{
convertRoute(mod.unoconv, mod.engine), convertRoute(mod.unoAPI, mod.engine),
}, nil }, nil
} }

View File

@@ -1,68 +1,14 @@
package libreoffice package libreoffice
import ( import (
"context"
"errors" "errors"
"reflect" "reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"go.uber.org/zap"
) )
type ProtoModule struct {
descriptor func() gotenberg.ModuleDescriptor
}
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
return mod.descriptor()
}
type ProtoUnoconvProvider struct {
ProtoModule
unoconv func() (unoconv.API, error)
}
func (mod ProtoUnoconvProvider) Unoconv() (unoconv.API, error) {
return mod.unoconv()
}
type ProtoUnoconvAPI struct {
pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ unoconv.Options) error
extensions func() []string
}
func (mod ProtoUnoconvAPI) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error {
return mod.pdf(ctx, logger, inputPath, outputPath, options)
}
func (mod ProtoUnoconvAPI) Extensions() []string {
return mod.extensions()
}
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 TestLibreOffice_Descriptor(t *testing.T) { func TestLibreOffice_Descriptor(t *testing.T) {
descriptor := LibreOffice{}.Descriptor() descriptor := LibreOffice{}.Descriptor()
@@ -75,15 +21,38 @@ func TestLibreOffice_Descriptor(t *testing.T) {
} }
func TestLibreOffice_Provision(t *testing.T) { func TestLibreOffice_Provision(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx *gotenberg.Context name string
expectErr bool ctx *gotenberg.Context
expectProvisionErr bool
}{ }{
{ {
name: "nominal behavior",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoModule }{} provider1 := struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} uno.ProviderMock
}{}
provider1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider1
}}
}
provider1.UNOMock = func() (uno.API, error) {
return uno.APIMock{}, nil
}
provider2 := struct {
gotenberg.ModuleMock
gotenberg.PDFEngineProviderMock
}{}
provider2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module {
return provider2
}}
}
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return gotenberg.PDFEngineMock{}, nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -91,41 +60,36 @@ func TestLibreOffice_Provision(t *testing.T) {
FlagSet: new(LibreOffice).Descriptor().FlagSet, FlagSet: new(LibreOffice).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), provider1.Descriptor(),
provider2.Descriptor(),
}, },
) )
}(), }(),
expectErr: true,
}, },
{ {
ctx: func() *gotenberg.Context { name: "no UNO API provider",
mod := struct{ ProtoUnoconvProvider }{} ctx: gotenberg.NewContext(
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ParsedFlags{
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} FlagSet: new(LibreOffice).Descriptor().FlagSet,
} },
mod.unoconv = func() (unoconv.API, error) { []gotenberg.ModuleDescriptor{},
return nil, errors.New("foo") ),
} expectProvisionErr: true,
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(LibreOffice).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
mod.Descriptor(),
},
)
}(),
expectErr: true,
}, },
{ {
name: "no API from UNO API provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoUnoconvProvider }{} provider := struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} uno.ProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
} }
mod.unoconv = func() (unoconv.API, error) { provider.UNOMock = func() (uno.API, error) {
return struct{ ProtoUnoconvAPI }{}, nil return uno.APIMock{}, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -133,28 +97,26 @@ func TestLibreOffice_Provision(t *testing.T) {
FlagSet: new(LibreOffice).Descriptor().FlagSet, FlagSet: new(LibreOffice).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), provider.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectProvisionErr: true,
}, },
{ {
name: "no PDF engine provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod1 := struct{ ProtoUnoconvProvider }{} provider := struct {
mod1.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }} uno.ProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
} }
mod1.unoconv = func() (unoconv.API, error) { provider.UNOMock = func() (uno.API, error) {
return struct{ ProtoUnoconvAPI }{}, nil return uno.APIMock{}, nil
}
mod2 := struct{ ProtoPDFEngineProvider }{}
mod2.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
}
mod2.pdfEngine = func() (gotenberg.PDFEngine, error) {
return nil, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -162,29 +124,39 @@ func TestLibreOffice_Provision(t *testing.T) {
FlagSet: new(LibreOffice).Descriptor().FlagSet, FlagSet: new(LibreOffice).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod1.Descriptor(), provider.Descriptor(),
mod2.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectProvisionErr: true,
}, },
{ {
name: "no PDF engine from PDF engine provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod1 := struct{ ProtoUnoconvProvider }{} provider1 := struct {
mod1.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }} uno.ProviderMock
}{}
provider1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider1
}}
} }
mod1.unoconv = func() (unoconv.API, error) { provider1.UNOMock = func() (uno.API, error) {
return struct{ ProtoUnoconvAPI }{}, nil return uno.APIMock{}, nil
} }
mod2 := struct{ ProtoPDFEngineProvider }{} provider2 := struct {
mod2.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }} gotenberg.PDFEngineProviderMock
}{}
provider2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module {
return provider2
}}
} }
mod2.pdfEngine = func() (gotenberg.PDFEngine, error) { provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return struct{ ProtoPDFEngine }{}, nil return gotenberg.PDFEngineMock{}, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -192,59 +164,60 @@ func TestLibreOffice_Provision(t *testing.T) {
FlagSet: new(LibreOffice).Descriptor().FlagSet, FlagSet: new(LibreOffice).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod1.Descriptor(), provider1.Descriptor(),
mod2.Descriptor(), provider2.Descriptor(),
}, },
) )
}(), }(),
expectProvisionErr: true,
}, },
} { }
mod := new(LibreOffice)
err := mod.Provision(tc.ctx)
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} mod := new(LibreOffice)
err := mod.Provision(tc.ctx)
if !tc.expectErr && err != nil { if tc.expectProvisionErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Error("expected mod.Provision() error, but got none")
} }
if !tc.expectProvisionErr && err != nil {
t.Errorf("expected no error from mod.Provision(), but got: %v", err)
}
})
} }
} }
func TestLibreOffice_Routes(t *testing.T) { func TestLibreOffice_Routes(t *testing.T) {
for i, tc := range []struct { tests := []struct {
expectRoutes int name string
disableRoutes bool mod LibreOffice
expectRoutesCount int
}{ }{
{ {
expectRoutes: 1, name: "route not disabled",
mod: LibreOffice{},
expectRoutesCount: 1,
}, },
{ {
disableRoutes: true, name: "route disabled",
mod: LibreOffice{
disableRoutes: true,
},
}, },
} { }
mod := new(LibreOffice)
mod.disableRoutes = tc.disableRoutes
routes, err := mod.Routes() for _, tc := range tests {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatalf("test %d: expected no error but got: %v", i, err) routes, err := tc.mod.Routes()
} if err != nil {
t.Fatalf("expected no error from mod.Routes(), but got: %v", err)
}
if tc.expectRoutes != len(routes) { if tc.expectRoutesCount != len(routes) {
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes)) t.Errorf("expected %d routes from mod.Routes(), but got %d", tc.expectRoutesCount, len(routes))
} }
})
} }
} }
// Interface guards.
var (
_ gotenberg.Module = (*ProtoModule)(nil)
_ unoconv.Provider = (*ProtoUnoconvProvider)(nil)
_ gotenberg.Module = (*ProtoUnoconvProvider)(nil)
_ unoconv.API = (*ProtoUnoconvAPI)(nil)
_ gotenberg.PDFEngineProvider = (*ProtoPDFEngineProvider)(nil)
_ gotenberg.Module = (*ProtoPDFEngineProvider)(nil)
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
)

View File

@@ -1,3 +1,4 @@
// Package pdfengine provides a module which abstracts the CLI tool unoconv and // Package pdfengine provides a module which interacts with the UNO
// implements the gotenberg.PDFEngine interface. // (Universal Network Objects) API and implements the gotenberg.PDFEngine
// interface.
package pdfengine package pdfengine

View File

@@ -2,75 +2,76 @@ package pdfengine
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"go.uber.org/zap" "go.uber.org/zap"
) )
func init() { func init() {
gotenberg.MustRegisterModule(UnoconvPDFEngine{}) gotenberg.MustRegisterModule(UNO{})
} }
// UnoconvPDFEngine abstracts the CLI tool unoconv and implements the // UNO interacts with the UNO (Universal Network Objects) API and implements
// gotenberg.PDFEngine interface. // the gotenberg.PDFEngine interface.
type UnoconvPDFEngine struct { type UNO struct {
unoconv unoconv.API unoAPI uno.API
} }
// Descriptor returns a UnoconvPDFEngine's module descriptor. // Descriptor returns a UNO's module descriptor.
func (UnoconvPDFEngine) Descriptor() gotenberg.ModuleDescriptor { func (UNO) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ return gotenberg.ModuleDescriptor{
ID: "unoconv-pdfengine", ID: "uno-pdfengine",
New: func() gotenberg.Module { return new(UnoconvPDFEngine) }, New: func() gotenberg.Module { return new(UNO) },
} }
} }
// Provision sets the module properties. // Provision sets the module properties.
func (engine *UnoconvPDFEngine) Provision(ctx *gotenberg.Context) error { func (engine *UNO) Provision(ctx *gotenberg.Context) error {
provider, err := ctx.Module(new(unoconv.Provider)) provider, err := ctx.Module(new(uno.Provider))
if err != nil { if err != nil {
return fmt.Errorf("get unoconv provider: %w", err) return fmt.Errorf("get unoconv provider: %w", err)
} }
uno, err := provider.(unoconv.Provider).Unoconv() unoAPI, err := provider.(uno.Provider).UNO()
if err != nil { if err != nil {
return fmt.Errorf("get unoconv API: %w", err) return fmt.Errorf("get unoconv API: %w", err)
} }
engine.unoconv = uno engine.unoAPI = unoAPI
return nil return nil
} }
// Merge is not available for this PDF engine. // Merge is not available for this PDF engine.
func (engine UnoconvPDFEngine) Merge(_ context.Context, _ *zap.Logger, _ []string, _ string) error { func (engine UNO) Merge(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
return fmt.Errorf("merge PDFs with unoconv: %w", gotenberg.ErrPDFEngineMethodNotAvailable) return fmt.Errorf("merge PDFs with unoconv: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
} }
// Convert converts the given PDF to a specific PDF format. Currently, only the // Convert converts the given PDF to a specific PDF format. Currently, only the
// PDF/A-1 format is available. If another PDF format is requested, it returns // PDF/A-1a, PDF/A-2b and PDF/A-3b formats are available. If another PDF format
// a gotenberg.ErrPDFFormatNotAvailable error. // is requested, it returns a gotenberg.ErrPDFFormatNotAvailable error.
func (engine UnoconvPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error { func (engine UNO) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
if format != gotenberg.FormatPDFA1a { err := engine.unoAPI.PDF(ctx, logger, inputPath, outputPath, uno.Options{
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, gotenberg.ErrPDFFormatNotAvailable) PDFformat: format,
}
err := engine.unoconv.PDF(ctx, logger, inputPath, outputPath, unoconv.Options{
PDFArchive: true,
}) })
if err == nil { if err == nil {
return nil return nil
} }
if errors.Is(err, uno.ErrInvalidPDFformat) {
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, gotenberg.ErrPDFFormatNotAvailable)
}
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err) return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err)
} }
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*UnoconvPDFEngine)(nil) _ gotenberg.Module = (*UNO)(nil)
_ gotenberg.Provisioner = (*UnoconvPDFEngine)(nil) _ gotenberg.Provisioner = (*UNO)(nil)
_ gotenberg.PDFEngine = (*UnoconvPDFEngine)(nil) _ gotenberg.PDFEngine = (*UNO)(nil)
) )

View File

@@ -7,191 +7,168 @@ import (
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
flag "github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
) )
type ProtoModule struct { func TestUNO_Descriptor(t *testing.T) {
descriptor func() gotenberg.ModuleDescriptor descriptor := UNO{}.Descriptor()
}
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
return mod.descriptor()
}
type ProtoUnoconvProvider struct {
ProtoModule
unoconv func() (unoconv.API, error)
}
func (mod ProtoUnoconvProvider) Unoconv() (unoconv.API, error) {
return mod.unoconv()
}
type ProtoUnoconvAPI struct {
pdf func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error
}
func (mod ProtoUnoconvAPI) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error {
return mod.pdf(ctx, logger, inputPath, outputPath, options)
}
func (mod ProtoUnoconvAPI) Extensions() []string {
return nil
}
func TestUnoconvPDFEngine_Descriptor(t *testing.T) {
descriptor := UnoconvPDFEngine{}.Descriptor()
actual := reflect.TypeOf(descriptor.New()) actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(UnoconvPDFEngine)) expect := reflect.TypeOf(new(UNO))
if actual != expect { if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual) t.Errorf("expected '%s' but got '%s'", expect, actual)
} }
} }
func TestUnoconvPDFEngine_Provision(t *testing.T) { func TestUNO_Provider(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx *gotenberg.Context name string
expectErr bool ctx *gotenberg.Context
expectProvisionErr bool
}{ }{
{ {
name: "nominal behavior",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoModule }{} provider := struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} uno.ProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.UNOMock = func() (uno.API, error) {
return uno.APIMock{}, nil
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError), FlagSet: new(UNO).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), provider.Descriptor(),
}, },
) )
}(), }(),
expectErr: true,
}, },
{ {
ctx: func() *gotenberg.Context { name: "no UNO API provider",
mod := struct{ ProtoUnoconvProvider }{} ctx: gotenberg.NewContext(
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ParsedFlags{
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} FlagSet: new(UNO).Descriptor().FlagSet,
} },
mod.unoconv = func() (unoconv.API, error) { []gotenberg.ModuleDescriptor{},
return nil, errors.New("foo") ),
} expectProvisionErr: true,
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError),
},
[]gotenberg.ModuleDescriptor{
mod.Descriptor(),
},
)
}(),
expectErr: true,
}, },
{ {
name: "no API from UNO API provider",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
mod := struct{ ProtoUnoconvProvider }{} provider := struct {
mod.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} uno.ProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
} }
mod.unoconv = func() (unoconv.API, error) { provider.UNOMock = func() (uno.API, error) {
return struct{ ProtoUnoconvAPI }{}, nil return uno.APIMock{}, errors.New("foo")
} }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError), FlagSet: new(UNO).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
mod.Descriptor(), provider.Descriptor(),
}, },
) )
}(), }(),
expectProvisionErr: true,
}, },
} { }
mod := new(UnoconvPDFEngine)
err := mod.Provision(tc.ctx)
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} mod := new(UNO)
err := mod.Provision(tc.ctx)
if !tc.expectErr && err != nil { if tc.expectProvisionErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Error("expected mod.Provision() error, but got none")
} }
if !tc.expectProvisionErr && err != nil {
t.Errorf("expected no error from mod.Provision(), but got: %v", err)
}
})
} }
} }
func TestUnoconvPDFEngine_Merge(t *testing.T) { func TestUNO_Merge(t *testing.T) {
mod := new(UnoconvPDFEngine) mod := new(UNO)
err := mod.Merge(context.TODO(), zap.NewNop(), nil, "") err := mod.Merge(context.Background(), zap.NewNop(), nil, "")
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) { if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err) t.Errorf("expected error %v from mod.Merge(), but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
} }
} }
func TestUnoconvPDFEngine_Convert(t *testing.T) { func TestUNO_Convert(t *testing.T) {
for i, tc := range []struct { tests := []struct {
api unoconv.API name string
format string mod UNO
expectErr bool expectConvertErr bool
}{ }{
{ {
format: "", name: "nominal behavior",
expectErr: true, mod: UNO{
unoAPI: uno.APIMock{
PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error {
return nil
},
},
},
}, },
{ {
api: func() unoconv.API { name: "invalid PDF format",
unoconvAPI := struct{ ProtoUnoconvAPI }{} mod: UNO{
unoconvAPI.pdf = func(_ context.Context, _ *zap.Logger, _, __ string, _ unoconv.Options) error { unoAPI: uno.APIMock{
return errors.New("foo") PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error {
} return uno.ErrInvalidPDFformat
},
return unoconvAPI },
}(), },
format: gotenberg.FormatPDFA1a, expectConvertErr: true,
expectErr: true,
}, },
{ {
api: func() unoconv.API { name: "convert fail",
unoconvAPI := struct{ ProtoUnoconvAPI }{} mod: UNO{
unoconvAPI.pdf = func(_ context.Context, _ *zap.Logger, _, __ string, _ unoconv.Options) error { unoAPI: uno.APIMock{
return nil PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error {
} return errors.New("foo")
},
return unoconvAPI },
}(), },
format: gotenberg.FormatPDFA1a, expectConvertErr: true,
}, },
} { }
mod := new(UnoconvPDFEngine)
mod.unoconv = tc.api
err := mod.Convert(context.TODO(), zap.NewNop(), tc.format, "", "") for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.mod.Convert(context.Background(), zap.NewNop(), "", "", "")
if tc.expectErr && err == nil { if tc.expectConvertErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("expected mod.Convert() error, but got none")
} }
if !tc.expectErr && err != nil { if !tc.expectConvertErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Fatalf("expected no error from mod.Convert(), but got: %v", err)
} }
})
} }
} }
// Interface guards.
var (
_ gotenberg.Module = (*ProtoModule)(nil)
_ unoconv.Provider = (*ProtoUnoconvProvider)(nil)
_ gotenberg.Module = (*ProtoUnoconvProvider)(nil)
_ unoconv.API = (*ProtoUnoconvAPI)(nil)
)

View File

@@ -7,13 +7,13 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv" "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// convertRoute returns an api.Route which can convert LibreOffice documents // convertRoute returns an api.Route which can convert LibreOffice documents
// to PDF. // to PDF.
func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route { func convertRoute(unoAPI uno.API, engine gotenberg.PDFEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/libreoffice/convert", Path: "/forms/libreoffice/convert",
@@ -27,15 +27,17 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
landscape bool landscape bool
nativePageRanges string nativePageRanges string
nativePDFA1aFormat bool nativePDFA1aFormat bool
nativePDFformat string
PDFformat string PDFformat string
merge bool merge bool
) )
err := ctx.FormData(). err := ctx.FormData().
MandatoryPaths(uno.Extensions(), &inputPaths). MandatoryPaths(unoAPI.Extensions(), &inputPaths).
Bool("landscape", &landscape, false). Bool("landscape", &landscape, false).
String("nativePageRanges", &nativePageRanges, ""). String("nativePageRanges", &nativePageRanges, "").
Bool("nativePdfA1aFormat", &nativePDFA1aFormat, false). Bool("nativePdfA1aFormat", &nativePDFA1aFormat, false).
String("nativePdfFormat", &nativePDFformat, "").
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &PDFformat, "").
Bool("merge", &merge, false). Bool("merge", &merge, false).
Validate() Validate()
@@ -44,13 +46,35 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
if nativePDFA1aFormat {
ctx.Log().Warn("'nativePdfA1aFormat' is deprecated; prefer 'nativePdfFormat' or 'pdfFormat' form fields instead")
}
if nativePDFA1aFormat && nativePDFformat != "" {
return api.WrapError(
errors.New("got both 'nativePdfFormat' and 'nativePdfA1aFormat' form fields"),
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'nativePdfFormat' and 'nativePdfA1aFormat' form fields are provided"),
)
}
if nativePDFA1aFormat && PDFformat != "" { if nativePDFA1aFormat && PDFformat != "" {
return api.WrapError( return api.WrapError(
errors.New("got both 'pdfFormat' and 'nativePdfA1aFormat' form values"), errors.New("got both 'pdfFormat' and 'nativePdfA1aFormat' form fields"),
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'pdfFormat' and 'nativePdfA1aFormat' form values are provided"), api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'pdfFormat' and 'nativePdfA1aFormat' form fields are provided"),
) )
} }
if nativePDFformat != "" && PDFformat != "" {
return api.WrapError(
errors.New("got both 'pdfFormat' and 'nativePdfFormat' form fields"),
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'pdfFormat' and 'nativePdfFormat' form fields are provided"),
)
}
if nativePDFA1aFormat {
nativePDFformat = gotenberg.FormatPDFA1a
}
// Alright, let's convert each document to PDF. // Alright, let's convert each document to PDF.
outputPaths := make([]string, len(inputPaths)) outputPaths := make([]string, len(inputPaths))
@@ -58,16 +82,16 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf") outputPaths[i] = ctx.GeneratePath(".pdf")
options := unoconv.Options{ options := uno.Options{
Landscape: landscape, Landscape: landscape,
PageRanges: nativePageRanges, PageRanges: nativePageRanges,
PDFArchive: nativePDFA1aFormat, PDFformat: nativePDFformat,
} }
err = uno.PDF(ctx, ctx.Log(), inputPath, outputPaths[i], options) err = unoAPI.PDF(ctx, ctx.Log(), inputPath, outputPaths[i], options)
if err != nil { if err != nil {
if errors.Is(err, unoconv.ErrMalformedPageRanges) { if errors.Is(err, uno.ErrMalformedPageRanges) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)), api.NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)),
@@ -92,10 +116,8 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
// Now, let's check if the client want to convert this result // Now, let's check if the client want to convert this result
// PDF to a specific PDF format. // PDF to a specific PDF format.
// Note: nativePdfA1aFormat has not been specified if we reach // Note: nativePdfA1aFormat/nativePdfFormat have not been
// this part of the code. Indeed, the handler returns early on // specified if PDFformat is not empty.
// an error if both nativePdfA1aFormat and pdfFormat are
// present.
if PDFformat != "" { if PDFformat != "" {
convertInputPath := outputPath convertInputPath := outputPath
@@ -135,9 +157,8 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
// Ok, we don't have to merge the PDFs. Let's check if the client // Ok, we don't have to merge the PDFs. Let's check if the client
// want to convert each PDF to a specific PDF format. // want to convert each PDF to a specific PDF format.
// Note: nativePdfA1aFormat has not been specified if we reach this // Note: nativePdfA1aFormat/nativePdfFormat have not been
// part of the code. Indeed, the handler returns early on an error // specified if PDFformat is not empty.
// if both nativePdfA1aFormat and pdfFormat are present.
if PDFformat != "" { if PDFformat != "" {
convertOutputPaths := make([]string, len(outputPaths)) convertOutputPaths := make([]string, len(outputPaths))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
// Package uno provides a module which interacts with the UNO
// (Universal Network Objects) API.
package uno

View File

@@ -0,0 +1,31 @@
package uno
import (
"fmt"
"net"
"strconv"
"go.uber.org/zap"
)
func freePort(logger *zap.Logger) (int, error) {
netListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, fmt.Errorf("listen on the local network address: %w", err)
}
defer func() {
err := netListener.Close()
if err != nil {
logger.Error(fmt.Sprintf("close network listener: %s", err.Error()))
}
}()
addr := netListener.Addr().String()
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
return 0, fmt.Errorf("get free port from host: %w", err)
}
return strconv.Atoi(portStr)
}

View File

@@ -0,0 +1,379 @@
package uno
import (
"context"
"fmt"
"net"
"os"
"sync"
"time"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
)
type listener interface {
start(logger *zap.Logger) error
stop(logger *zap.Logger) error
restart(logger *zap.Logger) error
lock(ctx context.Context, logger *zap.Logger) error
unlock(logger *zap.Logger) error
port() int
queue() int
healthy() bool
}
// TODO: this implementation, even if it's working, is way too complex.
type libreOfficeListener struct {
binPath string
startTimeout time.Duration
threshold int
socketPort int
userProfileDirPath string
cmd gotenberg.Cmd
cfgMu sync.RWMutex
usage int
hadFirstStart bool
hadFirstStartMu sync.RWMutex
restarting bool
restartingMu sync.RWMutex
queueLength int
queueLengthMu sync.RWMutex
lockChan chan struct{}
logger *zap.Logger
}
func newLibreOfficeListener(logger *zap.Logger, binPath string, startTimeout time.Duration, threshold int) listener {
return &libreOfficeListener{
binPath: binPath,
startTimeout: startTimeout,
threshold: threshold,
lockChan: make(chan struct{}, 1),
logger: logger.Named("listener"),
}
}
func (listener *libreOfficeListener) start(logger *zap.Logger) error {
listener.hadFirstStartMu.Lock()
listener.hadFirstStart = true
listener.hadFirstStartMu.Unlock()
port, err := freePort(logger)
if err != nil {
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()
args := []string{
"--headless",
"--invisible",
"--nocrashreport",
"--nodefault",
"--nologo",
"--nofirststartwizard",
"--norestore",
fmt.Sprintf("-env:UserInstallation=file://%s", userProfileDirPath),
fmt.Sprintf("--accept=socket,host=127.0.0.1,port=%d,tcpNoDelay=1;urp;StarOffice.ComponentContext", port),
}
ctx, cancel := context.WithTimeout(context.Background(), listener.startTimeout)
defer cancel()
cmd, err := gotenberg.CommandContext(ctx, logger, listener.binPath, args...)
if err != nil {
return fmt.Errorf("create LibreOffice listener command: %w", err)
}
// For whatever reason, LibreOffice requires a first start before being
// able to run as a daemon.
exitCode, err := cmd.Exec()
if err != nil && exitCode != 81 {
return fmt.Errorf("execute LibreOffice listener: %w", err)
}
logger.Debug("got exit code 81, e.g., LibreOffice listener first start")
// Second start (daemon).
cmd = gotenberg.Command(logger, listener.binPath, args...)
err = cmd.Start()
if err != nil {
return fmt.Errorf("start LibreOffice listener: %w", err)
}
waitChan := make(chan error, 1)
go func() {
// By waiting the process, we avoid the creation of a zombie process
// and make sure we catch an early exit if any.
waitChan <- cmd.Wait()
}()
connChan := make(chan error, 1)
go func() {
// As the LibreOffice socket may take some time to be available, we
// have to ensure that it is indeed accepting connections.
for {
if ctx.Err() != nil {
connChan <- ctx.Err()
break
}
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Duration(1)*time.Second)
if err != nil {
continue
}
connChan <- nil
err = conn.Close()
if err != nil {
logger.Debug(fmt.Sprintf("close connection after health checking the LibreOffice listener: %v", err))
}
break
}
}()
var success bool
defer func() {
if success {
listener.cfgMu.Lock()
listener.socketPort = port
listener.userProfileDirPath = userProfileDirPath
listener.cmd = cmd
listener.cfgMu.Unlock()
return
}
// Let's make sure the process is killed.
err = cmd.Kill()
if err != nil {
logger.Debug(fmt.Sprintf("kill LibreOffice listener process: %v", err))
}
}()
logger.Debug("waiting for the LibreOffice listener socket to be available...")
for {
select {
case err = <-connChan:
if err != nil {
return fmt.Errorf("LibreOffice listener socket not available: %w", err)
}
logger.Debug("LibreOffice listener socket available")
success = true
return nil
case err = <-waitChan:
return fmt.Errorf("LibreOffice listener process exited: %w", err)
}
}
}
func (listener *libreOfficeListener) stop(logger *zap.Logger) error {
listener.cfgMu.RLock()
defer func() {
defer listener.cfgMu.RUnlock()
err := os.RemoveAll(listener.userProfileDirPath)
if err != nil {
logger.Error(fmt.Sprintf("remove LibreOffice listener user profile directory: %v", err))
}
}()
err := listener.cmd.Kill()
if err != nil {
return fmt.Errorf("kill LibreOffice listener process: %w", err)
}
return nil
}
func (listener *libreOfficeListener) restart(logger *zap.Logger) error {
listener.restartingMu.Lock()
listener.restarting = true
listener.restartingMu.Unlock()
defer func() {
listener.restartingMu.Lock()
listener.restarting = false
listener.restartingMu.Unlock()
}()
err := listener.stop(logger)
if err != nil {
return fmt.Errorf("stop LibreOffice listener: %w", err)
}
err = listener.start(logger)
if err != nil {
return fmt.Errorf("start LibreOffice listener: %w", err)
}
listener.usage = 0
return nil
}
func (listener *libreOfficeListener) lock(ctx context.Context, logger *zap.Logger) error {
listener.queueLengthMu.Lock()
listener.queueLength += 1
listener.queueLengthMu.Unlock()
defer func() {
listener.queueLengthMu.Lock()
listener.queueLength -= 1
listener.queueLengthMu.Unlock()
}()
doWithContext := func(ctx context.Context, do func() error) error {
doChan := make(chan error, 1)
go func() {
doChan <- do()
}()
for {
select {
case err := <-doChan:
return err
case <-ctx.Done():
return ctx.Err()
}
}
}
select {
case listener.lockChan <- struct{}{}:
logger.Debug("LibreOffice listener lock acquired")
listener.hadFirstStartMu.RLock()
if !listener.hadFirstStart {
listener.hadFirstStartMu.RUnlock()
logger.Debug("starting LibreOffice listener...")
err := doWithContext(ctx, func() error {
return listener.start(logger)
})
if err == nil {
return nil
}
return fmt.Errorf("start long-running LibreOffice listener: %w", err)
}
listener.hadFirstStartMu.RUnlock()
if !listener.healthy() {
logger.Debug("LibreOffice listener is unhealthy, restarting it...")
err := doWithContext(ctx, func() error {
return listener.restart(logger)
})
if err == nil {
return nil
}
return fmt.Errorf("restart long-running LibreOffice listener: %w", err)
}
return nil
case <-ctx.Done():
logger.Debug("failed to acquire LibreOffice listener lock before deadline")
return fmt.Errorf("acquire LibreOffice listener lock: %w", ctx.Err())
}
}
func (listener *libreOfficeListener) unlock(logger *zap.Logger) error {
defer func() {
<-listener.lockChan
logger.Debug("LibreOffice listener lock released")
}()
if !listener.healthy() {
logger.Debug("LibreOffice listener is unhealthy, restarting it...")
err := listener.restart(logger)
if err == nil {
return nil
}
return fmt.Errorf("restart LibreOffice listener: %w", err)
}
listener.usage += 1
if listener.usage < listener.threshold {
return nil
}
logger.Debug("LibreOffice listener threshold reached, restarting it...")
err := listener.restart(logger)
if err == nil {
return nil
}
return fmt.Errorf("restart LibreOffice listener: %w", err)
}
func (listener *libreOfficeListener) port() int {
listener.cfgMu.RLock()
defer listener.cfgMu.RUnlock()
return listener.socketPort
}
func (listener *libreOfficeListener) queue() int {
listener.queueLengthMu.RLock()
defer listener.queueLengthMu.RUnlock()
return listener.queueLength
}
func (listener *libreOfficeListener) healthy() bool {
listener.hadFirstStartMu.RLock()
defer listener.hadFirstStartMu.RUnlock()
if !listener.hadFirstStart {
return true
}
listener.restartingMu.RLock()
defer listener.restartingMu.RUnlock()
if listener.restarting {
return true
}
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", listener.port()), time.Duration(1)*time.Second)
if err == nil {
err := conn.Close()
if err != nil {
listener.logger.Debug(fmt.Sprintf("close connection after health checking the LibreOffice listener: %v", err))
}
return true
}
return false
}
// Interface guards.
var (
_ listener = (*libreOfficeListener)(nil)
)

View File

@@ -0,0 +1,431 @@
package uno
import (
"context"
"os"
"testing"
"time"
"go.uber.org/zap"
)
func TestListener_start(t *testing.T) {
tests := []struct {
name string
listener listener
expectStartErr bool
}{
{
name: "nominal behavior",
listener: newLibreOfficeListener(zap.NewNop(), 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),
expectStartErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.listener.start(zap.NewNop())
if tc.expectStartErr && err == nil {
t.Fatalf("expected listener.start() error, but got none")
}
if !tc.expectStartErr && err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
if err != nil {
if tc.listener.healthy() {
t.Error("expected a non-running LibreOffice listener")
}
return
}
err = tc.listener.stop(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.stop(), but got: %v", err)
}
})
}
}
func TestListener_stop(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
err = listener.stop(zap.NewNop())
if err != nil {
t.Errorf("expected no error from listener.stop(), but got: %v", err)
}
}
func TestListener_restart(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
err = listener.restart(zap.NewNop())
if err != nil {
t.Errorf("expected no error from listener.stop(), but got: %v", err)
}
if !listener.healthy() {
t.Error("expected an healthy LibreOffice listener")
}
}
func TestListener_lock(t *testing.T) {
tests := []struct {
name string
listener listener
ctx context.Context
teardown func(listener listener) error
expectLockErr bool
}{
{
name: "nominal behavior",
listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
return listener
}(),
ctx: context.Background(),
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
},
},
{
name: "first start",
listener: newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10),
ctx: context.Background(),
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
},
},
{
name: "unhealthy listener",
listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
err = listener.stop(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.stop(), but got: %v", err)
}
return listener
}(),
ctx: context.Background(),
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
},
},
{
name: "context done",
listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
err = listener.lock(context.Background(), zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
}
return listener
}(),
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
}(),
expectLockErr: true,
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
defer func() {
err := tc.teardown(tc.listener)
if err != nil {
t.Errorf("expected no error from tc.teardown(), but got: %v", err)
}
}()
err := tc.listener.lock(tc.ctx, zap.NewNop())
if tc.expectLockErr && err == nil {
t.Fatalf("expected listener.lock() error, but got none")
}
if !tc.expectLockErr && err != nil {
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
}
})
}
}
func TestListener_unlock(t *testing.T) {
tests := []struct {
name string
listener listener
teardown func(listener listener) error
}{
{
name: "nominal behavior",
listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
err = listener.lock(context.Background(), zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
}
return listener
}(),
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
},
},
{
name: "unhealthy listener",
listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
err = listener.lock(context.Background(), zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
}
err = listener.stop(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.stop(), but got: %v", err)
}
return listener
}(),
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
},
},
{
name: "threshold reached",
listener: func() listener {
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 1)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
err = listener.lock(context.Background(), zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
}
return listener
}(),
teardown: func(listener listener) error {
return listener.stop(zap.NewNop())
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
defer func() {
err := tc.teardown(tc.listener)
if err != nil {
t.Errorf("expected no error from tc.teardown(), but got: %v", err)
}
}()
err := tc.listener.unlock(zap.NewNop())
if err != nil {
t.Errorf("expected no error from listener.unlock(), but got: %v", err)
}
})
}
}
func TestListener_port(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
port := listener.port()
if port == 0 {
t.Error("expected a non-zero value from listener.port")
}
err = listener.stop(zap.NewNop())
if err != nil {
t.Errorf("expected no error from listener.stop(), but got: %v", err)
}
}
func TestListener_queue(t *testing.T) {
listener := newLibreOfficeListener(
zap.NewNop(),
os.Getenv("LIBREOFFICE_BIN_PATH"),
time.Duration(10)*time.Second,
10,
)
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
defer func() {
err := listener.stop(zap.NewNop())
if err != nil {
t.Errorf("expected no error from listener.stop(), but got: %v", err)
}
}()
queueLength := listener.queue()
if queueLength != 0 {
t.Fatalf("expected a zero value from listener.queue(), but got %d", queueLength)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
err = listener.lock(ctx, zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
}
queueLength = listener.queue()
if queueLength != 0 {
t.Fatalf("expected a zero value from listener.queue(), but got %d", queueLength)
}
go func() {
_ = listener.lock(ctx, zap.NewNop())
}()
time.Sleep(time.Duration(100) * time.Millisecond)
queueLength = listener.queue()
if queueLength != 1 {
t.Fatalf("expected 1 from listener.queue(), but got %d", queueLength)
}
go func() {
_ = listener.lock(ctx, zap.NewNop())
}()
time.Sleep(time.Duration(100) * time.Millisecond)
queueLength = listener.queue()
if queueLength != 2 {
t.Fatalf("expected 2 from listener.queue(), but got %d", queueLength)
}
cancel()
time.Sleep(time.Duration(100) * time.Millisecond)
queueLength = listener.queue()
if queueLength != 0 {
t.Fatalf("expected a zero value from listener.queue(), but got %d", queueLength)
}
}
func TestListener_healthy(t *testing.T) {
listener := &libreOfficeListener{
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
startTimeout: time.Duration(10) * time.Second,
threshold: 10,
lockChan: make(chan struct{}, 1),
logger: zap.NewNop(),
}
// i.e., first start.
if !listener.healthy() {
t.Error("expected an healthy LibreOffice listener")
}
err := listener.start(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.start(), but got: %v", err)
}
if !listener.healthy() {
t.Error("expected an healthy LibreOffice listener")
}
err = listener.stop(zap.NewNop())
if err != nil {
t.Fatalf("expected no error from listener.stop(), but got: %v", err)
}
time.Sleep(time.Duration(1) * time.Second)
if listener.healthy() {
t.Errorf("expected a non-healthy LibreOffice listener")
}
listener.restarting = true
if !listener.healthy() {
t.Error("expected an healthy LibreOffice listener")
}
}

View File

@@ -0,0 +1,36 @@
package uno
import (
"context"
"go.uber.org/zap"
)
// APIMock is a mock for the API interface.
type APIMock struct {
PDFMock func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
ExtensionsMock func() []string
}
func (api APIMock) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
return api.PDFMock(ctx, logger, inputPath, outputPath, options)
}
func (api APIMock) Extensions() []string {
return api.ExtensionsMock()
}
// ProviderMock is a mock for the Provider interface.
type ProviderMock struct {
UNOMock func() (API, error)
}
func (provider ProviderMock) UNO() (API, error) {
return provider.UNOMock()
}
// Interface guards.
var (
_ API = (*APIMock)(nil)
_ Provider = (*ProviderMock)(nil)
)

View File

@@ -0,0 +1,42 @@
package uno
import (
"context"
"testing"
"go.uber.org/zap"
)
func TestAPIMock(t *testing.T) {
mock := APIMock{
PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
return nil
},
ExtensionsMock: func() []string {
return nil
},
}
err := mock.PDF(context.Background(), zap.NewNop(), "", "", Options{})
if err != nil {
t.Errorf("expected no error from mock.PDF(), but got: %v", err)
}
ext := mock.Extensions()
if ext != nil {
t.Errorf("expected no extensions from mock.Extensions(), but got: %+v", ext)
}
}
func TestProviderMock(t *testing.T) {
mock := ProviderMock{
UNOMock: func() (API, error) {
return APIMock{}, nil
},
}
_, err := mock.UNO()
if err != nil {
t.Errorf("expected no error from mock.UNO(), but got: %v", err)
}
}

View File

@@ -0,0 +1,512 @@
package uno
import (
"context"
"errors"
"fmt"
"os"
"sync"
"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"
)
func init() {
gotenberg.MustRegisterModule(UNO{})
}
var (
// ErrInvalidPDFformat happens if the PDF format option cannot be handled
// by LibreOffice.
ErrInvalidPDFformat = errors.New("invalid PDF format")
// ErrMalformedPageRanges happens if the page ranges option cannot be
// interpreted by LibreOffice.
ErrMalformedPageRanges = errors.New("page ranges are malformed")
)
// UNO is a module which provides an API to interact with LibreOffice.
type UNO struct {
unoconvBinPath string
libreOfficeBinPath string
libreOfficeStartTimeout time.Duration
libreOfficeRestartThreshold int
listener listener
logger *zap.Logger
}
// Options gathers available options when converting a document to PDF.
type Options struct {
// Landscape allows to change the orientation of the resulting PDF.
// Optional.
Landscape bool
// PageRanges allows to select the pages to convert.
// TODO: should prefer a method form PDFEngine.
// Optional.
PageRanges string
// PDFformat allows to convert the resulting PDF to PDF/A-1a, PDF/A-2b, or
// PDF/A-3b.
// Optional.
PDFformat string
}
// API is an abstraction on top of uno.
type API interface {
PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
Extensions() []string
}
// Provider is a module interface which exposes a method for creating an API
// for other modules.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(uno.Provider))
// unoAPI, _ := provider.(uno.Provider).UNO()
// }
type Provider interface {
UNO() (API, error)
}
// Descriptor returns a UNO's module descriptor.
func (UNO) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "uno",
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.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")
if err != nil {
panic(fmt.Errorf("create deprecated flags for the uno module: %v", err))
}
return fs
}(),
New: func() gotenberg.Module { return new(UNO) },
}
}
// Provision sets the module properties. It returns an error if the environment
// variables UNOCONV_BIN_PATH and LIBREOFFICE_BIN_PATH are not set.
func (mod *UNO) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags()
mod.libreOfficeStartTimeout = flags.MustDuration("uno-listener-start-timeout")
mod.libreOfficeRestartThreshold = flags.MustInt("uno-listener-restart-threshold")
disableListener := flags.MustBool("unoconv-disable-listener")
if disableListener {
mod.libreOfficeRestartThreshold = 0
}
unoconvBinPath, ok := os.LookupEnv("UNOCONV_BIN_PATH")
if !ok {
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
}
mod.unoconvBinPath = unoconvBinPath
libreOfficeBinPath, ok := os.LookupEnv("LIBREOFFICE_BIN_PATH")
if !ok {
return errors.New("LIBREOFFICE_BIN_PATH environment variable is not set")
}
mod.libreOfficeBinPath = libreOfficeBinPath
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
mod.listener = newLibreOfficeListener(
mod.logger,
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return nil
}
// Validate validates the module properties.
func (mod UNO) Validate() error {
var err error
_, statErr := os.Stat(mod.unoconvBinPath)
if os.IsNotExist(statErr) {
err = multierr.Append(err, fmt.Errorf("unoconv binary path does not exist: %w", statErr))
}
_, statErr = os.Stat(mod.libreOfficeBinPath)
if os.IsNotExist(statErr) {
err = multierr.Append(err, fmt.Errorf("LibreOffice binary path does not exist: %w", statErr))
}
return err
}
// Start does nothing: it is here to validate the contract from the
// gotenberg.App interface. The long-running LibreOffice Listener will be
// started on the first call to PDF.
func (mod UNO) Start() error {
return nil
}
// 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.
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")
<-ctx.Done()
err := mod.listener.stop(mod.logger)
if err == nil {
return nil
}
return fmt.Errorf("stop long-running LibreOffice listener")
}
// Metrics returns the metrics.
func (mod UNO) Metrics() ([]gotenberg.Metric, error) {
return []gotenberg.Metric{
{
Name: "unoconv_active_instances_count",
Description: "Current number of active unoconv instances.",
Read: func() float64 {
activeInstancesCountMu.RLock()
defer activeInstancesCountMu.RUnlock()
return activeInstancesCount
},
},
{
Name: "libreoffice_listener_active_instances_count",
Description: "Current number of active LibreOffice listener instances.",
Read: func() float64 {
if mod.libreOfficeRestartThreshold == 0 {
listenerActiveInstancesCountMu.RLock()
defer listenerActiveInstancesCountMu.RUnlock()
return listenerActiveInstancesCount
}
if mod.listener.healthy() {
return 1
}
return 0
},
},
{
Name: "unoconv_listener_active_instances_count",
Description: "Current number of active unoconv listener instances - deprecated, prefer libreoffice_listener_active_instances_count.",
Read: func() float64 {
if mod.libreOfficeRestartThreshold == 0 {
listenerActiveInstancesCountMu.RLock()
defer listenerActiveInstancesCountMu.RUnlock()
return listenerActiveInstancesCount
}
if mod.listener.healthy() {
return 1
}
return 0
},
},
{
Name: "libreoffice_listener_queue_length",
Description: "Current number of processes in the LibreOffice listener queue.",
Read: func() float64 {
return float64(mod.listener.queue())
},
},
{
Name: "unoconv_listener_queue_length",
Description: "Current number of processes in the queue - deprecated, prefer libreoffice_listener_queue_length.",
Read: func() float64 {
return float64(mod.listener.queue())
},
},
}, nil
}
// Checks adds a health check that verifies the health of the long-running
// LibreOffice listener.
func (mod UNO) Checks() ([]health.CheckerOption, error) {
if mod.libreOfficeRestartThreshold == 0 {
return nil, nil
}
return []health.CheckerOption{
health.WithCheck(health.Check{
Name: "uno",
Check: func(_ context.Context) error {
if mod.listener.healthy() {
return nil
}
return errors.New("long-running LibreOffice listener unhealthy")
},
}),
}, 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.
func (mod UNO) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
args := []string{
"--no-launch",
"--format",
"pdf",
}
switch mod.libreOfficeRestartThreshold {
case 0:
listener := newLibreOfficeListener(logger, mod.libreOfficeBinPath, mod.libreOfficeStartTimeout, 0)
err := listener.start(logger)
if err != nil {
return fmt.Errorf("start LibreOffice listener: %w", err)
}
defer func() {
err := listener.stop(logger)
if err != nil {
logger.Error(fmt.Sprintf("stop LibreOffice listener: %v", err))
}
}()
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()))
}
checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
if checkedEntry != nil {
args = append(args, "-vvv")
}
if options.Landscape {
args = append(args, "--printer", "PaperOrientation=landscape")
}
if options.PageRanges != "" {
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
}
switch options.PDFformat {
case "":
case gotenberg.FormatPDFA1a:
args = append(args, "--export", "SelectPdfVersion=1")
case gotenberg.FormatPDFA2b:
args = append(args, "--export", "SelectPdfVersion=2")
case gotenberg.FormatPDFA3b:
args = append(args, "--export", "SelectPdfVersion=3")
default:
return ErrInvalidPDFformat
}
args = append(args, "--output", outputPath, inputPath)
cmd, err := gotenberg.CommandContext(ctx, logger, mod.unoconvBinPath, args...)
if err != nil {
return fmt.Errorf("create unoconv command: %w", err)
}
logger.Debug(fmt.Sprintf("print to PDF with: %+v", options))
activeInstancesCountMu.Lock()
activeInstancesCount += 1
activeInstancesCountMu.Unlock()
exitCode, err := cmd.Exec()
activeInstancesCountMu.Lock()
activeInstancesCount -= 1
activeInstancesCountMu.Unlock()
if err == nil {
return nil
}
// Unoconv/LibreOffice errors are not explicit.
// That's why we have to make an educated guess according to the exit code
// and given inputs.
if exitCode == 5 && options.PageRanges != "" {
return ErrMalformedPageRanges
}
// Possible errors:
// 1. Unoconv/LibreOffice failed for some reason.
// 2. Context done.
//
// On the second scenario, LibreOffice might not have time to remove some
// of its temporary files, as it has been killed without warning. The
// garbage collector will delete them for us (if the module is loaded).
return fmt.Errorf("unoconv PDF: %w", err)
}
// Extensions returns the file extensions available for conversions.
func (mod UNO) Extensions() []string {
return []string{
".bib",
".doc",
".xml",
".docx",
".fodt",
".html",
".ltx",
".txt",
".odt",
".ott",
".pdb",
".pdf",
".psw",
".rtf",
".sdw",
".stw",
".sxw",
".uot",
".vor",
".wps",
".epub",
".png",
".bmp",
".emf",
".eps",
".fodg",
".gif",
".jpg",
".jpeg",
".met",
".odd",
".otg",
".pbm",
".pct",
".pgm",
".ppm",
".ras",
".std",
".svg",
".svm",
".swf",
".sxd",
".sxw",
".tif",
".tiff",
".xhtml",
".xpm",
".odp",
".fodp",
".potm",
".pot",
".pptx",
".pps",
".ppt",
".pwp",
".sda",
".sdd",
".sti",
".sxi",
".uop",
".wmf",
".csv",
".dbf",
".dif",
".fods",
".ods",
".ots",
".pxl",
".sdc",
".slk",
".stc",
".sxc",
".uos",
".xls",
".xlt",
".xlsx",
}
}
// UNO returns an API for interacting with LibreOffice.
func (mod UNO) UNO() (API, error) {
return mod, nil
}
var (
listenerActiveInstancesCount float64
listenerActiveInstancesCountMu sync.RWMutex
activeInstancesCount float64
activeInstancesCountMu sync.RWMutex
)
// Interface guards.
var (
_ gotenberg.Module = (*UNO)(nil)
_ gotenberg.Provisioner = (*UNO)(nil)
_ gotenberg.Validator = (*UNO)(nil)
_ gotenberg.App = (*UNO)(nil)
_ gotenberg.MetricsProvider = (*UNO)(nil)
_ api.HealthChecker = (*UNO)(nil)
_ API = (*UNO)(nil)
_ Provider = (*UNO)(nil)
)

View File

@@ -0,0 +1,817 @@
package uno
import (
"context"
"errors"
"os"
"reflect"
"testing"
"time"
"github.com/alexliesenfeld/health"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
flag "github.com/spf13/pflag"
"go.uber.org/zap"
)
func TestUNO_Descriptor(t *testing.T) {
descriptor := UNO{}.Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(UNO))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestUNO_Provision(t *testing.T) {
tests := []struct {
name string
ctx *gotenberg.Context
expectProvisionErr bool
}{
{
name: "nominal behavior",
ctx: func() *gotenberg.Context {
provider := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(UNO).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
},
)
}(),
},
{
name: "threshold from deprecated flag --unoconv-disable-listener",
ctx: func() *gotenberg.Context {
provider := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
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)
}
return fs
}(),
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
},
)
}(),
},
{
name: "no logger provider",
ctx: func() *gotenberg.Context {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(UNO).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{},
)
}(),
expectProvisionErr: true,
},
{
name: "no logger from logger provider",
ctx: func() *gotenberg.Context {
provider := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return nil, errors.New("foo")
}
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(UNO).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
},
)
}(),
expectProvisionErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mod := new(UNO)
err := mod.Provision(tc.ctx)
if tc.expectProvisionErr && err == nil {
t.Errorf("expected mod.Provision() error, but got none")
}
if !tc.expectProvisionErr && err != nil {
t.Errorf("expected no error from mod.Provision(), but got: %v", err)
}
})
}
}
func TestUNO_Validate(t *testing.T) {
tests := []struct {
name string
unoconvBinPath string
libreOfficeBinPath string
expectValidateErr bool
}{
{
name: "nominal behavior",
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
},
{
name: "unoconv bin path does not exist",
unoconvBinPath: "/foo",
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
expectValidateErr: true,
},
{
name: "LibreOffice bin path does not exist",
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
libreOfficeBinPath: "/foo",
expectValidateErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mod := UNO{
unoconvBinPath: tc.unoconvBinPath,
libreOfficeBinPath: tc.libreOfficeBinPath,
}
err := mod.Validate()
if tc.expectValidateErr && err == nil {
t.Errorf("expected mod.Validate() error, but got none")
}
if !tc.expectValidateErr && err != nil {
t.Errorf("expected no error from mod.Validate(), but got: %v", err)
}
})
}
}
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",
},
}
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)
}
})
}
}
func TestUNO_Stop(t *testing.T) {
tests := []struct {
name string
mod UNO
expectStopErr bool
}{
{
name: "nominal behavior",
mod: UNO{
libreOfficeRestartThreshold: 10,
listener: listenerMock{
stopMock: func(logger *zap.Logger) error {
return nil
},
},
logger: zap.NewNop(),
},
},
{
name: "no long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 0,
logger: zap.NewNop(),
},
},
{
name: "stop error",
mod: UNO{
libreOfficeRestartThreshold: 10,
listener: listenerMock{
stopMock: func(logger *zap.Logger) error {
return errors.New("foo")
},
},
logger: zap.NewNop(),
},
expectStopErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
cancel()
err := tc.mod.Stop(ctx)
if tc.expectStopErr && err == nil {
t.Errorf("expected mod.Stop() error, but got none")
}
if !tc.expectStopErr && err != nil {
t.Errorf("expected no error from mod.Stop(), but got: %v", err)
}
})
}
}
func TestUNO_Metrics(t *testing.T) {
tests := []struct {
name string
mod UNO
expectUnoconvActiveInstancesCount float64
expectLibreOfficeListenerActiveInstancesCount float64
expectLibreOfficeListenerQueueLength float64
}{
{
name: "with healthy long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 10,
listener: listenerMock{
queueMock: func() int {
return 0
},
healthyMock: func() bool {
return true
},
},
},
expectLibreOfficeListenerActiveInstancesCount: 1,
},
{
name: "with unhealthy long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 10,
listener: listenerMock{
queueMock: func() int {
return 0
},
healthyMock: func() bool {
return false
},
},
},
},
{
name: "with no long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 0,
listener: listenerMock{
queueMock: func() int {
return 0
},
healthyMock: func() bool {
return false
},
},
},
},
{
name: "with a queue of 3",
mod: UNO{
libreOfficeRestartThreshold: 0,
listener: listenerMock{
queueMock: func() int {
return 3
},
healthyMock: func() bool {
return true
},
},
},
expectLibreOfficeListenerQueueLength: 3,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
metrics, err := tc.mod.Metrics()
if err != nil {
t.Fatalf("expected no error from mod.Metrics(), but got: %v", err)
}
for _, metric := range metrics {
switch metric.Name {
case "unoconv_active_instances_count":
actual := metric.Read()
if actual != tc.expectUnoconvActiveInstancesCount {
t.Errorf("expected 'unoconv_active_instances_count' to be %.0f, but got %.0f", tc.expectUnoconvActiveInstancesCount, actual)
}
case "libreoffice_listener_active_instances_count":
actual := metric.Read()
if actual != tc.expectLibreOfficeListenerActiveInstancesCount {
t.Errorf("expected 'libreoffice_listener_active_instances_count' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerActiveInstancesCount, actual)
}
case "unoconv_listener_active_instances_count":
actual := metric.Read()
if actual != tc.expectLibreOfficeListenerActiveInstancesCount {
t.Errorf("expected 'unoconv_listener_active_instances_count' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerActiveInstancesCount, actual)
}
case "libreoffice_listener_queue_length":
actual := metric.Read()
if actual != tc.expectLibreOfficeListenerQueueLength {
t.Errorf("expected 'libreoffice_listener_queue_length' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerQueueLength, actual)
}
case "unoconv_listener_queue_length":
actual := metric.Read()
if actual != tc.expectLibreOfficeListenerQueueLength {
t.Errorf("expected 'unoconv_listener_queue_length' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerQueueLength, actual)
}
}
}
})
}
}
func TestUNO_Checks(t *testing.T) {
tests := []struct {
name string
mod UNO
expectAvailabilityStatus health.AvailabilityStatus
}{
{
name: "no long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 0,
},
},
{
name: "with healthy long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 10,
listener: listenerMock{
healthyMock: func() bool {
return true
},
},
},
expectAvailabilityStatus: health.StatusUp,
},
{
name: "with unhealthy long-running LibreOffice listener",
mod: UNO{
libreOfficeRestartThreshold: 10,
listener: listenerMock{
healthyMock: func() bool {
return false
},
},
},
expectAvailabilityStatus: health.StatusDown,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
checks, err := tc.mod.Checks()
if err != nil {
t.Fatalf("expected no error from mod.Checks(), but got: %v", err)
}
if len(checks) == 0 {
return
}
if len(checks) != 1 {
t.Fatalf("expected 1 check from mod.Checks(), but got %d", len(checks))
}
checker := health.NewChecker(checks...)
result := checker.Check(context.Background())
if result.Status != tc.expectAvailabilityStatus {
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
}
})
}
}
func TestUNO_PDF(t *testing.T) {
tests := []struct {
name string
mod UNO
ctx context.Context
logger *zap.Logger
inputPath string
options Options
expectPDFErr bool
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",
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,
mod.libreOfficeBinPath,
mod.libreOfficeStartTimeout,
mod.libreOfficeRestartThreshold,
)
return mod
}(),
ctx: context.Background(),
logger: zap.NewNop(),
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,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
Landscape: true,
},
},
{
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,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
PageRanges: "1-2",
},
},
{
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,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
PageRanges: "foo",
},
expectPDFErr: true,
},
{
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,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
PDFformat: gotenberg.FormatPDFA1a,
},
},
{
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,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
PDFformat: gotenberg.FormatPDFA2b,
},
},
{
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,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
PDFformat: gotenberg.FormatPDFA3b,
},
},
{
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,
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
PDFformat: "foo",
},
expectPDFErr: true,
},
{
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,
},
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
}(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
expectPDFErr: true,
},
{
name: "cannot lock 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: 10,
listener: listenerMock{
lockMock: func(ctx context.Context, logger *zap.Logger) error {
return errors.New("foo")
},
},
logger: zap.NewNop(),
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
expectPDFErr: true,
},
{
name: "cannot unlock 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: 10,
listener: listenerMock{
lockMock: func(ctx context.Context, logger *zap.Logger) error {
return nil
},
unlockMock: func(logger *zap.Logger) error {
return errors.New("foo")
},
portMock: func() int {
return 2002
},
},
logger: zap.NewNop(),
},
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
expectPDFErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
defer func() {
if tc.teardown == nil {
return
}
err := tc.teardown(tc.mod)
if err != nil {
t.Errorf("expected no error from tc.teardown(), but got: %v", err)
}
}()
outputDir, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("expected no error from gotenberg.MkdirAll(), but got: %v", err)
}
defer func() {
err := os.RemoveAll(outputDir)
if err != nil {
t.Errorf("expected no error from os.RemoveAll(), but got: %v", err)
}
}()
err = tc.mod.PDF(tc.ctx, tc.logger, tc.inputPath, outputDir+"/foo.pdf", tc.options)
if tc.expectPDFErr && err == nil {
t.Fatalf("expected mod.PDF() error, but got none")
}
if !tc.expectPDFErr && err != nil {
t.Fatalf("expected no error from mod.PDF(), but got: %v", err)
}
})
}
}
func TestUNO_Extensions(t *testing.T) {
mod := new(UNO)
extensions := mod.Extensions()
actual := len(extensions)
expect := 76
if actual != expect {
t.Errorf("expected %d extensions, but got %d", expect, actual)
}
}
func TestUNO_UNO(t *testing.T) {
mod := new(UNO)
_, err := mod.UNO()
if err != nil {
t.Errorf("expected no error from mod.UNO(), but got: %v", err)
}
}
type listenerMock struct {
startMock func(logger *zap.Logger) error
stopMock func(logger *zap.Logger) error
restartMock func(logger *zap.Logger) error
lockMock func(ctx context.Context, logger *zap.Logger) error
unlockMock func(logger *zap.Logger) error
portMock func() int
queueMock func() int
healthyMock func() bool
}
func (listener listenerMock) start(logger *zap.Logger) error {
return listener.startMock(logger)
}
func (listener listenerMock) stop(logger *zap.Logger) error {
return listener.stopMock(logger)
}
func (listener listenerMock) restart(logger *zap.Logger) error {
return listener.restartMock(logger)
}
func (listener listenerMock) lock(ctx context.Context, logger *zap.Logger) error {
return listener.lockMock(ctx, logger)
}
func (listener listenerMock) unlock(logger *zap.Logger) error {
return listener.unlockMock(logger)
}
func (listener listenerMock) port() int {
return listener.portMock()
}
func (listener listenerMock) queue() int {
return listener.queueMock()
}
func (listener listenerMock) healthy() bool {
return listener.healthyMock()
}
// Interface guards.
var (
_ listener = (*listenerMock)(nil)
)

View File

@@ -1,2 +0,0 @@
// Package unoconv provides a module which abstracts the CLI tool unoconv.
package unoconv

View File

@@ -1,326 +0,0 @@
package unoconv
import (
"context"
"errors"
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
)
func init() {
gotenberg.MustRegisterModule(Unoconv{})
}
// ErrMalformedPageRanges happens if the page ranges option cannot be
// interpreted by LibreOffice.
var ErrMalformedPageRanges = errors.New("page ranges are malformed")
// Unoconv is a module which provides an API to interact with unoconv.
type Unoconv struct {
binPath string
}
// Options gathers available options when converting a document to PDF.
type Options struct {
// Landscape allows to change the orientation of the resulting PDF.
// Optional.
Landscape bool
// PageRanges allows to select the pages to convert.
// TODO: should prefer a method form PDFEngine.
// Optional.
PageRanges string
// PDFArchive allows to convert the resulting PDF to PDF/A-1a.
// In a module, prefer the Convert method from the gotenberg.PDFEngine
// interface.
// Optional.
PDFArchive bool
}
// API is an abstraction on top of unoconv.
//
// See https://github.com/unoconv/unoconv.
type API interface {
PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
Extensions() []string
}
// Provider is a module interface which exposes a method for creating an API
// for other modules.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(unoconv.Provider))
// uno, _ := provider.(unoconv.Provider).Unoconv()
// }
type Provider interface {
Unoconv() (API, error)
}
// Descriptor returns a Unoconv's module descriptor.
func (Unoconv) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "unoconv",
New: func() gotenberg.Module { return new(Unoconv) },
}
}
// Provision sets the module properties. It returns an error if the environment
// variable UNOCONV_BIN_PATH is not set.
func (mod *Unoconv) Provision(_ *gotenberg.Context) error {
binPath, ok := os.LookupEnv("UNOCONV_BIN_PATH")
if !ok {
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
}
mod.binPath = binPath
return nil
}
// Validate validates the module properties.
func (mod Unoconv) Validate() error {
_, err := os.Stat(mod.binPath)
if os.IsNotExist(err) {
return fmt.Errorf("unoconv binary path does not exist: %w", err)
}
return nil
}
// Metrics returns the metrics.
func (mod Unoconv) Metrics() ([]gotenberg.Metric, error) {
return []gotenberg.Metric{
{
Name: "unoconv_active_instances_count",
Description: "Current number of active LibreOffice instances.",
Read: func() float64 {
activeInstancesCountMu.RLock()
defer activeInstancesCountMu.RUnlock()
return activeInstancesCount
},
},
}, nil
}
// Unoconv returns an API for interacting with unoconv.
func (mod Unoconv) Unoconv() (API, error) {
return mod, nil
}
// PDF converts a document to PDF. It creates a dedicated LibreOffice instance
// thanks to a custom user profile directory and a free port. Substantial calls
// to this method may increase CPU and memory usage drastically. In such a
// scenario, the given context may also be done before the end of the
// conversion.
func (mod Unoconv) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
port, err := func() (int, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, fmt.Errorf("listen on the local network address: %w", err)
}
defer func() {
err := listener.Close()
if err != nil {
logger.Error(fmt.Sprintf("close listener: %s", err.Error()))
}
}()
addr := listener.Addr().String()
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
return 0, fmt.Errorf("get free port from host: %w", err)
}
return strconv.Atoi(portStr)
}()
if err != nil {
return fmt.Errorf("get free port: %w", err)
}
userProfileDirPath := gotenberg.NewDirPath()
args := []string{
"--user-profile",
fmt.Sprintf("//%s", userProfileDirPath),
"--port",
fmt.Sprintf("%d", port),
"--format",
"pdf",
}
checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
if checkedEntry != nil {
args = append(args, "-vvv")
}
if options.Landscape {
args = append(args, "--printer", "PaperOrientation=landscape")
}
if options.PageRanges != "" {
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
}
if options.PDFArchive {
args = append(args, "--export", "SelectPdfVersion=1")
}
args = append(args, "--output", outputPath, inputPath)
cmd, err := gotenberg.CommandContext(ctx, logger, mod.binPath, args...)
if err != nil {
return fmt.Errorf("create unoconv command: %w", err)
}
logger.Debug(fmt.Sprintf("print to PDF with: %+v", options))
activeInstancesCountMu.Lock()
activeInstancesCount += 1
activeInstancesCountMu.Unlock()
err = cmd.Exec()
activeInstancesCountMu.Lock()
activeInstancesCount -= 1
activeInstancesCountMu.Unlock()
// Always remove the user profile directory created by LibreOffice.
// See https://github.com/gotenberg/gotenberg/issues/192.
go func() {
logger.Debug(fmt.Sprintf("remove user profile directory '%s'", userProfileDirPath))
err := os.RemoveAll(userProfileDirPath)
if err != nil {
logger.Error(fmt.Sprintf("remove user profile directory: %s", err))
}
}()
if err == nil {
return nil
}
// Unoconv/LibreOffice errors are not explicit.
// That's why we have to make an educated guess according to the exit code
// and given inputs.
if strings.Contains(err.Error(), "exit status 5") && options.PageRanges != "" {
return ErrMalformedPageRanges
}
// Possible errors:
// 1. Unoconv/LibreOffice failed for some reason.
// 2. Context done.
//
// On the second scenario, LibreOffice might not had time to remove some of
// its temporary files, as it has been killed without warning. The garbage
// collector will delete them for us (if the module is loaded).
return fmt.Errorf("unoconv PDF: %w", err)
}
// Extensions returns the file extensions available with unoconv.
func (mod Unoconv) Extensions() []string {
return []string{
".bib",
".doc",
".xml",
".docx",
".fodt",
".html",
".ltx",
".txt",
".odt",
".ott",
".pdb",
".pdf",
".psw",
".rtf",
".sdw",
".stw",
".sxw",
".uot",
".vor",
".wps",
".epub",
".png",
".bmp",
".emf",
".eps",
".fodg",
".gif",
".jpg",
".jpeg",
".met",
".odd",
".otg",
".pbm",
".pct",
".pgm",
".ppm",
".ras",
".std",
".svg",
".svm",
".swf",
".sxd",
".sxw",
".tif",
".tiff",
".xhtml",
".xpm",
".odp",
".fodp",
".potm",
".pot",
".pptx",
".pps",
".ppt",
".pwp",
".sda",
".sdd",
".sti",
".sxi",
".uop",
".wmf",
".csv",
".dbf",
".dif",
".fods",
".ods",
".ots",
".pxl",
".sdc",
".slk",
".stc",
".sxc",
".uos",
".xls",
".xlt",
".xlsx",
}
}
var (
activeInstancesCount float64
activeInstancesCountMu sync.RWMutex
)
// Interface guards.
var (
_ gotenberg.Module = (*Unoconv)(nil)
_ gotenberg.Provisioner = (*Unoconv)(nil)
_ gotenberg.Validator = (*Unoconv)(nil)
_ gotenberg.MetricsProvider = (*Unoconv)(nil)
_ API = (*Unoconv)(nil)
_ Provider = (*Unoconv)(nil)
)

View File

@@ -1,175 +0,0 @@
package unoconv
import (
"context"
"os"
"reflect"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
)
func TestUnoconv_Descriptor(t *testing.T) {
descriptor := Unoconv{}.Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(Unoconv))
if actual != expect {
t.Errorf("expected '%'s' but got '%s'", expect, actual)
}
}
func TestUnoconv_Provision(t *testing.T) {
mod := new(Unoconv)
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
err := mod.Provision(ctx)
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
func TestUnoconv_Validate(t *testing.T) {
for i, tc := range []struct {
binPath string
expectErr bool
}{
{
expectErr: true,
},
{
binPath: "/foo",
expectErr: true,
},
{
binPath: os.Getenv("UNOCONV_BIN_PATH"),
},
} {
mod := new(Unoconv)
mod.binPath = tc.binPath
err := mod.Validate()
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)
}
}
}
func TestChromium_Metrics(t *testing.T) {
metrics, err := new(Unoconv).Metrics()
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
if len(metrics) != 1 {
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
}
actual := metrics[0].Read()
if actual != 0 {
t.Errorf("expected %d unoconv instances, but got %f", 0, actual)
}
}
func TestUnoconv_Unoconv(t *testing.T) {
mod := new(Unoconv)
_, err := mod.Unoconv()
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
func TestUnoconv_PDF(t *testing.T) {
for i, tc := range []struct {
ctx context.Context
logger *zap.Logger
inputPath string
options Options
expectErr bool
}{
{
logger: zap.NewNop(),
expectErr: true,
},
{
ctx: context.Background(),
logger: zap.NewExample(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
Landscape: true,
PageRanges: "1-2",
PDFArchive: true,
},
},
{
ctx: context.Background(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
options: Options{
PageRanges: "foo",
},
expectErr: true,
},
{
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
return ctx
}(),
logger: zap.NewNop(),
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
expectErr: true,
},
} {
func() {
mod := new(Unoconv)
err := mod.Provision(nil)
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
outputDir, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
defer func() {
err := os.RemoveAll(outputDir)
if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
}()
err = mod.PDF(tc.ctx, tc.logger, tc.inputPath, outputDir+"/foo.pdf", tc.options)
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)
}
}()
}
}
func TestUnoconv_Extensions(t *testing.T) {
mod := new(Unoconv)
extensions := mod.Extensions()
actual := len(extensions)
expect := 76
if actual != expect {
t.Errorf("expected %d extensions but got %d", expect, actual)
}
}

View File

@@ -3,213 +3,178 @@ package pdfengines
import ( import (
"context" "context"
"errors" "errors"
"reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
) )
func TestNewMultiPDFEngines(t *testing.T) {
engine1 := &ProtoPDFEngine{
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
return nil
},
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return nil
},
}
engine2 := &ProtoPDFEngine{
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
return errors.New("foo")
},
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return errors.New("foo")
},
}
multi := newMultiPDFEngines(engine1, engine2)
if len(multi.engines) != 2 {
t.Fatalf("expected %d engines but got %d", 2, len(multi.engines))
}
if !reflect.DeepEqual(engine1, multi.engines[0]) {
t.Errorf("expected %v, but got: %v", engine1, multi.engines[0])
}
if !reflect.DeepEqual(engine2, multi.engines[1]) {
t.Errorf("expected %v, but got: %v", engine2, multi.engines[1])
}
}
func TestMultiPDFEngines_Merge(t *testing.T) { func TestMultiPDFEngines_Merge(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx context.Context name string
engines []gotenberg.PDFEngine engine *multiPDFEngines
expectErr bool ctx context.Context
expectMergeErr bool
}{ }{
{ {
ctx: context.TODO(), name: "nominal behavior",
engines: func() []gotenberg.PDFEngine { engine: newMultiPDFEngines(
return []gotenberg.PDFEngine{ gotenberg.PDFEngineMock{
ProtoPDFEngine{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { return nil
return nil
},
}, },
} },
}(), ),
ctx: context.Background(),
}, },
{ {
ctx: context.TODO(), name: "at least one engine does not return an error",
engines: func() []gotenberg.PDFEngine { engine: newMultiPDFEngines(
return []gotenberg.PDFEngine{ gotenberg.PDFEngineMock{
ProtoPDFEngine{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { return errors.New("foo")
return errors.New("foo")
},
}, },
ProtoPDFEngine{ },
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { gotenberg.PDFEngineMock{
return nil MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
}, return nil
}, },
} },
}(), ),
ctx: context.Background(),
}, },
{ {
ctx: context.TODO(), name: "all engines return an error",
engines: func() []gotenberg.PDFEngine { engine: newMultiPDFEngines(
return []gotenberg.PDFEngine{ gotenberg.PDFEngineMock{
ProtoPDFEngine{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { return errors.New("foo")
return errors.New("foo")
},
}, },
ProtoPDFEngine{ },
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { gotenberg.PDFEngineMock{
return errors.New("bar") MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
}, return errors.New("foo")
}, },
} },
}(), ),
expectErr: true, ctx: context.Background(),
expectMergeErr: true,
}, },
{ {
name: "context expired",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
),
ctx: func() context.Context { ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.TODO()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() cancel()
return ctx return ctx
}(), }(),
engines: func() []gotenberg.PDFEngine { expectMergeErr: true,
return []gotenberg.PDFEngine{
ProtoPDFEngine{
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
return nil
},
},
}
}(),
expectErr: true,
}, },
} { }
multi := newMultiPDFEngines(tc.engines...)
err := multi.Merge(tc.ctx, nil, nil, "")
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} err := tc.engine.Merge(tc.ctx, zap.NewNop(), nil, "")
if !tc.expectErr && err != nil { if tc.expectMergeErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("expected engine.Merge() error, but got none")
} }
if !tc.expectMergeErr && err != nil {
t.Errorf("expected no error from engine.Merge(), but got: %v", err)
}
})
} }
} }
func TestMultiPDFEngines_Convert(t *testing.T) { func TestMultiPDFEngines_Convert(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx context.Context name string
engines []gotenberg.PDFEngine engine *multiPDFEngines
expectErr bool ctx context.Context
expectConvertErr bool
}{ }{
{ {
ctx: context.TODO(), name: "nominal behavior",
engines: func() []gotenberg.PDFEngine { engine: newMultiPDFEngines(
return []gotenberg.PDFEngine{ gotenberg.PDFEngineMock{
ProtoPDFEngine{ ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { return nil
return nil
},
}, },
} },
}(), ),
ctx: context.Background(),
}, },
{ {
ctx: context.TODO(), name: "at least one engine does not return an error",
engines: func() []gotenberg.PDFEngine { engine: newMultiPDFEngines(
return []gotenberg.PDFEngine{ gotenberg.PDFEngineMock{
ProtoPDFEngine{ ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { return errors.New("foo")
return errors.New("foo")
},
}, },
ProtoPDFEngine{ },
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { gotenberg.PDFEngineMock{
return nil ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
}, return nil
}, },
} },
}(), ),
ctx: context.Background(),
}, },
{ {
ctx: context.TODO(), name: "all engines return an error",
engines: func() []gotenberg.PDFEngine { engine: newMultiPDFEngines(
return []gotenberg.PDFEngine{ gotenberg.PDFEngineMock{
ProtoPDFEngine{ ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { return errors.New("foo")
return errors.New("foo")
},
}, },
ProtoPDFEngine{ },
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { gotenberg.PDFEngineMock{
return errors.New("bar") ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
}, return errors.New("foo")
}, },
} },
}(), ),
expectErr: true, ctx: context.Background(),
expectConvertErr: true,
}, },
{ {
name: "context expired",
engine: newMultiPDFEngines(
gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
),
ctx: func() context.Context { ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.TODO()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() cancel()
return ctx return ctx
}(), }(),
engines: func() []gotenberg.PDFEngine { expectConvertErr: true,
return []gotenberg.PDFEngine{
ProtoPDFEngine{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return nil
},
},
}
}(),
expectErr: true,
}, },
} { }
multi := newMultiPDFEngines(tc.engines...)
err := multi.Convert(tc.ctx, nil, "", "", "")
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} err := tc.engine.Convert(tc.ctx, zap.NewNop(), "", "", "")
if !tc.expectErr && err != nil { if tc.expectConvertErr && err == nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("expected engine.Convert() error, but got none")
} }
if !tc.expectConvertErr && err != nil {
t.Errorf("expected no error from engine.Convert(), but got: %v", err)
}
})
} }
} }

View File

@@ -3,6 +3,7 @@ package pdfengines
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
@@ -36,7 +37,7 @@ func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
ID: "pdfengines", ID: "pdfengines",
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError) fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the PDF engines - all by default") fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the PDF engines and their order - all by default")
fs.Bool("pdfengines-disable-routes", false, "Disable the routes") fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
return fs return fs
@@ -52,6 +53,18 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
names := flags.MustStringSlice("pdfengines-engines") names := flags.MustStringSlice("pdfengines-engines")
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes") mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
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)
}
logger = logger.Named("pdfengines")
engines, err := ctx.Modules(new(gotenberg.PDFEngine)) engines, err := ctx.Modules(new(gotenberg.PDFEngine))
if err != nil { if err != nil {
return fmt.Errorf("get PDF engines: %w", err) return fmt.Errorf("get PDF engines: %w", err)
@@ -67,6 +80,13 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
// Selection from user. // Selection from user.
mod.names = names mod.names = names
for i, name := range names {
logger.Warn("unoconv-pdfengine is deprecated; prefer uno-pdfengine instead")
if name == "unoconv-pdfengine" {
mod.names[i] = "uno-pdfengine"
}
}
return nil return nil
} }
@@ -118,14 +138,25 @@ func (mod PDFEngines) Validate() error {
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines) return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
} }
// SystemMessages returns one message with the selected gotenberg.PDFEngine
// modules.
func (mod PDFEngines) SystemMessages() []string {
return []string{
strings.Join(mod.names[:], " "),
}
}
// PDFEngine returns a gotenberg.PDFEngine. // PDFEngine returns a gotenberg.PDFEngine.
func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) { func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
engines := make([]gotenberg.PDFEngine, len(mod.engines)) engines := make([]gotenberg.PDFEngine, len(mod.names))
i := 0 for i, name := range mod.names {
for _, engine := range mod.engines { for _, engine := range mod.engines {
engines[i] = engine if name == engine.(gotenberg.Module).Descriptor().ID {
i++ engines[i] = engine
break
}
}
} }
return newMultiPDFEngines(engines...), nil return newMultiPDFEngines(engines...), nil
@@ -155,6 +186,7 @@ var (
_ gotenberg.Module = (*PDFEngines)(nil) _ gotenberg.Module = (*PDFEngines)(nil)
_ gotenberg.Provisioner = (*PDFEngines)(nil) _ gotenberg.Provisioner = (*PDFEngines)(nil)
_ gotenberg.Validator = (*PDFEngines)(nil) _ gotenberg.Validator = (*PDFEngines)(nil)
_ gotenberg.SystemLogger = (*PDFEngines)(nil)
_ gotenberg.PDFEngineProvider = (*PDFEngines)(nil) _ gotenberg.PDFEngineProvider = (*PDFEngines)(nil)
_ api.Router = (*PDFEngines)(nil) _ api.Router = (*PDFEngines)(nil)
) )

View File

@@ -1,47 +1,16 @@
package pdfengines package pdfengines
import ( import (
"context"
"errors" "errors"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap" "go.uber.org/zap"
) )
type ProtoModule struct { func TestPDFEngines_Descriptor(t *testing.T) {
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 ProtoPDFEngine struct {
ProtoValidator
merge func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
convert func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath 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 TestPDFEngine_Descriptor(t *testing.T) {
descriptor := PDFEngines{}.Descriptor() descriptor := PDFEngines{}.Descriptor()
actual := reflect.TypeOf(descriptor.New()) actual := reflect.TypeOf(descriptor.New())
@@ -52,71 +21,98 @@ func TestPDFEngine_Descriptor(t *testing.T) {
} }
} }
func TestPDFEngine_Provision(t *testing.T) { func TestPDFEngines_Provision(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx *gotenberg.Context name string
expectNames []string ctx *gotenberg.Context
expectEnginesCount int expectPDFEngineNames []string
expectErr bool expectProvisionErr bool
}{ }{
{ {
name: "no selection from user",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
engine := struct{ ProtoPDFEngine }{} provider := struct {
engine.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }} gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
engine := struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
}
engine.ValidateMock = func() error {
return nil
} }
engine.validate = func() error { return errors.New("foo") }
return gotenberg.NewContext( return gotenberg.NewContext(
gotenberg.ParsedFlags{ gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet, FlagSet: new(PDFEngines).Descriptor().FlagSet,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
provider.Descriptor(),
engine.Descriptor(), engine.Descriptor(),
}, },
) )
}(), }(),
expectErr: true, expectPDFEngineNames: []string{"bar"},
}, },
{ {
name: "selection from user",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
engine := struct{ ProtoPDFEngine }{} provider := struct {
engine.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }} gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
} }
engine.validate = func() error { return nil }
return gotenberg.NewContext( engine1 := struct {
gotenberg.ParsedFlags{ gotenberg.ModuleMock
FlagSet: new(PDFEngines).Descriptor().FlagSet, gotenberg.ValidatorMock
}, gotenberg.PDFEngineMock
[]gotenberg.ModuleDescriptor{ }{}
engine.Descriptor(), engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
},
)
}(),
expectNames: []string{"foo"},
expectEnginesCount: 1,
},
{
ctx: func() *gotenberg.Context {
engine1 := struct{ ProtoPDFEngine }{}
engine1.descriptor = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }} return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
} }
engine1.validate = func() error { return nil } engine1.ValidateMock = func() error {
return nil
}
engine2 := struct{ ProtoPDFEngine }{} engine2 := struct {
engine2.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }} return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
} }
engine2.validate = func() error { return nil } engine2.ValidateMock = func() error {
return nil
}
fs := new(PDFEngines).Descriptor().FlagSet fs := new(PDFEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"}) err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
if err != nil { if err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -124,33 +120,47 @@ func TestPDFEngine_Provision(t *testing.T) {
FlagSet: fs, FlagSet: fs,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
provider.Descriptor(),
engine1.Descriptor(), engine1.Descriptor(),
engine2.Descriptor(), engine2.Descriptor(),
}, },
) )
}(), }(),
expectNames: []string{"b", "a"}, expectPDFEngineNames: []string{"b", "a"},
expectEnginesCount: 2,
}, },
{ {
name: "user select deprecated unoconv-pdfengine",
ctx: func() *gotenberg.Context { ctx: func() *gotenberg.Context {
engine1 := struct{ ProtoPDFEngine }{} provider := struct {
engine1.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }} gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
} }
engine1.validate = func() error { return nil }
engine2 := struct{ ProtoPDFEngine }{} engine := struct {
engine2.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }} gotenberg.ValidatorMock
gotenberg.PDFEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "uno-pdfengine", New: func() gotenberg.Module { return engine }}
}
engine.ValidateMock = func() error {
return nil
} }
engine2.validate = func() error { return nil }
fs := new(PDFEngines).Descriptor().FlagSet fs := new(PDFEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=b"}) err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
if err != nil { if err != nil {
t.Fatalf("expected error but got: %v", err) t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
} }
return gotenberg.NewContext( return gotenberg.NewContext(
@@ -158,56 +168,136 @@ func TestPDFEngine_Provision(t *testing.T) {
FlagSet: fs, FlagSet: fs,
}, },
[]gotenberg.ModuleDescriptor{ []gotenberg.ModuleDescriptor{
engine1.Descriptor(), provider.Descriptor(),
engine2.Descriptor(), engine.Descriptor(),
}, },
) )
}(), }(),
expectNames: []string{"b"}, expectPDFEngineNames: []string{"uno-pdfengine"},
expectEnginesCount: 2,
}, },
} { {
mod := new(PDFEngines) name: "no logger provider",
err := mod.Provision(tc.ctx) ctx: func() *gotenberg.Context {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{},
)
}(),
expectProvisionErr: true,
},
{
name: "no logger from logger provider",
ctx: func() *gotenberg.Context {
provider := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return nil, errors.New("foo")
}
if tc.expectErr && err == nil { return gotenberg.NewContext(
t.Errorf("test %d: expected error but got: %v", i, err) gotenberg.ParsedFlags{
} FlagSet: new(PDFEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
},
)
}(),
expectProvisionErr: true,
},
{
name: "no valid PDF engines",
ctx: func() *gotenberg.Context {
provider := struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
}{}
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
return provider
}}
}
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
if !tc.expectErr && err != nil { engine := struct {
t.Errorf("test %d: expected no error but got: %v", i, err) gotenberg.ModuleMock
} gotenberg.ValidatorMock
gotenberg.PDFEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
}
engine.ValidateMock = func() error {
return errors.New("foo")
}
if len(tc.expectNames) != len(mod.names) { return gotenberg.NewContext(
t.Errorf("test %d: expected %d names but got %d", i, len(tc.expectNames), len(mod.names)) gotenberg.ParsedFlags{
} FlagSet: new(PDFEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
engine.Descriptor(),
},
)
}(),
expectProvisionErr: true,
},
}
if tc.expectEnginesCount != len(mod.engines) { for _, tc := range tests {
t.Errorf("test %d: expected %d engines but got %d", i, tc.expectEnginesCount, len(mod.engines)) t.Run(tc.name, func(t *testing.T) {
} mod := new(PDFEngines)
err := mod.Provision(tc.ctx)
for index, name := range mod.names { if tc.expectProvisionErr && err == nil {
if name != tc.expectNames[index] { t.Fatal("expected mod.Provision() error, but got none")
t.Errorf("test %d: expected name at index %d to be %s, but got: %s", i, index, name, tc.expectNames[index])
} }
}
if !tc.expectProvisionErr && err != nil {
t.Fatalf("expected no error from mod.Provision(), but got: %v", err)
}
if len(tc.expectPDFEngineNames) != len(mod.names) {
t.Errorf("expected %d names but got %d", len(tc.expectPDFEngineNames), len(mod.names))
}
for index, name := range mod.names {
if name != tc.expectPDFEngineNames[index] {
t.Errorf("expected name at index %d to be %s, but got: %s", index, name, tc.expectPDFEngineNames[index])
}
}
})
} }
} }
func TestPDFEngine_Validate(t *testing.T) { func TestPDFEngines_Validate(t *testing.T) {
for i, tc := range []struct { tests := []struct {
names []string name string
engines []gotenberg.PDFEngine names []string
expectErr bool engines []gotenberg.PDFEngine
expectValidateErr bool
}{ }{
{ {
expectErr: true, name: "existing PDF engine",
},
{
names: []string{"foo"}, names: []string{"foo"},
engines: func() []gotenberg.PDFEngine { engines: func() []gotenberg.PDFEngine {
engine := struct{ ProtoPDFEngine }{} engine := struct {
engine.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
} }
@@ -217,15 +307,22 @@ func TestPDFEngine_Validate(t *testing.T) {
}(), }(),
}, },
{ {
name: "non-existing bar PDF engine",
names: []string{"foo", "bar", "baz"}, names: []string{"foo", "bar", "baz"},
engines: func() []gotenberg.PDFEngine { engines: func() []gotenberg.PDFEngine {
engine1 := struct{ ProtoPDFEngine }{} engine1 := struct {
engine1.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }} return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
} }
engine2 := struct{ ProtoPDFEngine }{} engine2 := struct {
engine2.descriptor = func() gotenberg.ModuleDescriptor { gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }} return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
} }
@@ -234,71 +331,115 @@ func TestPDFEngine_Validate(t *testing.T) {
engine2, engine2,
} }
}(), }(),
expectErr: true, expectValidateErr: true,
}, },
} { {
mod := new(PDFEngines) name: "no PDF engine",
mod.names = tc.names expectValidateErr: true,
mod.engines = tc.engines },
err := mod.Validate() }
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} mod := PDFEngines{
names: tc.names,
engines: tc.engines,
}
if !tc.expectErr && err != nil { err := mod.Validate()
t.Errorf("test %d: expected no error but got: %v", i, err)
} if tc.expectValidateErr && err == nil {
t.Errorf("expected mod.Validate() error, but got none")
}
if !tc.expectValidateErr && err != nil {
t.Errorf("expected no error from mod.Validate(), but got: %v", err)
}
})
} }
} }
func TestPDFEngine_PDFEngine(t *testing.T) { func TestPDFEngines_SystemMessages(t *testing.T) {
mod := new(PDFEngines) mod := new(PDFEngines)
mod.engines = []gotenberg.PDFEngine{ mod.names = []string{"foo", "bar"}
struct{ ProtoPDFEngine }{},
messages := mod.SystemMessages()
if len(messages) != 1 {
t.Errorf("expected one and only one message from mod.SystemMessages(), but got %d", len(messages))
}
expect := strings.Join(mod.names[:], " ")
if messages[0] != expect {
t.Errorf("expected message '%s' from mod.SystemMessages(), but got '%s'", expect, messages[0])
}
}
func TestPDFEngines_PDFEngine(t *testing.T) {
mod := PDFEngines{
names: []string{"foo", "bar"},
engines: func() []gotenberg.PDFEngine {
engine1 := struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
}
engine2 := struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
}
return []gotenberg.PDFEngine{
engine1,
engine2,
}
}(),
} }
_, err := mod.PDFEngine() _, err := mod.PDFEngine()
if err != nil { if err != nil {
t.Errorf("expected no error but got: %v", err) t.Errorf("expected no error from mod.PDFEngine, but got: %v", err)
} }
} }
func TestPDFEngine_Routes(t *testing.T) { func TestPDFEngines_Routes(t *testing.T) {
for i, tc := range []struct { tests := []struct {
expectRoutes int name string
disableRoutes bool mod PDFEngines
expectRoutesCount int
}{ }{
{ {
expectRoutes: 2, name: "route not disabled",
mod: PDFEngines{
engines: []gotenberg.PDFEngine{
gotenberg.PDFEngineMock{},
},
},
expectRoutesCount: 2,
}, },
{ {
disableRoutes: true, name: "route disabled",
mod: PDFEngines{
disableRoutes: true,
},
}, },
} { }
mod := new(PDFEngines)
mod.engines = []gotenberg.PDFEngine{
struct{ ProtoPDFEngine }{},
}
mod.disableRoutes = tc.disableRoutes
routes, err := mod.Routes() for _, tc := range tests {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatalf("test %d: expected no error but got: %v", i, err) routes, err := tc.mod.Routes()
} if err != nil {
t.Fatalf("expected no error from mod.Routes(), but got: %v", err)
}
if tc.expectRoutes != len(routes) { if tc.expectRoutesCount != len(routes) {
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes)) t.Errorf("expected %d routes from mod.Routes(), but got %d", tc.expectRoutesCount, len(routes))
} }
})
} }
} }
// Interface guards.
var (
_ gotenberg.Module = (*ProtoModule)(nil)
_ gotenberg.Validator = (*ProtoValidator)(nil)
_ gotenberg.Module = (*ProtoValidator)(nil)
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
_ gotenberg.Module = (*ProtoPDFEngine)(nil)
_ gotenberg.Validator = (*ProtoPDFEngine)(nil)
)

View File

@@ -13,8 +13,9 @@ import (
) )
func TestMergeHandler(t *testing.T) { func TestMergeHandler(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx *api.MockContext name string
ctx *api.ContextMock
engine gotenberg.PDFEngine engine gotenberg.PDFEngine
expectErr bool expectErr bool
expectHTTPErr bool expectHTTPErr bool
@@ -22,34 +23,75 @@ func TestMergeHandler(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
ctx: &api.MockContext{Context: &api.Context{}}, name: "nominal behavior",
expectErr: true, ctx: func() *api.ContextMock {
expectHTTPErr: true, ctx := &api.ContextMock{Context: &api.Context{}}
expectHTTPStatus: http.StatusBadRequest,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf", "foo.pdf": "/foo/foo.pdf",
}) })
return ctx return ctx
}(), }(),
engine: func() gotenberg.PDFEngine { engine: gotenberg.PDFEngineMock{
return &ProtoPDFEngine{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { return nil
return errors.New("foo") },
}, },
} expectOutputPathsCount: 1,
},
{
name: "invalid form data: no PDF",
ctx: &api.ContextMock{Context: &api.Context{}},
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "merge fail",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
return ctx
}(), }(),
engine: gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
expectErr: true, expectErr: true,
}, },
{ {
ctx: func() *api.MockContext { name: "nominal behavior with a PDF format",
ctx := &api.MockContext{Context: &api.Context{}} ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 1,
},
{
name: "convert to PDF format fail",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf", "foo.pdf": "/foo/foo.pdf",
}) })
@@ -61,24 +103,20 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: func() gotenberg.PDFEngine { engine: gotenberg.PDFEngineMock{
return &ProtoPDFEngine{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { return nil
return nil },
}, ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { return errors.New("foo")
return gotenberg.ErrPDFFormatNotAvailable },
}, },
} expectErr: true,
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { name: "invalid PDF format",
ctx := &api.MockContext{Context: &api.Context{}} ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf", "foo.pdf": "/foo/foo.pdf",
}) })
@@ -90,125 +128,82 @@ func TestMergeHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: func() gotenberg.PDFEngine { engine: gotenberg.PDFEngineMock{
return &ProtoPDFEngine{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { return nil
return nil },
}, ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { return gotenberg.ErrPDFFormatNotAvailable
return errors.New("foo") },
}, },
} expectErr: true,
}(), expectHTTPErr: true,
expectErr: true, expectHTTPStatus: http.StatusBadRequest,
}, },
{ {
ctx: func() *api.MockContext { name: "cannot add output paths",
ctx := &api.MockContext{Context: &api.Context{}} ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetCancelled(true) ctx.SetCancelled(true)
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
return ctx return ctx
}(), }(),
engine: func() gotenberg.PDFEngine { engine: gotenberg.PDFEngineMock{
return &ProtoPDFEngine{ MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error { return nil
return nil },
}, },
}
}(),
expectErr: true, expectErr: true,
}, },
{ }
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ for _, tc := range tests {
"foo.pdf": "/foo/foo.pdf", t.Run(tc.name, func(t *testing.T) {
}) c := echo.New().NewContext(nil, nil)
ctx.SetValues(map[string][]string{ c.Set("context", tc.ctx.Context)
"pdfFormat": {
"foo",
},
})
return ctx err := mergeRoute(tc.engine).Handler(c)
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
return nil
},
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return nil
},
}
}(),
expectOutputPathsCount: 1,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ if tc.expectErr && err == nil {
"foo.pdf": "/foo/foo.pdf", t.Fatal("expected error from merge handler, but got none")
})
return ctx
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
return nil
},
}
}(),
expectOutputPathsCount: 1,
},
} {
c := echo.New().NewContext(nil, nil)
c.Set("context", tc.ctx.Context)
err := mergeRoute(tc.engine).Handler(c)
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)
}
var httpErr api.HTTPError
isHTTPErr := errors.As(err, &httpErr)
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("test %d: expected HTTP error but got: %v", i, err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
} }
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) t.Fatalf("expected no error from merge handler, but got: %v", err)
} }
var httpErr api.HTTPError
isHTTPErr := errors.As(err, &httpErr)
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("expected HTTP error from merge handler, but got: %v", err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("expected no HTTP error from merge handler, but got one: %v", httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("expected %d HTTP status code from merge handler, but got %d", tc.expectHTTPStatus, status)
}
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths from merge handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
}
})
} }
} }
func TestConvertHandler(t *testing.T) { func TestConvertHandler(t *testing.T) {
for i, tc := range []struct { tests := []struct {
ctx *api.MockContext name string
ctx *api.ContextMock
engine gotenberg.PDFEngine engine gotenberg.PDFEngine
expectErr bool expectErr bool
expectHTTPErr bool expectHTTPErr bool
@@ -216,132 +211,109 @@ func TestConvertHandler(t *testing.T) {
expectOutputPathsCount int expectOutputPathsCount int
}{ }{
{ {
ctx: &api.MockContext{Context: &api.Context{}}, name: "nominal behavior",
expectErr: true, ctx: func() *api.ContextMock {
expectHTTPErr: true, ctx := &api.ContextMock{Context: &api.Context{}}
expectHTTPStatus: http.StatusBadRequest,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf", "foo.pdf": "/foo/foo.pdf",
}) })
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"pdfFormat": { "pdfFormat": {
"foo", gotenberg.FormatPDFA1a,
}, },
}) })
return ctx return ctx
}(), }(),
engine: func() gotenberg.PDFEngine { engine: gotenberg.PDFEngineMock{
return &ProtoPDFEngine{ ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { return nil
return gotenberg.ErrPDFFormatNotAvailable },
}, },
}
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return errors.New("foo")
},
}
}(),
expectErr: true,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetCancelled(true)
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return nil
},
}
}(),
expectErr: true,
},
{
ctx: func() *api.MockContext {
ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: func() gotenberg.PDFEngine {
return &ProtoPDFEngine{
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
return nil
},
}
}(),
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
}, },
{ {
ctx: func() *api.MockContext { name: "nominal behavior, but with 3 PDFs",
ctx := &api.MockContext{Context: &api.Context{}} ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"bar.pdf": "/bar/bar.pdf",
"baz.pdf": "/baz/baz.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 3,
},
{
name: "invalid form data: no PDF",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "invalid form data: no PDF format",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
return ctx
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "convert to PDF format fail",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectErr: true,
},
{
name: "PDF format not available",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf", "foo.pdf": "/foo/foo.pdf",
"bar.pdf": "/foo/bar.pdf",
}) })
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"pdfFormat": { "pdfFormat": {
@@ -351,49 +323,76 @@ func TestConvertHandler(t *testing.T) {
return ctx return ctx
}(), }(),
engine: func() gotenberg.PDFEngine { engine: gotenberg.PDFEngineMock{
return &ProtoPDFEngine{ ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error { return gotenberg.ErrPDFFormatNotAvailable
return nil },
}, },
} expectErr: true,
}(), expectHTTPErr: true,
expectOutputPathsCount: 2, expectHTTPStatus: http.StatusBadRequest,
}, },
} { {
c := echo.New().NewContext(nil, nil) name: "cannot add output paths",
c.Set("context", tc.ctx.Context) ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
ctx.SetCancelled(true)
err := convertRoute(tc.engine).Handler(c) return ctx
}(),
engine: gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectErr: true,
},
}
if tc.expectErr && err == nil { for _, tc := range tests {
t.Errorf("test %d: expected error but got: %v", i, err) t.Run(tc.name, func(t *testing.T) {
} c := echo.New().NewContext(nil, nil)
c.Set("context", tc.ctx.Context)
if !tc.expectErr && err != nil { err := convertRoute(tc.engine).Handler(c)
t.Errorf("test %d: expected no error but got: %v", i, err)
}
var httpErr api.HTTPError if tc.expectErr && err == nil {
isHTTPErr := errors.As(err, &httpErr) t.Fatal("expected error from convert handler, but got none")
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("test %d: expected HTTP error but got: %v", i, err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
} }
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) t.Fatalf("expected no error from convert handler, but got: %v", err)
} }
var httpErr api.HTTPError
isHTTPErr := errors.As(err, &httpErr)
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("expected HTTP error from convert handler, but got: %v", err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("expected no HTTP error from convert handler, but got one: %v", httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("expected %d HTTP status code from convert handler, but got %d", tc.expectHTTPStatus, status)
}
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths from convert handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
}
})
} }
} }

View File

@@ -83,7 +83,7 @@ func (engine PDFtk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
activeInstancesCount += 1 activeInstancesCount += 1
activeInstancesCountMu.Unlock() activeInstancesCountMu.Unlock()
err = cmd.Exec() _, err = cmd.Exec()
activeInstancesCountMu.Lock() activeInstancesCountMu.Lock()
activeInstancesCount -= 1 activeInstancesCount -= 1

View File

@@ -65,11 +65,11 @@ func TestPDFtk_Validate(t *testing.T) {
func TestPDFtk_Metrics(t *testing.T) { func TestPDFtk_Metrics(t *testing.T) {
metrics, err := new(PDFtk).Metrics() metrics, err := new(PDFtk).Metrics()
if err != nil { if err != nil {
t.Errorf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
if len(metrics) != 1 { if len(metrics) != 1 {
t.Errorf("expected %d metrics, but got %d", 1, len(metrics)) t.Fatalf("expected %d metrics, but got %d", 1, len(metrics))
} }
actual := metrics[0].Read() actual := metrics[0].Read()

View File

@@ -150,7 +150,7 @@ func (mod Prometheus) Start() error {
// StartupMessage returns a custom startup message. // StartupMessage returns a custom startup message.
func (mod Prometheus) StartupMessage() string { func (mod Prometheus) StartupMessage() string {
if mod.disableCollect { if mod.disableCollect {
return "application not started (collect disabled by user)" return "collect disabled"
} }
return "collecting metrics" return "collecting metrics"

View File

@@ -296,8 +296,8 @@ func TestPrometheus_StartupMessage(t *testing.T) {
expectMessage string expectMessage string
}{ }{
{ {
expectMessage: "application not started (collect disabled by user)",
disableCollect: true, disableCollect: true,
expectMessage: "collect disabled",
}, },
{ {
expectMessage: "collecting metrics", expectMessage: "collecting metrics",

3
pkg/modules/qpdf/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Package qpdf provides a module which abstracts the CLI tool QPDF and
// implements the gotenberg.PDFEngine interface.
package qpdf

117
pkg/modules/qpdf/qpdf.go Normal file
View File

@@ -0,0 +1,117 @@
package qpdf
import (
"context"
"errors"
"fmt"
"os"
"sync"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
)
func init() {
gotenberg.MustRegisterModule(QPDF{})
}
// QPDF abstracts the CLI tool QPDF and implements the gotenberg.PDFEngine
// interface.
type QPDF struct {
binPath string
}
// Descriptor returns a QPDF's module descriptor.
func (QPDF) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "qpdf",
New: func() gotenberg.Module { return new(QPDF) },
}
}
// Provision sets the modules properties. It returns an error if the
// environment variable QPDF_BIN_PATH is not set.
func (engine *QPDF) Provision(_ *gotenberg.Context) error {
binPath, ok := os.LookupEnv("QPDF_BIN_PATH")
if !ok {
return errors.New("QPDF_BIN_PATH environment variable is not set")
}
engine.binPath = binPath
return nil
}
// Validate validates the module properties.
func (engine QPDF) Validate() error {
_, err := os.Stat(engine.binPath)
if os.IsNotExist(err) {
return fmt.Errorf("QPDF binary path does not exist: %w", err)
}
return nil
}
// Metrics returns the metrics.
func (engine QPDF) Metrics() ([]gotenberg.Metric, error) {
return []gotenberg.Metric{
{
Name: "qpdf_active_instances_count",
Description: "Current number of active QPDF instances.",
Read: func() float64 {
activeInstancesCountMu.RLock()
defer activeInstancesCountMu.RUnlock()
return activeInstancesCount
},
},
}, nil
}
// Merge merges the given PDFs into a unique PDF.
func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
var args []string
args = append(args, "--empty")
args = append(args, "--pages")
args = append(args, inputPaths...)
args = append(args, "--", outputPath)
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil {
return fmt.Errorf("create command: %w", err)
}
activeInstancesCountMu.Lock()
activeInstancesCount += 1
activeInstancesCountMu.Unlock()
_, err = cmd.Exec()
activeInstancesCountMu.Lock()
activeInstancesCount -= 1
activeInstancesCountMu.Unlock()
if err == nil {
return nil
}
return fmt.Errorf("merge PDFs with QPDF: %w", err)
}
// Convert is not available for this PDF engine.
func (engine QPDF) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
return fmt.Errorf("convert PDF to '%s' with QPDF: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
}
var (
activeInstancesCount float64
activeInstancesCountMu sync.RWMutex
)
var (
_ gotenberg.Module = (*QPDF)(nil)
_ gotenberg.Provisioner = (*QPDF)(nil)
_ gotenberg.Validator = (*QPDF)(nil)
_ gotenberg.MetricsProvider = (*QPDF)(nil)
_ gotenberg.PDFEngine = (*QPDF)(nil)
)

View File

@@ -0,0 +1,152 @@
package qpdf
import (
"context"
"errors"
"os"
"reflect"
"testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
)
func TestQPDF_Descriptor(t *testing.T) {
descriptor := QPDF{}.Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(QPDF))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestQPDF_Provision(t *testing.T) {
mod := new(QPDF)
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
err := mod.Provision(ctx)
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
func TestQPDF_Validate(t *testing.T) {
for i, tc := range []struct {
binPath string
expectErr bool
}{
{
expectErr: true,
},
{
binPath: "/foo",
expectErr: true,
},
{
binPath: os.Getenv("QPDF_BIN_PATH"),
},
} {
mod := new(QPDF)
mod.binPath = tc.binPath
err := mod.Validate()
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)
}
}
}
func TestQPDF_Metrics(t *testing.T) {
metrics, err := new(QPDF).Metrics()
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
if len(metrics) != 1 {
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
}
actual := metrics[0].Read()
if actual != 0 {
t.Errorf("expected %d QPDF instances, but got %f", 0, actual)
}
}
func TestQPDF_Merge(t *testing.T) {
for i, tc := range []struct {
ctx context.Context
inputPaths []string
expectErr bool
}{
{
ctx: context.TODO(),
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
},
},
{
ctx: context.TODO(),
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
"/tests/test/testdata/pdfengines/sample2.pdf",
},
},
{
ctx: nil,
expectErr: true,
},
{
ctx: context.TODO(),
inputPaths: []string{
"foo",
},
expectErr: true,
},
} {
func() {
mod := new(QPDF)
err := mod.Provision(nil)
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
outputDir, err := gotenberg.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
}
defer func() {
err := os.RemoveAll(outputDir)
if err != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
}()
err = mod.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
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)
}
}()
}
}
func TestQPDF_Convert(t *testing.T) {
mod := new(QPDF)
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
}
}

View File

@@ -143,7 +143,7 @@ func webhookMiddleware(w Webhook) api.Middleware {
client: &retryablehttp.Client{ client: &retryablehttp.Client{
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Timeout: c.Get("writeTimeout").(time.Duration), Timeout: w.clientTimeout,
}, },
RetryMax: w.maxRetry, RetryMax: w.maxRetry,
RetryWaitMin: w.retryMinWait, RetryWaitMin: w.retryMinWait,

View File

@@ -6,7 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"math/rand" "math/rand"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@@ -252,7 +252,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
c := srv.NewContext(tc.request, httptest.NewRecorder()) c := srv.NewContext(tc.request, httptest.NewRecorder())
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetEchoContext(c) ctx.SetEchoContext(c)
c.Set("context", ctx.Context) c.Set("context", ctx.Context)
@@ -324,6 +324,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
maxRetry: 0, maxRetry: 0,
retryMinWait: 0, retryMinWait: 0,
retryMaxWait: 0, retryMaxWait: 0,
clientTimeout: time.Duration(30) * time.Second,
disable: false, disable: false,
} }
} }
@@ -397,9 +398,8 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
c.Set("traceHeader", "Gotenberg-Trace") c.Set("traceHeader", "Gotenberg-Trace")
c.Set("trace", "foo") c.Set("trace", "foo")
c.Set("startTime", time.Now()) c.Set("startTime", time.Now())
c.Set("writeTimeout", time.Duration(10)*time.Second)
ctx := &api.MockContext{Context: &api.Context{}} ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetLogger(zap.NewNop()) ctx.SetLogger(zap.NewNop())
ctx.SetEchoContext(c) ctx.SetEchoContext(c)
@@ -450,7 +450,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
} }
if contentType == echo.MIMEApplicationJSONCharsetUTF8 { if contentType == echo.MIMEApplicationJSONCharsetUTF8 {
body, err := ioutil.ReadAll(c.Request().Body) body, err := io.ReadAll(c.Request().Body)
if err != nil { if err != nil {
errChan <- err errChan <- err
return nil return nil
@@ -489,7 +489,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
t.Errorf("test %d: expected '%s' '%s' to contain '%s'", i, echo.HeaderContentDisposition, contentDisposition, tc.expectWebhookFilename) t.Errorf("test %d: expected '%s' '%s' to contain '%s'", i, echo.HeaderContentDisposition, contentDisposition, tc.expectWebhookFilename)
} }
body, err := ioutil.ReadAll(c.Request().Body) body, err := io.ReadAll(c.Request().Body)
if err != nil { if err != nil {
errChan <- err errChan <- err
return nil return nil

View File

@@ -25,6 +25,7 @@ type Webhook struct {
maxRetry int maxRetry int
retryMinWait time.Duration retryMinWait time.Duration
retryMaxWait time.Duration retryMaxWait time.Duration
clientTimeout time.Duration
disable bool disable bool
} }
@@ -55,7 +56,7 @@ func (Webhook) Descriptor() gotenberg.ModuleDescriptor {
err = multierr.Append(err, fs.MarkDeprecated("api-disable-webhook", "use webhook-disable instead")) err = multierr.Append(err, fs.MarkDeprecated("api-disable-webhook", "use webhook-disable instead"))
if err != nil { if err != nil {
panic(fmt.Errorf("create deprecated flags for webhook module: %v", err)) panic(fmt.Errorf("create deprecated flags for the webhook module: %v", err))
} }
// New flags. // New flags.
@@ -66,6 +67,7 @@ func (Webhook) Descriptor() gotenberg.ModuleDescriptor {
fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature") fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
fs.Duration("webhook-retry-min-wait", time.Duration(1)*time.Second, "Set the minimum duration to wait before trying to call the webhook again") fs.Duration("webhook-retry-min-wait", time.Duration(1)*time.Second, "Set the minimum duration to wait before trying to call the webhook again")
fs.Duration("webhook-retry-max-wait", time.Duration(30)*time.Second, "Set the maximum duration to wait before trying to call the webhook again") fs.Duration("webhook-retry-max-wait", time.Duration(30)*time.Second, "Set the maximum duration to wait before trying to call the webhook again")
fs.Duration("webhook-client-timeout", time.Duration(30)*time.Second, "Set the time limit for requests to the webhook")
fs.Bool("webhook-disable", false, "Disable the webhook feature") fs.Bool("webhook-disable", false, "Disable the webhook feature")
return fs return fs
@@ -84,6 +86,7 @@ func (w *Webhook) Provision(ctx *gotenberg.Context) error {
w.maxRetry = flags.MustDeprecatedInt("api-webhook-max-retry", "webhook-max-retry") w.maxRetry = flags.MustDeprecatedInt("api-webhook-max-retry", "webhook-max-retry")
w.retryMinWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-min-wait") w.retryMinWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-min-wait")
w.retryMaxWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-max-wait") w.retryMaxWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-max-wait")
w.clientTimeout = flags.MustDuration("webhook-client-timeout")
w.disable = flags.MustDeprecatedBool("api-disable-webhook", "webhook-disable") w.disable = flags.MustDeprecatedBool("api-disable-webhook", "webhook-disable")
return nil return nil

View File

@@ -7,11 +7,12 @@ import (
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/gc" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/logging" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/logging"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfcpu" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfcpu"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/qpdf"
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook" _ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook"
) )

View File

@@ -6,8 +6,9 @@ GOLANG_VERSION="$1"
GOTENBERG_VERSION="$2" GOTENBERG_VERSION="$2"
GOTENBERG_USER_GID="$3" GOTENBERG_USER_GID="$3"
GOTENBERG_USER_UID="$4" GOTENBERG_USER_UID="$4"
PDFTK_VERSION="$5" NOTO_COLOR_EMOJI_VERSION="$5"
DOCKER_REPOSITORY="$6" PDFTK_VERSION="$6"
DOCKER_REPOSITORY="$7"
GOTENBERG_VERSION="${GOTENBERG_VERSION//v}" GOTENBERG_VERSION="${GOTENBERG_VERSION//v}"
SEMVER=( ${GOTENBERG_VERSION//./ } ) SEMVER=( ${GOTENBERG_VERSION//./ } )
@@ -23,6 +24,7 @@ docker buildx build \
--build-arg GOTENBERG_VERSION="$GOTENBERG_VERSION" \ --build-arg GOTENBERG_VERSION="$GOTENBERG_VERSION" \
--build-arg GOTENBERG_USER_GID="$GOTENBERG_USER_GID" \ --build-arg GOTENBERG_USER_GID="$GOTENBERG_USER_GID" \
--build-arg GOTENBERG_USER_UID="$GOTENBERG_USER_UID" \ --build-arg GOTENBERG_USER_UID="$GOTENBERG_USER_UID" \
--build-arg NOTO_COLOR_EMOJI_VERSION="$NOTO_COLOR_EMOJI_VERSION" \
--build-arg PDFTK_VERSION="$PDFTK_VERSION" \ --build-arg PDFTK_VERSION="$PDFTK_VERSION" \
--platform linux/amd64 \ --platform linux/amd64 \
--platform linux/arm64 \ --platform linux/arm64 \
@@ -36,13 +38,11 @@ docker buildx build \
-f build/Dockerfile . -f build/Dockerfile .
# Cloud Run variant. # Cloud Run variant.
# Only linux/amd64! See https://github.com/gotenberg/gotenberg/issues/505#issuecomment-1264679278.
docker buildx build \ docker buildx build \
--build-arg DOCKER_REPOSITORY="$DOCKER_REPOSITORY" \ --build-arg DOCKER_REPOSITORY="$DOCKER_REPOSITORY" \
--build-arg GOTENBERG_VERSION="$GOTENBERG_VERSION" \ --build-arg GOTENBERG_VERSION="$GOTENBERG_VERSION" \
--platform linux/amd64 \ --platform linux/amd64 \
--platform linux/arm64 \
--platform linux/arm/v7 \
--platform linux/386 \
-t "$DOCKER_REPOSITORY/gotenberg:latest-cloudrun" \ -t "$DOCKER_REPOSITORY/gotenberg:latest-cloudrun" \
-t "$DOCKER_REPOSITORY/gotenberg:${SEMVER[0]}-cloudrun" \ -t "$DOCKER_REPOSITORY/gotenberg:${SEMVER[0]}-cloudrun" \
-t "$DOCKER_REPOSITORY/gotenberg:${SEMVER[0]}.${SEMVER[1]}-cloudrun" \ -t "$DOCKER_REPOSITORY/gotenberg:${SEMVER[0]}.${SEMVER[1]}-cloudrun" \

View File

@@ -3,7 +3,7 @@ ARG DOCKER_REPOSITORY
ARG GOTENBERG_VERSION ARG GOTENBERG_VERSION
ARG GOLANGCI_LINT_VERSION ARG GOLANGCI_LINT_VERSION
FROM golang:$GOLANG_VERSION-stretch as golang FROM golang:$GOLANG_VERSION-bullseye as golang
# We're extending the Gotenberg's Docker image because our code relies on external # We're extending the Gotenberg's Docker image because our code relies on external
# dependencies like Google Chrome, LibreOffice, etc. # dependencies like Google Chrome, LibreOffice, etc.
@@ -42,4 +42,4 @@ RUN apt-get update -qq &&\
# Pristine working directory. # Pristine working directory.
WORKDIR /tests WORKDIR /tests
ENTRYPOINT [ "docker-entrypoint.sh" ] ENTRYPOINT [ "/usr/bin/tini", "--", "docker-entrypoint.sh" ]

View File

@@ -2,5 +2,6 @@
set -x set -x
go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./... # TODO: remove -buildvcs=false when fix for https://github.com/golang/go/issues/51723 is live.
go test -buildvcs=false -race -covermode=atomic -coverprofile=/tests/coverage.txt ./...
go tool cover -html=coverage.txt -o /tests/coverage.html go tool cover -html=coverage.txt -o /tests/coverage.html

Binary file not shown.

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset=v"utf-8">
<title>Gutenberg</title>
</head>
<body>
<p id="extra">Extra link tags <span id="not">not</span> working</p>
</body>
</html>

View File

@@ -1,2 +0,0 @@
let extra = document.getElementById('extra')
extra.innerText = extra.innerText + ' and extra JavaScript tags too!'

View File

@@ -1,14 +0,0 @@
@font-face {
font-family: 'Local';
src: url('font.woff') format('woff');
font-weight: normal;
font-style: normal;
}
#extra {
font-family: 'Local'
}
#not {
display: none;
}