mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b788cf823d | ||
|
|
11cbc0288f | ||
|
|
2269c80b02 | ||
|
|
c922cc042e | ||
|
|
9cb6f44f72 | ||
|
|
f376cd5ab1 | ||
|
|
8b8a02b68c | ||
|
|
5759b1336a | ||
|
|
76f17cb6d7 | ||
|
|
7149c421cf | ||
|
|
353c599767 | ||
|
|
285c43dab7 | ||
|
|
bd5933f4c2 | ||
|
|
0faac08237 | ||
|
|
14f94b1566 | ||
|
|
415e2ff265 | ||
|
|
26e97358d1 | ||
|
|
0ed6fb33c5 | ||
|
|
df98e7512b | ||
|
|
99ede6477f | ||
|
|
ee0debe14a | ||
|
|
dd1e11f102 | ||
|
|
ed47493459 | ||
|
|
5b8391067d | ||
|
|
4f41a59ceb | ||
|
|
4525cb38e4 | ||
|
|
704dfc25e6 |
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.42
|
version: v1.43
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -12,7 +12,7 @@ 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.028 # See https://github.com/googlefonts/noto-emoji/releases.
|
||||||
PDFTK_VERSION=1527259628 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
PDFTK_VERSION=1527259628 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||||
GOLANGCI_LINT_VERSION=v1.42.1 # See https://github.com/golangci/golangci-lint/releases.
|
GOLANGCI_LINT_VERSION=v1.43.0 # See https://github.com/golangci/golangci-lint/releases.
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: ## Build the Gotenberg's Docker image
|
build: ## Build the Gotenberg's Docker image
|
||||||
@@ -35,13 +35,14 @@ API_WRITE_TIMEOUT=30s
|
|||||||
API_ROOT_PATH=/
|
API_ROOT_PATH=/
|
||||||
API_TRACE_HEADER=Gotenberg-Trace
|
API_TRACE_HEADER=Gotenberg-Trace
|
||||||
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||||
CHROMIUM_USER_AGENT=
|
|
||||||
CHROMIUM_INCOGNITO=false
|
CHROMIUM_INCOGNITO=false
|
||||||
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
||||||
|
CHROMIUM_DISABLE_WEB_SECURITY=false
|
||||||
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
||||||
CHROMIUM_PROXY_SERVER=
|
CHROMIUM_PROXY_SERVER=
|
||||||
CHROMIUM_ALLOW_LIST=
|
CHROMIUM_ALLOW_LIST=
|
||||||
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
|
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
|
||||||
|
CHROMIUM_DISABLE_JAVASCRIPT=false
|
||||||
CHROMIUM_DISABLE_ROUTES=false
|
CHROMIUM_DISABLE_ROUTES=false
|
||||||
LIBREOFFICE_DISABLES_ROUTES=false
|
LIBREOFFICE_DISABLES_ROUTES=false
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
@@ -52,6 +53,7 @@ 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
|
||||||
WEBHOOK_ALLOW_LIST=
|
WEBHOOK_ALLOW_LIST=
|
||||||
WEBHOOK_DENY_LIST=
|
WEBHOOK_DENY_LIST=
|
||||||
WEBHOOK_ERROR_ALLOW_LIST=
|
WEBHOOK_ERROR_ALLOW_LIST=
|
||||||
@@ -76,13 +78,14 @@ run: ## Start a Gotenberg container
|
|||||||
--api-root-path=$(API_ROOT_PATH) \
|
--api-root-path=$(API_ROOT_PATH) \
|
||||||
--api-trace-header=$(API_TRACE_HEADER) \
|
--api-trace-header=$(API_TRACE_HEADER) \
|
||||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||||
--chromium-user-agent=$(CHROMIUM_USER_AGENT) \
|
|
||||||
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
||||||
--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-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
|
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
|
||||||
--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) \
|
||||||
|
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
|
||||||
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
|
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
|
||||||
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLES_ROUTES) \
|
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLES_ROUTES) \
|
||||||
--log-level=$(LOG_LEVEL) \
|
--log-level=$(LOG_LEVEL) \
|
||||||
@@ -93,6 +96,7 @@ 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) \
|
||||||
--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) \
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
|
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
|
||||||
<h3 align="center">Gotenberg</h3>
|
<h3 align="center">Gotenberg</h3>
|
||||||
<p align="center">A Docker-powered stateless API for PDF files</p>
|
<p align="center">A Docker-powered stateless API for PDF files</p>
|
||||||
<p align="center"><a href="https://gotenberg.dev/docs/about">Documentation</a> · 🔥 <a href="https://gotenberg.dev/docs/get-started/live-demo">Live Demo</a></p>
|
<p align="center"><a href="https://gotenberg.dev/docs/about">Documentation</a> · 🔥 <a href="https://gotenberg.dev/docs/get-started/live-demo">Live Demo</a></p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ RUN \
|
|||||||
# 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/pdftk-java/pdftk/-/jobs/$PDFTK_VERSION/artifacts/raw/build/libs/pdftk-all.jar" &&\
|
||||||
chmod a+x /usr/bin/pdftk-all.jar &&\
|
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||||
|
# Download QPDF.
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
|
||||||
# See https://github.com/nextcloud/docker/issues/380.
|
# See https://github.com/nextcloud/docker/issues/380.
|
||||||
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
||||||
# Cleanup.
|
# Cleanup.
|
||||||
@@ -142,7 +144,8 @@ RUN \
|
|||||||
chromium --version &&\
|
chromium --version &&\
|
||||||
libreoffice --version &&\
|
libreoffice --version &&\
|
||||||
unoconv --version &&\
|
unoconv --version &&\
|
||||||
pdftk --version
|
pdftk --version &&\
|
||||||
|
qpdf --version
|
||||||
|
|
||||||
# Copy the Gotenberg binary from the builder stage.
|
# Copy the Gotenberg binary from the builder stage.
|
||||||
COPY --from=builder /home/gotenberg /usr/bin/
|
COPY --from=builder /home/gotenberg /usr/bin/
|
||||||
@@ -152,6 +155,7 @@ 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 PDFTK_BIN_PATH /usr/bin/pdftk
|
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
||||||
|
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.
|
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.
|
||||||
@@ -33,7 +34,7 @@ var Version = "snapshot"
|
|||||||
func Run() {
|
func Run() {
|
||||||
fmt.Printf(banner, Version)
|
fmt.Printf(banner, Version)
|
||||||
|
|
||||||
// Creates the roo` FlagSet and adds the modules flags to it.
|
// Create the root FlagSet and adds the modules flags to it.
|
||||||
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
||||||
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
|
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
|
||||||
|
|
||||||
@@ -46,14 +47,14 @@ func Run() {
|
|||||||
|
|
||||||
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
|
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
|
||||||
|
|
||||||
// Parses the flags...
|
// Parse the flags...
|
||||||
err := fs.Parse(os.Args[1:])
|
err := fs.Parse(os.Args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...and creates a wrapper around those.
|
// ...and create a wrapper around those.
|
||||||
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
|
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
// Get the graceful shutdown duration.
|
// Get the graceful shutdown duration.
|
||||||
@@ -61,7 +62,7 @@ func Run() {
|
|||||||
|
|
||||||
ctx := gotenberg.NewContext(parsedFlags, descriptors)
|
ctx := gotenberg.NewContext(parsedFlags, descriptors)
|
||||||
|
|
||||||
// Starts application modules.
|
// Start application modules.
|
||||||
apps, err := ctx.Modules(new(gotenberg.App))
|
apps, err := ctx.Modules(new(gotenberg.App))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[FATAL] %s\n", err)
|
fmt.Printf("[FATAL] %s\n", err)
|
||||||
@@ -87,7 +88,23 @@ func Run() {
|
|||||||
|
|
||||||
fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage)
|
fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage)
|
||||||
}(a.(gotenberg.App))
|
}(a.(gotenberg.App))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get modules that want to print system messages.
|
||||||
|
sysLoggers, err := ctx.Modules(new(gotenberg.SystemLogger))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[FATAL] %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range sysLoggers {
|
||||||
|
go func(logger gotenberg.SystemLogger) {
|
||||||
|
id := logger.(gotenberg.Module).Descriptor().ID
|
||||||
|
|
||||||
|
for _, message := range logger.SystemMessages() {
|
||||||
|
fmt.Printf("[SYSTEM] %s: %s\n", id, message)
|
||||||
|
}
|
||||||
|
}(l.(gotenberg.SystemLogger))
|
||||||
}
|
}
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
@@ -101,18 +118,40 @@ func Run() {
|
|||||||
gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration)
|
gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
forceQuit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(forceQuit, os.Interrupt)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// In case of force quit, cancel the context.
|
||||||
|
<-forceQuit
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
|
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
|
||||||
|
|
||||||
|
eg, _ := errgroup.WithContext(gracefulShutdownCtx)
|
||||||
|
|
||||||
for _, a := range apps {
|
for _, a := range apps {
|
||||||
id := a.(gotenberg.Module).Descriptor().ID
|
eg.Go(func(app gotenberg.App) func() error {
|
||||||
app := a.(gotenberg.App)
|
return func() error {
|
||||||
|
id := app.(gotenberg.Module).Descriptor().ID
|
||||||
|
|
||||||
err = app.Stop(gracefulShutdownCtx)
|
err = app.Stop(gracefulShutdownCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ERROR] stopping %s: %s\n", id, err)
|
return fmt.Errorf("stopping %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
|
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}(a.(gotenberg.App)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[FATAL] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
14
go.mod
14
go.mod
@@ -4,9 +4,9 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexliesenfeld/health v0.6.0
|
github.com/alexliesenfeld/health v0.6.0
|
||||||
github.com/andybalholm/brotli v1.0.3 // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20211025030258-2570df970243
|
github.com/chromedp/cdproto v0.0.0-20211205231339-d2673e93eee4
|
||||||
github.com/chromedp/chromedp v0.7.4
|
github.com/chromedp/chromedp v0.7.6
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
@@ -14,13 +14,13 @@ require (
|
|||||||
github.com/klauspost/compress v1.13.6 // indirect
|
github.com/klauspost/compress v1.13.6 // 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.6.1
|
||||||
github.com/labstack/gommon v0.3.0
|
github.com/labstack/gommon v0.3.1
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // 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.16
|
||||||
github.com/nwaples/rardecode v1.1.2 // indirect
|
github.com/nwaples/rardecode v1.1.2 // indirect
|
||||||
github.com/pdfcpu/pdfcpu v0.3.12
|
github.com/pdfcpu/pdfcpu v0.3.12
|
||||||
github.com/pierrec/lz4/v4 v4.1.10 // indirect
|
github.com/pierrec/lz4/v4 v4.1.12 // indirect
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.11.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
|
||||||
@@ -32,7 +32,7 @@ require (
|
|||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e
|
golang.org/x/net v0.0.0-20210913180222-943fd674d43e
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
|
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.3.7
|
||||||
)
|
)
|
||||||
@@ -52,7 +52,7 @@ require (
|
|||||||
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect
|
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
|
|||||||
29
go.sum
29
go.sum
@@ -41,8 +41,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
|
|||||||
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=
|
||||||
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
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=
|
||||||
@@ -55,11 +55,11 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
|
|||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20210713064928-7d28b402946a/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
github.com/chromedp/cdproto v0.0.0-20211126220118-81fa0469ad77/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||||
github.com/chromedp/cdproto v0.0.0-20211025030258-2570df970243 h1:09MpdVvSYOhIByW2Z1kWYkh0cjui9k9Imx28pk01KXA=
|
github.com/chromedp/cdproto v0.0.0-20211205231339-d2673e93eee4 h1:St4rQbn3gGWL59ygb4NBxchIeAIW0CTz5Kw4m5JTemU=
|
||||||
github.com/chromedp/cdproto v0.0.0-20211025030258-2570df970243/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
github.com/chromedp/cdproto v0.0.0-20211205231339-d2673e93eee4/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||||
github.com/chromedp/chromedp v0.7.4 h1:U+0d3WbB/Oj4mDuBOI0P7S3PJEued5UZIl5AJ3QulwU=
|
github.com/chromedp/chromedp v0.7.6 h1:2juGaktzjwULlsn+DnvIZXFUckEp5xs+GOBroaea+jA=
|
||||||
github.com/chromedp/chromedp v0.7.4/go.mod h1:dBj+SXuQHznp6ZPwZeDDEBZKwclUwDLbZ0hjMialMYs=
|
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
@@ -198,14 +198,16 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M=
|
github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M=
|
||||||
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
||||||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
|
||||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
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.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.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
|
||||||
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.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
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.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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
@@ -231,8 +233,8 @@ github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzb
|
|||||||
github.com/pdfcpu/pdfcpu v0.3.12 h1:B+MdKisilWNSk5OCO58Z9U6H93usH73xqk6hMOaZCls=
|
github.com/pdfcpu/pdfcpu v0.3.12 h1:B+MdKisilWNSk5OCO58Z9U6H93usH73xqk6hMOaZCls=
|
||||||
github.com/pdfcpu/pdfcpu v0.3.12/go.mod h1:8XVBtVxuuIuSZL4Ez15Q4QoC+H8zeAaGnuiOEwAk8jA=
|
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.10 h1:H0LgOg/8kTVhiN8ESXFa+gvt9udNp1hQ+mYNDdcMPTM=
|
github.com/pierrec/lz4/v4 v4.1.12 h1:44l88ehTZAUGW4VlO1QC4zkilL99M6Y9MXNwEs0uzP8=
|
||||||
github.com/pierrec/lz4/v4 v4.1.10/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.12/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -440,13 +442,14 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
|
|
||||||
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-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
|
||||||
|
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/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-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func Command(logger *zap.Logger, binPath string, args ...string) Cmd {
|
|||||||
|
|
||||||
return Cmd{
|
return Cmd{
|
||||||
ctx: nil,
|
ctx: nil,
|
||||||
logger: logger.Named("cmd"),
|
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
|
||||||
process: cmd,
|
process: cmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func CommandContext(ctx context.Context, logger *zap.Logger, binPath string, arg
|
|||||||
|
|
||||||
return Cmd{
|
return Cmd{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
logger: logger.Named("cmd"),
|
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
|
||||||
process: cmd,
|
process: cmd,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ type App interface {
|
|||||||
Stop(ctx context.Context) error
|
Stop(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemLogger is a module interface for modules which want to display
|
||||||
|
// messages on startup.
|
||||||
|
type SystemLogger interface {
|
||||||
|
SystemMessages() []string
|
||||||
|
}
|
||||||
|
|
||||||
// MustRegisterModule registers a module.
|
// MustRegisterModule registers a module.
|
||||||
//
|
//
|
||||||
// To register a module, create an init() method in the module main go file:
|
// To register a module, create an init() method in the module main go file:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type SentinelHTTPError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSentinelHTTPError creates a SentinelHTTPError. The message will be sent
|
// NewSentinelHTTPError creates a SentinelHTTPError. The message will be sent
|
||||||
// as the response's body if returned from an handler, so make sure to not leak
|
// as the response's body if returned from a handler, so make sure to not leak
|
||||||
// sensible information.
|
// sensible information.
|
||||||
func NewSentinelHTTPError(status int, message string) SentinelHTTPError {
|
func NewSentinelHTTPError(status int, message string) SentinelHTTPError {
|
||||||
return SentinelHTTPError{
|
return SentinelHTTPError{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -147,10 +148,17 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
|||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
startTime := c.Get("startTime").(time.Time)
|
startTime := c.Get("startTime").(time.Time)
|
||||||
trace := c.Get("trace").(string)
|
trace := c.Get("trace").(string)
|
||||||
|
rootPath := c.Get("rootPath").(string)
|
||||||
|
|
||||||
// Create the request logger and add it to our locals.
|
// Create the request logger and add it to our locals.
|
||||||
reqLogger := logger.With(zap.String("trace", trace))
|
reqLogger := logger.With(zap.String("trace", trace))
|
||||||
c.Set("logger", reqLogger)
|
c.Set("logger", reqLogger.Named(func() string {
|
||||||
|
return strings.ReplaceAll(
|
||||||
|
strings.ReplaceAll(c.Request().URL.Path, rootPath, ""),
|
||||||
|
"/",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
}()))
|
||||||
|
|
||||||
// Call the next middleware in the chain.
|
// Call the next middleware in the chain.
|
||||||
err := next(c)
|
err := next(c)
|
||||||
@@ -159,7 +167,6 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range disableLoggingForPaths {
|
for _, path := range disableLoggingForPaths {
|
||||||
rootPath := c.Get("rootPath").(string)
|
|
||||||
URI := fmt.Sprintf("%s%s", rootPath, path)
|
URI := fmt.Sprintf("%s%s", rootPath, path)
|
||||||
|
|
||||||
if c.Request().RequestURI == URI {
|
if c.Request().RequestURI == URI {
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
"github.com/chromedp/cdproto/page"
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/cdproto/runtime"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -30,6 +33,14 @@ var (
|
|||||||
// allowed/denied lists.
|
// allowed/denied lists.
|
||||||
ErrURLNotAuthorized = errors.New("URL not authorized")
|
ErrURLNotAuthorized = errors.New("URL not authorized")
|
||||||
|
|
||||||
|
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
||||||
|
// "screen" nor "print". Empty value are allowed though.
|
||||||
|
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
||||||
|
|
||||||
|
// ErrInvalidEvaluationExpression happens if an evaluation expression
|
||||||
|
// returns an exception or undefined.
|
||||||
|
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
|
||||||
|
|
||||||
// ErrInvalidPrinterSettings happens if the Options have one or more
|
// ErrInvalidPrinterSettings happens if the Options have one or more
|
||||||
// aberrant values.
|
// aberrant values.
|
||||||
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
||||||
@@ -41,6 +52,11 @@ var (
|
|||||||
// ErrRpccMessageTooLarge happens when the messages received by
|
// ErrRpccMessageTooLarge happens when the messages received by
|
||||||
// ChromeDevTools are larger than 100 MB.
|
// ChromeDevTools are larger than 100 MB.
|
||||||
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
||||||
|
|
||||||
|
// ErrConsoleExceptions happens when there are exceptions in the Chromium
|
||||||
|
// console. It also happens only if the Options.FailOnConsoleExceptions is
|
||||||
|
// set to true.
|
||||||
|
ErrConsoleExceptions = errors.New("console exceptions")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chromium is a module which provides both an API and routes for converting
|
// Chromium is a module which provides both an API and routes for converting
|
||||||
@@ -51,15 +67,36 @@ type Chromium struct {
|
|||||||
userAgent string
|
userAgent string
|
||||||
incognito bool
|
incognito bool
|
||||||
ignoreCertificateErrors bool
|
ignoreCertificateErrors bool
|
||||||
|
disableWebSecurity bool
|
||||||
allowFileAccessFromFiles bool
|
allowFileAccessFromFiles bool
|
||||||
proxyServer string
|
proxyServer string
|
||||||
allowList *regexp.Regexp
|
allowList *regexp.Regexp
|
||||||
denyList *regexp.Regexp
|
denyList *regexp.Regexp
|
||||||
|
disableJavaScript bool
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LinkTag represents an HTML <link> element.
|
||||||
|
type LinkTag struct {
|
||||||
|
// Href is the "href" attribute of the HTML <link> element.
|
||||||
|
// Required.
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScriptTag represents an HTML <script> element.
|
||||||
|
type ScriptTag struct {
|
||||||
|
// Src is the "src" attribute of the HTML <link> element.
|
||||||
|
// Required.
|
||||||
|
Src string `json:"src"`
|
||||||
|
}
|
||||||
|
|
||||||
// Options are the available options for converting HTML document to PDF.
|
// Options are the available options for converting HTML document to PDF.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
// FailOnConsoleExceptions sets if the conversion should fail if there are
|
||||||
|
// exceptions in the Chromium console.
|
||||||
|
// Optional.
|
||||||
|
FailOnConsoleExceptions bool
|
||||||
|
|
||||||
// WaitDelay is the duration to wait when loading an HTML document before
|
// WaitDelay is the duration to wait when loading an HTML document before
|
||||||
// converting it to PDF.
|
// converting it to PDF.
|
||||||
// Optional.
|
// Optional.
|
||||||
@@ -70,11 +107,33 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
WaitWindowStatus string
|
WaitWindowStatus string
|
||||||
|
|
||||||
|
// WaitForExpression is the custom JavaScript expression to wait before
|
||||||
|
// converting an HTML document to PDF until it returns true
|
||||||
|
// Optional.
|
||||||
|
WaitForExpression string
|
||||||
|
|
||||||
|
// UserAgent overrides the default User-Agent header.
|
||||||
|
// Optional.
|
||||||
|
UserAgent string
|
||||||
|
|
||||||
// ExtraHTTPHeaders are the HTTP headers to send by Chromium while loading
|
// ExtraHTTPHeaders are the HTTP headers to send by Chromium while loading
|
||||||
// the HTML document.
|
// the HTML document.
|
||||||
// Optional.
|
// Optional.
|
||||||
ExtraHTTPHeaders map[string]string
|
ExtraHTTPHeaders map[string]string
|
||||||
|
|
||||||
|
// ExtraLinkTags are HTML <link> attributes that are added on the fly.
|
||||||
|
// Optional.
|
||||||
|
ExtraLinkTags []LinkTag
|
||||||
|
|
||||||
|
// EmulatedMediaType is the media type to emulate, either "screen" or
|
||||||
|
// "print".
|
||||||
|
// Optional.
|
||||||
|
EmulatedMediaType string
|
||||||
|
|
||||||
|
// ExtraScriptTags are HTML <script> attributes that are added on the fly.
|
||||||
|
// Optional.
|
||||||
|
ExtraScriptTags []ScriptTag
|
||||||
|
|
||||||
// Landscape sets the paper orientation.
|
// Landscape sets the paper orientation.
|
||||||
// Optional.
|
// Optional.
|
||||||
Landscape bool
|
Landscape bool
|
||||||
@@ -142,22 +201,28 @@ type Options struct {
|
|||||||
// DefaultOptions returns the default values for Options.
|
// DefaultOptions returns the default values for Options.
|
||||||
func DefaultOptions() Options {
|
func DefaultOptions() Options {
|
||||||
return Options{
|
return Options{
|
||||||
WaitDelay: 0,
|
FailOnConsoleExceptions: false,
|
||||||
WaitWindowStatus: "",
|
WaitDelay: 0,
|
||||||
ExtraHTTPHeaders: nil,
|
WaitWindowStatus: "",
|
||||||
Landscape: false,
|
WaitForExpression: "",
|
||||||
PrintBackground: false,
|
UserAgent: "",
|
||||||
Scale: 1.0,
|
ExtraHTTPHeaders: nil,
|
||||||
PaperWidth: 8.5,
|
ExtraLinkTags: nil,
|
||||||
PaperHeight: 11,
|
EmulatedMediaType: "",
|
||||||
MarginTop: 0.39,
|
ExtraScriptTags: nil,
|
||||||
MarginBottom: 0.39,
|
Landscape: false,
|
||||||
MarginLeft: 0.39,
|
PrintBackground: false,
|
||||||
MarginRight: 0.39,
|
Scale: 1.0,
|
||||||
PageRanges: "",
|
PaperWidth: 8.5,
|
||||||
HeaderTemplate: "<html><head></head><body></body></html>",
|
PaperHeight: 11,
|
||||||
FooterTemplate: "<html><head></head><body></body></html>",
|
MarginTop: 0.39,
|
||||||
PreferCSSPageSize: false,
|
MarginBottom: 0.39,
|
||||||
|
MarginLeft: 0.39,
|
||||||
|
MarginRight: 0.39,
|
||||||
|
PageRanges: "",
|
||||||
|
HeaderTemplate: "<html><head></head><body></body></html>",
|
||||||
|
FooterTemplate: "<html><head></head><body></body></html>",
|
||||||
|
PreferCSSPageSize: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,12 +251,19 @@ func (mod Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
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-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-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-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")
|
||||||
|
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
|
||||||
fs.Bool("chromium-disable-routes", false, "Disable the routes")
|
fs.Bool("chromium-disable-routes", false, "Disable the routes")
|
||||||
|
|
||||||
|
err := fs.MarkDeprecated("chromium-user-agent", "use the userAgent form field instead")
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("create deprecated flags for chromium module: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
New: func() gotenberg.Module { return new(Chromium) },
|
New: func() gotenberg.Module { return new(Chromium) },
|
||||||
@@ -203,10 +275,12 @@ 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.ignoreCertificateErrors = flags.MustBool("chromium-ignore-certificate-errors")
|
mod.ignoreCertificateErrors = flags.MustBool("chromium-ignore-certificate-errors")
|
||||||
|
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.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")
|
||||||
|
mod.disableJavaScript = flags.MustBool("chromium-disable-javascript")
|
||||||
mod.disableRoutes = flags.MustBool("chromium-disable-routes")
|
mod.disableRoutes = flags.MustBool("chromium-disable-routes")
|
||||||
|
|
||||||
binPath, ok := os.LookupEnv("CHROMIUM_BIN_PATH")
|
binPath, ok := os.LookupEnv("CHROMIUM_BIN_PATH")
|
||||||
@@ -281,7 +355,7 @@ func (mod Chromium) Routes() ([]api.Route, error) {
|
|||||||
// drastically. In such a scenario, the given context may also be done before
|
// drastically. In such a scenario, the given context may also be done before
|
||||||
// the end of the conversion.
|
// the end of the conversion.
|
||||||
func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
|
func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
|
||||||
debug := debugLogger{logger: logger.Named("chromium.debug")}
|
debug := debugLogger{logger: logger.Named("browser")}
|
||||||
userProfileDirPath := gotenberg.NewDirPath()
|
userProfileDirPath := gotenberg.NewDirPath()
|
||||||
|
|
||||||
args := append(chromedp.DefaultExecAllocatorOptions[:],
|
args := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
@@ -299,7 +373,8 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
chromedp.UserDataDir(userProfileDirPath),
|
chromedp.UserDataDir(userProfileDirPath),
|
||||||
)
|
)
|
||||||
|
|
||||||
if mod.userAgent != "" {
|
if mod.userAgent != "" && options.UserAgent == "" {
|
||||||
|
// Deprecated.
|
||||||
args = append(args, chromedp.UserAgent(mod.userAgent))
|
args = append(args, chromedp.UserAgent(mod.userAgent))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,6 +386,10 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
args = append(args, chromedp.IgnoreCertErrors)
|
args = append(args, chromedp.IgnoreCertErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mod.disableWebSecurity {
|
||||||
|
args = append(args, chromedp.Flag("disable-web-security", true))
|
||||||
|
}
|
||||||
|
|
||||||
if mod.allowFileAccessFromFiles {
|
if mod.allowFileAccessFromFiles {
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/356.
|
// See https://github.com/gotenberg/gotenberg/issues/356.
|
||||||
args = append(args, chromedp.Flag("allow-file-access-from-files", true))
|
args = append(args, chromedp.Flag("allow-file-access-from-files", true))
|
||||||
@@ -321,6 +400,10 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
args = append(args, chromedp.ProxyServer(mod.proxyServer))
|
args = append(args, chromedp.ProxyServer(mod.proxyServer))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.UserAgent != "" {
|
||||||
|
args = append(args, chromedp.UserAgent(options.UserAgent))
|
||||||
|
}
|
||||||
|
|
||||||
allocatorCtx, cancel := chromedp.NewExecAllocator(ctx, args...)
|
allocatorCtx, cancel := chromedp.NewExecAllocator(ctx, args...)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -338,14 +421,42 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
return fmt.Errorf("'%s' matches the expression from the denied list: %w", URL, ErrURLNotAuthorized)
|
return fmt.Errorf("'%s' matches the expression from the denied list: %w", URL, ErrURLNotAuthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
consoleExceptions error
|
||||||
|
consoleExceptionsMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
printToPDF := func(URL string, options Options, result *[]byte) chromedp.Tasks {
|
printToPDF := func(URL string, options Options, result *[]byte) 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)
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
|
if options.FailOnConsoleExceptions && !mod.disableJavaScript {
|
||||||
|
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
||||||
|
}
|
||||||
|
|
||||||
return chromedp.Tasks{
|
return chromedp.Tasks{
|
||||||
network.Enable(),
|
network.Enable(),
|
||||||
fetch.Enable(),
|
fetch.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/175.
|
||||||
|
if !mod.disableJavaScript {
|
||||||
|
logger.Debug("JavaScript not disabled")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("disable JavaScript")
|
||||||
|
|
||||||
|
err := emulation.SetScriptExecutionDisabled(true).Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("disable JavaScript: %w", err)
|
||||||
|
}),
|
||||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
if len(options.ExtraHTTPHeaders) == 0 {
|
if len(options.ExtraHTTPHeaders) == 0 {
|
||||||
logger.Debug("no extra HTTP headers")
|
logger.Debug("no extra HTTP headers")
|
||||||
@@ -415,60 +526,210 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
return fmt.Errorf("add CSS for exact colors: %w", err)
|
return fmt.Errorf("add CSS for exact colors: %w", err)
|
||||||
}),
|
}),
|
||||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
if options.WaitDelay > 0 {
|
if len(options.ExtraLinkTags) == 0 {
|
||||||
// We wait for a given amount of time so that JavaScript
|
logger.Debug("no extra link tags")
|
||||||
// scripts have a chance to finish before printing the page
|
|
||||||
// to PDF.
|
|
||||||
logger.Debug(fmt.Sprintf("wait '%s' before print", options.WaitDelay))
|
|
||||||
|
|
||||||
select {
|
return nil
|
||||||
case <-ctx.Done():
|
}
|
||||||
return fmt.Errorf("wait delay: %w", ctx.Err())
|
|
||||||
case <-time.After(options.WaitDelay):
|
logger.Debug(fmt.Sprintf("extra link tags: %+v", options.ExtraLinkTags))
|
||||||
return nil
|
|
||||||
|
addLinkTag := func(i int, linkTag LinkTag) func() error {
|
||||||
|
return func() error {
|
||||||
|
script := `
|
||||||
|
(() => {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.href = '%s';
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
document.head.appendChild(link);
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
|
||||||
|
evaluate := chromedp.Evaluate(fmt.Sprintf(script, linkTag.Href), nil)
|
||||||
|
err := evaluate.Do(ctx)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("add extra link tag %d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
for i, linkTag := range options.ExtraLinkTags {
|
||||||
|
eg.Go(addLinkTag(i, linkTag))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := eg.Wait()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("add extra link tags: %w", err)
|
||||||
|
}),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
if options.EmulatedMediaType == "" {
|
||||||
|
logger.Debug("no emulated media type")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.EmulatedMediaType != "screen" && options.EmulatedMediaType != "print" {
|
||||||
|
return fmt.Errorf("validate emulated media type '%s': %w", options.EmulatedMediaType, ErrInvalidEmulatedMediaType)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("emulate media type '%s'", options.EmulatedMediaType))
|
||||||
|
|
||||||
|
emulatedMedia := emulation.SetEmulatedMedia()
|
||||||
|
|
||||||
|
err := emulatedMedia.WithMedia(options.EmulatedMediaType).Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("emulate media type '%s': %w", options.EmulatedMediaType, err)
|
||||||
|
}),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
if mod.disableJavaScript {
|
||||||
|
logger.Debug("JavaScript disabled, skipping extra script tags")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(options.ExtraScriptTags) == 0 {
|
||||||
|
logger.Debug("no extra script tags")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("extra script tags: %+v", options.ExtraScriptTags))
|
||||||
|
|
||||||
|
addScriptTag := func(i int, scriptTag ScriptTag) func() error {
|
||||||
|
return func() error {
|
||||||
|
script := `
|
||||||
|
(() => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = '%s';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
|
||||||
|
evaluate := chromedp.Evaluate(fmt.Sprintf(script, scriptTag.Src), nil)
|
||||||
|
err := evaluate.Do(ctx)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("add extra script tag %d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
for i, scriptTag := range options.ExtraScriptTags {
|
||||||
|
eg.Go(addScriptTag(i, scriptTag))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := eg.Wait()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("add extra script tags: %w", err)
|
||||||
|
}),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
if mod.disableJavaScript {
|
||||||
|
logger.Debug("JavaScript disabled, skipping wait delay")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.WaitDelay <= 0 {
|
||||||
|
logger.Debug("no wait delay")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wait for a given amount of time so that JavaScript
|
||||||
|
// scripts have a chance to finish before printing the page
|
||||||
|
// to PDF.
|
||||||
|
logger.Debug(fmt.Sprintf("wait '%s' before print", options.WaitDelay))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("wait delay: %w", ctx.Err())
|
||||||
|
case <-time.After(options.WaitDelay):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
if mod.disableJavaScript {
|
||||||
|
logger.Debug("JavaScript disabled, skipping wait for window status / wait for expression")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.WaitWindowStatus == "" && options.WaitForExpression == "" {
|
||||||
|
logger.Debug("no wait for window status nor wait for expression")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate := func(expression string) error {
|
||||||
|
// We wait until the evaluation of the expression is true or
|
||||||
|
// until the context is done.
|
||||||
|
logger.Debug(fmt.Sprintf("wait until '%s' is true before print", expression))
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
|
||||||
|
return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err())
|
||||||
|
case <-ticker.C:
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
evaluate := chromedp.Evaluate(expression, &ok)
|
||||||
|
err := evaluate.Do(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
ticker.Stop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.WaitWindowStatus != "" {
|
||||||
|
logger.Warn("option 'WaitWindowStatus' is deprecated; prefer 'WaitForExpression' instead")
|
||||||
|
|
||||||
|
err := evaluate(fmt.Sprintf("window.status === '%s'", options.WaitWindowStatus))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for window.status === '%s': %w", options.WaitWindowStatus, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.WaitForExpression != "" {
|
||||||
|
err := evaluate(options.WaitForExpression)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for expression '%s': %w", options.WaitForExpression, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
||||||
if options.WaitWindowStatus == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We wait until the evaluation of
|
|
||||||
// "window.status === options.WaitWindowStatus" is true or
|
|
||||||
// until the context is done.
|
|
||||||
logger.Debug(fmt.Sprintf("wait for window.status === '%s' before print", options.WaitWindowStatus))
|
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
ticker.Stop()
|
|
||||||
|
|
||||||
return fmt.Errorf("wait for window.status === '%s': %w", options.WaitWindowStatus, ctx.Err())
|
|
||||||
case <-ticker.C:
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
evaluate := chromedp.Evaluate(fmt.Sprintf("window.status === '%s'", options.WaitWindowStatus), &ok)
|
|
||||||
err := evaluate.Do(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("evaluate: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
ticker.Stop()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
printToPDF := page.PrintToPDF().
|
printToPDF := page.PrintToPDF().
|
||||||
WithLandscape(options.Landscape).
|
WithLandscape(options.Landscape).
|
||||||
@@ -540,6 +801,14 @@ func (mod Chromium) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath
|
|||||||
return fmt.Errorf("chromium PDF: %w", err)
|
return fmt.Errorf("chromium PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
|
consoleExceptionsMu.RLock()
|
||||||
|
defer consoleExceptionsMu.RUnlock()
|
||||||
|
|
||||||
|
if consoleExceptions != nil {
|
||||||
|
return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions)
|
||||||
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(outputPath, buffer, 0600)
|
err = ioutil.WriteFile(outputPath, buffer, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("write result to output path: %w", err)
|
return fmt.Errorf("write result to output path: %w", err)
|
||||||
|
|||||||
@@ -176,11 +176,11 @@ func TestChromium_Validate(t *testing.T) {
|
|||||||
func TestChromium_Metrics(t *testing.T) {
|
func TestChromium_Metrics(t *testing.T) {
|
||||||
metrics, err := new(Chromium).Metrics()
|
metrics, err := new(Chromium).Metrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(metrics) != 1 {
|
if len(metrics) != 1 {
|
||||||
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
|
t.Fatalf("expected %d metrics, but got %d", 1, len(metrics))
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
actual := metrics[0].Read()
|
||||||
@@ -233,10 +233,12 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
userAgent string
|
userAgent string
|
||||||
incognito bool
|
incognito bool
|
||||||
ignoreCertificateErrors bool
|
ignoreCertificateErrors bool
|
||||||
|
disableWebSecurity bool
|
||||||
allowFileAccessFromFiles bool
|
allowFileAccessFromFiles bool
|
||||||
proxyServer string
|
proxyServer string
|
||||||
allowList *regexp.Regexp
|
allowList *regexp.Regexp
|
||||||
denyList *regexp.Regexp
|
denyList *regexp.Regexp
|
||||||
|
disableJavaScript bool
|
||||||
expectErr bool
|
expectErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -249,6 +251,23 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
denyList: regexp.MustCompile("file:///tests/*"),
|
denyList: regexp.MustCompile("file:///tests/*"),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
|
options: Options{
|
||||||
|
UserAgent: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample10/index.html",
|
||||||
|
options: Options{
|
||||||
|
FailOnConsoleExceptions: true,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample9/index.html",
|
||||||
|
disableJavaScript: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
options: Options{
|
options: Options{
|
||||||
@@ -257,6 +276,48 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
|
||||||
|
options: Options{
|
||||||
|
ExtraLinkTags: []LinkTag{
|
||||||
|
{
|
||||||
|
Href: "font.woff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Href: "style.css",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
||||||
|
options: Options{
|
||||||
|
EmulatedMediaType: "foo",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
||||||
|
options: Options{
|
||||||
|
EmulatedMediaType: "screen",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
||||||
|
options: Options{
|
||||||
|
EmulatedMediaType: "print",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
|
||||||
|
options: Options{
|
||||||
|
ExtraScriptTags: []ScriptTag{
|
||||||
|
{
|
||||||
|
Src: "script.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
options: Options{
|
options: Options{
|
||||||
@@ -278,6 +339,28 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
WaitWindowStatus: "ready",
|
WaitWindowStatus: "ready",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
timeout: time.Duration(3) * time.Second,
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
||||||
|
options: Options{
|
||||||
|
WaitForExpression: "window.status === 'foo'",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: time.Duration(3) * time.Second,
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
||||||
|
options: Options{
|
||||||
|
WaitForExpression: "window.status === 'ready'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
|
options: Options{
|
||||||
|
WaitForExpression: "return undefined",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
||||||
options: Options{
|
options: Options{
|
||||||
@@ -297,6 +380,7 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
userAgent: "foo",
|
userAgent: "foo",
|
||||||
incognito: true,
|
incognito: true,
|
||||||
ignoreCertificateErrors: true,
|
ignoreCertificateErrors: true,
|
||||||
|
disableWebSecurity: true,
|
||||||
allowFileAccessFromFiles: true,
|
allowFileAccessFromFiles: true,
|
||||||
proxyServer: "foo",
|
proxyServer: "foo",
|
||||||
},
|
},
|
||||||
@@ -349,6 +433,7 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
mod.userAgent = tc.userAgent
|
mod.userAgent = tc.userAgent
|
||||||
mod.incognito = tc.incognito
|
mod.incognito = tc.incognito
|
||||||
mod.ignoreCertificateErrors = tc.ignoreCertificateErrors
|
mod.ignoreCertificateErrors = tc.ignoreCertificateErrors
|
||||||
|
mod.disableWebSecurity = tc.disableWebSecurity
|
||||||
mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles
|
mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles
|
||||||
mod.proxyServer = tc.proxyServer
|
mod.proxyServer = tc.proxyServer
|
||||||
|
|
||||||
@@ -362,6 +447,7 @@ func TestChromium_PDF(t *testing.T) {
|
|||||||
|
|
||||||
mod.allowList = tc.allowList
|
mod.allowList = tc.allowList
|
||||||
mod.denyList = tc.denyList
|
mod.denyList = tc.denyList
|
||||||
|
mod.disableJavaScript = tc.disableJavaScript
|
||||||
|
|
||||||
outputDir, err := gotenberg.MkdirAll()
|
outputDir, err := gotenberg.MkdirAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
"github.com/chromedp/cdproto/fetch"
|
"github.com/chromedp/cdproto/fetch"
|
||||||
"github.com/chromedp/cdproto/network"
|
"github.com/chromedp/cdproto/network"
|
||||||
"github.com/chromedp/cdproto/page"
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/cdproto/runtime"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
@@ -59,6 +62,23 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenForEventExceptionThrown listens for exceptions in the console and
|
||||||
|
// appends those exceptions to the given error pointer.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
|
func listenForEventExceptionThrown(ctx context.Context, logger *zap.Logger, consoleExceptions *error, consoleExceptionsMu *sync.RWMutex) {
|
||||||
|
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||||
|
switch ev := ev.(type) {
|
||||||
|
case *runtime.EventExceptionThrown:
|
||||||
|
logger.Debug(fmt.Sprintf("event EventExceptionThrown fired: %+v", ev.ExceptionDetails))
|
||||||
|
|
||||||
|
consoleExceptionsMu.Lock()
|
||||||
|
defer consoleExceptionsMu.Unlock()
|
||||||
|
|
||||||
|
*consoleExceptions = multierr.Append(*consoleExceptions, fmt.Errorf("\n%+v", ev.ExceptionDetails))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// waitForEventDomContentEventFired waits until the event DomContentEventFired
|
// waitForEventDomContentEventFired waits until the event DomContentEventFired
|
||||||
// is fired or the context timeout.
|
// is fired or the context timeout.
|
||||||
func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) func() error {
|
func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) func() error {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
@@ -26,9 +27,13 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
defaultOptions := DefaultOptions()
|
defaultOptions := DefaultOptions()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
failOnConsoleExceptions bool
|
||||||
waitDelay time.Duration
|
waitDelay time.Duration
|
||||||
waitWindowStatus string
|
waitWindowStatus string
|
||||||
|
waitForExpression string
|
||||||
|
userAgent string
|
||||||
extraHTTPHeaders map[string]string
|
extraHTTPHeaders map[string]string
|
||||||
|
emulatedMediaType string
|
||||||
landscape, printBackground bool
|
landscape, printBackground bool
|
||||||
scale, paperWidth, paperHeight float64
|
scale, paperWidth, paperHeight float64
|
||||||
marginTop, marginBottom, marginLeft, marginRight float64
|
marginTop, marginBottom, marginLeft, marginRight float64
|
||||||
@@ -38,8 +43,11 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
form := ctx.FormData().
|
form := ctx.FormData().
|
||||||
|
Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
|
||||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||||
|
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||||
|
String("userAgent", &userAgent, defaultOptions.UserAgent).
|
||||||
Custom("extraHttpHeaders", func(value string) error {
|
Custom("extraHttpHeaders", func(value string) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
extraHTTPHeaders = defaultOptions.ExtraHTTPHeaders
|
extraHTTPHeaders = defaultOptions.ExtraHTTPHeaders
|
||||||
@@ -54,6 +62,21 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
|
Custom("emulatedMediaType", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
emulatedMediaType = defaultOptions.EmulatedMediaType
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != "screen" && value != "print" {
|
||||||
|
return fmt.Errorf("wrong value, expected either 'screen', 'print' or empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
emulatedMediaType = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
Bool("landscape", &landscape, defaultOptions.Landscape).
|
Bool("landscape", &landscape, defaultOptions.Landscape).
|
||||||
Bool("printBackground", &printBackground, defaultOptions.PrintBackground).
|
Bool("printBackground", &printBackground, defaultOptions.PrintBackground).
|
||||||
Float64("scale", &scale, defaultOptions.Scale).
|
Float64("scale", &scale, defaultOptions.Scale).
|
||||||
@@ -69,22 +92,28 @@ func FormDataChromiumPDFOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize)
|
Bool("preferCssPageSize", &preferCSSPageSize, defaultOptions.PreferCSSPageSize)
|
||||||
|
|
||||||
options := Options{
|
options := Options{
|
||||||
WaitDelay: waitDelay,
|
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||||
WaitWindowStatus: waitWindowStatus,
|
WaitDelay: waitDelay,
|
||||||
ExtraHTTPHeaders: extraHTTPHeaders,
|
WaitWindowStatus: waitWindowStatus,
|
||||||
Landscape: landscape,
|
WaitForExpression: waitForExpression,
|
||||||
PrintBackground: printBackground,
|
UserAgent: userAgent,
|
||||||
Scale: scale,
|
ExtraHTTPHeaders: extraHTTPHeaders,
|
||||||
PaperWidth: paperWidth,
|
ExtraLinkTags: defaultOptions.ExtraLinkTags,
|
||||||
PaperHeight: paperHeight,
|
EmulatedMediaType: emulatedMediaType,
|
||||||
MarginTop: marginTop,
|
ExtraScriptTags: defaultOptions.ExtraScriptTags,
|
||||||
MarginBottom: marginBottom,
|
Landscape: landscape,
|
||||||
MarginLeft: marginLeft,
|
PrintBackground: printBackground,
|
||||||
MarginRight: marginRight,
|
Scale: scale,
|
||||||
PageRanges: pageRanges,
|
PaperWidth: paperWidth,
|
||||||
HeaderTemplate: headerTemplate,
|
PaperHeight: paperHeight,
|
||||||
FooterTemplate: footerTemplate,
|
MarginTop: marginTop,
|
||||||
PreferCSSPageSize: preferCSSPageSize,
|
MarginBottom: marginBottom,
|
||||||
|
MarginLeft: marginLeft,
|
||||||
|
MarginRight: marginRight,
|
||||||
|
PageRanges: pageRanges,
|
||||||
|
HeaderTemplate: headerTemplate,
|
||||||
|
FooterTemplate: footerTemplate,
|
||||||
|
PreferCSSPageSize: preferCSSPageSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
return form, options
|
return form, options
|
||||||
@@ -101,19 +130,209 @@ 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.
|
||||||
MandatoryString("url", &URL).
|
MandatoryString("url", &URL).
|
||||||
String("pdfFormat", &PDFformat, "").
|
String("pdfFormat", &PDFformat, "").
|
||||||
|
Custom("extraLinkTags", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(value), &options.ExtraLinkTags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal extra link tags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Custom("extraScriptTags", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(value), &options.ExtraScriptTags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal extra script tags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -288,6 +507,23 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrInvalidEvaluationExpression) {
|
||||||
|
if options.WaitForExpression == "" {
|
||||||
|
// We do not expect the 'waitWindowStatus' form field to return
|
||||||
|
// an ErrInvalidEvaluationExpression error. In such a scenario,
|
||||||
|
// we return a 500.
|
||||||
|
return fmt.Errorf("convert to PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.WrapError(
|
||||||
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
|
api.NewSentinelHTTPError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if errors.Is(err, ErrInvalidPrinterSettings) {
|
if errors.Is(err, ErrInvalidPrinterSettings) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
@@ -308,6 +544,16 @@ func convertURL(ctx *api.Context, chromium API, engine gotenberg.PDFEngine, URL,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrConsoleExceptions) {
|
||||||
|
return api.WrapError(
|
||||||
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
|
api.NewSentinelHTTPError(
|
||||||
|
http.StatusConflict,
|
||||||
|
fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Errorf("convert to PDF: %w", err)
|
return fmt.Errorf("convert to PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package chromium
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -53,6 +54,50 @@ func TestFormDataChromiumPDFOptions(t *testing.T) {
|
|||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"extraHttpHeaders": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"emulatedMediaType": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"emulatedMediaType": {
|
||||||
|
"screen",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
options: func() Options {
|
||||||
|
options := DefaultOptions()
|
||||||
|
options.EmulatedMediaType = "screen"
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
@@ -60,7 +105,7 @@ func TestFormDataChromiumPDFOptions(t *testing.T) {
|
|||||||
_, actual := FormDataChromiumPDFOptions(tc.ctx.Context)
|
_, actual := FormDataChromiumPDFOptions(tc.ctx.Context)
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, tc.options) {
|
if !reflect.DeepEqual(actual, tc.options) {
|
||||||
t.Errorf("test %d: expected %v but got: %v", i, tc.options, actual)
|
t.Errorf("test %d: expected %+v but got: %+v", i, tc.options, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +182,226 @@ func TestConvertURLHandler(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"url": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
"extraLinkTags": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
api: func() API {
|
||||||
|
chromiumAPI := struct{ ProtoAPI }{}
|
||||||
|
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return chromiumAPI
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
expectHTTPErr: true,
|
||||||
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{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{
|
||||||
|
"url": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
"extraScriptTags": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
api: func() API {
|
||||||
|
chromiumAPI := struct{ ProtoAPI }{}
|
||||||
|
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return chromiumAPI
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
expectHTTPErr: true,
|
||||||
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"url": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"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": {
|
||||||
|
`[{"src":"https://cdn.foo"},{"src":"b.js"}]`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"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: "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 chromiumAPI
|
||||||
|
}(),
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
c := echo.New().NewContext(nil, nil)
|
c := echo.New().NewContext(nil, nil)
|
||||||
c.Set("context", tc.ctx.Context)
|
c.Set("context", tc.ctx.Context)
|
||||||
@@ -523,6 +788,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
api API
|
api API
|
||||||
engine gotenberg.PDFEngine
|
engine gotenberg.PDFEngine
|
||||||
PDFformat string
|
PDFformat string
|
||||||
|
options Options
|
||||||
expectErr bool
|
expectErr bool
|
||||||
expectHTTPErr bool
|
expectHTTPErr bool
|
||||||
expectHTTPStatus int
|
expectHTTPStatus int
|
||||||
@@ -538,10 +804,44 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusForbidden,
|
expectHTTPStatus: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ctx: &api.MockContext{Context: &api.Context{}},
|
||||||
|
api: func() API {
|
||||||
|
chromiumAPI := struct{ ProtoAPI }{}
|
||||||
|
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
|
||||||
|
return ErrInvalidEvaluationExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
return chromiumAPI
|
||||||
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: &api.MockContext{Context: &api.Context{}},
|
||||||
|
api: func() API {
|
||||||
|
chromiumAPI := struct{ ProtoAPI }{}
|
||||||
|
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
|
||||||
|
return ErrInvalidEvaluationExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
return chromiumAPI
|
||||||
|
}(),
|
||||||
|
options: func() Options {
|
||||||
|
options := DefaultOptions()
|
||||||
|
options.WaitForExpression = "foo"
|
||||||
|
|
||||||
|
return options
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
expectHTTPErr: true,
|
||||||
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ctx: &api.MockContext{Context: &api.Context{}},
|
ctx: &api.MockContext{Context: &api.Context{}},
|
||||||
api: func() API {
|
api: func() API {
|
||||||
@@ -552,6 +852,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
@@ -566,10 +867,26 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ctx: &api.MockContext{Context: &api.Context{}},
|
||||||
|
api: func() API {
|
||||||
|
chromiumAPI := struct{ ProtoAPI }{}
|
||||||
|
chromiumAPI.pdf = func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error {
|
||||||
|
return ErrConsoleExceptions
|
||||||
|
}
|
||||||
|
|
||||||
|
return chromiumAPI
|
||||||
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
|
expectErr: true,
|
||||||
|
expectHTTPErr: true,
|
||||||
|
expectHTTPStatus: http.StatusConflict,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ctx: &api.MockContext{Context: &api.Context{}},
|
ctx: &api.MockContext{Context: &api.Context{}},
|
||||||
api: func() API {
|
api: func() API {
|
||||||
@@ -580,6 +897,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -600,6 +918,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
PDFformat: "foo",
|
PDFformat: "foo",
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
@@ -622,6 +941,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
PDFformat: "foo",
|
PDFformat: "foo",
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -642,6 +962,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
PDFformat: "foo",
|
PDFformat: "foo",
|
||||||
|
options: DefaultOptions(),
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -659,6 +980,7 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -671,10 +993,11 @@ func TestConvertURL(t *testing.T) {
|
|||||||
|
|
||||||
return chromiumAPI
|
return chromiumAPI
|
||||||
}(),
|
}(),
|
||||||
|
options: DefaultOptions(),
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, DefaultOptions())
|
err := convertURL(tc.ctx.Context, tc.api, tc.engine, "", tc.PDFformat, tc.options)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectErr && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -202,19 +201,10 @@ func (gc *GarbageCollector) Stop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Block until the context is done so that other module may gracefully stop
|
// Block until the context is done so that other module may gracefully stop
|
||||||
// before we do a shutdown cleanup. We skip this step if we receive a
|
// before we do a shutdown cleanup.
|
||||||
// SIGINT in the meantime.
|
|
||||||
gc.logger.Debug("wait for the end of grace duration")
|
gc.logger.Debug("wait for the end of grace duration")
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
<-ctx.Done()
|
||||||
signal.Notify(quit, os.Interrupt)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.ticker.Stop()
|
gc.ticker.Stop()
|
||||||
gc.done <- true
|
gc.done <- true
|
||||||
|
|||||||
31
pkg/modules/libreoffice/unoconv/freeport.go
Normal file
31
pkg/modules/libreoffice/unoconv/freeport.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package unoconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func freePort(logger *zap.Logger) (int, error) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := listener.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close listener: %s", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
addr := listener.Addr().String()
|
||||||
|
|
||||||
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("get free port from host: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Atoi(portStr)
|
||||||
|
}
|
||||||
@@ -4,13 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +23,12 @@ var ErrMalformedPageRanges = errors.New("page ranges are malformed")
|
|||||||
|
|
||||||
// Unoconv is a module which provides an API to interact with unoconv.
|
// Unoconv is a module which provides an API to interact with unoconv.
|
||||||
type Unoconv struct {
|
type Unoconv struct {
|
||||||
binPath string
|
binPath string
|
||||||
|
disableListener bool
|
||||||
|
|
||||||
|
listenerCmd gotenberg.Cmd
|
||||||
|
listenerPort int
|
||||||
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options gathers available options when converting a document to PDF.
|
// Options gathers available options when converting a document to PDF.
|
||||||
@@ -67,14 +71,23 @@ type Provider interface {
|
|||||||
// Descriptor returns a Unoconv's module descriptor.
|
// Descriptor returns a Unoconv's module descriptor.
|
||||||
func (Unoconv) Descriptor() gotenberg.ModuleDescriptor {
|
func (Unoconv) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "unoconv",
|
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) },
|
New: func() gotenberg.Module { return new(Unoconv) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets the module properties. It returns an error if the environment
|
// Provision sets the module properties. It returns an error if the environment
|
||||||
// variable UNOCONV_BIN_PATH is not set.
|
// variable UNOCONV_BIN_PATH is not set.
|
||||||
func (mod *Unoconv) Provision(_ *gotenberg.Context) error {
|
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")
|
binPath, ok := os.LookupEnv("UNOCONV_BIN_PATH")
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
|
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
|
||||||
@@ -82,6 +95,18 @@ func (mod *Unoconv) Provision(_ *gotenberg.Context) error {
|
|||||||
|
|
||||||
mod.binPath = binPath
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,12 +120,92 @@ func (mod Unoconv) Validate() error {
|
|||||||
return nil
|
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.
|
// Metrics returns the metrics.
|
||||||
func (mod Unoconv) Metrics() ([]gotenberg.Metric, error) {
|
func (mod Unoconv) Metrics() ([]gotenberg.Metric, error) {
|
||||||
return []gotenberg.Metric{
|
return []gotenberg.Metric{
|
||||||
{
|
{
|
||||||
Name: "unoconv_active_instances_count",
|
Name: "unoconv_active_instances_count",
|
||||||
Description: "Current number of active LibreOffice instances.",
|
Description: "Current number of active unoconv instances.",
|
||||||
Read: func() float64 {
|
Read: func() float64 {
|
||||||
activeInstancesCountMu.RLock()
|
activeInstancesCountMu.RLock()
|
||||||
defer activeInstancesCountMu.RUnlock()
|
defer activeInstancesCountMu.RUnlock()
|
||||||
@@ -108,57 +213,75 @@ func (mod Unoconv) Metrics() ([]gotenberg.Metric, error) {
|
|||||||
return activeInstancesCount
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unoconv returns an API for interacting with unoconv.
|
// Unoconv returns an API for interacting with unoconv.
|
||||||
func (mod Unoconv) Unoconv() (API, error) {
|
func (mod *Unoconv) Unoconv() (API, error) {
|
||||||
return mod, nil
|
return mod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDF converts a document to PDF. It creates a dedicated LibreOffice instance
|
// PDF converts a document to PDF.
|
||||||
// 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
|
// In stateless mode, it creates a dedicated LibreOffice instance thanks to a
|
||||||
// scenario, the given context may also be done before the end of the
|
// custom user profile directory and a free port. Substantial calls to this
|
||||||
// conversion.
|
// 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 {
|
func (mod Unoconv) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
port, err := func() (int, error) {
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := listener.Close()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("close listener: %s", err.Error()))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
addr := listener.Addr().String()
|
|
||||||
|
|
||||||
_, portStr, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("get free port from host: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strconv.Atoi(portStr)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get free port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userProfileDirPath := gotenberg.NewDirPath()
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"--user-profile",
|
|
||||||
fmt.Sprintf("//%s", userProfileDirPath),
|
|
||||||
"--port",
|
|
||||||
fmt.Sprintf("%d", port),
|
|
||||||
"--format",
|
"--format",
|
||||||
"pdf",
|
"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 {
|
if options.Landscape {
|
||||||
args = append(args, "--printer", "PaperOrientation=landscape")
|
args = append(args, "--printer", "PaperOrientation=landscape")
|
||||||
}
|
}
|
||||||
@@ -173,6 +296,36 @@ func (mod Unoconv) PDF(ctx context.Context, logger *zap.Logger, inputPath, outpu
|
|||||||
|
|
||||||
args = append(args, "--output", outputPath, inputPath)
|
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...)
|
cmd, err := gotenberg.CommandContext(ctx, logger, mod.binPath, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create unoconv command: %w", err)
|
return fmt.Errorf("create unoconv command: %w", err)
|
||||||
@@ -190,16 +343,18 @@ func (mod Unoconv) PDF(ctx context.Context, logger *zap.Logger, inputPath, outpu
|
|||||||
activeInstancesCount -= 1
|
activeInstancesCount -= 1
|
||||||
activeInstancesCountMu.Unlock()
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
// Always remove the user profile directory created by LibreOffice.
|
if mod.disableListener {
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/192.
|
// Always remove the user profile directory created by LibreOffice.
|
||||||
go func() {
|
// See https://github.com/gotenberg/gotenberg/issues/192.
|
||||||
logger.Debug(fmt.Sprintf("remove user profile directory '%s'", userProfileDirPath))
|
go func() {
|
||||||
|
logger.Debug(fmt.Sprintf("remove user profile directory '%s'", userProfileDirPath))
|
||||||
|
|
||||||
err := os.RemoveAll(userProfileDirPath)
|
err := os.RemoveAll(userProfileDirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("remove user profile directory: %s", err))
|
logger.Error(fmt.Sprintf("remove user profile directory: %s", err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -306,8 +461,13 @@ func (mod Unoconv) Extensions() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
activeInstancesCount float64
|
listenerLock = make(chan struct{}, 1)
|
||||||
activeInstancesCountMu sync.RWMutex
|
listenerQueueLength float64
|
||||||
|
listenerQueueLengthMu sync.RWMutex
|
||||||
|
listenerActiveInstancesCount float64
|
||||||
|
listenerActiveInstancesCountMu sync.RWMutex
|
||||||
|
activeInstancesCount float64
|
||||||
|
activeInstancesCountMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
@@ -315,6 +475,7 @@ var (
|
|||||||
_ gotenberg.Module = (*Unoconv)(nil)
|
_ gotenberg.Module = (*Unoconv)(nil)
|
||||||
_ gotenberg.Provisioner = (*Unoconv)(nil)
|
_ gotenberg.Provisioner = (*Unoconv)(nil)
|
||||||
_ gotenberg.Validator = (*Unoconv)(nil)
|
_ gotenberg.Validator = (*Unoconv)(nil)
|
||||||
|
_ gotenberg.App = (*Unoconv)(nil)
|
||||||
_ gotenberg.MetricsProvider = (*Unoconv)(nil)
|
_ gotenberg.MetricsProvider = (*Unoconv)(nil)
|
||||||
_ API = (*Unoconv)(nil)
|
_ API = (*Unoconv)(nil)
|
||||||
_ Provider = (*Unoconv)(nil)
|
_ Provider = (*Unoconv)(nil)
|
||||||
|
|||||||
@@ -2,14 +2,42 @@ package unoconv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ProtoModule struct {
|
||||||
|
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) {
|
func TestUnoconv_Descriptor(t *testing.T) {
|
||||||
descriptor := Unoconv{}.Descriptor()
|
descriptor := Unoconv{}.Descriptor()
|
||||||
|
|
||||||
@@ -22,12 +50,69 @@ func TestUnoconv_Descriptor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconv_Provision(t *testing.T) {
|
func TestUnoconv_Provision(t *testing.T) {
|
||||||
mod := new(Unoconv)
|
for i, tc := range []struct {
|
||||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
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") }
|
||||||
|
|
||||||
err := mod.Provision(ctx)
|
return gotenberg.NewContext(
|
||||||
if err != nil {
|
gotenberg.ParsedFlags{
|
||||||
t.Errorf("expected no error but got: %v", err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +132,10 @@ func TestUnoconv_Validate(t *testing.T) {
|
|||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(Unoconv)
|
mod := Unoconv{
|
||||||
mod.binPath = tc.binPath
|
binPath: tc.binPath,
|
||||||
|
}
|
||||||
|
|
||||||
err := mod.Validate()
|
err := mod.Validate()
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectErr && err == nil {
|
||||||
@@ -61,20 +148,149 @@ func TestUnoconv_Validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChromium_Metrics(t *testing.T) {
|
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()
|
metrics, err := new(Unoconv).Metrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(metrics) != 1 {
|
if len(metrics) != 3 {
|
||||||
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
|
t.Fatalf("expected %d metrics, but got %d", 1, len(metrics))
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
actual := metrics[0].Read()
|
||||||
if actual != 0 {
|
if actual != 0 {
|
||||||
t.Errorf("expected %d unoconv instances, but got %f", 0, actual)
|
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) {
|
func TestUnoconv_Unoconv(t *testing.T) {
|
||||||
@@ -89,15 +305,27 @@ func TestUnoconv_Unoconv(t *testing.T) {
|
|||||||
func TestUnoconv_PDF(t *testing.T) {
|
func TestUnoconv_PDF(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
mod Unoconv
|
||||||
|
logger *zap.Logger
|
||||||
inputPath string
|
inputPath string
|
||||||
options Options
|
options Options
|
||||||
expectErr bool
|
expectErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
mod: Unoconv{
|
||||||
|
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
disableListener: true,
|
||||||
|
},
|
||||||
|
logger: zap.NewNop(),
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
mod: Unoconv{
|
||||||
|
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
disableListener: true,
|
||||||
|
},
|
||||||
|
logger: zap.NewExample(),
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
options: Options{
|
options: Options{
|
||||||
Landscape: true,
|
Landscape: true,
|
||||||
@@ -106,7 +334,12 @@ func TestUnoconv_PDF(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
mod: Unoconv{
|
||||||
|
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
disableListener: true,
|
||||||
|
},
|
||||||
|
logger: zap.NewNop(),
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
options: Options{
|
options: Options{
|
||||||
PageRanges: "foo",
|
PageRanges: "foo",
|
||||||
@@ -120,18 +353,40 @@ func TestUnoconv_PDF(t *testing.T) {
|
|||||||
|
|
||||||
return ctx
|
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",
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
func() {
|
||||||
mod := new(Unoconv)
|
|
||||||
|
|
||||||
err := mod.Provision(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputDir, err := gotenberg.MkdirAll()
|
outputDir, err := gotenberg.MkdirAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||||
@@ -144,7 +399,17 @@ func TestUnoconv_PDF(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = mod.PDF(tc.ctx, zap.NewNop(), tc.inputPath, outputDir+"/foo.pdf", tc.options)
|
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 {
|
if tc.expectErr && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
@@ -153,6 +418,16 @@ func TestUnoconv_PDF(t *testing.T) {
|
|||||||
if !tc.expectErr && err != nil {
|
if !tc.expectErr && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Errorf("test %d: expected 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,3 +443,11 @@ func TestUnoconv_Extensions(t *testing.T) {
|
|||||||
t.Errorf("expected %d extensions but got %d", expect, actual)
|
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,6 +3,7 @@ package pdfengines
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
@@ -36,7 +37,7 @@ func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
ID: "pdfengines",
|
ID: "pdfengines",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
||||||
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the PDF engines - all by default")
|
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the PDF engines and their order - all by default")
|
||||||
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
@@ -118,14 +119,25 @@ func (mod PDFEngines) Validate() error {
|
|||||||
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
|
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemMessages returns one message with the selected gotenberg.PDFEngine
|
||||||
|
// modules.
|
||||||
|
func (mod PDFEngines) SystemMessages() []string {
|
||||||
|
return []string{
|
||||||
|
strings.Join(mod.names[:], " "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PDFEngine returns a gotenberg.PDFEngine.
|
// PDFEngine returns a gotenberg.PDFEngine.
|
||||||
func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
|
func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
|
||||||
engines := make([]gotenberg.PDFEngine, len(mod.engines))
|
engines := make([]gotenberg.PDFEngine, len(mod.names))
|
||||||
|
|
||||||
i := 0
|
for i, name := range mod.names {
|
||||||
for _, engine := range mod.engines {
|
for _, engine := range mod.engines {
|
||||||
engines[i] = engine
|
if name == engine.(gotenberg.Module).Descriptor().ID {
|
||||||
i++
|
engines[i] = engine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newMultiPDFEngines(engines...), nil
|
return newMultiPDFEngines(engines...), nil
|
||||||
@@ -155,6 +167,7 @@ var (
|
|||||||
_ gotenberg.Module = (*PDFEngines)(nil)
|
_ gotenberg.Module = (*PDFEngines)(nil)
|
||||||
_ gotenberg.Provisioner = (*PDFEngines)(nil)
|
_ gotenberg.Provisioner = (*PDFEngines)(nil)
|
||||||
_ gotenberg.Validator = (*PDFEngines)(nil)
|
_ gotenberg.Validator = (*PDFEngines)(nil)
|
||||||
|
_ gotenberg.SystemLogger = (*PDFEngines)(nil)
|
||||||
_ gotenberg.PDFEngineProvider = (*PDFEngines)(nil)
|
_ gotenberg.PDFEngineProvider = (*PDFEngines)(nil)
|
||||||
_ api.Router = (*PDFEngines)(nil)
|
_ api.Router = (*PDFEngines)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
@@ -252,11 +253,40 @@ func TestPDFEngine_Validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPDFEngines_SystemMessages(t *testing.T) {
|
||||||
|
mod := new(PDFEngines)
|
||||||
|
mod.names = []string{"foo", "bar"}
|
||||||
|
|
||||||
|
messages := mod.SystemMessages()
|
||||||
|
if len(messages) != 1 {
|
||||||
|
t.Errorf("expected one and only one message but got %d", len(messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect := strings.Join(mod.names[:], " ")
|
||||||
|
if messages[0] != expect {
|
||||||
|
t.Errorf("expected message '%s' but got '%s'", expect, messages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPDFEngine_PDFEngine(t *testing.T) {
|
func TestPDFEngine_PDFEngine(t *testing.T) {
|
||||||
mod := new(PDFEngines)
|
mod := new(PDFEngines)
|
||||||
mod.engines = []gotenberg.PDFEngine{
|
mod.names = []string{"foo", "bar"}
|
||||||
struct{ ProtoPDFEngine }{},
|
mod.engines = func() []gotenberg.PDFEngine {
|
||||||
}
|
engine1 := struct{ ProtoPDFEngine }{}
|
||||||
|
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine2 := struct{ ProtoPDFEngine }{}
|
||||||
|
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []gotenberg.PDFEngine{
|
||||||
|
engine1,
|
||||||
|
engine2,
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, err := mod.PDFEngine()
|
_, err := mod.PDFEngine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ func TestPDFtk_Validate(t *testing.T) {
|
|||||||
func TestPDFtk_Metrics(t *testing.T) {
|
func TestPDFtk_Metrics(t *testing.T) {
|
||||||
metrics, err := new(PDFtk).Metrics()
|
metrics, err := new(PDFtk).Metrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(metrics) != 1 {
|
if len(metrics) != 1 {
|
||||||
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
|
t.Fatalf("expected %d metrics, but got %d", 1, len(metrics))
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
actual := metrics[0].Read()
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func (mod Prometheus) Start() error {
|
|||||||
// StartupMessage returns a custom startup message.
|
// StartupMessage returns a custom startup message.
|
||||||
func (mod Prometheus) StartupMessage() string {
|
func (mod Prometheus) StartupMessage() string {
|
||||||
if mod.disableCollect {
|
if mod.disableCollect {
|
||||||
return "application not started (collect disabled by user)"
|
return "collect disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "collecting metrics"
|
return "collecting metrics"
|
||||||
|
|||||||
@@ -296,8 +296,8 @@ func TestPrometheus_StartupMessage(t *testing.T) {
|
|||||||
expectMessage string
|
expectMessage string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectMessage: "application not started (collect disabled by user)",
|
|
||||||
disableCollect: true,
|
disableCollect: true,
|
||||||
|
expectMessage: "collect disabled",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expectMessage: "collecting metrics",
|
expectMessage: "collecting metrics",
|
||||||
|
|||||||
3
pkg/modules/qpdf/doc.go
Normal file
3
pkg/modules/qpdf/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package qpdf provides a module which abstracts the CLI tool QPDF and
|
||||||
|
// implements the gotenberg.PDFEngine interface.
|
||||||
|
package qpdf
|
||||||
117
pkg/modules/qpdf/qpdf.go
Normal file
117
pkg/modules/qpdf/qpdf.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package qpdf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gotenberg.MustRegisterModule(QPDF{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// QPDF abstracts the CLI tool QPDF and implements the gotenberg.PDFEngine
|
||||||
|
// interface.
|
||||||
|
type QPDF struct {
|
||||||
|
binPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor returns a QPDF's module descriptor.
|
||||||
|
func (QPDF) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{
|
||||||
|
ID: "qpdf",
|
||||||
|
New: func() gotenberg.Module { return new(QPDF) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets the modules properties. It returns an error if the
|
||||||
|
// environment variable QPDF_BIN_PATH is not set.
|
||||||
|
func (engine *QPDF) Provision(_ *gotenberg.Context) error {
|
||||||
|
binPath, ok := os.LookupEnv("QPDF_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("QPDF_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.binPath = binPath
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the module properties.
|
||||||
|
func (engine QPDF) Validate() error {
|
||||||
|
_, err := os.Stat(engine.binPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("QPDF binary path does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics returns the metrics.
|
||||||
|
func (engine QPDF) Metrics() ([]gotenberg.Metric, error) {
|
||||||
|
return []gotenberg.Metric{
|
||||||
|
{
|
||||||
|
Name: "qpdf_active_instances_count",
|
||||||
|
Description: "Current number of active QPDF instances.",
|
||||||
|
Read: func() float64 {
|
||||||
|
activeInstancesCountMu.RLock()
|
||||||
|
defer activeInstancesCountMu.RUnlock()
|
||||||
|
|
||||||
|
return activeInstancesCount
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges the given PDFs into a unique PDF.
|
||||||
|
func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
var args []string
|
||||||
|
args = append(args, "--empty")
|
||||||
|
args = append(args, "--pages")
|
||||||
|
args = append(args, inputPaths...)
|
||||||
|
args = append(args, "--", outputPath)
|
||||||
|
|
||||||
|
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeInstancesCountMu.Lock()
|
||||||
|
activeInstancesCount += 1
|
||||||
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
|
err = cmd.Exec()
|
||||||
|
|
||||||
|
activeInstancesCountMu.Lock()
|
||||||
|
activeInstancesCount -= 1
|
||||||
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("merge PDFs with QPDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert is not available for this PDF engine.
|
||||||
|
func (engine QPDF) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
|
||||||
|
return fmt.Errorf("convert PDF to '%s' with QPDF: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
activeInstancesCount float64
|
||||||
|
activeInstancesCountMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*QPDF)(nil)
|
||||||
|
_ gotenberg.Provisioner = (*QPDF)(nil)
|
||||||
|
_ gotenberg.Validator = (*QPDF)(nil)
|
||||||
|
_ gotenberg.MetricsProvider = (*QPDF)(nil)
|
||||||
|
_ gotenberg.PDFEngine = (*QPDF)(nil)
|
||||||
|
)
|
||||||
152
pkg/modules/qpdf/qpdf_test.go
Normal file
152
pkg/modules/qpdf/qpdf_test.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package qpdf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQPDF_Descriptor(t *testing.T) {
|
||||||
|
descriptor := QPDF{}.Descriptor()
|
||||||
|
|
||||||
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
|
expect := reflect.TypeOf(new(QPDF))
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Provision(t *testing.T) {
|
||||||
|
mod := new(QPDF)
|
||||||
|
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||||
|
|
||||||
|
err := mod.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Validate(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
binPath string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binPath: "/foo",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binPath: os.Getenv("QPDF_BIN_PATH"),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
mod := new(QPDF)
|
||||||
|
mod.binPath = tc.binPath
|
||||||
|
err := mod.Validate()
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Metrics(t *testing.T) {
|
||||||
|
metrics, err := new(QPDF).Metrics()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(metrics) != 1 {
|
||||||
|
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := metrics[0].Read()
|
||||||
|
if actual != 0 {
|
||||||
|
t.Errorf("expected %d QPDF instances, but got %f", 0, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Merge(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
ctx context.Context
|
||||||
|
inputPaths []string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPaths: []string{
|
||||||
|
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPaths: []string{
|
||||||
|
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: nil,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPaths: []string{
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
mod := new(QPDF)
|
||||||
|
|
||||||
|
err := mod.Provision(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDir, err := gotenberg.MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(outputDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = mod.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Convert(t *testing.T) {
|
||||||
|
mod := new(QPDF)
|
||||||
|
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,5 +13,6 @@ import (
|
|||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus"
|
||||||
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/qpdf"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|||||||
24
test/testdata/chromium/html/sample10/index.html
vendored
Normal file
24
test/testdata/chromium/html/sample10/index.html
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Gutenberg</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>Console API</p>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
console.log("a simple message")
|
||||||
|
console.debug("a debug message")
|
||||||
|
console.warn("a warning message")
|
||||||
|
console.error("an error message")
|
||||||
|
</script>
|
||||||
|
<script type="application/javascript">
|
||||||
|
throw new Error("Exception 1")
|
||||||
|
</script>
|
||||||
|
<script type="application/javascript">
|
||||||
|
throw new Error("Exception 2")
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
test/testdata/chromium/html/sample11/font.woff
vendored
Normal file
BIN
test/testdata/chromium/html/sample11/font.woff
vendored
Normal file
Binary file not shown.
12
test/testdata/chromium/html/sample11/index.html
vendored
Normal file
12
test/testdata/chromium/html/sample11/index.html
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
2
test/testdata/chromium/html/sample11/script.js
vendored
Normal file
2
test/testdata/chromium/html/sample11/script.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
let extra = document.getElementById('extra')
|
||||||
|
extra.innerText = extra.innerText + ' and extra JavaScript tags too!'
|
||||||
14
test/testdata/chromium/html/sample11/style.css
vendored
Normal file
14
test/testdata/chromium/html/sample11/style.css
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Local';
|
||||||
|
src: url('font.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#extra {
|
||||||
|
font-family: 'Local'
|
||||||
|
}
|
||||||
|
|
||||||
|
#not {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
19
test/testdata/chromium/html/sample8/index.html
vendored
Normal file
19
test/testdata/chromium/html/sample8/index.html
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Gutenberg</title>
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
#screen { display: none }
|
||||||
|
}
|
||||||
|
@media screen {
|
||||||
|
#print { display: none }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p id="print">Print media type</p>
|
||||||
|
<p id="screen">Screen media type</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
test/testdata/chromium/html/sample9/index.html
vendored
Normal file
17
test/testdata/chromium/html/sample9/index.html
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Gutenberg</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p id="javascript">
|
||||||
|
JavaScript disabled.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
document.getElementById('javascript').innerText = 'JavaScript not disabled.'
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user