mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
875f97813e | ||
|
|
6a29385762 | ||
|
|
fbc9f9dec9 | ||
|
|
4607ade8e9 | ||
|
|
e4a553c022 | ||
|
|
bc733bab0c | ||
|
|
1a522ddf48 | ||
|
|
1de0f4c4fa | ||
|
|
71eb5c25b5 | ||
|
|
ea34979a29 | ||
|
|
1f784ad44c | ||
|
|
f89726752f | ||
|
|
921b6b7ba4 | ||
|
|
6c5cc7a0ce | ||
|
|
f851e2f2cf | ||
|
|
b73207f29a | ||
|
|
8d8f0b55ea | ||
|
|
0e1e9b9cf3 | ||
|
|
ab4eed78af | ||
|
|
8a36c15d5c | ||
|
|
bd895286bc | ||
|
|
b76990ec16 | ||
|
|
9bc93c5cf5 | ||
|
|
1d8ae79f84 | ||
|
|
4269aa2422 | ||
|
|
c887dc1ec8 | ||
|
|
8bbacd691d | ||
|
|
d8f3f85fb7 | ||
|
|
159c0b3350 | ||
|
|
45788f3235 | ||
|
|
4c85c6ccdf | ||
|
|
08408ae005 | ||
|
|
d470279fb7 | ||
|
|
8260458f8f | ||
|
|
0512dd1e86 | ||
|
|
4972019cb9 | ||
|
|
5e615cab52 | ||
|
|
64ea2b6d3b | ||
|
|
27018fa8e2 | ||
|
|
263d634801 | ||
|
|
c61c33f71c | ||
|
|
bfb30b8fed | ||
|
|
ca3e27937e | ||
|
|
50c34a4186 | ||
|
|
84875aaf82 | ||
|
|
b7a8b95820 | ||
|
|
5cad5ca11a | ||
|
|
7598de29dd | ||
|
|
af6ffe67c8 | ||
|
|
6944c4b3bf | ||
|
|
24817d5707 | ||
|
|
3d855712ee |
2
.github/workflows/continuous_integration.yml
vendored
2
.github/workflows/continuous_integration.yml
vendored
@@ -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.50.1
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Julien Neuhart
|
Copyright (c) 2023 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
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -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.50.1 # 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
|
||||||
@@ -34,9 +34,11 @@ 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].*"
|
||||||
@@ -51,7 +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
|
||||||
UNOCONV_DISABLE_LISTENER=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=
|
||||||
@@ -76,9 +79,11 @@ run: ## Start a Gotenberg container
|
|||||||
--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) \
|
||||||
@@ -93,7 +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) \
|
||||||
--unoconv-disable-listener=$(UNOCONV_DISABLE_LISTENER) \
|
--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) \
|
||||||
|
|||||||
@@ -43,4 +43,3 @@ Head to the [documentation](https://gotenberg.dev/docs/about) to learn how to in
|
|||||||
[](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
|
[](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
|
||||||
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
|
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
|
||||||
[](https://codecov.io/gh/gotenberg/gotenberg)
|
[](https://codecov.io/gh/gotenberg/gotenberg)
|
||||||
[](https://goreportcard.com/report/gotenberg/gotenberg)
|
|
||||||
|
|||||||
@@ -118,26 +118,36 @@ 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 bullseye-backports distribution to get the latest LibreOffice version.
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends libreoffice &&\
|
||||||
|
# Next lines are not currently relevant, as latest versions of LibreOffice
|
||||||
|
# (i.e., > 7.0.4) are causing troubles.
|
||||||
# See:
|
# See:
|
||||||
# https://github.com/gotenberg/gotenberg/pull/322.
|
# https://packages.debian.org/search?keywords=libreoffice.
|
||||||
# https://github.com/gotenberg/gotenberg/issues/403.
|
# https://github.com/gotenberg/gotenberg/issues/442.
|
||||||
echo "deb https://httpredir.debian.org/debian/ bullseye-backports main contrib non-free" >> /etc/apt/sources.list &&\
|
# https://github.com/gotenberg/gotenberg/issues/516.
|
||||||
apt-get update -qq &&\
|
# https://github.com/gotenberg/gotenberg/issues/529.
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bullseye-backports libreoffice &&\
|
# https://github.com/gotenberg/gotenberg/issues/537.
|
||||||
|
# https://github.com/gotenberg/gotenberg/issues/568.
|
||||||
|
# Note: we use the bullseye-backports distribution to get the latest LibreOffice version.
|
||||||
|
# See:
|
||||||
|
# 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 &&\
|
||||||
|
#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.
|
# Download QPDF.
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends 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/man1 &&\
|
||||||
# Cleanup.
|
# Cleanup.
|
||||||
# Note: the Debian image does automatically a clean after each install thanks to a hook.
|
# Note: the Debian image does automatically a clean after each install thanks to a hook.
|
||||||
# Therefore, there is no need for apt-get clean.
|
# Therefore, there is no need for apt-get clean.
|
||||||
@@ -157,6 +167,7 @@ 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
|
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
||||||
|
|
||||||
|
|||||||
65
go.mod
65
go.mod
@@ -1,46 +1,46 @@
|
|||||||
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-20211226214557-5a9a0564ad26
|
github.com/chromedp/cdproto v0.0.0-20230109101555-6b041c6303cc
|
||||||
github.com/chromedp/chromedp v0.7.6
|
github.com/chromedp/chromedp v0.8.7
|
||||||
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.2
|
||||||
github.com/klauspost/compress v1.13.6 // indirect
|
github.com/klauspost/compress v1.15.14 // 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.10.0
|
||||||
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.17 // 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.12 // indirect
|
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.14.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.11 // 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.9.0
|
||||||
go.uber.org/zap v1.19.1
|
go.uber.org/zap v1.24.0
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
golang.org/x/crypto v0.5.0 // indirect
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
golang.org/x/image v0.3.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e
|
golang.org/x/net v0.5.0
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.1.0
|
||||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
|
golang.org/x/sys v0.4.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
golang.org/x/term v0.4.0
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
@@ -51,16 +51,17 @@ require (
|
|||||||
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect
|
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect
|
||||||
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/kr/pretty v0.3.1 // 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.4 // 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.3.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.39.0 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.9.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.2 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
599
go.sum
599
go.sum
@@ -1,43 +1,3 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
|
||||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
|
||||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
|
||||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
|
||||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
|
||||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
|
||||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
|
||||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
|
||||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
|
||||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
|
||||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
|
||||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
|
||||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
|
||||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
|
||||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
|
||||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
|
||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
|
||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
|
||||||
github.com/alexliesenfeld/health v0.6.0 h1:HRBTCgybNSe4lqGEk7nU82c3bjwh9W+3b46W6UvD4CQ=
|
github.com/alexliesenfeld/health v0.6.0 h1:HRBTCgybNSe4lqGEk7nU82c3bjwh9W+3b46W6UvD4CQ=
|
||||||
github.com/alexliesenfeld/health v0.6.0/go.mod h1:N4NDIeQtlWumG+6z1ne1v62eQxktz5ylEgGgH9emdMw=
|
github.com/alexliesenfeld/health v0.6.0/go.mod h1:N4NDIeQtlWumG+6z1ne1v62eQxktz5ylEgGgH9emdMw=
|
||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
@@ -46,572 +6,191 @@ 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 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=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/chromedp/cdproto v0.0.0-20230109101555-6b041c6303cc h1:pgBIUnhrsNh0Xi+8qUdHIP/nljIVz3laeRv23eqr45M=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/chromedp/cdproto v0.0.0-20230109101555-6b041c6303cc/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20211126220118-81fa0469ad77/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
github.com/chromedp/chromedp v0.8.7 h1:dYOYc5ynTBzwSLOi+1IfgHwPr8r2BqV48l/RC+3OuJ0=
|
||||||
github.com/chromedp/cdproto v0.0.0-20211226214557-5a9a0564ad26 h1:sj/nGJwGadNGvxuA/dKFC+3o9hHi0PTGb+iIMhlCX6A=
|
github.com/chromedp/chromedp v0.8.7/go.mod h1:iL+ywnwk3eG3EVXV1ackXBMNzdEh3Ye/KHvQkq1KRKU=
|
||||||
github.com/chromedp/cdproto v0.0.0-20211226214557-5a9a0564ad26/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
|
||||||
github.com/chromedp/chromedp v0.7.6 h1:2juGaktzjwULlsn+DnvIZXFUckEp5xs+GOBroaea+jA=
|
|
||||||
github.com/chromedp/chromedp v0.7.6/go.mod h1:ayT4YU/MGAALNfOg9gNrpGSAdnU51PMx+FCeuT1iXzo=
|
|
||||||
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
|
||||||
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.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
|
||||||
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=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
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/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/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-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
|
||||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.4.1/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.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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
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.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||||
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/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=
|
||||||
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ=
|
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ=
|
||||||
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
|
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
|
||||||
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94=
|
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94=
|
||||||
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok=
|
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
|
||||||
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.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
|
||||||
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.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc=
|
||||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.15.14/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=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||||
github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
|
||||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
|
||||||
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
|
||||||
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
|
||||||
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/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/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-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 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
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/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.12 h1:44l88ehTZAUGW4VlO1QC4zkilL99M6Y9MXNwEs0uzP8=
|
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||||
github.com/pierrec/lz4/v4 v4.1.12/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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
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=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
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.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/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.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.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
|
||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
|
||||||
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=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
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.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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
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.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.11/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/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||||
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.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
|
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
|
||||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
|
||||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
|
||||||
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-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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||||
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-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
|
||||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
|
||||||
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-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.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
|
||||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/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.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-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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
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-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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8=
|
|
||||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
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-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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/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-20210220032951-036812b2e83c/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-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-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-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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/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-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-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-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-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-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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
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-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-20210510120138-977fb7262007/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/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-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-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.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
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.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.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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
|
||||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
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-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-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=
|
|
||||||
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.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
|
||||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
|
||||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
|
||||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
|
||||||
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/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
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.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.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.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=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
66
pkg/gotenberg/mocks.go
Normal 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)
|
||||||
|
)
|
||||||
82
pkg/gotenberg/mocks_test.go
Normal file
82
pkg/gotenberg/mocks_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -57,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 {
|
||||||
@@ -75,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 {
|
||||||
@@ -98,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 {
|
||||||
@@ -123,7 +123,7 @@ func traceMiddleware(header string) echo.MiddlewareFunc {
|
|||||||
// 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 {
|
||||||
@@ -197,8 +197,8 @@ 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(timeout 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 {
|
||||||
|
|||||||
75
pkg/modules/api/mocks.go
Normal file
75
pkg/modules/api/mocks.go
Normal 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
|
||||||
|
}
|
||||||
105
pkg/modules/api/mocks_test.go
Normal file
105
pkg/modules/api/mocks_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package chromium
|
package chromium
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/cdp"
|
||||||
"github.com/chromedp/cdproto/emulation"
|
"github.com/chromedp/cdproto/emulation"
|
||||||
"github.com/chromedp/cdproto/fetch"
|
"github.com/chromedp/cdproto/fetch"
|
||||||
"github.com/chromedp/cdproto/network"
|
"github.com/chromedp/cdproto/network"
|
||||||
@@ -33,6 +34,10 @@ var (
|
|||||||
// allowed/denied lists.
|
// allowed/denied lists.
|
||||||
ErrURLNotAuthorized = errors.New("URL not authorized")
|
ErrURLNotAuthorized = errors.New("URL not authorized")
|
||||||
|
|
||||||
|
// ErrOmitBackgroundWithoutPrintBackground happens if
|
||||||
|
// Options.OmitBackground is set to true but not Options.PrintBackground.
|
||||||
|
ErrOmitBackgroundWithoutPrintBackground = errors.New("omit background without print background")
|
||||||
|
|
||||||
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
||||||
// "screen" nor "print". Empty value are allowed though.
|
// "screen" nor "print". Empty value are allowed though.
|
||||||
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
||||||
@@ -66,9 +71,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
|
||||||
@@ -142,6 +149,11 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
PrintBackground bool
|
PrintBackground bool
|
||||||
|
|
||||||
|
// OmitBackground hides default white background and allows generating PDFs
|
||||||
|
// with transparency.
|
||||||
|
// Optional.
|
||||||
|
OmitBackground bool
|
||||||
|
|
||||||
// Scale is the scale of the page rendering.
|
// Scale is the scale of the page rendering.
|
||||||
// Optional.
|
// Optional.
|
||||||
Scale float64
|
Scale float64
|
||||||
@@ -212,6 +224,7 @@ func DefaultOptions() Options {
|
|||||||
ExtraScriptTags: nil,
|
ExtraScriptTags: nil,
|
||||||
Landscape: false,
|
Landscape: false,
|
||||||
PrintBackground: false,
|
PrintBackground: false,
|
||||||
|
OmitBackground: false,
|
||||||
Scale: 1.0,
|
Scale: 1.0,
|
||||||
PaperWidth: 8.5,
|
PaperWidth: 8.5,
|
||||||
PaperHeight: 11,
|
PaperHeight: 11,
|
||||||
@@ -250,9 +263,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 +289,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")
|
||||||
@@ -382,6 +399,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 +417,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 +431,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 +461,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)
|
||||||
@@ -500,6 +535,35 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
|
|
||||||
return fmt.Errorf("wait for events: %w", err)
|
return fmt.Errorf("wait for events: %w", err)
|
||||||
}),
|
}),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/226.
|
||||||
|
if !options.OmitBackground {
|
||||||
|
logger.Debug("default white background not hidden")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !options.PrintBackground {
|
||||||
|
// See https://github.com/chromedp/chromedp/issues/1179#issuecomment-1284794416.
|
||||||
|
return fmt.Errorf("validate omit background: %w", ErrOmitBackgroundWithoutPrintBackground)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("hide default white background")
|
||||||
|
|
||||||
|
err := emulation.SetDefaultBackgroundColorOverride().WithColor(
|
||||||
|
&cdp.RGBA{
|
||||||
|
R: 0,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 0,
|
||||||
|
}).Do(ctx)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("hide default white background: %w", err)
|
||||||
|
}),
|
||||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
// See:
|
// See:
|
||||||
// https://github.com/gotenberg/gotenberg/issues/354
|
// https://github.com/gotenberg/gotenberg/issues/354
|
||||||
@@ -732,6 +796,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,21 +806,65 @@ 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).
|
|
||||||
WithHeaderTemplate(options.HeaderTemplate).
|
|
||||||
WithFooterTemplate(options.FooterTemplate).
|
|
||||||
WithPreferCSSPageSize(options.PreferCSSPageSize)
|
WithPreferCSSPageSize(options.PreferCSSPageSize)
|
||||||
|
|
||||||
|
hasCustomHeaderFooter := options.HeaderTemplate != DefaultOptions().HeaderTemplate ||
|
||||||
|
options.FooterTemplate != DefaultOptions().FooterTemplate
|
||||||
|
|
||||||
|
if !hasCustomHeaderFooter {
|
||||||
|
logger.Debug("no custom header nor footer")
|
||||||
|
|
||||||
|
printToPDF = printToPDF.WithDisplayHeaderFooter(false)
|
||||||
|
} else {
|
||||||
|
logger.Debug("with custom header and/or footer")
|
||||||
|
|
||||||
|
printToPDF = printToPDF.
|
||||||
|
WithDisplayHeaderFooter(true).
|
||||||
|
WithHeaderTemplate(options.HeaderTemplate).
|
||||||
|
WithFooterTemplate(options.FooterTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
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 +875,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 +917,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package chromium
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -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,52 @@ 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 omit background but not print background",
|
||||||
|
timeout: time.Duration(60) * time.Second,
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
|
options: Options{
|
||||||
|
OmitBackground: true,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with omit background and print background",
|
||||||
|
timeout: time.Duration(60) * time.Second,
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
|
options: Options{
|
||||||
|
OmitBackground: true,
|
||||||
|
PrintBackground: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 +366,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 +383,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 +391,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 +400,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 +408,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 properties",
|
||||||
|
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 +478,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 +488,61 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample5/index.html",
|
name: "with custom header template only",
|
||||||
|
timeout: time.Duration(60) * time.Second,
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
|
options: Options{
|
||||||
|
HeaderTemplate: func() string {
|
||||||
|
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}(),
|
||||||
|
FooterTemplate: DefaultOptions().FooterTemplate,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "with custom footer template only",
|
||||||
|
timeout: time.Duration(60) * time.Second,
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
|
options: Options{
|
||||||
|
HeaderTemplate: DefaultOptions().HeaderTemplate,
|
||||||
|
FooterTemplate: func() string {
|
||||||
|
b, err := os.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without custom header and footer templates",
|
||||||
|
timeout: time.Duration(60) * time.Second,
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
|
options: Options{
|
||||||
|
HeaderTemplate: DefaultOptions().HeaderTemplate,
|
||||||
|
FooterTemplate: DefaultOptions().FooterTemplate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 +550,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 +571,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 +591,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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -34,7 +33,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
userAgent string
|
userAgent string
|
||||||
extraHTTPHeaders map[string]string
|
extraHTTPHeaders map[string]string
|
||||||
emulatedMediaType string
|
emulatedMediaType string
|
||||||
landscape, printBackground bool
|
landscape, printBackground, omitBackground bool
|
||||||
scale, paperWidth, paperHeight float64
|
scale, paperWidth, paperHeight float64
|
||||||
marginTop, marginBottom, marginLeft, marginRight float64
|
marginTop, marginBottom, marginLeft, marginRight float64
|
||||||
pageRanges string
|
pageRanges string
|
||||||
@@ -79,6 +78,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
}).
|
}).
|
||||||
Bool("landscape", &landscape, defaultOptions.Landscape).
|
Bool("landscape", &landscape, defaultOptions.Landscape).
|
||||||
Bool("printBackground", &printBackground, defaultOptions.PrintBackground).
|
Bool("printBackground", &printBackground, defaultOptions.PrintBackground).
|
||||||
|
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground).
|
||||||
Float64("scale", &scale, defaultOptions.Scale).
|
Float64("scale", &scale, defaultOptions.Scale).
|
||||||
Float64("paperWidth", &paperWidth, defaultOptions.PaperWidth).
|
Float64("paperWidth", &paperWidth, defaultOptions.PaperWidth).
|
||||||
Float64("paperHeight", &paperHeight, defaultOptions.PaperHeight).
|
Float64("paperHeight", &paperHeight, defaultOptions.PaperHeight).
|
||||||
@@ -103,6 +103,7 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
ExtraScriptTags: defaultOptions.ExtraScriptTags,
|
ExtraScriptTags: defaultOptions.ExtraScriptTags,
|
||||||
Landscape: landscape,
|
Landscape: landscape,
|
||||||
PrintBackground: printBackground,
|
PrintBackground: printBackground,
|
||||||
|
OmitBackground: omitBackground,
|
||||||
Scale: scale,
|
Scale: scale,
|
||||||
PaperWidth: paperWidth,
|
PaperWidth: paperWidth,
|
||||||
PaperHeight: paperHeight,
|
PaperHeight: paperHeight,
|
||||||
@@ -130,10 +131,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 +162,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 +272,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)
|
||||||
}
|
}
|
||||||
@@ -507,6 +342,16 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
|
||||||
|
return api.WrapError(
|
||||||
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
|
api.NewSentinelHTTPError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"omitBackground requires printBackground set to true",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if errors.Is(err, ErrInvalidEvaluationExpression) {
|
if errors.Is(err, ErrInvalidEvaluationExpression) {
|
||||||
if options.WaitForExpression == "" {
|
if options.WaitForExpression == "" {
|
||||||
// We do not expect the 'waitWindowStatus' form field to return
|
// We do not expect the 'waitWindowStatus' form field to return
|
||||||
|
|||||||
@@ -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,22 @@ 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 {
|
||||||
|
chromiumAPI := struct{ ProtoAPI }{}
|
||||||
|
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
|
||||||
|
return ErrOmitBackgroundWithoutPrintBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
return chromiumAPI
|
||||||
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
|
expectErr: true,
|
||||||
|
expectHTTPErr: true,
|
||||||
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 +696,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 +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 {
|
||||||
@@ -858,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 {
|
||||||
@@ -873,7 +746,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 +761,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 +774,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 +797,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 +818,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 +839,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 +857,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 {
|
||||||
|
|||||||
117
pkg/modules/chromium/stream.go
Normal file
117
pkg/modules/chromium/stream.go
Normal 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)
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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
3
pkg/modules/libreoffice/uno/doc.go
Normal file
3
pkg/modules/libreoffice/uno/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package uno provides a module which interacts with the UNO
|
||||||
|
// (Universal Network Objects) API.
|
||||||
|
package uno
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package unoconv
|
package uno
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,18 +9,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func freePort(logger *zap.Logger) (int, error) {
|
func freePort(logger *zap.Logger) (int, error) {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
netListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
err := listener.Close()
|
err := netListener.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("close listener: %s", err.Error()))
|
logger.Error(fmt.Sprintf("close network listener: %s", err.Error()))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
addr := listener.Addr().String()
|
addr := netListener.Addr().String()
|
||||||
|
|
||||||
_, portStr, err := net.SplitHostPort(addr)
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
379
pkg/modules/libreoffice/uno/listener.go
Normal file
379
pkg/modules/libreoffice/uno/listener.go
Normal 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)
|
||||||
|
)
|
||||||
431
pkg/modules/libreoffice/uno/listener_test.go
Normal file
431
pkg/modules/libreoffice/uno/listener_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
36
pkg/modules/libreoffice/uno/mocks.go
Normal file
36
pkg/modules/libreoffice/uno/mocks.go
Normal 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)
|
||||||
|
)
|
||||||
42
pkg/modules/libreoffice/uno/mocks_test.go
Normal file
42
pkg/modules/libreoffice/uno/mocks_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
515
pkg/modules/libreoffice/uno/uno.go
Normal file
515
pkg/modules/libreoffice/uno/uno.go
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
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",
|
||||||
|
".odg",
|
||||||
|
".dotx",
|
||||||
|
".xltx",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
)
|
||||||
817
pkg/modules/libreoffice/uno/uno_test.go
Normal file
817
pkg/modules/libreoffice/uno/uno_test.go
Normal 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 := 79
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
// Package unoconv provides a module which abstracts the CLI tool unoconv.
|
|
||||||
package unoconv
|
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
package unoconv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
"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
|
|
||||||
disableListener bool
|
|
||||||
|
|
||||||
listenerCmd gotenberg.Cmd
|
|
||||||
listenerPort int
|
|
||||||
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
|
|
||||||
|
|
||||||
// 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",
|
|
||||||
FlagSet: func() *flag.FlagSet {
|
|
||||||
fs := flag.NewFlagSet("unoconv", flag.ExitOnError)
|
|
||||||
fs.Bool("unoconv-disable-listener", false, "Do not start a unoconv listener - save resources in detriment of performance")
|
|
||||||
|
|
||||||
return fs
|
|
||||||
}(),
|
|
||||||
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(ctx *gotenberg.Context) error {
|
|
||||||
flags := ctx.ParsedFlags()
|
|
||||||
mod.disableListener = flags.MustBool("unoconv-disable-listener")
|
|
||||||
|
|
||||||
binPath, ok := os.LookupEnv("UNOCONV_BIN_PATH")
|
|
||||||
if !ok {
|
|
||||||
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.binPath = binPath
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod *Unoconv) Start() error {
|
|
||||||
if mod.disableListener {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := freePort(mod.logger)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get free port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.listenerPort = port
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"--listener",
|
|
||||||
"--user-profile",
|
|
||||||
// Just to make sure LibreOffice does not leak files in an unknown
|
|
||||||
// directory. The directory will be removed anyway by the garbage
|
|
||||||
// collector.
|
|
||||||
fmt.Sprintf("//%s", gotenberg.NewDirPath()),
|
|
||||||
"--port",
|
|
||||||
fmt.Sprintf("%d", mod.listenerPort),
|
|
||||||
}
|
|
||||||
|
|
||||||
checkedEntry := mod.logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
|
|
||||||
if checkedEntry != nil {
|
|
||||||
args = append(args, "-vvv")
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.listenerCmd = gotenberg.Command(mod.logger, mod.binPath, args...)
|
|
||||||
|
|
||||||
err = mod.listenerCmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("start unoconv listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listenerActiveInstancesCountMu.Lock()
|
|
||||||
listenerActiveInstancesCount += 1
|
|
||||||
listenerActiveInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartupMessage returns a custom startup message.
|
|
||||||
func (mod Unoconv) StartupMessage() string {
|
|
||||||
if mod.disableListener {
|
|
||||||
return "listener disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("listener started on port %d", mod.listenerPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the HTTP server.
|
|
||||||
func (mod *Unoconv) Stop(ctx context.Context) error {
|
|
||||||
if mod.disableListener {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
return errors.New("no context dead line")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block until the context is done so that other module may gracefully stop
|
|
||||||
// before we do a shutdown cleanup.
|
|
||||||
mod.logger.Debug("wait for the end of grace duration")
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
|
|
||||||
err := mod.listenerCmd.Kill()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("kill unoconv listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listenerActiveInstancesCountMu.Lock()
|
|
||||||
listenerActiveInstancesCount -= 1
|
|
||||||
listenerActiveInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
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 unoconv instances.",
|
|
||||||
Read: func() float64 {
|
|
||||||
activeInstancesCountMu.RLock()
|
|
||||||
defer activeInstancesCountMu.RUnlock()
|
|
||||||
|
|
||||||
return activeInstancesCount
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "unoconv_listener_active_instances_count",
|
|
||||||
Description: "Current number of active unoconv listener instances.",
|
|
||||||
Read: func() float64 {
|
|
||||||
listenerActiveInstancesCountMu.RLock()
|
|
||||||
defer listenerActiveInstancesCountMu.RUnlock()
|
|
||||||
|
|
||||||
return listenerActiveInstancesCount
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "unoconv_listener_queue_length",
|
|
||||||
Description: "Current number of processes in the queue.",
|
|
||||||
Read: func() float64 {
|
|
||||||
listenerQueueLengthMu.RLock()
|
|
||||||
defer listenerQueueLengthMu.RUnlock()
|
|
||||||
|
|
||||||
return listenerQueueLength
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unoconv returns an API for interacting with unoconv.
|
|
||||||
func (mod *Unoconv) Unoconv() (API, error) {
|
|
||||||
return mod, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF converts a document to PDF.
|
|
||||||
//
|
|
||||||
// In stateless mode, 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.
|
|
||||||
//
|
|
||||||
// In listener mode, it calls the unoconv listener to interact with
|
|
||||||
// LibreOffice, improving substantially the performance. However, it cannot
|
|
||||||
// perform parallel operations and have to wait for the lock to be available.
|
|
||||||
func (mod Unoconv) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
|
||||||
args := []string{
|
|
||||||
"--format",
|
|
||||||
"pdf",
|
|
||||||
}
|
|
||||||
|
|
||||||
var userProfileDirPath string
|
|
||||||
|
|
||||||
if mod.disableListener {
|
|
||||||
port, err := freePort(logger)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get free port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userProfileDirPath = gotenberg.NewDirPath()
|
|
||||||
|
|
||||||
args = append(args,
|
|
||||||
"--port",
|
|
||||||
fmt.Sprintf("%d", port),
|
|
||||||
"--user-profile",
|
|
||||||
fmt.Sprintf("//%s", userProfileDirPath),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
args = append(args, "--port", fmt.Sprintf("%d", mod.listenerPort))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if !mod.disableListener {
|
|
||||||
listenerQueueLengthMu.Lock()
|
|
||||||
listenerQueueLength += 1
|
|
||||||
listenerQueueLengthMu.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case listenerLock <- struct{}{}:
|
|
||||||
logger.Debug("unoconv lock acquired")
|
|
||||||
|
|
||||||
listenerQueueLengthMu.Lock()
|
|
||||||
listenerQueueLength -= 1
|
|
||||||
listenerQueueLengthMu.Unlock()
|
|
||||||
|
|
||||||
break
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.Debug("failed to acquire the unoconv lock before deadline")
|
|
||||||
|
|
||||||
listenerQueueLengthMu.Lock()
|
|
||||||
listenerQueueLength -= 1
|
|
||||||
listenerQueueLengthMu.Unlock()
|
|
||||||
|
|
||||||
return fmt.Errorf("acquire unoconv lock: %w", ctx.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
<-listenerLock
|
|
||||||
logger.Debug("unoconv lock released")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
if mod.disableListener {
|
|
||||||
// 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 (
|
|
||||||
listenerLock = make(chan struct{}, 1)
|
|
||||||
listenerQueueLength float64
|
|
||||||
listenerQueueLengthMu sync.RWMutex
|
|
||||||
listenerActiveInstancesCount float64
|
|
||||||
listenerActiveInstancesCountMu sync.RWMutex
|
|
||||||
activeInstancesCount float64
|
|
||||||
activeInstancesCountMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.Provisioner = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.Validator = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.App = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.MetricsProvider = (*Unoconv)(nil)
|
|
||||||
_ API = (*Unoconv)(nil)
|
|
||||||
_ Provider = (*Unoconv)(nil)
|
|
||||||
)
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
package unoconv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoValidator struct {
|
|
||||||
ProtoModule
|
|
||||||
validate func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoValidator) Validate() error {
|
|
||||||
return mod.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoLoggerProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
logger func(mod gotenberg.Module) (*zap.Logger, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory ProtoLoggerProvider) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return factory.logger(mod)
|
|
||||||
}
|
|
||||||
|
|
||||||
func 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) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
ctx *gotenberg.Context
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
ctx: gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{FlagSet: new(Unoconv).Descriptor().FlagSet},
|
|
||||||
make([]gotenberg.ModuleDescriptor, 0),
|
|
||||||
),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return nil, errors.New("foo") }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: new(Unoconv).Descriptor().FlagSet,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: new(Unoconv).Descriptor().FlagSet,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
err := mod.Provision(tc.ctx)
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 := Unoconv{
|
|
||||||
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 TestUnoconv_Start(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
mod *Unoconv
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
mod: &Unoconv{
|
|
||||||
disableListener: true,
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mod: &Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: false,
|
|
||||||
logger: zap.NewExample(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
err := tc.mod.Start()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(1)*time.Nanosecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = tc.mod.Stop(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d: expected not error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_StartupMessage(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
disableListener bool
|
|
||||||
expectMessage string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
disableListener: true,
|
|
||||||
expectMessage: "listener disabled",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectMessage: "listener started on port 0",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
mod := Unoconv{
|
|
||||||
disableListener: tc.disableListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := mod.StartupMessage()
|
|
||||||
if actual != tc.expectMessage {
|
|
||||||
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectMessage, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Stop(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
start bool
|
|
||||||
disableListener bool
|
|
||||||
timeout time.Duration
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
start: true,
|
|
||||||
timeout: time.Duration(1) * time.Nanosecond,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
mod := &Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: tc.disableListener,
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.start {
|
|
||||||
err := mod.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if tc.timeout == 0 {
|
|
||||||
err = mod.Stop(context.TODO())
|
|
||||||
} else {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = mod.Stop(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Metrics(t *testing.T) {
|
|
||||||
metrics, err := new(Unoconv).Metrics()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(metrics) != 3 {
|
|
||||||
t.Fatalf("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)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[1].Read()
|
|
||||||
if actual != 0 {
|
|
||||||
t.Errorf("expected %d unoconv listener instances, but got %f", 0, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[2].Read()
|
|
||||||
if actual != 0 {
|
|
||||||
t.Errorf("expected %d processes in the queue, 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
|
|
||||||
mod Unoconv
|
|
||||||
logger *zap.Logger
|
|
||||||
inputPath string
|
|
||||||
options Options
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
logger: zap.NewExample(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
options: Options{
|
|
||||||
Landscape: true,
|
|
||||||
PageRanges: "1-2",
|
|
||||||
PDFArchive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() context.Context {
|
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if !tc.mod.disableListener {
|
|
||||||
err = tc.mod.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's give it some room to start.
|
|
||||||
time.Sleep(time.Duration(1) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tc.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.mod.disableListener {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(1)*time.Nanosecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = tc.mod.Stop(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoValidator)(nil)
|
|
||||||
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
|
|
||||||
)
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,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)
|
||||||
@@ -68,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package pdfengines
|
package pdfengines
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -11,38 +10,7 @@ import (
|
|||||||
"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())
|
||||||
@@ -53,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(
|
||||||
@@ -125,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(
|
||||||
@@ -159,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 }}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,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 }}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,21 +331,31 @@ 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,76 +365,81 @@ func TestPDFEngines_SystemMessages(t *testing.T) {
|
|||||||
|
|
||||||
messages := mod.SystemMessages()
|
messages := mod.SystemMessages()
|
||||||
if len(messages) != 1 {
|
if len(messages) != 1 {
|
||||||
t.Errorf("expected one and only one message but got %d", len(messages))
|
t.Errorf("expected one and only one message from mod.SystemMessages(), but got %d", len(messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect := strings.Join(mod.names[:], " ")
|
expect := strings.Join(mod.names[:], " ")
|
||||||
if messages[0] != expect {
|
if messages[0] != expect {
|
||||||
t.Errorf("expected message '%s' but got '%s'", expect, messages[0])
|
t.Errorf("expected message '%s' from mod.SystemMessages(), but got '%s'", expect, messages[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_PDFEngine(t *testing.T) {
|
func TestPDFEngines_PDFEngine(t *testing.T) {
|
||||||
mod := new(PDFEngines)
|
mod := PDFEngines{
|
||||||
mod.names = []string{"foo", "bar"}
|
names: []string{"foo", "bar"},
|
||||||
mod.engines = func() []gotenberg.PDFEngine {
|
engines: func() []gotenberg.PDFEngine {
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
engine1 := struct {
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
gotenberg.PDFEngineMock
|
||||||
}
|
}{}
|
||||||
|
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||||
|
}
|
||||||
|
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
engine2 := struct {
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
gotenberg.PDFEngineMock
|
||||||
}
|
}{}
|
||||||
|
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
||||||
|
}
|
||||||
|
|
||||||
return []gotenberg.PDFEngine{
|
return []gotenberg.PDFEngine{
|
||||||
engine1,
|
engine1,
|
||||||
engine2,
|
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)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []s
|
|||||||
activeInstancesCount += 1
|
activeInstancesCount += 1
|
||||||
activeInstancesCountMu.Unlock()
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
err = cmd.Exec()
|
_, err = cmd.Exec()
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
activeInstancesCountMu.Lock()
|
||||||
activeInstancesCount -= 1
|
activeInstancesCount -= 1
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package webhook
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ func (c client) send(body io.Reader, headers map[string]string, erroed bool) err
|
|||||||
// Worse, the "Content-Length" header is also removed. Therefore, in
|
// Worse, the "Content-Length" header is also removed. Therefore, in
|
||||||
// order to keep this valuable information, we have to trust the caller
|
// order to keep this valuable information, we have to trust the caller
|
||||||
// by reading the value of the "Content-Length" entry and set it as the
|
// by reading the value of the "Content-Length" entry and set it as the
|
||||||
// content length of the request. It's kinda sub-optimal, but hey, at
|
// content length of the request. It's kinda suboptimal, but hey, at
|
||||||
// least it works.
|
// least it works.
|
||||||
|
|
||||||
bodySize, err := strconv.ParseInt(contentLength, 10, 64)
|
bodySize, err := strconv.ParseInt(contentLength, 10, 64)
|
||||||
@@ -78,6 +79,10 @@ func (c client) send(body io.Reader, headers map[string]string, erroed bool) err
|
|||||||
return fmt.Errorf("send '%s' request to '%s': %w", method, URL, err)
|
return fmt.Errorf("send '%s' request to '%s': %w", method, URL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return fmt.Errorf("send '%s' request to '%s': got status: '%s'", method, URL, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := resp.Body.Close()
|
err := resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -339,6 +339,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
expectWebhookFilename string
|
expectWebhookFilename string
|
||||||
expectWebhookErrorStatus int
|
expectWebhookErrorStatus int
|
||||||
expectWebhookErrorMessage string
|
expectWebhookErrorMessage string
|
||||||
|
returnedError *echo.HTTPError
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
request: buildMultipartFormDataRequest(),
|
request: buildMultipartFormDataRequest(),
|
||||||
@@ -358,13 +359,13 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
next: func() echo.HandlerFunc {
|
next: func() echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
return nil
|
return api.NewSentinelHTTPError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||||
expectWebhookMethod: http.MethodPost,
|
expectWebhookMethod: http.MethodPost,
|
||||||
expectWebhookErrorStatus: http.StatusInternalServerError,
|
expectWebhookErrorStatus: http.StatusBadRequest,
|
||||||
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
|
expectWebhookErrorMessage: http.StatusText(http.StatusBadRequest),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
request: func() *http.Request {
|
request: func() *http.Request {
|
||||||
@@ -387,6 +388,28 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
expectWebhookFilename: "foo",
|
expectWebhookFilename: "foo",
|
||||||
expectWebhookExtraHTTPHeaders: map[string]string{"foo": "bar"},
|
expectWebhookExtraHTTPHeaders: map[string]string{"foo": "bar"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
request: func() *http.Request {
|
||||||
|
req := buildMultipartFormDataRequest()
|
||||||
|
req.Header.Set("Gotenberg-Output-Filename", "foo")
|
||||||
|
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
mod: buildWebhookModule(),
|
||||||
|
next: func() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
return ctx.AddOutputPaths("/tests/test/testdata/api/sample1.pdf")
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
returnedError: echo.ErrInternalServerError,
|
||||||
|
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||||
|
expectWebhookMethod: http.MethodPost,
|
||||||
|
expectWebhookErrorStatus: http.StatusInternalServerError,
|
||||||
|
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
|
||||||
|
expectWebhookFilename: "foo",
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
func() {
|
||||||
srv := echo.New()
|
srv := echo.New()
|
||||||
@@ -399,7 +422,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
c.Set("trace", "foo")
|
c.Set("trace", "foo")
|
||||||
c.Set("startTime", time.Now())
|
c.Set("startTime", time.Now())
|
||||||
|
|
||||||
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 +473,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 +512,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
|
||||||
@@ -500,6 +523,11 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errChan <- nil
|
errChan <- nil
|
||||||
|
|
||||||
|
if tc.returnedError != nil {
|
||||||
|
return tc.returnedError
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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"
|
||||||
|
|||||||
@@ -38,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" \
|
||||||
|
|||||||
@@ -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" ]
|
||||||
BIN
test/testdata/chromium/html/sample11/font.woff
vendored
BIN
test/testdata/chromium/html/sample11/font.woff
vendored
Binary file not shown.
12
test/testdata/chromium/html/sample11/index.html
vendored
12
test/testdata/chromium/html/sample11/index.html
vendored
@@ -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>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
let extra = document.getElementById('extra')
|
|
||||||
extra.innerText = extra.innerText + ' and extra JavaScript tags too!'
|
|
||||||
14
test/testdata/chromium/html/sample11/style.css
vendored
14
test/testdata/chromium/html/sample11/style.css
vendored
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user