mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 11:22:15 +01:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a11f6a39f | ||
|
|
c63235cd3d | ||
|
|
f4ce196a34 | ||
|
|
e644b27201 | ||
|
|
d2b14582ee | ||
|
|
ca9194fd96 | ||
|
|
773d3ab13c | ||
|
|
a02a4a07a5 | ||
|
|
0a8625227f | ||
|
|
dc613aa2bf | ||
|
|
2008ac5663 | ||
|
|
da326f1925 | ||
|
|
71911cb1a7 | ||
|
|
31e7582216 | ||
|
|
91f63047e0 | ||
|
|
c9aa4938f4 | ||
|
|
b9e6a38037 | ||
|
|
eb054146d2 | ||
|
|
a780373cfc | ||
|
|
5ad90ccdb8 | ||
|
|
ef97d81ca8 | ||
|
|
c9f7b642c5 | ||
|
|
770962be3f | ||
|
|
d6c857f4a8 | ||
|
|
7b7ffb427d | ||
|
|
b80318d1b6 | ||
|
|
3997d69e49 | ||
|
|
4af41b33c0 | ||
|
|
0f8bbead66 | ||
|
|
804fcff5b6 | ||
|
|
d18e6920fc | ||
|
|
c95174ff11 | ||
|
|
c70329c22b | ||
|
|
34f69263a2 | ||
|
|
bf5056f8fe | ||
|
|
c47f5b06ba | ||
|
|
63055e3df2 | ||
|
|
8ee555140b | ||
|
|
859f78e986 | ||
|
|
366eebb68c | ||
|
|
25ce0b9aee | ||
|
|
376a7db545 | ||
|
|
f906ed741d | ||
|
|
ad152e62e5 | ||
|
|
7f3a61ef43 | ||
|
|
a7ba8c8213 | ||
|
|
b1f3d6d4c0 | ||
|
|
5707ae027f | ||
|
|
770208024d | ||
|
|
975a9f5344 | ||
|
|
a45008411e | ||
|
|
9091ffd1b3 | ||
|
|
089f161d1e | ||
|
|
e6d7131701 | ||
|
|
ed82170c2c | ||
|
|
3472d42cdd | ||
|
|
b8926350b9 | ||
|
|
e5194b313d | ||
|
|
94228375ad | ||
|
|
0e8385a5a7 | ||
|
|
afd3763d96 | ||
|
|
1122f09093 | ||
|
|
1123af15c9 | ||
|
|
b2f6e865a7 | ||
|
|
93b9ac2d13 | ||
|
|
64af65b0e7 | ||
|
|
390c8437c5 | ||
|
|
cc65ca9b6a | ||
|
|
4bd7cba247 | ||
|
|
92e2f7f8b7 | ||
|
|
98054ddff6 | ||
|
|
4e5c1aba2e | ||
|
|
5eb44320ae | ||
|
|
f751269289 | ||
|
|
0e836622ec | ||
|
|
a6ac0a813d | ||
|
|
21c47f60f2 |
6
.github/workflows/continuous_integration.yml
vendored
6
.github/workflows/continuous_integration.yml
vendored
@@ -17,14 +17,14 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.22'
|
||||||
cache: false
|
cache: false
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Run linters
|
- name: Run linters
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v4
|
||||||
with:
|
with:
|
||||||
version: v1.54.2
|
version: v1.56.2
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
@@ -3,35 +3,57 @@ linters-settings:
|
|||||||
sections:
|
sections:
|
||||||
- standard
|
- standard
|
||||||
- default
|
- default
|
||||||
- prefix(github.com/gotenberg/gotenberg/v7)
|
- prefix(github.com/gotenberg/gotenberg/v8)
|
||||||
skip-generated: true
|
skip-generated: true
|
||||||
skip-vendor: true
|
|
||||||
custom-order: true
|
custom-order: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
- decorder
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- dupword
|
||||||
|
- durationcheck
|
||||||
- errcheck
|
- errcheck
|
||||||
|
- errname
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
- gci
|
- gci
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
- gofumpt
|
- gofumpt
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
|
- importas
|
||||||
- misspell
|
- misspell
|
||||||
- prealloc
|
- prealloc
|
||||||
|
- promlinter
|
||||||
|
- sloglint
|
||||||
|
- sqlclosecheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- tenv
|
||||||
|
- testableexamples
|
||||||
|
- testifylint
|
||||||
|
- tparallel
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- zerologlint
|
||||||
|
|
||||||
run:
|
run:
|
||||||
deadline: 5m
|
timeout: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: false
|
tests: false
|
||||||
|
|
||||||
output:
|
output:
|
||||||
format: 'colored-line-number'
|
|
||||||
print-issued-lines: true
|
print-issued-lines: true
|
||||||
print-linter-name: true
|
print-linter-name: true
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 Julien Neuhart
|
Copyright (c) 2024 Julien Neuhart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
28
Makefile
28
Makefile
@@ -5,14 +5,14 @@ help: ## Show the help
|
|||||||
.PHONY: it
|
.PHONY: it
|
||||||
it: build build-tests ## Initialize the development environment
|
it: build build-tests ## Initialize the development environment
|
||||||
|
|
||||||
GOLANG_VERSION=1.21
|
GOLANG_VERSION=1.22
|
||||||
DOCKER_REPOSITORY=gotenberg
|
DOCKER_REPOSITORY=gotenberg
|
||||||
GOTENBERG_VERSION=snapshot
|
GOTENBERG_VERSION=snapshot
|
||||||
GOTENBERG_USER_GID=1001
|
GOTENBERG_USER_GID=1001
|
||||||
GOTENBERG_USER_UID=1001
|
GOTENBERG_USER_UID=1001
|
||||||
NOTO_COLOR_EMOJI_VERSION=v2.040 # See https://github.com/googlefonts/noto-emoji/releases.
|
NOTO_COLOR_EMOJI_VERSION=v2.042 # See https://github.com/googlefonts/noto-emoji/releases.
|
||||||
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||||
GOLANGCI_LINT_VERSION=v1.54.2 # See https://github.com/golangci/golangci-lint/releases.
|
GOLANGCI_LINT_VERSION=v1.56.2 # 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,6 +35,7 @@ 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_RESTART_AFTER=0
|
CHROMIUM_RESTART_AFTER=0
|
||||||
|
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||||
CHROMIUM_AUTO_START=false
|
CHROMIUM_AUTO_START=false
|
||||||
CHROMIUM_START_TIMEOUT=20s
|
CHROMIUM_START_TIMEOUT=20s
|
||||||
CHROMIUM_INCOGNITO=false
|
CHROMIUM_INCOGNITO=false
|
||||||
@@ -45,10 +46,13 @@ CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
|||||||
CHROMIUM_HOST_RESOLVER_RULES=
|
CHROMIUM_HOST_RESOLVER_RULES=
|
||||||
CHROMIUM_PROXY_SERVER=
|
CHROMIUM_PROXY_SERVER=
|
||||||
CHROMIUM_ALLOW_LIST=
|
CHROMIUM_ALLOW_LIST=
|
||||||
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
|
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
|
||||||
|
CHROMIUM_CLEAR_CACHE=false
|
||||||
|
CHROMIUM_CLEAR_COOKIES=false
|
||||||
CHROMIUM_DISABLE_JAVASCRIPT=false
|
CHROMIUM_DISABLE_JAVASCRIPT=false
|
||||||
CHROMIUM_DISABLE_ROUTES=false
|
CHROMIUM_DISABLE_ROUTES=false
|
||||||
LIBREOFFICE_RESTART_AFTER=10
|
LIBREOFFICE_RESTART_AFTER=10
|
||||||
|
LIBREOFFICE_MAX_QUEUE_SIZE=0
|
||||||
LIBREOFFICE_AUTO_START=false
|
LIBREOFFICE_AUTO_START=false
|
||||||
LIBREOFFICE_START_TIMEOUT=20s
|
LIBREOFFICE_START_TIMEOUT=20s
|
||||||
LIBREOFFICE_DISABLE_ROUTES=false
|
LIBREOFFICE_DISABLE_ROUTES=false
|
||||||
@@ -87,6 +91,7 @@ run: ## Start a Gotenberg container
|
|||||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||||
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||||
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||||
|
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
|
||||||
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
|
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
|
||||||
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
||||||
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
|
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
|
||||||
@@ -95,11 +100,14 @@ run: ## Start a Gotenberg container
|
|||||||
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
|
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
|
||||||
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
|
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
|
||||||
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
|
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
|
||||||
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
|
--chromium-allow-list="$(CHROMIUM_ALLOW_LIST)" \
|
||||||
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \
|
--chromium-deny-list="$(CHROMIUM_DENY_LIST)" \
|
||||||
|
--chromium-clear-cache=$(CHROMIUM_CLEAR_CACHE) \
|
||||||
|
--chromium-clear-cookies=$(CHROMIUM_CLEAR_COOKIES) \
|
||||||
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
|
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
|
||||||
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
|
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
|
||||||
--libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \
|
--libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \
|
||||||
|
--libreoffice-max-queue-size=$(LIBREOFFICE_MAX_QUEUE_SIZE) \
|
||||||
--libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \
|
--libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \
|
||||||
--libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \
|
--libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \
|
||||||
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \
|
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \
|
||||||
@@ -112,8 +120,8 @@ run: ## Start a Gotenberg container
|
|||||||
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
||||||
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
|
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
|
||||||
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
|
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
|
||||||
--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) \
|
||||||
--webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \
|
--webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \
|
||||||
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
|
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
|
||||||
@@ -151,13 +159,13 @@ tests-once: ## Run the tests once (prefer the "tests" command while developing)
|
|||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: ## Format the code and "optimize" the dependencies
|
fmt: ## Format the code and "optimize" the dependencies
|
||||||
gofumpt -l -w .
|
gofumpt -l -w .
|
||||||
gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v7)" --skip-generated --skip-vendor --custom-order .
|
gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v8)" --skip-generated --skip-vendor --custom-order .
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
# go install golang.org/x/tools/cmd/godoc@latest
|
# go install golang.org/x/tools/cmd/godoc@latest
|
||||||
.PHONY: godoc
|
.PHONY: godoc
|
||||||
godoc: ## Run a webserver with Gotenberg godoc
|
godoc: ## Run a webserver with Gotenberg godoc
|
||||||
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v7)
|
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v8)
|
||||||
godoc -http=:6060
|
godoc -http=:6060
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ numerous document formats (HTML, Markdown, Word, Excel, etc.) into PDF files, an
|
|||||||
Open a terminal and run the following command:
|
Open a terminal and run the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --rm -p 3000:3000 gotenberg/gotenberg:7
|
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, using the historic Docker repository from our sponsor [TheCodingMachine](https://www.thecodingmachine.com):
|
Alternatively, using the historic Docker repository from our sponsor [TheCodingMachine](https://www.thecodingmachine.com):
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:7
|
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:8
|
||||||
```
|
```
|
||||||
|
|
||||||
The API is now available on your host at http://localhost:3000.
|
The API is now available on your host at http://localhost:3000.
|
||||||
@@ -41,5 +41,5 @@ Head to the [documentation](https://gotenberg.dev/docs/getting-started/introduct
|
|||||||
[](https://hub.docker.com/r/gotenberg/gotenberg)
|
[](https://hub.docker.com/r/gotenberg/gotenberg)
|
||||||
[](https://hub.docker.com/r/thecodingmachine/gotenberg)
|
[](https://hub.docker.com/r/thecodingmachine/gotenberg)
|
||||||
[](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
|
[](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
|
||||||
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
|
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v8)
|
||||||
[](https://codecov.io/gh/gotenberg/gotenberg)
|
[](https://codecov.io/gh/gotenberg/gotenberg)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ RUN go mod download &&\
|
|||||||
COPY cmd ./cmd
|
COPY cmd ./cmd
|
||||||
COPY pkg ./pkg
|
COPY pkg ./pkg
|
||||||
|
|
||||||
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v7/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
||||||
|
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
# Final stage
|
# Final stage
|
||||||
@@ -164,19 +164,20 @@ RUN \
|
|||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
# Install PDFtk & QPDF (PDF engines).
|
# Install PDFtk, QPDF & ExifTool (PDF engines).
|
||||||
# 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/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
|
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
|
||||||
chmod a+x /usr/bin/pdftk-all.jar &&\
|
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||||
echo '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
echo '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
||||||
chmod +x /usr/bin/pdftk &&\
|
chmod +x /usr/bin/pdftk &&\
|
||||||
apt-get update -qq &&\
|
apt-get update -qq &&\
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf exiftool &&\
|
||||||
# See https://github.com/nextcloud/docker/issues/380.
|
# See https://github.com/nextcloud/docker/issues/380.
|
||||||
mkdir -p /usr/share/man/man1 &&\
|
mkdir -p /usr/share/man/man1 &&\
|
||||||
# Verify installations.
|
# Verify installations.
|
||||||
pdftk --version &&\
|
pdftk --version &&\
|
||||||
qpdf --version &&\
|
qpdf --version &&\
|
||||||
|
exiftool --version &&\
|
||||||
# Cleanup.
|
# Cleanup.
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
@@ -195,6 +196,7 @@ ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin
|
|||||||
ENV UNOCONVERTER_BIN_PATH /usr/bin/unoconverter
|
ENV UNOCONVERTER_BIN_PATH /usr/bin/unoconverter
|
||||||
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
||||||
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
||||||
|
ENV EXIFTOOL_BIN_PATH /usr/bin/exiftool
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -75,7 +75,6 @@ func Run() {
|
|||||||
go func(app gotenberg.App) {
|
go func(app gotenberg.App) {
|
||||||
id := app.(gotenberg.Module).Descriptor().ID
|
id := app.(gotenberg.Module).Descriptor().ID
|
||||||
err = app.Start()
|
err = app.Start()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[FATAL] starting %s: %s\n", id, err)
|
fmt.Printf("[FATAL] starting %s: %s\n", id, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd"
|
gotenbergcmd "github.com/gotenberg/gotenberg/v8/cmd"
|
||||||
// Gotenberg modules.
|
// Gotenberg modules.
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/standard"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,744 +0,0 @@
|
|||||||
openapi: 3.0.3
|
|
||||||
info:
|
|
||||||
title: Gotenberg
|
|
||||||
version: 7.x
|
|
||||||
license:
|
|
||||||
name: MIT
|
|
||||||
url: 'https://github.com/gotenberg/gotenberg/blob/main/LICENSE'
|
|
||||||
contact:
|
|
||||||
url: 'https://github.com/gotenberg/gotenberg'
|
|
||||||
description: >-
|
|
||||||
A Docker-powered stateless API for PDF files.
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev
|
|
||||||
servers:
|
|
||||||
- url: 'http://localhost:3000'
|
|
||||||
description: Local server with the default Docker image and port
|
|
||||||
tags:
|
|
||||||
- name: chromium
|
|
||||||
description: Operations of the Chromium module
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/chromium
|
|
||||||
- name: libreoffice
|
|
||||||
description: Operations of the Libreoffice module
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/libreoffice
|
|
||||||
- name: pdfengines
|
|
||||||
description: Operations of the PDF Engines module
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/pdf-engines
|
|
||||||
paths:
|
|
||||||
/forms/chromium/convert/url:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- chromium
|
|
||||||
summary: Convert the contents of a given URL to PDF
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/chromium
|
|
||||||
description: >-
|
|
||||||
Send a URL in your API request via the `url` form field
|
|
||||||
Send a remote URL in your API request via the `remoteURL` parameter, and
|
|
||||||
get the resulting PDF file. The API will fetch the given URL and render
|
|
||||||
the page to PDF using the underlying headless Chrome instance.
|
|
||||||
You can optionally include `header.html` and `footer.html` files as part of the request as well.
|
|
||||||
See externalDocs for more details.
|
|
||||||
parameters:
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Output-Filename
|
|
||||||
description: >-
|
|
||||||
By default, the API generates a UUID filename.
|
|
||||||
However, you may also specify the filename per request,
|
|
||||||
thanks to the Gotenberg-Output-Filename header.
|
|
||||||
Caution! The API adds the file extension automatically; you don't have to set it.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Trace
|
|
||||||
description: >-
|
|
||||||
The trace, or request ID, identifies a request in the logs.
|
|
||||||
|
|
||||||
By default, the API generates a UUID trace for each request.
|
|
||||||
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
description: >-
|
|
||||||
The request must be `multipart/form-data` that includes a `url` form field.
|
|
||||||
The API uses a headless Chrome instance to do the conversion, therefore print
|
|
||||||
parameter such as margins and paper size are also accepted as optional parameters.
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/URLConvertRequestBody'
|
|
||||||
examples: { }
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/SuccessfulPDF'
|
|
||||||
'400':
|
|
||||||
description: Bad Request
|
|
||||||
'503':
|
|
||||||
$ref: '#/components/responses/Timeout'
|
|
||||||
|
|
||||||
/forms/chromium/convert/html:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- chromium
|
|
||||||
summary: Convert a given HTML file to PDF
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/chromium
|
|
||||||
description: >-
|
|
||||||
Send an HTML file called `index.html` as a multipart form request, and
|
|
||||||
get the resulting PDF file. You can optionally include `header.html` and
|
|
||||||
`footer.html` files as part of the request as well.
|
|
||||||
See externalDocs for more details.
|
|
||||||
parameters:
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Output-Filename
|
|
||||||
description: >-
|
|
||||||
By default, the API generates a UUID filename.
|
|
||||||
However, you may also specify the filename per request,
|
|
||||||
thanks to the Gotenberg-Output-Filename header.
|
|
||||||
Caution! The API adds the file extension automatically; you don't have to set it.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Trace
|
|
||||||
description: >-
|
|
||||||
The trace, or request ID, identifies a request in the logs.
|
|
||||||
|
|
||||||
By default, the API generates a UUID trace for each request.
|
|
||||||
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/HTMLConvertRequestBody'
|
|
||||||
description: >-
|
|
||||||
The request body must have an `index.html` file in the `files` array,
|
|
||||||
as well as all the referred resources on the same level as the
|
|
||||||
`index.html` file. The request can also include `header.html` and
|
|
||||||
`footer.html`, given the limitations in the API description above.
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/SuccessfulPDF'
|
|
||||||
'400':
|
|
||||||
description: Bad Request
|
|
||||||
'503':
|
|
||||||
$ref: '#/components/responses/Timeout'
|
|
||||||
|
|
||||||
/forms/chromium/convert/markdown:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- chromium
|
|
||||||
summary: Convert a Markdown file to PDF
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/chromium
|
|
||||||
description: >-
|
|
||||||
Accepts an HTML file called `index.html` plus markdown files as a multipart
|
|
||||||
form request and embeds the markdown files into the HTML file using the Golang template
|
|
||||||
function `toHTML`.
|
|
||||||
The API will convert the markdown to HTML and embed it into your `index.html` file,
|
|
||||||
then render the resulting page. You can include your own styling and more in your HTML file.
|
|
||||||
Refer to the HTML conversion page for all the options you can use when converting
|
|
||||||
Markdown documents as well. You can optionally include `header.html` and
|
|
||||||
`footer.html` files as part of the request as well.
|
|
||||||
See externalDocs for more details.
|
|
||||||
parameters:
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Output-Filename
|
|
||||||
description: >-
|
|
||||||
By default, the API generates a UUID filename.
|
|
||||||
However, you may also specify the filename per request,
|
|
||||||
thanks to the Gotenberg-Output-Filename header.
|
|
||||||
Caution! The API adds the file extension automatically; you don't have to set it.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Trace
|
|
||||||
description: >-
|
|
||||||
The trace, or request ID, identifies a request in the logs.
|
|
||||||
|
|
||||||
By default, the API generates a UUID trace for each request.
|
|
||||||
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/MarkdownConvertRequestBody'
|
|
||||||
description: >-
|
|
||||||
The request body must have an `index.html` file in the `files` array,
|
|
||||||
as well as all the referred markdown resources on the same level as
|
|
||||||
the `index.html` file.
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/SuccessfulPDF'
|
|
||||||
'400':
|
|
||||||
description: Bad Request
|
|
||||||
'503':
|
|
||||||
$ref: '#/components/responses/Timeout'
|
|
||||||
|
|
||||||
/forms/libreoffice/convert:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- libreoffice
|
|
||||||
summary: Convert an Office document to PDF
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/libreoffice
|
|
||||||
description: >-
|
|
||||||
This route accepts multipart/form-data requests and files with the following extensions:
|
|
||||||
|
|
||||||
.bib .doc .xml .docx .fodt .html .ltx .txt .odt .ott .pdb .pdf .psw .rtf
|
|
||||||
.sdw .stw .sxw .uot .vor .wps .epub .png .bmp .emf .eps .fodg .gif .jpg
|
|
||||||
.met .odd .otg .pbm .pct .pgm .ppm .ras .std .svg .svm .swf .sxd .sxw
|
|
||||||
.tiff .xhtml .xpm .fodp .potm .pot .pptx .pps .ppt .pwp .sda .sdd .sti
|
|
||||||
.sxi .uop .wmf .csv .dbf .dif .fods .ods .ots .pxl .sdc .slk .stc .sxc
|
|
||||||
.uos .xls .xlt .xlsx .tif .jpeg .odp
|
|
||||||
|
|
||||||
By default, if you send more than one file to convert, the route returns a ZIP archive of the
|
|
||||||
resulting PDF files. However, you may prefer to merge all the PDF files into an individual PDF file.
|
|
||||||
|
|
||||||
> **Attention:** The files will be merged alphabetically for the
|
|
||||||
resulting PDF.
|
|
||||||
|
|
||||||
You may also specify the page ranges to convert from the incoming Office
|
|
||||||
documents. The expected format is the same as the one from the print
|
|
||||||
options of LibreOffice, e.g. `1-1` or `1-4`.
|
|
||||||
|
|
||||||
> **Attention:** if more than one document, the page ranges will be
|
|
||||||
applied for each document.
|
|
||||||
|
|
||||||
See externalDocs for more details.
|
|
||||||
parameters:
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Output-Filename
|
|
||||||
description: >-
|
|
||||||
By default, the API generates a UUID filename.
|
|
||||||
However, you may also specify the filename per request,
|
|
||||||
thanks to the Gotenberg-Output-Filename header.
|
|
||||||
Caution! The API adds the file extension automatically; you don't have to set it.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Trace
|
|
||||||
description: >-
|
|
||||||
The trace, or request ID, identifies a request in the logs.
|
|
||||||
|
|
||||||
By default, the API generates a UUID trace for each request.
|
|
||||||
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/OfficeConvertRequestBody'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/SuccessfulPDF'
|
|
||||||
'400':
|
|
||||||
description: Bad Request, e.g. Both 'pdfFormat' and 'nativePdfA1aFormat' form values are provided
|
|
||||||
'503':
|
|
||||||
$ref: '#/components/responses/Timeout'
|
|
||||||
|
|
||||||
/forms/pdfengines/merge:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- pdfengines
|
|
||||||
summary: Merge multiple PDFs into a single PDF
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/pdf-engines
|
|
||||||
description: >-
|
|
||||||
You can send multiple PDF files to this endpoint, the API will merge
|
|
||||||
them into a single PDF and return the resulting PDF file.
|
|
||||||
|
|
||||||
> **Attention:** The PDF files will be merged alphabetically.
|
|
||||||
parameters:
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Output-Filename
|
|
||||||
description: >-
|
|
||||||
By default, the API generates a UUID filename.
|
|
||||||
However, you may also specify the filename per request,
|
|
||||||
thanks to the Gotenberg-Output-Filename header.
|
|
||||||
Caution! The API adds the file extension automatically; you don't have to set it.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Trace
|
|
||||||
description: >-
|
|
||||||
The trace, or request ID, identifies a request in the logs.
|
|
||||||
|
|
||||||
By default, the API generates a UUID trace for each request.
|
|
||||||
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
files:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
pdfFormat:
|
|
||||||
type: string
|
|
||||||
description: The PDF format of the resulting PDF
|
|
||||||
example: PDF/A-1a
|
|
||||||
required:
|
|
||||||
- files
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/SuccessfulPDF'
|
|
||||||
'400':
|
|
||||||
description: Bad Request
|
|
||||||
'503':
|
|
||||||
$ref: '#/components/responses/Timeout'
|
|
||||||
|
|
||||||
/forms/pdfengines/convert:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- pdfengines
|
|
||||||
summary: Convert PDFs into the given formats
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/pdf-engines
|
|
||||||
description: >-
|
|
||||||
This route accepts PDF files and a form field pdfFormat for converting them into the specified format.
|
|
||||||
parameters:
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Output-Filename
|
|
||||||
description: >-
|
|
||||||
By default, the API generates a UUID filename.
|
|
||||||
However, you may also specify the filename per request,
|
|
||||||
thanks to the Gotenberg-Output-Filename header.
|
|
||||||
Caution! The API adds the file extension automatically; you don't have to set it.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Trace
|
|
||||||
description: >-
|
|
||||||
The trace, or request ID, identifies a request in the logs.
|
|
||||||
|
|
||||||
By default, the API generates a UUID trace for each request.
|
|
||||||
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
files:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
pdfFormat:
|
|
||||||
type: string
|
|
||||||
description: The PDF format of the resulting PDF
|
|
||||||
example: PDF/A-1a
|
|
||||||
required:
|
|
||||||
- files
|
|
||||||
- pdfFormat
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/SuccessfulPDF'
|
|
||||||
'400':
|
|
||||||
description: >-
|
|
||||||
Bad Request, e.g. Invalid form data: no form file found for extensions: [.pdf]; form value 'pdfFormat' is required
|
|
||||||
'503':
|
|
||||||
$ref: '#/components/responses/Timeout'
|
|
||||||
|
|
||||||
components:
|
|
||||||
schemas:
|
|
||||||
HTMLConvertRequestBody:
|
|
||||||
title: HTML Conversion Request Body
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
files:
|
|
||||||
type: array
|
|
||||||
description: >-
|
|
||||||
List of HTML files to be converted to PDF. An `index.html` file is
|
|
||||||
required, and any other resources that are referenced through the
|
|
||||||
HTML file must be included as well. All the referenced files must be
|
|
||||||
on the same level as the `index.html` file.
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
marginTop:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Top margin for the page in inches.
|
|
||||||
marginBottom:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Bottom margin for the page in inches.
|
|
||||||
marginLeft:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Left margin for the page in inches.
|
|
||||||
marginRight:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Right margin for the page in inches.
|
|
||||||
paperWidth:
|
|
||||||
type: number
|
|
||||||
example: 8.27
|
|
||||||
description: >-
|
|
||||||
Paper width to be used while rendering the PDF. The default page
|
|
||||||
size is A4.
|
|
||||||
paperHeight:
|
|
||||||
type: number
|
|
||||||
example: 11.69
|
|
||||||
description: >-
|
|
||||||
Paper height to be used while rendering the PDF. The default page
|
|
||||||
size is A4.
|
|
||||||
preferCssPageSize:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Define whether to prefer page size as defined by CSS (default false)
|
|
||||||
default: false
|
|
||||||
printBackground:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Print the background graphics (default false)
|
|
||||||
default: false
|
|
||||||
landscape:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
default: false
|
|
||||||
description: >-
|
|
||||||
The default orientation for rendering the page is "portrait" mode.
|
|
||||||
By sending "landscape" parameter, you can ask the output to be
|
|
||||||
landscape.
|
|
||||||
scale:
|
|
||||||
type: number
|
|
||||||
minimum: 0.1
|
|
||||||
maximum: 2.0
|
|
||||||
example: 1.5
|
|
||||||
description: >-
|
|
||||||
The scale of the page rendering
|
|
||||||
default: 1.0
|
|
||||||
waitDelay:
|
|
||||||
type: string
|
|
||||||
example: 5s
|
|
||||||
description: >-
|
|
||||||
When the page relies on JavaScript for rendering, and you don't have access to the page's code,
|
|
||||||
you may want to wait a certain amount of time to make sure Chromium has fully rendered the page
|
|
||||||
you're trying to generate.
|
|
||||||
waitWindowStatus:
|
|
||||||
type: string
|
|
||||||
example: done
|
|
||||||
description: >-
|
|
||||||
If you have access to the page's code, you may set the window status and tell Gotenberg to wait for a specific value.
|
|
||||||
For instance
|
|
||||||
await promises()
|
|
||||||
window.status = 'ready'
|
|
||||||
Prefer this option over waitDelay.
|
|
||||||
extraHttpHeaders:
|
|
||||||
type: string
|
|
||||||
description: HTTP headers to send by Chromium while loading the HTML document (JSON format)
|
|
||||||
nativePageRanges:
|
|
||||||
type: string
|
|
||||||
example: 1-4
|
|
||||||
description: >-
|
|
||||||
The page ranges to be converted to PDF for the incoming Office
|
|
||||||
documents.
|
|
||||||
pdfFormat:
|
|
||||||
type: string
|
|
||||||
description: >-
|
|
||||||
The PDF format of the resulting PDF.
|
|
||||||
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
|
|
||||||
example: PDF/A-1a
|
|
||||||
required:
|
|
||||||
- files
|
|
||||||
MarkdownConvertRequestBody:
|
|
||||||
title: Markdown Conversion Request Body
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
files:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
marginTop:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Top margin for the page in inches.
|
|
||||||
marginBottom:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Bottom margin for the page in inches.
|
|
||||||
marginLeft:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Left margin for the page in inches.
|
|
||||||
marginRight:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Right margin for the page in inches.
|
|
||||||
paperWidth:
|
|
||||||
type: number
|
|
||||||
example: 8.27
|
|
||||||
description: >-
|
|
||||||
Paper width to be used while rendering the PDF. The default page
|
|
||||||
size is A4.
|
|
||||||
paperHeight:
|
|
||||||
type: number
|
|
||||||
example: 11.69
|
|
||||||
description: >-
|
|
||||||
Paper height to be used while rendering the PDF. The default page
|
|
||||||
size is A4.
|
|
||||||
preferCssPageSize:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Define whether to prefer page size as defined by CSS (default false)
|
|
||||||
default: false
|
|
||||||
printBackground:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Print the background graphics (default false)
|
|
||||||
default: false
|
|
||||||
landscape:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
default: false
|
|
||||||
description: >-
|
|
||||||
The default orientation for rendering the page is "portrait" mode.
|
|
||||||
By sending "landscape" parameter, you can ask the output to be
|
|
||||||
landscape.
|
|
||||||
scale:
|
|
||||||
type: number
|
|
||||||
minimum: 0.1
|
|
||||||
maximum: 2.0
|
|
||||||
example: 1.5
|
|
||||||
description: >-
|
|
||||||
The scale of the page rendering
|
|
||||||
default: 1.0
|
|
||||||
waitDelay:
|
|
||||||
type: string
|
|
||||||
example: 5s
|
|
||||||
description: >-
|
|
||||||
When the page relies on JavaScript for rendering, and you don't have access to the page's code,
|
|
||||||
you may want to wait a certain amount of time to make sure Chromium has fully rendered the page
|
|
||||||
you're trying to generate.
|
|
||||||
waitWindowStatus:
|
|
||||||
type: string
|
|
||||||
example: done
|
|
||||||
description: >-
|
|
||||||
If you have access to the page's code, you may set the window status and tell Gotenberg to wait for a specific value.
|
|
||||||
For instance
|
|
||||||
await promises()
|
|
||||||
window.status = 'ready'
|
|
||||||
Prefer this option over waitDelay.
|
|
||||||
extraHttpHeaders:
|
|
||||||
type: string
|
|
||||||
description: HTTP headers to send by Chromium while loading the HTML document (JSON format)
|
|
||||||
nativePageRanges:
|
|
||||||
type: string
|
|
||||||
example: 1-4
|
|
||||||
description: >-
|
|
||||||
The page ranges to be converted to PDF for the incoming Office
|
|
||||||
documents.
|
|
||||||
pdfFormat:
|
|
||||||
type: string
|
|
||||||
description: >-
|
|
||||||
The PDF format of the resulting PDF.
|
|
||||||
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
|
|
||||||
example: PDF/A-1a
|
|
||||||
required:
|
|
||||||
- files
|
|
||||||
URLConvertRequestBody:
|
|
||||||
title: URL Conversion Request Body
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
url:
|
|
||||||
type: string
|
|
||||||
example: 'https://google.com'
|
|
||||||
files:
|
|
||||||
description: Optional files named header.html and footer.html
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
marginTop:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Top margin for the page in inches.
|
|
||||||
marginBottom:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Bottom margin for the page in inches.
|
|
||||||
marginLeft:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Left margin for the page in inches.
|
|
||||||
marginRight:
|
|
||||||
type: number
|
|
||||||
example: 0
|
|
||||||
default: 1
|
|
||||||
description: Right margin for the page in inches.
|
|
||||||
paperWidth:
|
|
||||||
type: number
|
|
||||||
example: 8.27
|
|
||||||
description: >-
|
|
||||||
Paper width to be used while rendering the PDF. The default page
|
|
||||||
size is A4.
|
|
||||||
paperHeight:
|
|
||||||
type: number
|
|
||||||
example: 11.69
|
|
||||||
description: >-
|
|
||||||
Paper height to be used while rendering the PDF. The default page
|
|
||||||
size is A4.
|
|
||||||
preferCssPageSize:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Define whether to prefer page size as defined by CSS (default false)
|
|
||||||
default: false
|
|
||||||
printBackground:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Print the background graphics (default false)
|
|
||||||
default: false
|
|
||||||
landscape:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
default: false
|
|
||||||
description: >-
|
|
||||||
The default orientation for rendering the page is "portrait" mode.
|
|
||||||
By sending "landscape" parameter, you can ask the output to be
|
|
||||||
landscape.
|
|
||||||
scale:
|
|
||||||
type: number
|
|
||||||
minimum: 0.1
|
|
||||||
maximum: 2.0
|
|
||||||
example: 1.5
|
|
||||||
description: >-
|
|
||||||
The scale of the page rendering
|
|
||||||
default: 1.0
|
|
||||||
waitDelay:
|
|
||||||
type: string
|
|
||||||
example: 5s
|
|
||||||
description: >-
|
|
||||||
When the page relies on JavaScript for rendering, and you don't have access to the page's code,
|
|
||||||
you may want to wait a certain amount of time to make sure Chromium has fully rendered the page
|
|
||||||
you're trying to generate.
|
|
||||||
waitWindowStatus:
|
|
||||||
type: string
|
|
||||||
example: done
|
|
||||||
description: >-
|
|
||||||
If you have access to the page's code, you may set the window status and tell Gotenberg to wait for a specific value.
|
|
||||||
For instance
|
|
||||||
await promises()
|
|
||||||
window.status = 'ready'
|
|
||||||
Prefer this option over waitDelay.
|
|
||||||
extraHttpHeaders:
|
|
||||||
type: string
|
|
||||||
description: HTTP headers to send by Chromium while loading the HTML document (JSON format)
|
|
||||||
nativePageRanges:
|
|
||||||
type: string
|
|
||||||
example: 1-4
|
|
||||||
description: >-
|
|
||||||
The page ranges to be converted to PDF for the incoming Office
|
|
||||||
documents.
|
|
||||||
pdfFormat:
|
|
||||||
type: string
|
|
||||||
description: >-
|
|
||||||
The PDF format of the resulting PDF.
|
|
||||||
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
|
|
||||||
example: PDF/A-1a
|
|
||||||
required:
|
|
||||||
- url
|
|
||||||
OfficeConvertRequestBody:
|
|
||||||
title: Office Conversion Request Body
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
files:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
nativePageRanges:
|
|
||||||
type: string
|
|
||||||
example: 1-4
|
|
||||||
description: >-
|
|
||||||
The page ranges to be converted to PDF for the incoming Office
|
|
||||||
documents. **If there are multiple files sent to the API, this page
|
|
||||||
range will apply to all of the documents**. Empty means all pages.
|
|
||||||
nativePdfA1aFormat:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Use unoconv to convert the resulting PDF to the 'PDF/A-1a' format.
|
|
||||||
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
|
|
||||||
pdfFormat:
|
|
||||||
type: string
|
|
||||||
description: >-
|
|
||||||
The PDF format of the resulting PDF.
|
|
||||||
Caution! You cannot use both nativePdfA1aFormat and pdfFormat form fields.
|
|
||||||
example: PDF/A-1a
|
|
||||||
landscape:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
default: false
|
|
||||||
description: >-
|
|
||||||
The default orientation for rendering the page is "portrait" mode.
|
|
||||||
By sending "landscape" parameter, you can ask the output to be
|
|
||||||
landscape.
|
|
||||||
merge:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
Merge all PDF files into an individual PDF file.
|
|
||||||
required:
|
|
||||||
- files
|
|
||||||
MergeFilesRequestBody:
|
|
||||||
title: Merge Files Request Body
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
files:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
required:
|
|
||||||
- files
|
|
||||||
securitySchemes: { }
|
|
||||||
responses:
|
|
||||||
SuccessfulPDF:
|
|
||||||
description: Resulting PDF file from the conversion.
|
|
||||||
content:
|
|
||||||
application/pdf:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: binary
|
|
||||||
Timeout:
|
|
||||||
description: Service Unavailable
|
|
||||||
52
go.mod
52
go.mod
@@ -1,41 +1,44 @@
|
|||||||
module github.com/gotenberg/gotenberg/v7
|
module github.com/gotenberg/gotenberg/v8
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexliesenfeld/health v0.8.0
|
github.com/alexliesenfeld/health v0.8.0
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61
|
github.com/barasher/go-exiftool v1.10.0
|
||||||
github.com/chromedp/chromedp v0.9.3
|
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154
|
||||||
|
github.com/chromedp/chromedp v0.9.5
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/uuid v1.5.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5
|
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||||
github.com/klauspost/compress v1.17.4 // indirect
|
github.com/klauspost/compress v1.17.7 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/labstack/echo/v4 v4.11.3
|
github.com/labstack/echo/v4 v4.11.4
|
||||||
github.com/labstack/gommon v0.4.1
|
github.com/labstack/gommon v0.4.2
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mholt/archiver/v3 v3.5.1
|
github.com/mholt/archiver/v3 v3.5.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||||
github.com/pdfcpu/pdfcpu v0.6.0
|
github.com/pdfcpu/pdfcpu v0.7.0
|
||||||
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/prometheus/client_golang v1.17.0
|
github.com/prometheus/client_golang v1.19.0
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.11.0
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.16.0 // indirect
|
golang.org/x/crypto v0.21.0 // indirect
|
||||||
golang.org/x/image v0.14.0 // indirect
|
golang.org/x/image v0.15.0 // indirect
|
||||||
golang.org/x/net v0.19.0
|
golang.org/x/net v0.22.0
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.6.0
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
golang.org/x/term v0.15.0
|
golang.org/x/term v0.18.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/dlclark/regexp2 v1.11.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
@@ -44,7 +47,7 @@ require (
|
|||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.3.1 // indirect
|
github.com/gobwas/ws v1.3.2 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||||
@@ -52,15 +55,14 @@ require (
|
|||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.6.0 // indirect
|
||||||
github.com/prometheus/common v0.45.0 // indirect
|
github.com/prometheus/common v0.51.0 // indirect
|
||||||
github.com/prometheus/procfs v0.12.0 // indirect
|
github.com/prometheus/procfs v0.13.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
115
go.sum
115
go.sum
@@ -1,23 +1,28 @@
|
|||||||
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
|
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
|
||||||
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
|
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
|
||||||
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.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
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/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
|
||||||
|
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61 h1:XD280QPATe9jaz20dylKe3vBsNcH1w3mkssGY0lidn8=
|
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154 h1:jeAmkzyOAQBPRmZMhX+i/CJv0VViLkHk1nF0qx8s0Mk=
|
||||||
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
||||||
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
||||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
@@ -25,18 +30,16 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
|
|||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
||||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
@@ -53,8 +56,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
@@ -63,10 +66,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
|
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||||
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||||
github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
@@ -78,8 +81,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
|
||||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||||
@@ -89,35 +90,37 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l
|
|||||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/pdfcpu/pdfcpu v0.6.0 h1:z4kARP5bcWa39TTYMcN/kjBnm7MvhTWjXgeYmkdAGMI=
|
github.com/pdfcpu/pdfcpu v0.7.0 h1:cd7/z7hAyyDuzdciKfNZyQ3TYreJza2DsuPdIHYURcA=
|
||||||
github.com/pdfcpu/pdfcpu v0.6.0/go.mod h1:kmpD0rk8YnZj0l3qSeGBlAB+XszHUgNv//ORH/E7EYo=
|
github.com/pdfcpu/pdfcpu v0.7.0/go.mod h1:kmpD0rk8YnZj0l3qSeGBlAB+XszHUgNv//ORH/E7EYo=
|
||||||
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.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
|
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
github.com/prometheus/common v0.51.0 h1:vT5R9NAlW4V6k8Wruk7ikrHaHRsrPbduM/cKTOdQM/k=
|
||||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
github.com/prometheus/common v0.51.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
|
||||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
@@ -130,32 +133,32 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
|||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||||
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
53
pkg/gotenberg/filter.go
Normal file
53
pkg/gotenberg/filter.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrFiltered happens if a value is filtered by the [FilterDeadline] function.
|
||||||
|
var ErrFiltered = errors.New("value filtered")
|
||||||
|
|
||||||
|
// FilterDeadline checks if given value is allowed and not denied according to
|
||||||
|
// regex patterns. It returns a [context.DeadlineExceeded] if it takes too long
|
||||||
|
// to process.
|
||||||
|
func FilterDeadline(allowed, denied *regexp2.Regexp, s string, deadline time.Time) error {
|
||||||
|
// FIXME: not ideal to compile everytime, but is there another way to create a clone?
|
||||||
|
if allowed.String() != "" {
|
||||||
|
allow := regexp2.MustCompile(allowed.String(), 0)
|
||||||
|
allow.MatchTimeout = time.Until(deadline)
|
||||||
|
|
||||||
|
ok, err := allow.MatchString(s)
|
||||||
|
if err != nil {
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
return fmt.Errorf("'%s' cannot handle '%s': %w", allow.String(), s, err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", s, ErrFiltered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if denied.String() != "" {
|
||||||
|
deny := regexp2.MustCompile(denied.String(), 0)
|
||||||
|
deny.MatchTimeout = time.Until(deadline)
|
||||||
|
|
||||||
|
ok, err := deny.MatchString(s)
|
||||||
|
if err != nil {
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
return fmt.Errorf("'%s' cannot handle '%s': %w", deny.String(), s, err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return fmt.Errorf("'%s' matches the expression from the denied list: %w", s, ErrFiltered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
83
pkg/gotenberg/filter_test.go
Normal file
83
pkg/gotenberg/filter_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilterDeadline(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
allowed *regexp2.Regexp
|
||||||
|
denied *regexp2.Regexp
|
||||||
|
s string
|
||||||
|
deadline time.Time
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "DeadlineExceeded (allowed)",
|
||||||
|
allowed: regexp2.MustCompile("foo", 0),
|
||||||
|
denied: regexp2.MustCompile("", 0),
|
||||||
|
s: "foo",
|
||||||
|
deadline: time.Now().Add(time.Duration(-1) * time.Hour),
|
||||||
|
expectError: true,
|
||||||
|
expectedError: context.DeadlineExceeded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrFiltered (allowed)",
|
||||||
|
allowed: regexp2.MustCompile("foo", 0),
|
||||||
|
denied: regexp2.MustCompile("", 0),
|
||||||
|
s: "bar",
|
||||||
|
deadline: time.Now().Add(time.Duration(5) * time.Second),
|
||||||
|
expectError: true,
|
||||||
|
expectedError: ErrFiltered,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "DeadlineExceeded (denied)",
|
||||||
|
allowed: regexp2.MustCompile("", 0),
|
||||||
|
denied: regexp2.MustCompile("foo", 0),
|
||||||
|
s: "foo",
|
||||||
|
deadline: time.Now().Add(time.Duration(-1) * time.Hour),
|
||||||
|
expectError: true,
|
||||||
|
expectedError: context.DeadlineExceeded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrFiltered (denied)",
|
||||||
|
allowed: regexp2.MustCompile("", 0),
|
||||||
|
denied: regexp2.MustCompile("foo", 0),
|
||||||
|
s: "foo",
|
||||||
|
deadline: time.Now().Add(time.Duration(5) * time.Second),
|
||||||
|
expectError: true,
|
||||||
|
expectedError: ErrFiltered,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
allowed: regexp2.MustCompile("", 0),
|
||||||
|
denied: regexp2.MustCompile("", 0),
|
||||||
|
s: "foo",
|
||||||
|
deadline: time.Now().Add(time.Duration(5) * time.Second),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := FilterDeadline(tc.allowed, tc.denied, tc.s, tc.deadline)
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected an error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package gotenberg
|
package gotenberg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
"github.com/labstack/gommon/bytes"
|
"github.com/labstack/gommon/bytes"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
@@ -199,19 +199,19 @@ func (f *ParsedFlags) MustDeprecatedHumanReadableBytesString(deprecated string,
|
|||||||
|
|
||||||
// MustRegexp returns the regular expression of a flag given by name.
|
// MustRegexp returns the regular expression of a flag given by name.
|
||||||
// It panics if an error occurs.
|
// It panics if an error occurs.
|
||||||
func (f *ParsedFlags) MustRegexp(name string) *regexp.Regexp {
|
func (f *ParsedFlags) MustRegexp(name string) *regexp2.Regexp {
|
||||||
val, err := f.GetString(name)
|
val, err := f.GetString(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return regexp.MustCompile(val)
|
return regexp2.MustCompile(val, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustDeprecatedRegexp returns the regular expression of a deprecated flag if
|
// MustDeprecatedRegexp returns the regular expression of a deprecated flag if
|
||||||
// it was explicitly set or the regular expression of the new flag.
|
// it was explicitly set or the regular expression of the new flag.
|
||||||
// It panics if an error occurs.
|
// It panics if an error occurs.
|
||||||
func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp.Regexp {
|
func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp2.Regexp {
|
||||||
if f.Changed(deprecated) {
|
if f.Changed(deprecated) {
|
||||||
return f.MustRegexp(deprecated)
|
return f.MustRegexp(deprecated)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,3 +51,11 @@ func (fs *FileSystem) MkdirAll() (string, error) {
|
|||||||
|
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PathRename defines the method signature for renaming files. Implement this
|
||||||
|
// interface if you don't want to rely on [os.Rename], notably for testing
|
||||||
|
// purpose.
|
||||||
|
type PathRename interface {
|
||||||
|
// Rename uses the same signature as [os.Rename].
|
||||||
|
Rename(oldpath, newpath string) error
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ func (mod *ValidatorMock) Validate() error {
|
|||||||
|
|
||||||
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
||||||
type PdfEngineMock struct {
|
type PdfEngineMock struct {
|
||||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
|
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||||
|
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
@@ -47,6 +49,14 @@ func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, fo
|
|||||||
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return engine.ReadMetadataMock(ctx, logger, inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
||||||
type PdfEngineProviderMock struct {
|
type PdfEngineProviderMock struct {
|
||||||
PdfEngineMock func() (PdfEngine, error)
|
PdfEngineMock func() (PdfEngine, error)
|
||||||
@@ -127,6 +137,15 @@ func (provider *MetricsProviderMock) Metrics() ([]Metric, error) {
|
|||||||
return provider.MetricsMock()
|
return provider.MetricsMock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PathRenameMock is a mock for the [PathRename] interface.
|
||||||
|
type PathRenameMock struct {
|
||||||
|
RenameMock func(oldpath, newpath string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rename *PathRenameMock) Rename(oldpath, newpath string) error {
|
||||||
|
return rename.RenameMock(oldpath, newpath)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ Module = (*ModuleMock)(nil)
|
_ Module = (*ModuleMock)(nil)
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ func TestPDFEngineMock(t *testing.T) {
|
|||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
@@ -66,6 +72,16 @@ func TestPDFEngineMock(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = mock.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineMock.ReadMetadata, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.WriteMetadata(context.Background(), zap.NewNop(), map[string]interface{}{}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineMock.WriteMetadata but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngineProviderMock(t *testing.T) {
|
func TestPDFEngineProviderMock(t *testing.T) {
|
||||||
@@ -188,3 +204,16 @@ func TestMetricsProviderMock(t *testing.T) {
|
|||||||
t.Errorf("expected no error from MetricsProviderMock.Metrics, but got: %v", err)
|
t.Errorf("expected no error from MetricsProviderMock.Metrics, but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPathRenameMock(t *testing.T) {
|
||||||
|
mock := &PathRenameMock{
|
||||||
|
RenameMock: func(oldpath, newpath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Rename("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PathRenameMock.Rename, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ type SystemLogger interface {
|
|||||||
// gotenberg.MustRegisterModule(YourModule{})
|
// gotenberg.MustRegisterModule(YourModule{})
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Then, in the main command (github.com/gotenberg/gotenberg/v7/cmd/gotenberg),
|
// Then, in the main command (github.com/gotenberg/gotenberg/v8/cmd/gotenberg),
|
||||||
// import the module:
|
// import the module:
|
||||||
//
|
//
|
||||||
// imports (
|
// imports (
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ var (
|
|||||||
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
||||||
// PdfEngine interface does not support a requested PDF format conversion.
|
// PdfEngine interface does not support a requested PDF format conversion.
|
||||||
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
|
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
|
||||||
|
|
||||||
|
// ErrPdfEngineMetadataValueNotSupported is returned when a metadata value
|
||||||
|
// is not supported.
|
||||||
|
ErrPdfEngineMetadataValueNotSupported = errors.New("metadata value not supported")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -64,6 +68,12 @@ type PdfEngine interface {
|
|||||||
// Convert transforms a given PDF to the specified formats defined in
|
// Convert transforms a given PDF to the specified formats defined in
|
||||||
// PdfFormats. If no format, it does nothing.
|
// PdfFormats. If no format, it does nothing.
|
||||||
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
|
|
||||||
|
// ReadMetadata extracts the metadata of a given PDF file.
|
||||||
|
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||||
|
|
||||||
|
// WriteMetadata writes the metadata into a given PDF file.
|
||||||
|
WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
||||||
|
|||||||
69
pkg/gotenberg/sort.go
Normal file
69
pkg/gotenberg/sort.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlphanumericSort implements sort.Interface and helps to sort strings
|
||||||
|
// alphanumerically.
|
||||||
|
//
|
||||||
|
// See: https://github.com/gotenberg/gotenberg/issues/805.
|
||||||
|
type AlphanumericSort []string
|
||||||
|
|
||||||
|
func (s AlphanumericSort) Len() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AlphanumericSort) Swap(i, j int) {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AlphanumericSort) Less(i, j int) bool {
|
||||||
|
numI, restI := extractPrefix(s[i])
|
||||||
|
numJ, restJ := extractPrefix(s[j])
|
||||||
|
|
||||||
|
// Compares numerical prefixes if they exist.
|
||||||
|
if numI != -1 && numJ != -1 {
|
||||||
|
if numI != numJ {
|
||||||
|
return numI < numJ
|
||||||
|
}
|
||||||
|
// If numbers are equal, falls back to string comparison of the rest.
|
||||||
|
return restI < restJ
|
||||||
|
}
|
||||||
|
|
||||||
|
// If one has a numerical prefix and the other doesn't, the one with the
|
||||||
|
// number comes first.
|
||||||
|
if numI != -1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if numJ != -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If neither has a numerical prefix, compare as strings
|
||||||
|
return s[i] < s[j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPrefix attempts to extract a numerical prefix and the rest of the filename
|
||||||
|
func extractPrefix(filename string) (int, string) {
|
||||||
|
matches := numPrefixRegexp.FindStringSubmatch(filename)
|
||||||
|
if len(matches) > 2 {
|
||||||
|
prefix, err := strconv.Atoi(matches[1])
|
||||||
|
if err == nil {
|
||||||
|
return prefix, matches[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns -1 if no numerical prefix is found, indicating to just compare
|
||||||
|
// as strings.
|
||||||
|
return -1, filename
|
||||||
|
}
|
||||||
|
|
||||||
|
var numPrefixRegexp = regexp.MustCompile(`^(\d+)(.*)$`)
|
||||||
|
|
||||||
|
// Interface guard.
|
||||||
|
var (
|
||||||
|
_ sort.Interface = (*AlphanumericSort)(nil)
|
||||||
|
)
|
||||||
34
pkg/gotenberg/sort_test.go
Normal file
34
pkg/gotenberg/sort_test.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlphanumericSort(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
values []string
|
||||||
|
expectedSort []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "numeric and letters",
|
||||||
|
values: []string{"10qux.pdf", "2_baz.txt", "2_aza.txt", "1bar.pdf", "Afoo.txt", "Bbar.docx", "25zeta.txt", "3.pdf", "4_foo.pdf"},
|
||||||
|
expectedSort: []string{"1bar.pdf", "2_aza.txt", "2_baz.txt", "3.pdf", "4_foo.pdf", "10qux.pdf", "25zeta.txt", "Afoo.txt", "Bbar.docx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "hrtime (PHP library)",
|
||||||
|
values: []string{"245654773395259", "245654773395039", "245654773395149", "245654773394919", "245654773394369"},
|
||||||
|
expectedSort: []string{"245654773394369", "245654773394919", "245654773395039", "245654773395149", "245654773395259"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
sort.Sort(AlphanumericSort(tc.values))
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tc.values, tc.expectedSort) {
|
||||||
|
t.Fatalf("expected %+v but got: %+v", tc.expectedSort, tc.values)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ import (
|
|||||||
// to restart an already restarting [Process].
|
// to restart an already restarting [Process].
|
||||||
var ErrProcessAlreadyRestarting = errors.New("process already restarting")
|
var ErrProcessAlreadyRestarting = errors.New("process already restarting")
|
||||||
|
|
||||||
|
// ErrMaximumQueueSizeExceeded happens if Run() is called but the maximum queue
|
||||||
|
// size is already used.
|
||||||
|
var ErrMaximumQueueSizeExceeded = errors.New("maximum queue size exceeded")
|
||||||
|
|
||||||
// Process is an interface that represents an abstract process
|
// Process is an interface that represents an abstract process
|
||||||
// and provides methods for starting, stopping, and checking the health of the
|
// and provides methods for starting, stopping, and checking the health of the
|
||||||
// process.
|
// process.
|
||||||
@@ -74,6 +78,7 @@ type processSupervisor struct {
|
|||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
process Process
|
process Process
|
||||||
maxReqLimit int64
|
maxReqLimit int64
|
||||||
|
maxQueueSize int64
|
||||||
mutexChan chan struct{}
|
mutexChan chan struct{}
|
||||||
firstStart atomic.Bool
|
firstStart atomic.Bool
|
||||||
reqCounter atomic.Int64
|
reqCounter atomic.Int64
|
||||||
@@ -83,12 +88,13 @@ type processSupervisor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessSupervisor initializes a new [ProcessSupervisor].
|
// NewProcessSupervisor initializes a new [ProcessSupervisor].
|
||||||
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit int64) ProcessSupervisor {
|
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit, maxQueueSize int64) ProcessSupervisor {
|
||||||
b := &processSupervisor{
|
b := &processSupervisor{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
process: process,
|
process: process,
|
||||||
mutexChan: make(chan struct{}, 1),
|
mutexChan: make(chan struct{}, 1),
|
||||||
maxReqLimit: maxReqLimit,
|
maxReqLimit: maxReqLimit,
|
||||||
|
maxQueueSize: maxQueueSize,
|
||||||
}
|
}
|
||||||
b.reqCounter.Store(0)
|
b.reqCounter.Store(0)
|
||||||
b.reqQueueSize.Store(0)
|
b.reqQueueSize.Store(0)
|
||||||
@@ -167,6 +173,11 @@ func (s *processSupervisor) Healthy() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
|
currentQueueSize := s.reqQueueSize.Load()
|
||||||
|
if s.maxQueueSize > 0 && currentQueueSize >= s.maxQueueSize {
|
||||||
|
return ErrMaximumQueueSizeExceeded
|
||||||
|
}
|
||||||
|
|
||||||
s.reqQueueSize.Add(1)
|
s.reqQueueSize.Add(1)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func TestProcessSupervisor_Launch(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
|
||||||
if tc.firstStartSet {
|
if tc.firstStartSet {
|
||||||
ps.firstStart.Store(true)
|
ps.firstStart.Store(true)
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ func TestProcessSupervisor_Shutdown(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, 5)
|
ps := NewProcessSupervisor(logger, process, 5, 0)
|
||||||
err := ps.Shutdown()
|
err := ps.Shutdown()
|
||||||
|
|
||||||
if !tc.expectError && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
@@ -154,7 +154,7 @@ func TestProcessSupervisor_restart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
|
||||||
if tc.initiallyRestarting {
|
if tc.initiallyRestarting {
|
||||||
ps.isRestarting.Store(true)
|
ps.isRestarting.Store(true)
|
||||||
}
|
}
|
||||||
@@ -217,7 +217,7 @@ func TestProcessSupervisor_Healthy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
|
||||||
if tc.initiallyStarted {
|
if tc.initiallyStarted {
|
||||||
ps.firstStart.Store(true)
|
ps.firstStart.Store(true)
|
||||||
}
|
}
|
||||||
@@ -249,6 +249,8 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
expectedStartCalls int64
|
expectedStartCalls int64
|
||||||
expectedHealthyCalls int64
|
expectedHealthyCalls int64
|
||||||
expectedStopCalls int64
|
expectedStopCalls int64
|
||||||
|
currentQueueSize int64
|
||||||
|
maxQueueSize int64
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "successfully run task on non-started process",
|
scenario: "successfully run task on non-started process",
|
||||||
@@ -348,6 +350,34 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
expectedHealthyCalls: 1,
|
expectedHealthyCalls: 1,
|
||||||
expectedStopCalls: 0,
|
expectedStopCalls: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "queue size exceeded",
|
||||||
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: true,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: true,
|
||||||
|
expectedStartCalls: 0,
|
||||||
|
expectedHealthyCalls: 0,
|
||||||
|
expectedStopCalls: 0,
|
||||||
|
currentQueueSize: 1,
|
||||||
|
maxQueueSize: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "queue size not exceeded",
|
||||||
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: true,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: true,
|
||||||
|
expectedStartCalls: 1,
|
||||||
|
expectedHealthyCalls: 1,
|
||||||
|
expectedStopCalls: 0,
|
||||||
|
currentQueueSize: 1,
|
||||||
|
maxQueueSize: 2,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
logger := zap.NewNop()
|
logger := zap.NewNop()
|
||||||
@@ -372,13 +402,16 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit, tc.maxQueueSize).(*processSupervisor)
|
||||||
if tc.initiallyStarted {
|
if tc.initiallyStarted {
|
||||||
ps.firstStart.Store(true)
|
ps.firstStart.Store(true)
|
||||||
}
|
}
|
||||||
if tc.isRestarting {
|
if tc.isRestarting {
|
||||||
ps.isRestarting.Store(true)
|
ps.isRestarting.Store(true)
|
||||||
}
|
}
|
||||||
|
if tc.currentQueueSize > 0 {
|
||||||
|
ps.reqQueueSize.Store(tc.currentQueueSize)
|
||||||
|
}
|
||||||
|
|
||||||
task := func() error {
|
task := func() error {
|
||||||
return tc.taskError
|
return tc.taskError
|
||||||
@@ -451,13 +484,13 @@ func TestProcessSupervisor_runWithDeadline(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0).(*processSupervisor)
|
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0, 0).(*processSupervisor)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if tc.ctxDone {
|
if tc.ctxDone {
|
||||||
cancel()
|
cancel()
|
||||||
|
} else {
|
||||||
|
defer cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ps.runWithDeadline(ctx, func() error {
|
err := ps.runWithDeadline(ctx, func() error {
|
||||||
@@ -485,7 +518,7 @@ func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
|
||||||
|
|
||||||
// Simulating a lock.
|
// Simulating a lock.
|
||||||
ps.mutexChan <- struct{}{}
|
ps.mutexChan <- struct{}{}
|
||||||
@@ -586,7 +619,7 @@ func TestProcessSupervisor_RestartsCount(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
|
||||||
ps.restartsCounter.Store(tc.initialRestartsCount)
|
ps.restartsCounter.Store(tc.initialRestartsCount)
|
||||||
|
|
||||||
for i := 0; i < tc.restartAttempts; i++ {
|
for i := 0; i < tc.restartAttempts; i++ {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -30,8 +30,6 @@ func init() {
|
|||||||
// middlewares or health checks.
|
// middlewares or health checks.
|
||||||
type Api struct {
|
type Api struct {
|
||||||
port int
|
port int
|
||||||
readTimeout time.Duration
|
|
||||||
writeTimeout time.Duration
|
|
||||||
startTimeout time.Duration
|
startTimeout time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
rootPath string
|
rootPath string
|
||||||
@@ -162,23 +160,11 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
fs.Int("api-port", 3000, "Set the port on which the API should listen")
|
||||||
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
fs.String("api-port-from-env", "", "Set the environment variable with the port on which the API should listen - override the default port")
|
||||||
fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
|
fs.Duration("api-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
|
||||||
fs.Duration("api-read-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to read a complete request, including the body")
|
|
||||||
fs.Duration("api-process-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to process a request")
|
|
||||||
fs.Duration("api-write-timeout", time.Duration(30)*time.Second, "Set the maximum duration before timing out writes of the response")
|
|
||||||
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
||||||
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
||||||
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
||||||
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
||||||
|
|
||||||
var err error
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-read-timeout", "use api-timeout instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-process-timeout", "use api-timeout instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-write-timeout", "use api-timeout instead"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("create deprecated flags for the api module: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
New: func() gotenberg.Module { return new(Api) },
|
New: func() gotenberg.Module { return new(Api) },
|
||||||
@@ -190,9 +176,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
a.port = flags.MustInt("api-port")
|
a.port = flags.MustInt("api-port")
|
||||||
a.startTimeout = flags.MustDuration("api-start-timeout")
|
a.startTimeout = flags.MustDuration("api-start-timeout")
|
||||||
a.readTimeout = flags.MustDeprecatedDuration("api-read-timeout", "api-timeout")
|
a.timeout = flags.MustDuration("api-timeout")
|
||||||
a.writeTimeout = flags.MustDeprecatedDuration("api-write-timeout", "api-timeout")
|
|
||||||
a.timeout = flags.MustDeprecatedDuration("api-process-timeout", "api-timeout")
|
|
||||||
a.rootPath = flags.MustString("api-root-path")
|
a.rootPath = flags.MustString("api-root-path")
|
||||||
a.traceHeader = flags.MustString("api-trace-header")
|
a.traceHeader = flags.MustString("api-trace-header")
|
||||||
a.disableHealthCheckLogging = flags.MustBool("api-disable-health-check-logging")
|
a.disableHealthCheckLogging = flags.MustBool("api-disable-health-check-logging")
|
||||||
@@ -380,10 +364,10 @@ func (a *Api) Start() error {
|
|||||||
a.srv = echo.New()
|
a.srv = echo.New()
|
||||||
a.srv.HideBanner = true
|
a.srv.HideBanner = true
|
||||||
a.srv.HidePort = true
|
a.srv.HidePort = true
|
||||||
a.srv.Server.ReadTimeout = a.readTimeout
|
a.srv.Server.ReadTimeout = a.timeout
|
||||||
a.srv.Server.IdleTimeout = a.timeout
|
a.srv.Server.IdleTimeout = a.timeout
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/396.
|
// See https://github.com/gotenberg/gotenberg/issues/396.
|
||||||
a.srv.Server.WriteTimeout = a.writeTimeout + a.writeTimeout
|
a.srv.Server.WriteTimeout = a.timeout + a.timeout
|
||||||
a.srv.HTTPErrorHandler = httpErrorHandler()
|
a.srv.HTTPErrorHandler = httpErrorHandler()
|
||||||
|
|
||||||
// Let's prepare the modules' routes.
|
// Let's prepare the modules' routes.
|
||||||
@@ -418,7 +402,7 @@ func (a *Api) Start() error {
|
|||||||
a.srv.Pre(externalMiddleware.Handler)
|
a.srv.Pre(externalMiddleware.Handler)
|
||||||
case MultipartStack:
|
case MultipartStack:
|
||||||
externalMultipartMiddlewares = append(externalMultipartMiddlewares, externalMiddleware)
|
externalMultipartMiddlewares = append(externalMultipartMiddlewares, externalMiddleware)
|
||||||
default:
|
case DefaultStack:
|
||||||
a.srv.Use(externalMiddleware.Handler)
|
a.srv.Use(externalMiddleware.Handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApi_Descriptor(t *testing.T) {
|
func TestApi_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/text/unicode/norm"
|
"golang.org/x/text/unicode/norm"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -34,18 +34,24 @@ var (
|
|||||||
|
|
||||||
// Context is the request context for a "multipart/form-data" requests.
|
// Context is the request context for a "multipart/form-data" requests.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
dirPath string
|
dirPath string
|
||||||
values map[string][]string
|
values map[string][]string
|
||||||
files map[string]string
|
files map[string]string
|
||||||
|
|
||||||
outputPaths []string
|
outputPaths []string
|
||||||
|
cancelled bool
|
||||||
|
|
||||||
cancelled bool
|
logger *zap.Logger
|
||||||
logger *zap.Logger
|
echoCtx echo.Context
|
||||||
echoCtx echo.Context
|
pathRename gotenberg.PathRename
|
||||||
context.Context
|
context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type osPathRename struct{}
|
||||||
|
|
||||||
|
func (o *osPathRename) Rename(oldpath, newpath string) error {
|
||||||
|
return os.Rename(oldpath, newpath)
|
||||||
|
}
|
||||||
|
|
||||||
// newContext returns a [Context] by parsing a "multipart/form-data" request.
|
// newContext returns a [Context] by parsing a "multipart/form-data" request.
|
||||||
func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSystem, timeout time.Duration) (*Context, context.CancelFunc, error) {
|
func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSystem, timeout time.Duration) (*Context, context.CancelFunc, error) {
|
||||||
processCtx, processCancel := context.WithTimeout(context.Background(), timeout)
|
processCtx, processCancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@@ -55,6 +61,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
|||||||
cancelled: false,
|
cancelled: false,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
echoCtx: echoCtx,
|
echoCtx: echoCtx,
|
||||||
|
pathRename: new(osPathRename),
|
||||||
Context: processCtx,
|
Context: processCtx,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +93,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
|||||||
|
|
||||||
form, err := echoCtx.MultipartForm()
|
form, err := echoCtx.MultipartForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
if errors.Is(err, http.ErrNotMultipart) {
|
if errors.Is(err, http.ErrNotMultipart) {
|
||||||
return nil, cancel, WrapError(
|
return nil, cancel, WrapError(
|
||||||
fmt.Errorf("get multipart form: %w", err),
|
fmt.Errorf("get multipart form: %w", err),
|
||||||
@@ -164,7 +170,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
|||||||
for _, files := range form.File {
|
for _, files := range form.File {
|
||||||
for _, fh := range files {
|
for _, fh := range files {
|
||||||
err = copyToDisk(fh)
|
err = copyToDisk(fh)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
|
return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
|
||||||
}
|
}
|
||||||
@@ -191,10 +196,20 @@ func (ctx *Context) FormData() *FormData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePath generates a path within the context's working directory. It
|
// GeneratePath generates a path within the context's working directory.
|
||||||
// does not create a file.
|
// It generates a new UUID-based filename. It does not create a file.
|
||||||
func (ctx *Context) GeneratePath(extension string) string {
|
func (ctx *Context) GeneratePath(extension string) string {
|
||||||
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension)
|
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New().String(), extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename is just a wrapper around [os.Rename], as we need to mock this
|
||||||
|
// behavior in our tests.
|
||||||
|
func (ctx *Context) Rename(oldpath, newpath string) error {
|
||||||
|
err := ctx.pathRename.Rename(oldpath, newpath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rename path: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddOutputPaths adds the given paths. Those paths will be used later to build
|
// AddOutputPaths adds the given paths. Those paths will be used later to build
|
||||||
@@ -269,3 +284,8 @@ func (ctx *Context) OutputFilename(outputPath string) string {
|
|||||||
|
|
||||||
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
|
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface guard.
|
||||||
|
var (
|
||||||
|
_ gotenberg.PathRename = (*osPathRename)(nil)
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,21 +3,77 @@ package api
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestOsPathRename_Rename(t *testing.T) {
|
||||||
|
dirPath, err := gotenberg.NewFileSystem().MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/tests/test/testdata/api/sample1.txt"
|
||||||
|
copyPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||||
|
|
||||||
|
in, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := in.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := os.Create(copyPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create new file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := out.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close new file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("copy file to new file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rename := new(osPathRename)
|
||||||
|
newPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||||
|
|
||||||
|
err = rename.Rename(copyPath, newPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.RemoveAll(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("remove working directory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewContext(t *testing.T) {
|
func TestNewContext(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
@@ -191,12 +247,46 @@ func TestContext_GeneratePath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path := ctx.GeneratePath(".pdf")
|
path := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
if !strings.HasPrefix(path, ctx.dirPath) {
|
if !strings.HasPrefix(path, ctx.dirPath) {
|
||||||
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
|
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext_Rename(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *Context
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "failure",
|
||||||
|
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
|
return errors.New("cannot rename")
|
||||||
|
}}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
|
return nil
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.ctx.Rename("", "")
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContext_AddOutputPaths(t *testing.T) {
|
func TestContext_AddOutputPaths(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -11,6 +12,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FormData is a helper for validating and hydrating values from a
|
// FormData is a helper for validating and hydrating values from a
|
||||||
@@ -143,6 +146,91 @@ func (form *FormData) MandatoryDuration(key string, target *time.Duration) *Form
|
|||||||
return form.mustMandatoryField(key, target)
|
return form.mustMandatoryField(key, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inches binds a form field to a float64 variable. It populates an error
|
||||||
|
// if the value cannot be computed back to inches.
|
||||||
|
//
|
||||||
|
// var foo float64
|
||||||
|
//
|
||||||
|
// ctx.FormData().Inches("foo", &foo, 2.0)
|
||||||
|
func (form *FormData) Inches(key string, target *float64, defaultValue float64) *FormData {
|
||||||
|
form.inches(key, target)
|
||||||
|
if *target == -math.MaxFloat64 {
|
||||||
|
*target = defaultValue
|
||||||
|
}
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
// MandatoryInches binds a form field to a float64 variable. It populates
|
||||||
|
// an error if the value cannot be computed back to inches, is empty, or the
|
||||||
|
// "key" does not exist.
|
||||||
|
//
|
||||||
|
// var foo float64
|
||||||
|
//
|
||||||
|
// ctx.FormData().MandatoryInches("foo", &foo)
|
||||||
|
func (form *FormData) MandatoryInches(key string, target *float64) *FormData {
|
||||||
|
val, ok := form.values[key]
|
||||||
|
if !ok || val[0] == "" {
|
||||||
|
form.append(
|
||||||
|
fmt.Errorf("form field '%s' is required", key),
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
return form.inches(key, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inches tries to compute a string value to inches.
|
||||||
|
func (form *FormData) inches(key string, target *float64) *FormData {
|
||||||
|
var value string
|
||||||
|
form.mustValue(key, &value, "")
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
*target = -math.MaxFloat64
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, unit := range []string{"pt", "px", "in", "mm", "cm", "pc"} {
|
||||||
|
if !strings.HasSuffix(value, unit) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := strconv.ParseFloat(strings.TrimSuffix(value, unit), 64)
|
||||||
|
if err != nil {
|
||||||
|
form.append(
|
||||||
|
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
switch unit {
|
||||||
|
case "pt":
|
||||||
|
*target = val * (1.0 / 72.0)
|
||||||
|
case "px":
|
||||||
|
*target = val * (1.0 / 96.0)
|
||||||
|
case "in":
|
||||||
|
*target = val
|
||||||
|
case "mm":
|
||||||
|
*target = val * (1.0 / 25.4)
|
||||||
|
case "cm":
|
||||||
|
*target = val * (1.0 / 2.54)
|
||||||
|
case "pc":
|
||||||
|
*target = val * (1.0 / 6.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err != nil {
|
||||||
|
form.append(
|
||||||
|
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
*target = val
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
// Custom helps to define a custom binding function for a form field.
|
// Custom helps to define a custom binding function for a form field.
|
||||||
//
|
//
|
||||||
// var foo map[string]string
|
// var foo map[string]string
|
||||||
@@ -304,7 +392,7 @@ func (form *FormData) paths(extensions []string, target *[]string) *FormData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/139.
|
// See https://github.com/gotenberg/gotenberg/issues/139.
|
||||||
sort.Strings(*target)
|
sort.Sort(gotenberg.AlphanumericSort(*target))
|
||||||
|
|
||||||
return form
|
return form
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -768,6 +769,333 @@ func TestFormData_MandatoryDuration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormData_Inches(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
form *FormData
|
||||||
|
defaultValue float64
|
||||||
|
expect float64
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "key does not exist, fallback to default zero value",
|
||||||
|
form: &FormData{},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does not exist, fallback to default value",
|
||||||
|
form: &FormData{},
|
||||||
|
defaultValue: 2.5,
|
||||||
|
expect: 2.5,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist, but empty value, fallback to default value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist, value has a unit, but the rest is not float64 compatible",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foomm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist, but value has no unit and is invalid",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a pt value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"72pt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a px value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"96px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with an in value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"1in",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a mm value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"25.4mm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a cm value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"2.54cm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with a pc value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"6pc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "key does exist with no unit in the value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultValue: 0.0,
|
||||||
|
expect: 100,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
var actual float64
|
||||||
|
|
||||||
|
tc.form.Inches("foo", &actual, tc.defaultValue)
|
||||||
|
|
||||||
|
if fmt.Sprintf("%.1f", actual) != fmt.Sprintf("%.1f", tc.expect) {
|
||||||
|
t.Errorf("expected %.1f but got %.1f", tc.expect, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && tc.form.errors == nil {
|
||||||
|
t.Fatal("expected error but got none", tc.form.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && tc.form.errors != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", tc.form.errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormData_MandatoryInches(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
form *FormData
|
||||||
|
expect float64
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing mandatory key",
|
||||||
|
form: &FormData{},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "mandatory value is empty",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "mandatory value has a unit, but the rest is not float64 compatible",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foomm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "mandatory value has no unit and is invalid",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 0.0,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a pt mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"72pt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a px mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"96px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "an in mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"1in",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a mm mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"25.4mm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a cm mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"2.54cm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "a pc mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"6pc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 1.0,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no unit in the mandatory value",
|
||||||
|
form: &FormData{
|
||||||
|
values: map[string][]string{
|
||||||
|
"foo": {
|
||||||
|
"100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: 100,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
var actual float64
|
||||||
|
|
||||||
|
tc.form.MandatoryInches("foo", &actual)
|
||||||
|
|
||||||
|
if fmt.Sprintf("%.1f", actual) != fmt.Sprintf("%.1f", tc.expect) {
|
||||||
|
t.Errorf("expected %.1f but got %.1f", tc.expect, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && tc.form.errors == nil {
|
||||||
|
t.Fatal("expected error but got none", tc.form.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && tc.form.errors != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", tc.form.errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormData_Custom(t *testing.T) {
|
func TestFormData_Custom(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrAsyncProcess happens when a handler or middleware handles a request in an
|
var (
|
||||||
// asynchronous fashion.
|
// ErrAsyncProcess happens when a handler or middleware handles a request
|
||||||
var ErrAsyncProcess = errors.New("async process")
|
// in an asynchronous fashion.
|
||||||
|
ErrAsyncProcess = errors.New("async process")
|
||||||
|
|
||||||
|
// ErrNoOutputFile happens when a handler or middleware handles a request
|
||||||
|
// without sending any output file.
|
||||||
|
ErrNoOutputFile = errors.New("no output file")
|
||||||
|
)
|
||||||
|
|
||||||
// ParseError parses an error and returns the corresponding HTTP status and
|
// ParseError parses an error and returns the corresponding HTTP status and
|
||||||
// HTTP message.
|
// HTTP message.
|
||||||
@@ -32,6 +38,22 @@ func ParseError(err error) (int, string) {
|
|||||||
return http.StatusServiceUnavailable, http.StatusText(http.StatusServiceUnavailable)
|
return http.StatusServiceUnavailable, http.StatusText(http.StatusServiceUnavailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, gotenberg.ErrFiltered) {
|
||||||
|
return http.StatusForbidden, http.StatusText(http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded) {
|
||||||
|
return http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||||
|
return http.StatusBadRequest, "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues"
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
|
||||||
|
return http.StatusBadRequest, "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues"
|
||||||
|
}
|
||||||
|
|
||||||
var httpErr HttpError
|
var httpErr HttpError
|
||||||
if errors.As(err, &httpErr) {
|
if errors.As(err, &httpErr) {
|
||||||
return httpErr.HttpError()
|
return httpErr.HttpError()
|
||||||
@@ -230,6 +252,13 @@ func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration) echo.Mid
|
|||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
if errors.Is(err, ErrNoOutputFile) {
|
||||||
|
// A middleware/handler tells us that it's handling the process
|
||||||
|
// in an asynchronous fashion. Therefore, we must not cancel
|
||||||
|
// the context nor send an output file.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseError(t *testing.T) {
|
func TestParseError(t *testing.T) {
|
||||||
@@ -29,9 +29,24 @@ func TestParseError(t *testing.T) {
|
|||||||
expectMessage: http.StatusText(http.StatusInternalServerError),
|
expectMessage: http.StatusText(http.StatusInternalServerError),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
err: context.DeadlineExceeded,
|
err: gotenberg.ErrFiltered,
|
||||||
expectStatus: http.StatusServiceUnavailable,
|
expectStatus: http.StatusForbidden,
|
||||||
expectMessage: http.StatusText(http.StatusServiceUnavailable),
|
expectMessage: http.StatusText(http.StatusForbidden),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: gotenberg.ErrMaximumQueueSizeExceeded,
|
||||||
|
expectStatus: http.StatusTooManyRequests,
|
||||||
|
expectMessage: http.StatusText(http.StatusTooManyRequests),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: gotenberg.ErrPdfFormatNotSupported,
|
||||||
|
expectStatus: http.StatusBadRequest,
|
||||||
|
expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||||
|
expectStatus: http.StatusBadRequest,
|
||||||
|
expectMessage: "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
err: WrapError(
|
err: WrapError(
|
||||||
@@ -320,6 +335,15 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
expectStatus: http.StatusNoContent,
|
expectStatus: http.StatusNoContent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
request: buildMultipartFormDataRequest(),
|
||||||
|
next: func() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return ErrNoOutputFile
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
request: buildMultipartFormDataRequest(),
|
request: buildMultipartFormDataRequest(),
|
||||||
next: func() echo.HandlerFunc {
|
next: func() echo.HandlerFunc {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContextMock is a helper for tests.
|
// ContextMock is a helper for tests.
|
||||||
@@ -76,7 +78,7 @@ func (ctx *ContextMock) SetLogger(logger *zap.Logger) {
|
|||||||
ctx.logger = logger
|
ctx.logger = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEchoContext sets the echo.Context.
|
// SetEchoContext sets the [echo.Context].
|
||||||
//
|
//
|
||||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
// ctx.setEchoContext(c)
|
// ctx.setEchoContext(c)
|
||||||
@@ -84,6 +86,14 @@ func (ctx *ContextMock) SetEchoContext(c echo.Context) {
|
|||||||
ctx.Context.echoCtx = c
|
ctx.Context.echoCtx = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPathRename sets the [gotenberg.PathRename].
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.setPathRename(rename)
|
||||||
|
func (ctx *ContextMock) SetPathRename(rename gotenberg.PathRename) {
|
||||||
|
ctx.Context.pathRename = rename
|
||||||
|
}
|
||||||
|
|
||||||
// RouterMock is a mock for the [Router] interface.
|
// RouterMock is a mock for the [Router] interface.
|
||||||
type RouterMock struct {
|
type RouterMock struct {
|
||||||
RoutesMock func() ([]Route, error)
|
RoutesMock func() ([]Route, error)
|
||||||
|
|||||||
@@ -117,6 +117,19 @@ func TestContextMock_SetEchoContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextMock_SetPathRename(t *testing.T) {
|
||||||
|
mock := ContextMock{&Context{}}
|
||||||
|
|
||||||
|
expect := new(osPathRename)
|
||||||
|
mock.SetPathRename(expect)
|
||||||
|
|
||||||
|
actual := mock.pathRename
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected %v but got %v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRouterMock(t *testing.T) {
|
func TestRouterMock(t *testing.T) {
|
||||||
mock := &RouterMock{
|
mock := &RouterMock{
|
||||||
RoutesMock: func() ([]Route, error) {
|
RoutesMock: func() ([]Route, error) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -15,14 +14,16 @@ import (
|
|||||||
"github.com/chromedp/cdproto/network"
|
"github.com/chromedp/cdproto/network"
|
||||||
"github.com/chromedp/cdproto/runtime"
|
"github.com/chromedp/cdproto/runtime"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type browser interface {
|
type browser interface {
|
||||||
gotenberg.Process
|
gotenberg.Process
|
||||||
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type browserArguments struct {
|
type browserArguments struct {
|
||||||
@@ -38,8 +39,10 @@ type browserArguments struct {
|
|||||||
wsUrlReadTimeout time.Duration
|
wsUrlReadTimeout time.Duration
|
||||||
|
|
||||||
// Tasks specific.
|
// Tasks specific.
|
||||||
allowList *regexp.Regexp
|
allowList *regexp2.Regexp
|
||||||
denyList *regexp.Regexp
|
denyList *regexp2.Regexp
|
||||||
|
clearCache bool
|
||||||
|
clearCookies bool
|
||||||
disableJavaScript bool
|
disableJavaScript bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +90,8 @@ func (b *chromiumBrowser) Start(logger *zap.Logger) error {
|
|||||||
// https://github.com/puppeteer/puppeteer/issues/2410
|
// https://github.com/puppeteer/puppeteer/issues/2410
|
||||||
chromedp.Flag("font-render-hinting", "none"),
|
chromedp.Flag("font-render-hinting", "none"),
|
||||||
chromedp.UserDataDir(b.userProfileDirPath),
|
chromedp.UserDataDir(b.userProfileDirPath),
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/831.
|
||||||
|
chromedp.Flag("disable-pdf-tagging", true),
|
||||||
)
|
)
|
||||||
|
|
||||||
if b.arguments.incognito {
|
if b.arguments.incognito {
|
||||||
@@ -161,7 +166,7 @@ func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
|
|||||||
go func() {
|
go func() {
|
||||||
// FIXME: Chromium seems to recreate the user profile directory
|
// FIXME: Chromium seems to recreate the user profile directory
|
||||||
// right after its deletion if we do not wait a certain amount
|
// right after its deletion if we do not wait a certain amount
|
||||||
// of time before re-deleting it.
|
// of time before deleting it.
|
||||||
<-time.After(10 * time.Second)
|
<-time.After(10 * time.Second)
|
||||||
|
|
||||||
err := os.RemoveAll(userProfileDirPath)
|
err := os.RemoveAll(userProfileDirPath)
|
||||||
@@ -211,18 +216,53 @@ func (b *chromiumBrowser) Healthy(logger *zap.Logger) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
|
||||||
|
network.Enable(),
|
||||||
|
fetch.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||||
|
forceExactColorsActionFunc(),
|
||||||
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
// PDF specific.
|
||||||
|
printToPdfActionFunc(logger, outputPath, options),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
|
||||||
|
network.Enable(),
|
||||||
|
fetch.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||||
|
forceExactColorsActionFunc(),
|
||||||
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
// Screenshot specific.
|
||||||
|
captureScreenshotActionFunc(logger, outputPath, options),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string, options Options, tasks chromedp.Tasks) error {
|
||||||
if !b.isStarted.Load() {
|
if !b.isStarted.Load() {
|
||||||
return errors.New("browser not started, cannot handle PDF conversion")
|
return errors.New("browser not started, cannot handle tasks")
|
||||||
}
|
|
||||||
|
|
||||||
// We validate the "main" URL against our allow / deny lists.
|
|
||||||
if !b.arguments.allowList.MatchString(url) {
|
|
||||||
return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", url, ErrUrlNotAuthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.arguments.denyList.String() != "" && b.arguments.denyList.MatchString(url) {
|
|
||||||
return fmt.Errorf("'%s' matches the expression from the denied list: %w", url, ErrUrlNotAuthorized)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deadline, ok := ctx.Deadline()
|
deadline, ok := ctx.Deadline()
|
||||||
@@ -230,6 +270,12 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
return errors.New("context has no deadline")
|
return errors.New("context has no deadline")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We validate the "main" URL against our allow / deny lists.
|
||||||
|
err := gotenberg.FilterDeadline(b.arguments.allowList, b.arguments.denyList, url, deadline)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filter URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
b.ctxMu.RLock()
|
b.ctxMu.RLock()
|
||||||
defer b.ctxMu.RUnlock()
|
defer b.ctxMu.RUnlock()
|
||||||
|
|
||||||
@@ -243,6 +289,16 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
// 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, b.arguments.allowList, b.arguments.denyList)
|
listenForEventRequestPaused(taskCtx, logger, b.arguments.allowList, b.arguments.denyList)
|
||||||
|
|
||||||
|
var (
|
||||||
|
invalidHttpStatusCode error
|
||||||
|
invalidHttpStatusCodeMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||||
|
if len(options.FailOnHttpStatusCodes) != 0 {
|
||||||
|
listenForEventResponseReceived(taskCtx, logger, url, options.FailOnHttpStatusCodes, &invalidHttpStatusCode, &invalidHttpStatusCodeMu)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
consoleExceptions error
|
consoleExceptions error
|
||||||
consoleExceptionsMu sync.RWMutex
|
consoleExceptionsMu sync.RWMutex
|
||||||
@@ -253,22 +309,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks := chromedp.Tasks{
|
err = chromedp.Run(taskCtx, tasks...)
|
||||||
network.Enable(),
|
|
||||||
fetch.Enable(),
|
|
||||||
runtime.Enable(),
|
|
||||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
|
||||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
|
||||||
navigateActionFunc(logger, url),
|
|
||||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
|
||||||
forceExactColorsActionFunc(),
|
|
||||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
|
||||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
|
||||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
|
||||||
printToPdfActionFunc(logger, outputPath, options),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := chromedp.Run(taskCtx, tasks...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMessage := err.Error()
|
errMessage := err.Error()
|
||||||
|
|
||||||
@@ -284,7 +325,15 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
return ErrRpccMessageTooLarge
|
return ErrRpccMessageTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("print to PDF: %w", err)
|
return fmt.Errorf("handle tasks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||||
|
invalidHttpStatusCodeMu.RLock()
|
||||||
|
defer invalidHttpStatusCodeMu.RUnlock()
|
||||||
|
|
||||||
|
if invalidHttpStatusCode != nil {
|
||||||
|
return fmt.Errorf("%v: %w", invalidHttpStatusCode, ErrInvalidHttpStatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/262.
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,10 @@ import (
|
|||||||
|
|
||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"go.uber.org/multierr"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -21,14 +20,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrUrlNotAuthorized happens if a URL is not acceptable according to the
|
|
||||||
// allowed/denied lists.
|
|
||||||
ErrUrlNotAuthorized = errors.New("URL not authorized")
|
|
||||||
|
|
||||||
// ErrOmitBackgroundWithoutPrintBackground happens if
|
|
||||||
// Options.OmitBackground is set to true but not Options.PrintBackground.
|
|
||||||
ErrOmitBackgroundWithoutPrintBackground = errors.New("omit background without print background")
|
|
||||||
|
|
||||||
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
||||||
// "screen" nor "print". Empty value are allowed though.
|
// "screen" nor "print". Empty value are allowed though.
|
||||||
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
||||||
@@ -37,22 +28,32 @@ var (
|
|||||||
// returns an exception or undefined.
|
// returns an exception or undefined.
|
||||||
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
|
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
|
||||||
|
|
||||||
// ErrInvalidPrinterSettings happens if the Options have one or more
|
|
||||||
// aberrant values.
|
|
||||||
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
|
||||||
|
|
||||||
// ErrPageRangesSyntaxError happens if the Options have an invalid page
|
|
||||||
// ranges.
|
|
||||||
ErrPageRangesSyntaxError = errors.New("page ranges syntax error")
|
|
||||||
|
|
||||||
// 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")
|
||||||
|
|
||||||
|
// ErrInvalidHttpStatusCode happens when the status code from the main page
|
||||||
|
// matches with one of the entry in [Options.FailOnHttpStatusCodes].
|
||||||
|
ErrInvalidHttpStatusCode = errors.New("invalid HTTP status code")
|
||||||
|
|
||||||
// ErrConsoleExceptions happens when there are exceptions in the Chromium
|
// ErrConsoleExceptions happens when there are exceptions in the Chromium
|
||||||
// console. It also happens only if the [Options.FailOnConsoleExceptions]
|
// console. It also happens only if the [Options.FailOnConsoleExceptions]
|
||||||
// is set to true.
|
// is set to true.
|
||||||
ErrConsoleExceptions = errors.New("console exceptions")
|
ErrConsoleExceptions = errors.New("console exceptions")
|
||||||
|
|
||||||
|
// PDF specific.
|
||||||
|
|
||||||
|
// ErrOmitBackgroundWithoutPrintBackground happens if
|
||||||
|
// PdfOptions.OmitBackground is set to true but not PdfOptions.PrintBackground.
|
||||||
|
ErrOmitBackgroundWithoutPrintBackground = errors.New("omit background without print background")
|
||||||
|
|
||||||
|
// ErrInvalidPrinterSettings happens if the PdfOptions have one or more
|
||||||
|
// aberrant values.
|
||||||
|
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
||||||
|
|
||||||
|
// ErrPageRangesSyntaxError happens if the PdfOptions have an invalid page
|
||||||
|
// ranges.
|
||||||
|
ErrPageRangesSyntaxError = errors.New("page ranges syntax error")
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
@@ -68,25 +69,37 @@ type Chromium struct {
|
|||||||
engine gotenberg.PdfEngine
|
engine gotenberg.PdfEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options are the available expectedOptions for converting HTML document to PDF.
|
// Options are the common options for all conversions.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
// SkipNetworkIdleEvent set if the conversion should wait for the
|
||||||
|
// "networkIdle" event, drastically improving the conversion speed. It may
|
||||||
|
// not be suitable for all HTML documents, as some may not be fully
|
||||||
|
// rendered until this event is fired.
|
||||||
|
// Optional.
|
||||||
|
SkipNetworkIdleEvent bool
|
||||||
|
|
||||||
|
// FailOnHttpStatusCodes sets if the conversion should fail if the status
|
||||||
|
// code from the main page matches with one of its entries.
|
||||||
|
// Optional.
|
||||||
|
FailOnHttpStatusCodes []int64
|
||||||
|
|
||||||
// FailOnConsoleExceptions sets if the conversion should fail if there are
|
// FailOnConsoleExceptions sets if the conversion should fail if there are
|
||||||
// exceptions in the Chromium console.
|
// exceptions in the Chromium console.
|
||||||
// Optional.
|
// Optional.
|
||||||
FailOnConsoleExceptions bool
|
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.
|
||||||
// Optional.
|
// Optional.
|
||||||
WaitDelay time.Duration
|
WaitDelay time.Duration
|
||||||
|
|
||||||
// WaitWindowStatus is the window.status value to wait for before
|
// WaitWindowStatus is the window.status value to wait for before
|
||||||
// converting an HTML document to PDF.
|
// converting an HTML document.
|
||||||
// Optional.
|
// Optional.
|
||||||
WaitWindowStatus string
|
WaitWindowStatus string
|
||||||
|
|
||||||
// WaitForExpression is the custom JavaScript expression to wait before
|
// WaitForExpression is the custom JavaScript expression to wait before
|
||||||
// converting an HTML document to PDF until it returns true
|
// converting an HTML document until it returns true
|
||||||
// Optional.
|
// Optional.
|
||||||
WaitForExpression string
|
WaitForExpression string
|
||||||
|
|
||||||
@@ -100,6 +113,31 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
EmulatedMediaType string
|
EmulatedMediaType string
|
||||||
|
|
||||||
|
// OmitBackground hides default white background and allows generating PDFs
|
||||||
|
// with transparency.
|
||||||
|
// Optional.
|
||||||
|
OmitBackground bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions returns the default values for Options.
|
||||||
|
func DefaultOptions() Options {
|
||||||
|
return Options{
|
||||||
|
SkipNetworkIdleEvent: false,
|
||||||
|
FailOnHttpStatusCodes: []int64{499, 599},
|
||||||
|
FailOnConsoleExceptions: false,
|
||||||
|
WaitDelay: 0,
|
||||||
|
WaitWindowStatus: "",
|
||||||
|
WaitForExpression: "",
|
||||||
|
ExtraHttpHeaders: nil,
|
||||||
|
EmulatedMediaType: "",
|
||||||
|
OmitBackground: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PdfOptions are the available options for converting an HTML document to PDF.
|
||||||
|
type PdfOptions struct {
|
||||||
|
Options
|
||||||
|
|
||||||
// Landscape sets the paper orientation.
|
// Landscape sets the paper orientation.
|
||||||
// Optional.
|
// Optional.
|
||||||
Landscape bool
|
Landscape bool
|
||||||
@@ -108,15 +146,15 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
PrintBackground bool
|
PrintBackground bool
|
||||||
|
|
||||||
// OmitBackground hides default white background and allows generating PDFs
|
|
||||||
// with transparency.
|
|
||||||
// Optional.
|
|
||||||
OmitBackground bool
|
|
||||||
|
|
||||||
// Scale is the scale of the page rendering.
|
// Scale is the scale of the page rendering.
|
||||||
// Optional.
|
// Optional.
|
||||||
Scale float64
|
Scale float64
|
||||||
|
|
||||||
|
// SinglePage defines whether to print the entire content in one single
|
||||||
|
// page.
|
||||||
|
// Optional.
|
||||||
|
SinglePage bool
|
||||||
|
|
||||||
// PaperWidth is the paper width, in inches.
|
// PaperWidth is the paper width, in inches.
|
||||||
// Optional.
|
// Optional.
|
||||||
PaperWidth float64
|
PaperWidth float64
|
||||||
@@ -169,35 +207,61 @@ type Options struct {
|
|||||||
PreferCssPageSize bool
|
PreferCssPageSize bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOptions returns the default values for Options.
|
// DefaultPdfOptions returns the default values for PdfOptions.
|
||||||
func DefaultOptions() Options {
|
func DefaultPdfOptions() PdfOptions {
|
||||||
return Options{
|
return PdfOptions{
|
||||||
FailOnConsoleExceptions: false,
|
Options: DefaultOptions(),
|
||||||
WaitDelay: 0,
|
Landscape: false,
|
||||||
WaitWindowStatus: "",
|
PrintBackground: false,
|
||||||
WaitForExpression: "",
|
Scale: 1.0,
|
||||||
ExtraHttpHeaders: nil,
|
SinglePage: false,
|
||||||
EmulatedMediaType: "",
|
PaperWidth: 8.5,
|
||||||
Landscape: false,
|
PaperHeight: 11,
|
||||||
PrintBackground: false,
|
MarginTop: 0.39,
|
||||||
OmitBackground: false,
|
MarginBottom: 0.39,
|
||||||
Scale: 1.0,
|
MarginLeft: 0.39,
|
||||||
PaperWidth: 8.5,
|
MarginRight: 0.39,
|
||||||
PaperHeight: 11,
|
PageRanges: "",
|
||||||
MarginTop: 0.39,
|
HeaderTemplate: "<html><head></head><body></body></html>",
|
||||||
MarginBottom: 0.39,
|
FooterTemplate: "<html><head></head><body></body></html>",
|
||||||
MarginLeft: 0.39,
|
PreferCssPageSize: false,
|
||||||
MarginRight: 0.39,
|
}
|
||||||
PageRanges: "",
|
}
|
||||||
HeaderTemplate: "<html><head></head><body></body></html>",
|
|
||||||
FooterTemplate: "<html><head></head><body></body></html>",
|
// ScreenshotOptions are the available options for capturing a screenshot from
|
||||||
PreferCssPageSize: false,
|
// an HTML document.
|
||||||
|
type ScreenshotOptions struct {
|
||||||
|
Options
|
||||||
|
|
||||||
|
// Format is the image compression format, either "png" or "jpeg" or
|
||||||
|
// "webp".
|
||||||
|
// Optional.
|
||||||
|
Format string
|
||||||
|
|
||||||
|
// Quality is the compression quality from range [0..100] (jpeg only).
|
||||||
|
// Optional.
|
||||||
|
Quality int
|
||||||
|
|
||||||
|
// OptimizeForSpeed defines whether to optimize image encoding for speed,
|
||||||
|
// not for resulting size.
|
||||||
|
// Optional.
|
||||||
|
OptimizeForSpeed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultScreenshotOptions returns the default values for ScreenshotOptions.
|
||||||
|
func DefaultScreenshotOptions() ScreenshotOptions {
|
||||||
|
return ScreenshotOptions{
|
||||||
|
Options: DefaultOptions(),
|
||||||
|
Format: "png",
|
||||||
|
Quality: 100,
|
||||||
|
OptimizeForSpeed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Api helps to interact with Chromium for converting HTML documents to PDF.
|
// Api helps to interact with Chromium for converting HTML documents to PDF.
|
||||||
type Api interface {
|
type Api interface {
|
||||||
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider is a module interface which exposes a method for creating an [Api]
|
// Provider is a module interface which exposes a method for creating an [Api]
|
||||||
@@ -217,19 +281,8 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
ID: "chromium",
|
ID: "chromium",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("chromium", flag.ExitOnError)
|
fs := flag.NewFlagSet("chromium", flag.ExitOnError)
|
||||||
// Deprecated flags.
|
|
||||||
fs.String("chromium-user-agent", "", "Override the default User-Agent header")
|
|
||||||
fs.Int("chromium-failed-starts-threshold", 5, "Set the number of consecutive failed starts after which the module is considered unhealthy - 0 means ignore")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("chromium-user-agent", "use the extraHttpHeaders form field instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("chromium-failed-starts-threshold", "use the chromium-restart-after property instead"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("create deprecated flags for the Chromium module: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
||||||
|
fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for Chromium. Set to 0 to disable this feature")
|
||||||
fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion")
|
fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion")
|
||||||
fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart")
|
fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart")
|
||||||
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
|
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
|
||||||
@@ -240,7 +293,9 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.String("chromium-host-resolver-rules", "", "Set custom mappings to the host resolver")
|
fs.String("chromium-host-resolver-rules", "", "Set custom mappings to the host resolver")
|
||||||
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
|
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
|
||||||
fs.String("chromium-allow-list", "", "Set the allowed URLs for Chromium using a regular expression")
|
fs.String("chromium-allow-list", "", "Set the allowed URLs for Chromium using a regular expression")
|
||||||
fs.String("chromium-deny-list", "^file:///[^tmp].*", "Set the denied URLs for Chromium using a regular expression")
|
fs.String("chromium-deny-list", `^file:(?!//\/tmp/).*`, "Set the denied URLs for Chromium using a regular expression")
|
||||||
|
fs.Bool("chromium-clear-cache", false, "Clear Chromium cache between each conversion")
|
||||||
|
fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion")
|
||||||
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
|
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")
|
||||||
|
|
||||||
@@ -274,6 +329,8 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
|||||||
|
|
||||||
allowList: flags.MustRegexp("chromium-allow-list"),
|
allowList: flags.MustRegexp("chromium-allow-list"),
|
||||||
denyList: flags.MustRegexp("chromium-deny-list"),
|
denyList: flags.MustRegexp("chromium-deny-list"),
|
||||||
|
clearCache: flags.MustBool("chromium-clear-cache"),
|
||||||
|
clearCookies: flags.MustBool("chromium-clear-cookies"),
|
||||||
disableJavaScript: flags.MustBool("chromium-disable-javascript"),
|
disableJavaScript: flags.MustBool("chromium-disable-javascript"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +347,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
|||||||
|
|
||||||
// Process.
|
// Process.
|
||||||
mod.browser = newChromiumBrowser(mod.args)
|
mod.browser = newChromiumBrowser(mod.args)
|
||||||
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"))
|
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"), flags.MustInt64("chromium-max-queue-size"))
|
||||||
|
|
||||||
// PDF Engine.
|
// PDF Engine.
|
||||||
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))
|
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||||
@@ -359,22 +416,6 @@ func (mod *Chromium) Stop(ctx context.Context) error {
|
|||||||
// Metrics returns the metrics.
|
// Metrics returns the metrics.
|
||||||
func (mod *Chromium) Metrics() ([]gotenberg.Metric, error) {
|
func (mod *Chromium) Metrics() ([]gotenberg.Metric, error) {
|
||||||
return []gotenberg.Metric{
|
return []gotenberg.Metric{
|
||||||
{
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
Name: "chromium_active_instances_count",
|
|
||||||
Description: "Current number of active Chromium instances - deprecated.",
|
|
||||||
Read: func() float64 {
|
|
||||||
return 1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
Name: "chromium_failed_starts_count",
|
|
||||||
Description: "Current number of Chromium consecutive starting failures - deprecated.",
|
|
||||||
Read: func() float64 {
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "chromium_requests_queue_size",
|
Name: "chromium_requests_queue_size",
|
||||||
Description: "Current number of Chromium conversion requests waiting to be treated.",
|
Description: "Current number of Chromium conversion requests waiting to be treated.",
|
||||||
@@ -450,19 +491,31 @@ func (mod *Chromium) Routes() ([]api.Route, error) {
|
|||||||
|
|
||||||
return []api.Route{
|
return []api.Route{
|
||||||
convertUrlRoute(mod, mod.engine),
|
convertUrlRoute(mod, mod.engine),
|
||||||
|
screenshotUrlRoute(mod),
|
||||||
convertHtmlRoute(mod, mod.engine),
|
convertHtmlRoute(mod, mod.engine),
|
||||||
|
screenshotHtmlRoute(mod),
|
||||||
convertMarkdownRoute(mod, mod.engine),
|
convertMarkdownRoute(mod, mod.engine),
|
||||||
|
screenshotMarkdownRoute(mod),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pdf converts a URL to PDF.
|
// Pdf converts a URL to PDF.
|
||||||
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 PdfOptions) error {
|
||||||
// FIXME: no error wrapping because it leaks on console exceptions output.
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
return mod.supervisor.Run(ctx, logger, func() error {
|
return mod.supervisor.Run(ctx, logger, func() error {
|
||||||
return mod.browser.pdf(ctx, logger, url, outputPath, options)
|
return mod.browser.pdf(ctx, logger, url, outputPath, options)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mod *Chromium) Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return mod.supervisor.Run(ctx, logger, func() error {
|
||||||
|
return mod.browser.screenshot(ctx, logger, url, outputPath, options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*Chromium)(nil)
|
_ gotenberg.Module = (*Chromium)(nil)
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import (
|
|||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultOptions(t *testing.T) {
|
func TestDefaultOptions(t *testing.T) {
|
||||||
actual := DefaultOptions()
|
actual := DefaultPdfOptions()
|
||||||
notExpect := Options{}
|
notExpect := PdfOptions{}
|
||||||
|
|
||||||
if reflect.DeepEqual(actual, notExpect) {
|
if reflect.DeepEqual(actual, notExpect) {
|
||||||
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
||||||
@@ -334,26 +334,16 @@ func TestChromium_Metrics(t *testing.T) {
|
|||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(metrics) != 4 {
|
if len(metrics) != 2 {
|
||||||
t.Fatalf("expected %d metrics, but got %d", 4, len(metrics))
|
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
actual := metrics[0].Read()
|
||||||
if actual != float64(1) {
|
|
||||||
t.Errorf("expected %f for chromium_active_instances_count, but got %f", float64(1), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[1].Read()
|
|
||||||
if actual != float64(0) {
|
|
||||||
t.Errorf("expected %f for chromium_failed_starts_count, but got %f", float64(0), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[2].Read()
|
|
||||||
if actual != float64(10) {
|
if actual != float64(10) {
|
||||||
t.Errorf("expected %f for chromium_requests_queue_size, but got %f", float64(10), actual)
|
t.Errorf("expected %f for chromium_requests_queue_size, but got %f", float64(10), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = metrics[3].Read()
|
actual = metrics[1].Read()
|
||||||
if actual != float64(0) {
|
if actual != float64(0) {
|
||||||
t.Errorf("expected %f for chromium_restarts_count, but got %f", float64(0), actual)
|
t.Errorf("expected %f for chromium_restarts_count, but got %f", float64(0), actual)
|
||||||
}
|
}
|
||||||
@@ -471,7 +461,7 @@ func TestChromium_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 3,
|
expectRoutes: 6,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -505,14 +495,14 @@ func TestChromium_Pdf(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "PDF task success",
|
scenario: "PDF task success",
|
||||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "PDF task error",
|
scenario: "PDF task error",
|
||||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return errors.New("PDF task error")
|
return errors.New("PDF task error")
|
||||||
}},
|
}},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
@@ -525,7 +515,49 @@ func TestChromium_Pdf(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
mod.browser = tc.browser
|
mod.browser = tc.browser
|
||||||
|
|
||||||
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChromium_Screenshot(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
supervisor gotenberg.ProcessSupervisor
|
||||||
|
browser browser
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "Screenshot task success",
|
||||||
|
browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "Screenshot task error",
|
||||||
|
browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return errors.New("screenshot task error")
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
|
return task()
|
||||||
|
}}
|
||||||
|
mod.browser = tc.browser
|
||||||
|
|
||||||
|
err := mod.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||||
|
|
||||||
if !tc.expectError && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package chromium
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
@@ -12,14 +12,17 @@ import (
|
|||||||
"github.com/chromedp/cdproto/page"
|
"github.com/chromedp/cdproto/page"
|
||||||
"github.com/chromedp/cdproto/runtime"
|
"github.com/chromedp/cdproto/runtime"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// listenForEventRequestPaused listens for requests to check if they are
|
// listenForEventRequestPaused listens for requests to check if they are
|
||||||
// allowed or not.
|
// allowed or not.
|
||||||
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp.Regexp, denyList *regexp.Regexp) {
|
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp2.Regexp, denyList *regexp2.Regexp) {
|
||||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||||
switch e := ev.(type) {
|
switch e := ev.(type) {
|
||||||
case *fetch.EventRequestPaused:
|
case *fetch.EventRequestPaused:
|
||||||
@@ -27,13 +30,15 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
|||||||
logger.Debug(fmt.Sprintf("event EventRequestPaused fired for '%s'", e.Request.URL))
|
logger.Debug(fmt.Sprintf("event EventRequestPaused fired for '%s'", e.Request.URL))
|
||||||
allow := true
|
allow := true
|
||||||
|
|
||||||
if !allowList.MatchString(e.Request.URL) {
|
deadline, ok := ctx.Deadline()
|
||||||
logger.Warn(fmt.Sprintf("'%s' does not match the expression from the allowed list", e.Request.URL))
|
if !ok {
|
||||||
allow = false
|
logger.Error("context has no deadline, cannot filter URL")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if denyList.String() != "" && denyList.MatchString(e.Request.URL) {
|
err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline)
|
||||||
logger.Warn(fmt.Sprintf("'%s' matches the expression from the denied list", e.Request.URL))
|
if err != nil {
|
||||||
|
logger.Warn(err.Error())
|
||||||
allow = false
|
allow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,16 +47,15 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
|||||||
|
|
||||||
if allow {
|
if allow {
|
||||||
req := fetch.ContinueRequest(e.RequestID)
|
req := fetch.ContinueRequest(e.RequestID)
|
||||||
err := req.Do(executorCtx)
|
err = req.Do(executorCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("continue request: %s", err))
|
logger.Error(fmt.Sprintf("continue request: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
|
req := fetch.FailRequest(e.RequestID, network.ErrorReasonAccessDenied)
|
||||||
err := req.Do(executorCtx)
|
err = req.Do(executorCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("fail request: %s", err))
|
logger.Error(fmt.Sprintf("fail request: %s", err))
|
||||||
}
|
}
|
||||||
@@ -60,6 +64,37 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenForEventResponseReceived listens for an invalid HTTP status code is
|
||||||
|
// returned by the main page.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||||
|
func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url string, failOnHttpStatusCodes []int64, invalidHttpStatusCode *error, invalidHttpStatusCodeMu *sync.RWMutex) {
|
||||||
|
for _, code := range []int64{199, 299, 399, 499, 599} {
|
||||||
|
if slices.Contains(failOnHttpStatusCodes, code) {
|
||||||
|
for i := code - 99; i <= code; i++ {
|
||||||
|
failOnHttpStatusCodes = append(failOnHttpStatusCodes, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||||
|
switch ev := ev.(type) {
|
||||||
|
case *network.EventResponseReceived:
|
||||||
|
if ev.Response.URL != url {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response))
|
||||||
|
|
||||||
|
if slices.Contains(failOnHttpStatusCodes, ev.Response.Status) {
|
||||||
|
invalidHttpStatusCodeMu.Lock()
|
||||||
|
defer invalidHttpStatusCodeMu.Unlock()
|
||||||
|
|
||||||
|
*invalidHttpStatusCode = fmt.Errorf("%d: %s", ev.Response.Status, ev.Response.StatusText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// listenForEventExceptionThrown listens for exceptions in the console and
|
// listenForEventExceptionThrown listens for exceptions in the console and
|
||||||
// appends those exceptions to the given error pointer.
|
// appends those exceptions to the given error pointer.
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/262.
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
|
|||||||
@@ -5,28 +5,38 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApiMock is a mock for the [Api] interface.
|
// ApiMock is a mock for the [Api] interface.
|
||||||
type ApiMock struct {
|
type ApiMock struct {
|
||||||
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
ScreenshotMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return api.PdfMock(ctx, logger, url, outputPath, options)
|
return api.PdfMock(ctx, logger, url, outputPath, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *ApiMock) Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return api.ScreenshotMock(ctx, logger, url, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
// browserMock is a mock for the [browser] interface.
|
// browserMock is a mock for the [browser] interface.
|
||||||
type browserMock struct {
|
type browserMock struct {
|
||||||
gotenberg.ProcessMock
|
gotenberg.ProcessMock
|
||||||
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
screenshotMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return b.pdfMock(ctx, logger, url, outputPath, options)
|
return b.pdfMock(ctx, logger, url, outputPath, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *browserMock) screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return b.screenshotMock(ctx, logger, url, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ Api = (*ApiMock)(nil)
|
_ Api = (*ApiMock)(nil)
|
||||||
|
|||||||
@@ -9,26 +9,42 @@ import (
|
|||||||
|
|
||||||
func TestApiMock(t *testing.T) {
|
func TestApiMock(t *testing.T) {
|
||||||
mock := &ApiMock{
|
mock := &ApiMock{
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mock.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ApiMock.Screenshot, but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBrowserMock(t *testing.T) {
|
func TestBrowserMock(t *testing.T) {
|
||||||
mock := &browserMock{
|
mock := &browserMock{
|
||||||
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
err := mock.pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from browserMock.pdf, but got: %v", err)
|
t.Errorf("expected no error from browserMock.pdf, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mock.screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from browserMock.screenshot, but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,47 +18,55 @@ import (
|
|||||||
"github.com/russross/blackfriday/v2"
|
"github.com/russross/blackfriday/v2"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FormDataChromiumPdfOptions creates [Options] from the form data. Fallback to
|
// FormDataChromiumOptions creates [Options] from the form data. Fallback to
|
||||||
// default value if the considered key is not present.
|
// default value if the considered key is not present.
|
||||||
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||||
defaultOptions := DefaultOptions()
|
defaultOptions := DefaultOptions()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
failOnConsoleExceptions bool
|
skipNetworkIdleEvent bool
|
||||||
waitDelay time.Duration
|
failOnHttpStatusCodes []int64
|
||||||
waitWindowStatus string
|
failOnConsoleExceptions bool
|
||||||
waitForExpression string
|
waitDelay time.Duration
|
||||||
userAgent string
|
waitWindowStatus string
|
||||||
extraHttpHeaders map[string]string
|
waitForExpression string
|
||||||
emulatedMediaType string
|
extraHttpHeaders map[string]string
|
||||||
landscape, printBackground, omitBackground bool
|
emulatedMediaType string
|
||||||
scale, paperWidth, paperHeight float64
|
omitBackground bool
|
||||||
marginTop, marginBottom, marginLeft, marginRight float64
|
|
||||||
pageRanges string
|
|
||||||
headerTemplate, footerTemplate string
|
|
||||||
preferCssPageSize bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
form := ctx.FormData().
|
form := ctx.FormData().
|
||||||
|
Bool("skipNetworkIdleEvent", &skipNetworkIdleEvent, defaultOptions.SkipNetworkIdleEvent).
|
||||||
|
Custom("failOnHttpStatusCodes", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
failOnHttpStatusCodes = defaultOptions.FailOnHttpStatusCodes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(value), &failOnHttpStatusCodes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal failOnHttpStatusCodes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
|
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("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||||
String("userAgent", &userAgent, ""). // FIXME: deprecated.
|
|
||||||
Custom("extraHttpHeaders", func(value string) error {
|
Custom("extraHttpHeaders", func(value string) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
|
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
|
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unmarshal extra HTTP headers: %w", err)
|
return fmt.Errorf("unmarshal extraHttpHeaders: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -65,103 +74,180 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
Custom("emulatedMediaType", func(value string) error {
|
Custom("emulatedMediaType", func(value string) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
emulatedMediaType = defaultOptions.EmulatedMediaType
|
emulatedMediaType = defaultOptions.EmulatedMediaType
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != "screen" && value != "print" {
|
if value != "screen" && value != "print" {
|
||||||
return fmt.Errorf("wrong value, expected either 'screen', 'print' or empty")
|
return errors.New("wrong value, expected either 'screen', 'print' or empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
emulatedMediaType = value
|
emulatedMediaType = value
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
Bool("landscape", &landscape, defaultOptions.Landscape).
|
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground)
|
||||||
Bool("printBackground", &printBackground, defaultOptions.PrintBackground).
|
|
||||||
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground).
|
|
||||||
Float64("scale", &scale, defaultOptions.Scale).
|
|
||||||
Float64("paperWidth", &paperWidth, defaultOptions.PaperWidth).
|
|
||||||
Float64("paperHeight", &paperHeight, defaultOptions.PaperHeight).
|
|
||||||
Float64("marginTop", &marginTop, defaultOptions.MarginTop).
|
|
||||||
Float64("marginBottom", &marginBottom, defaultOptions.MarginBottom).
|
|
||||||
Float64("marginLeft", &marginLeft, defaultOptions.MarginLeft).
|
|
||||||
Float64("marginRight", &marginRight, defaultOptions.MarginRight).
|
|
||||||
String("nativePageRanges", &pageRanges, defaultOptions.PageRanges).
|
|
||||||
Content("header.html", &headerTemplate, defaultOptions.HeaderTemplate).
|
|
||||||
Content("footer.html", &footerTemplate, defaultOptions.FooterTemplate).
|
|
||||||
Bool("preferCssPageSize", &preferCssPageSize, defaultOptions.PreferCssPageSize)
|
|
||||||
|
|
||||||
// FIXME: deprecated.
|
|
||||||
if userAgent != "" {
|
|
||||||
ctx.Log().Warn("'userAgent' is deprecated; prefer the 'extraHttpHeaders' form field instead")
|
|
||||||
|
|
||||||
if extraHttpHeaders == nil {
|
|
||||||
extraHttpHeaders = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
extraHttpHeaders["User-Agent"] = userAgent
|
|
||||||
}
|
|
||||||
|
|
||||||
options := Options{
|
options := Options{
|
||||||
|
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
||||||
|
FailOnHttpStatusCodes: failOnHttpStatusCodes,
|
||||||
FailOnConsoleExceptions: failOnConsoleExceptions,
|
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||||
WaitDelay: waitDelay,
|
WaitDelay: waitDelay,
|
||||||
WaitWindowStatus: waitWindowStatus,
|
WaitWindowStatus: waitWindowStatus,
|
||||||
WaitForExpression: waitForExpression,
|
WaitForExpression: waitForExpression,
|
||||||
ExtraHttpHeaders: extraHttpHeaders,
|
ExtraHttpHeaders: extraHttpHeaders,
|
||||||
EmulatedMediaType: emulatedMediaType,
|
EmulatedMediaType: emulatedMediaType,
|
||||||
Landscape: landscape,
|
|
||||||
PrintBackground: printBackground,
|
|
||||||
OmitBackground: omitBackground,
|
OmitBackground: omitBackground,
|
||||||
Scale: scale,
|
|
||||||
PaperWidth: paperWidth,
|
|
||||||
PaperHeight: paperHeight,
|
|
||||||
MarginTop: marginTop,
|
|
||||||
MarginBottom: marginBottom,
|
|
||||||
MarginLeft: marginLeft,
|
|
||||||
MarginRight: marginRight,
|
|
||||||
PageRanges: pageRanges,
|
|
||||||
HeaderTemplate: headerTemplate,
|
|
||||||
FooterTemplate: footerTemplate,
|
|
||||||
PreferCssPageSize: preferCssPageSize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return form, options
|
return form, options
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
// FormDataChromiumPdfOptions creates [PdfOptions] from the form data. Fallback to
|
||||||
// data. Fallback to default value if the considered key is not present.
|
// default value if the considered key is not present.
|
||||||
func FormDataChromiumPdfFormats(ctx *api.Context) gotenberg.PdfFormats {
|
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||||
|
form, options := FormDataChromiumOptions(ctx)
|
||||||
|
defaultPdfOptions := DefaultPdfOptions()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pdfFormat string
|
landscape, printBackground, singlePage bool
|
||||||
pdfa string
|
scale, paperWidth, paperHeight float64
|
||||||
pdfua bool
|
marginTop, marginBottom, marginLeft, marginRight float64
|
||||||
|
pageRanges string
|
||||||
|
headerTemplate, footerTemplate string
|
||||||
|
preferCssPageSize bool
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx.FormData().
|
form.
|
||||||
String("pdfFormat", &pdfFormat, "").
|
Bool("landscape", &landscape, defaultPdfOptions.Landscape).
|
||||||
|
Bool("printBackground", &printBackground, defaultPdfOptions.PrintBackground).
|
||||||
|
Float64("scale", &scale, defaultPdfOptions.Scale).
|
||||||
|
Bool("singlePage", &singlePage, defaultPdfOptions.SinglePage).
|
||||||
|
Inches("paperWidth", &paperWidth, defaultPdfOptions.PaperWidth).
|
||||||
|
Inches("paperHeight", &paperHeight, defaultPdfOptions.PaperHeight).
|
||||||
|
Inches("marginTop", &marginTop, defaultPdfOptions.MarginTop).
|
||||||
|
Inches("marginBottom", &marginBottom, defaultPdfOptions.MarginBottom).
|
||||||
|
Inches("marginLeft", &marginLeft, defaultPdfOptions.MarginLeft).
|
||||||
|
Inches("marginRight", &marginRight, defaultPdfOptions.MarginRight).
|
||||||
|
String("nativePageRanges", &pageRanges, defaultPdfOptions.PageRanges).
|
||||||
|
Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate).
|
||||||
|
Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).
|
||||||
|
Bool("preferCssPageSize", &preferCssPageSize, defaultPdfOptions.PreferCssPageSize)
|
||||||
|
|
||||||
|
pdfOptions := PdfOptions{
|
||||||
|
Options: options,
|
||||||
|
Landscape: landscape,
|
||||||
|
PrintBackground: printBackground,
|
||||||
|
Scale: scale,
|
||||||
|
SinglePage: singlePage,
|
||||||
|
PaperWidth: paperWidth,
|
||||||
|
PaperHeight: paperHeight,
|
||||||
|
MarginTop: marginTop,
|
||||||
|
MarginBottom: marginBottom,
|
||||||
|
MarginLeft: marginLeft,
|
||||||
|
MarginRight: marginRight,
|
||||||
|
PageRanges: pageRanges,
|
||||||
|
HeaderTemplate: headerTemplate,
|
||||||
|
FooterTemplate: footerTemplate,
|
||||||
|
PreferCssPageSize: preferCssPageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
return form, pdfOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormDataChromiumScreenshotOptions creates [ScreenshotOptions] from the form
|
||||||
|
// data. Fallback to default value if the considered key is not present.
|
||||||
|
func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, ScreenshotOptions) {
|
||||||
|
form, options := FormDataChromiumOptions(ctx)
|
||||||
|
defaultScreenshotOptions := DefaultScreenshotOptions()
|
||||||
|
|
||||||
|
var (
|
||||||
|
format string
|
||||||
|
quality int
|
||||||
|
optimizeForSpeed bool
|
||||||
|
)
|
||||||
|
|
||||||
|
form.
|
||||||
|
Custom("format", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
format = defaultScreenshotOptions.Format
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != "png" && value != "jpeg" && value != "webp" {
|
||||||
|
return fmt.Errorf("wrong value, expected either 'png', 'jpeg' or 'webp'")
|
||||||
|
}
|
||||||
|
|
||||||
|
format = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Custom("quality", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
quality = defaultScreenshotOptions.Quality
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
intValue, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if intValue < 0 {
|
||||||
|
return errors.New("value is negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if intValue > 100 {
|
||||||
|
return errors.New("value is superior to 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
quality = intValue
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Bool("optimizeForSpeed", &optimizeForSpeed, defaultScreenshotOptions.OptimizeForSpeed)
|
||||||
|
|
||||||
|
screenshotOptions := ScreenshotOptions{
|
||||||
|
Options: options,
|
||||||
|
Format: format,
|
||||||
|
Quality: quality,
|
||||||
|
OptimizeForSpeed: optimizeForSpeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
return form, screenshotOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
||||||
|
// data. Fallback to default value if the considered key is not present.
|
||||||
|
func FormDataChromiumPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||||
|
var (
|
||||||
|
pdfa string
|
||||||
|
pdfua bool
|
||||||
|
)
|
||||||
|
|
||||||
|
form.
|
||||||
String("pdfa", &pdfa, "").
|
String("pdfa", &pdfa, "").
|
||||||
Bool("pdfua", &pdfua, false)
|
Bool("pdfua", &pdfua, false)
|
||||||
|
|
||||||
// FIXME: deprecated.
|
|
||||||
// pdfa > pdfFormat.
|
|
||||||
var actualPdfArchive string
|
|
||||||
|
|
||||||
if pdfFormat != "" {
|
|
||||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
|
||||||
actualPdfArchive = pdfFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
if pdfa != "" {
|
|
||||||
actualPdfArchive = pdfa
|
|
||||||
}
|
|
||||||
|
|
||||||
return gotenberg.PdfFormats{
|
return gotenberg.PdfFormats{
|
||||||
PdfA: actualPdfArchive,
|
PdfA: pdfa,
|
||||||
PdfUa: pdfua,
|
PdfUa: pdfua,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormDataPdfMetadata creates metadata object from the form data.
|
||||||
|
func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
|
||||||
|
var metadata map[string]interface{}
|
||||||
|
form.Custom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
|
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
|
||||||
func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
@@ -171,7 +257,8 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
metadata := FormDataPdfMetadata(form)
|
||||||
|
|
||||||
var url string
|
var url string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -181,7 +268,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -191,6 +278,35 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// screenshotUrlRoute returns an [api.Route] which can take a screenshot from a
|
||||||
|
// URL.
|
||||||
|
func screenshotUrlRoute(chromium Api) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/chromium/screenshot/url",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
form, options := FormDataChromiumScreenshotOptions(ctx)
|
||||||
|
|
||||||
|
var url string
|
||||||
|
err := form.
|
||||||
|
MandatoryString("url", &url).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = screenshotUrl(ctx, chromium, url, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("URL screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
|
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
|
||||||
// PDF.
|
// PDF.
|
||||||
func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||||
@@ -201,7 +317,8 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
metadata := FormDataPdfMetadata(form)
|
||||||
|
|
||||||
var inputPath string
|
var inputPath string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -212,7 +329,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("file://%s", inputPath)
|
url := fmt.Sprintf("file://%s", inputPath)
|
||||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -222,6 +339,36 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// screenshotHtmlRoute returns an [api.Route] which can take a screenshot from
|
||||||
|
// an HTML file.
|
||||||
|
func screenshotHtmlRoute(chromium Api) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/chromium/screenshot/html",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
form, options := FormDataChromiumScreenshotOptions(ctx)
|
||||||
|
|
||||||
|
var inputPath string
|
||||||
|
err := form.
|
||||||
|
MandatoryPath("index.html", &inputPath).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("file://%s", inputPath)
|
||||||
|
err = screenshotUrl(ctx, chromium, url, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTML screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
|
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
|
||||||
// to PDF.
|
// to PDF.
|
||||||
func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||||
@@ -232,7 +379,8 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
metadata := FormDataPdfMetadata(form)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
inputPath string
|
inputPath string
|
||||||
@@ -247,80 +395,12 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have to convert each markdown file referenced in the HTML
|
url, err := markdownToHtml(ctx, inputPath, markdownPaths)
|
||||||
// file to... HTML. Thanks to the "html/template" package, we are
|
|
||||||
// able to provide the "toHTML" function which the user may call
|
|
||||||
// directly inside the HTML file.
|
|
||||||
|
|
||||||
var markdownFilesNotFoundErr error
|
|
||||||
|
|
||||||
tmpl, err := template.
|
|
||||||
New(filepath.Base(inputPath)).
|
|
||||||
Funcs(template.FuncMap{
|
|
||||||
"toHTML": func(filename string) (template.HTML, error) {
|
|
||||||
var path string
|
|
||||||
|
|
||||||
for _, markdownPath := range markdownPaths {
|
|
||||||
markdownFilename := filepath.Base(markdownPath)
|
|
||||||
|
|
||||||
if filename == markdownFilename {
|
|
||||||
path = markdownPath
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
markdownFilesNotFoundErr = multierr.Append(
|
|
||||||
markdownFilesNotFoundErr,
|
|
||||||
fmt.Errorf("'%s'", filename),
|
|
||||||
)
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read markdown file '%s': %w", filename, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe := blackfriday.Run(b)
|
|
||||||
sanitized := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
|
||||||
|
|
||||||
// #nosec
|
|
||||||
return template.HTML(sanitized), nil
|
|
||||||
},
|
|
||||||
}).ParseFiles(inputPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse template file: %w", err)
|
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||||
|
|
||||||
err = tmpl.Execute(&buffer, &struct{}{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("execute template: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if markdownFilesNotFoundErr != nil {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("markdown files not found: %w", markdownFilesNotFoundErr),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Markdown file(s) not found: %s", markdownFilesNotFoundErr),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
inputPath = ctx.GeneratePath(".html")
|
|
||||||
|
|
||||||
err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("write template result: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("file://%s", inputPath)
|
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -330,23 +410,126 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertUrl is a stub which is called by the other methods of this file.
|
// screenshotMarkdownRoute returns an [api.Route] which can take a screenshot
|
||||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options Options) error {
|
// from markdown files.
|
||||||
|
func screenshotMarkdownRoute(chromium Api) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/chromium/screenshot/markdown",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
form, options := FormDataChromiumScreenshotOptions(ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
inputPath string
|
||||||
|
markdownPaths []string
|
||||||
|
)
|
||||||
|
|
||||||
|
err := form.
|
||||||
|
MandatoryPath("index.html", &inputPath).
|
||||||
|
MandatoryPaths([]string{".md"}, &markdownPaths).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := markdownToHtml(ctx, inputPath, markdownPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = screenshotUrl(ctx, chromium, url, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("markdown screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string) (string, error) {
|
||||||
|
// We have to convert each markdown file referenced in the HTML
|
||||||
|
// file to... HTML. Thanks to the "html/template" package, we are
|
||||||
|
// able to provide the "toHTML" function which the user may call
|
||||||
|
// directly inside the HTML file.
|
||||||
|
|
||||||
|
var markdownFilesNotFoundErr error
|
||||||
|
|
||||||
|
tmpl, err := template.
|
||||||
|
New(filepath.Base(inputPath)).
|
||||||
|
Funcs(template.FuncMap{
|
||||||
|
"toHTML": func(filename string) (template.HTML, error) {
|
||||||
|
var path string
|
||||||
|
|
||||||
|
for _, markdownPath := range markdownPaths {
|
||||||
|
markdownFilename := filepath.Base(markdownPath)
|
||||||
|
|
||||||
|
if filename == markdownFilename {
|
||||||
|
path = markdownPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
markdownFilesNotFoundErr = multierr.Append(
|
||||||
|
markdownFilesNotFoundErr,
|
||||||
|
fmt.Errorf("'%s'", filename),
|
||||||
|
)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read markdown file '%s': %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe := blackfriday.Run(b)
|
||||||
|
sanitized := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||||
|
|
||||||
|
// #nosec
|
||||||
|
return template.HTML(sanitized), nil
|
||||||
|
},
|
||||||
|
}).ParseFiles(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse template file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
err = tmpl.Execute(&buffer, &struct{}{})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("execute template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if markdownFilesNotFoundErr != nil {
|
||||||
|
return "", api.WrapError(
|
||||||
|
fmt.Errorf("markdown files not found: %w", markdownFilesNotFoundErr),
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Markdown file(s) not found: %s", markdownFilesNotFoundErr),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath = ctx.GeneratePath(".html")
|
||||||
|
|
||||||
|
err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("write template result: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("file://%s", inputPath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}) error {
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
||||||
|
err = handleChromiumError(err, options.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
if errors.Is(err, ErrUrlNotAuthorized) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusForbidden,
|
|
||||||
fmt.Sprintf("'%s' does not match the authorized URLs", url),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
|
if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
@@ -357,23 +540,6 @@ 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),
|
||||||
@@ -394,16 +560,6 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,18 +572,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF: %w", err)
|
return fmt.Errorf("convert PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +580,14 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
outputPath = convertOutputPath
|
outputPath = convertOutputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes and potentially overrides metadata entries, if any.
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = ctx.AddOutputPaths(outputPath)
|
err = ctx.AddOutputPaths(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add output path: %w", err)
|
return fmt.Errorf("add output path: %w", err)
|
||||||
@@ -442,3 +595,66 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func screenshotUrl(ctx *api.Context, chromium Api, url string, options ScreenshotOptions) error {
|
||||||
|
ext := fmt.Sprintf(".%s", options.Format)
|
||||||
|
outputPath := ctx.GeneratePath(ext)
|
||||||
|
|
||||||
|
err := chromium.Screenshot(ctx, ctx.Log(), url, outputPath, options)
|
||||||
|
err = handleChromiumError(err, options.Options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.AddOutputPaths(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleChromiumError(err error, options Options) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.WrapError(
|
||||||
|
err,
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrInvalidHttpStatusCode) {
|
||||||
|
return api.WrapError(
|
||||||
|
err,
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusConflict,
|
||||||
|
fmt.Sprintf("Invalid HTTP status code from the main page: %s", strings.ReplaceAll(err.Error(), fmt.Sprintf(": %s", ErrInvalidHttpStatusCode.Error()), "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrConsoleExceptions) {
|
||||||
|
return api.WrapError(
|
||||||
|
err,
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusConflict,
|
||||||
|
fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,24 +15,44 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options) chromedp.ActionFunc {
|
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
paperHeight := options.PaperHeight
|
||||||
|
pageRanges := options.PageRanges
|
||||||
|
|
||||||
|
if options.SinglePage {
|
||||||
|
logger.Debug("single page PDF")
|
||||||
|
|
||||||
|
_, _, _, _, _, cssContentSize, err := page.GetLayoutMetrics().Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get layout metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are 96 CSS pixels per inch.
|
||||||
|
// See https://issues.chromium.org/issues/40267771#comment14.
|
||||||
|
paperHeight = cssContentSize.Height / 96
|
||||||
|
pageRanges = "1" // little dirty hack to avoid leftovers.
|
||||||
|
}
|
||||||
|
|
||||||
printToPdf := page.PrintToPDF().
|
printToPdf := page.PrintToPDF().
|
||||||
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
|
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
|
||||||
WithLandscape(options.Landscape).
|
WithLandscape(options.Landscape).
|
||||||
WithPrintBackground(options.PrintBackground).
|
WithPrintBackground(options.PrintBackground).
|
||||||
WithScale(options.Scale).
|
WithScale(options.Scale).
|
||||||
WithPaperWidth(options.PaperWidth).
|
WithPaperWidth(options.PaperWidth).
|
||||||
WithPaperHeight(options.PaperHeight).
|
WithPaperHeight(paperHeight).
|
||||||
WithMarginTop(options.MarginTop).
|
WithMarginTop(options.MarginTop).
|
||||||
WithMarginBottom(options.MarginBottom).
|
WithMarginBottom(options.MarginBottom).
|
||||||
WithMarginLeft(options.MarginLeft).
|
WithMarginLeft(options.MarginLeft).
|
||||||
WithMarginRight(options.MarginRight).
|
WithMarginRight(options.MarginRight).
|
||||||
WithPageRanges(options.PageRanges).
|
WithPageRanges(pageRanges).
|
||||||
WithPreferCSSPageSize(options.PreferCssPageSize)
|
WithPreferCSSPageSize(options.PreferCssPageSize).
|
||||||
|
// Does not seem to work.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/831.
|
||||||
|
WithGenerateTaggedPDF(false)
|
||||||
|
|
||||||
hasCustomHeaderFooter := options.HeaderTemplate != DefaultOptions().HeaderTemplate ||
|
hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
|
||||||
options.FooterTemplate != DefaultOptions().FooterTemplate
|
options.FooterTemplate != DefaultPdfOptions().FooterTemplate
|
||||||
|
|
||||||
if !hasCustomHeaderFooter {
|
if !hasCustomHeaderFooter {
|
||||||
logger.Debug("no custom header nor footer")
|
logger.Debug("no custom header nor footer")
|
||||||
@@ -63,7 +83,7 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := reader.Close()
|
err = reader.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("close reader: %s", err))
|
logger.Error(fmt.Sprintf("close reader: %s", err))
|
||||||
}
|
}
|
||||||
@@ -75,7 +95,7 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := file.Close()
|
err = file.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("close output path: %s", err))
|
logger.Error(fmt.Sprintf("close output path: %s", err))
|
||||||
}
|
}
|
||||||
@@ -92,12 +112,90 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func captureScreenshotActionFunc(logger *zap.Logger, outputPath string, options ScreenshotOptions) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
captureScreenshot := page.CaptureScreenshot().
|
||||||
|
WithCaptureBeyondViewport(true).
|
||||||
|
WithFromSurface(true).
|
||||||
|
WithOptimizeForSpeed(options.OptimizeForSpeed).
|
||||||
|
WithFormat(page.CaptureScreenshotFormat(options.Format))
|
||||||
|
|
||||||
|
if options.Format == "jpeg" {
|
||||||
|
captureScreenshot = captureScreenshot.
|
||||||
|
WithQuality(int64(options.Quality))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("capture screenshot with: %+v", captureScreenshot))
|
||||||
|
|
||||||
|
buffer, err := captureScreenshot.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("capture screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close output path: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = file.Write(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write result to output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearCacheActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/753.
|
||||||
|
if !clear {
|
||||||
|
logger.Debug("cache not cleared")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("clear cache")
|
||||||
|
|
||||||
|
err := network.ClearBrowserCache().Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("clear cache: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearCookiesActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/753.
|
||||||
|
if !clear {
|
||||||
|
logger.Debug("cookies not cleared")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("clear cookies")
|
||||||
|
|
||||||
|
err := network.ClearBrowserCookies().Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("clear cookies: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.ActionFunc {
|
func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/175.
|
// See https://github.com/gotenberg/gotenberg/issues/175.
|
||||||
if !disable {
|
if !disable {
|
||||||
logger.Debug("JavaScript not disabled")
|
logger.Debug("JavaScript not disabled")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +214,6 @@ func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if len(extraHttpHeaders) == 0 {
|
if len(extraHttpHeaders) == 0 {
|
||||||
logger.Debug("no extra HTTP headers")
|
logger.Debug("no extra HTTP headers")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +233,7 @@ func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func navigateActionFunc(logger *zap.Logger, url string) chromedp.ActionFunc {
|
func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleEvent bool) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger.Debug(fmt.Sprintf("navigate to '%s'", url))
|
logger.Debug(fmt.Sprintf("navigate to '%s'", url))
|
||||||
|
|
||||||
@@ -145,12 +242,21 @@ func navigateActionFunc(logger *zap.Logger, url string) chromedp.ActionFunc {
|
|||||||
return fmt.Errorf("navigate to '%s': %w", url, err)
|
return fmt.Errorf("navigate to '%s': %w", url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = runBatch(
|
waitFunc := []func() error{
|
||||||
ctx,
|
|
||||||
waitForEventDomContentEventFired(ctx, logger),
|
waitForEventDomContentEventFired(ctx, logger),
|
||||||
waitForEventLoadEventFired(ctx, logger),
|
waitForEventLoadEventFired(ctx, logger),
|
||||||
waitForEventNetworkIdle(ctx, logger),
|
|
||||||
waitForEventLoadingFinished(ctx, logger),
|
waitForEventLoadingFinished(ctx, logger),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skipNetworkIdleEvent {
|
||||||
|
waitFunc = append(waitFunc, waitForEventNetworkIdle(ctx, logger))
|
||||||
|
} else {
|
||||||
|
logger.Debug("skipping network idle event")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runBatch(
|
||||||
|
ctx,
|
||||||
|
waitFunc...,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -166,7 +272,6 @@ func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackground, pr
|
|||||||
// See https://github.com/gotenberg/gotenberg/issues/226.
|
// See https://github.com/gotenberg/gotenberg/issues/226.
|
||||||
if !omitBackground {
|
if !omitBackground {
|
||||||
logger.Debug("default white background not hidden")
|
logger.Debug("default white background not hidden")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +330,6 @@ func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string) chromedp.A
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if mediaType == "" {
|
if mediaType == "" {
|
||||||
logger.Debug("no emulated media type")
|
logger.Debug("no emulated media type")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,13 +353,11 @@ func waitDelayBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool,
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if disableJavaScript {
|
if disableJavaScript {
|
||||||
logger.Debug("JavaScript disabled, skipping wait delay")
|
logger.Debug("JavaScript disabled, skipping wait delay")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if delay <= 0 {
|
if delay <= 0 {
|
||||||
logger.Debug("no wait delay")
|
logger.Debug("no wait delay")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,13 +378,11 @@ func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScrip
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if disableJavaScript {
|
if disableJavaScript {
|
||||||
logger.Debug("JavaScript disabled, skipping wait expression")
|
logger.Debug("JavaScript disabled, skipping wait expression")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if expression == "" {
|
if expression == "" {
|
||||||
logger.Debug("no wait expression")
|
logger.Debug("no wait expression")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
pkg/modules/exiftool/doc.go
Normal file
11
pkg/modules/exiftool/doc.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Package exiftool provides an implementation of the gotenberg.PdfEngine
|
||||||
|
// interface using the ExifTool command-line tool. This package allows for:
|
||||||
|
//
|
||||||
|
// 1. The reading of metadata.
|
||||||
|
// 2. The writing of metadata.
|
||||||
|
//
|
||||||
|
// The path to the exiftool binary must be specified using the
|
||||||
|
// EXIFTOOL_BIN_PATH environment variable.
|
||||||
|
//
|
||||||
|
// See: https://exiftool.org.
|
||||||
|
package exiftool
|
||||||
143
pkg/modules/exiftool/exiftool.go
Normal file
143
pkg/modules/exiftool/exiftool.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package exiftool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/barasher/go-exiftool"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gotenberg.MustRegisterModule(new(ExifTool))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExifTool abstracts the CLI tool ExifTool and implements the
|
||||||
|
// [gotenberg.PdfEngine] interface.
|
||||||
|
type ExifTool struct {
|
||||||
|
binPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor returns [ExifTool]'s module descriptor.
|
||||||
|
func (engine *ExifTool) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{
|
||||||
|
ID: "exiftool",
|
||||||
|
New: func() gotenberg.Module { return new(ExifTool) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets the module properties.
|
||||||
|
func (engine *ExifTool) Provision(ctx *gotenberg.Context) error {
|
||||||
|
binPath, ok := os.LookupEnv("EXIFTOOL_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("EXIFTOOL_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.binPath = binPath
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the module properties.
|
||||||
|
func (engine *ExifTool) Validate() error {
|
||||||
|
_, err := os.Stat(engine.binPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("ExifTool binary path does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge is not available in this implementation.
|
||||||
|
func (engine *ExifTool) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return fmt.Errorf("merge PDFs with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert is not available in this implementation.
|
||||||
|
func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMetadata extracts the metadata of a given PDF file.
|
||||||
|
func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new ExifTool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(exifTool *exiftool.Exiftool) {
|
||||||
|
err := exifTool.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close ExifTool: %v", err))
|
||||||
|
}
|
||||||
|
}(exifTool)
|
||||||
|
|
||||||
|
fileMetadata := exifTool.ExtractMetadata(inputPath)
|
||||||
|
if fileMetadata[0].Err != nil {
|
||||||
|
return nil, fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileMetadata[0].Fields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata writes the metadata into a given PDF file.
|
||||||
|
func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new ExifTool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(exifTool *exiftool.Exiftool) {
|
||||||
|
err := exifTool.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close ExifTool: %v", err))
|
||||||
|
}
|
||||||
|
}(exifTool)
|
||||||
|
|
||||||
|
fileMetadata := exifTool.ExtractMetadata(inputPath)
|
||||||
|
if fileMetadata[0].Err != nil {
|
||||||
|
return fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range metadata {
|
||||||
|
switch val := value.(type) {
|
||||||
|
case string:
|
||||||
|
fileMetadata[0].SetString(key, val)
|
||||||
|
case []string:
|
||||||
|
fileMetadata[0].SetStrings(key, val)
|
||||||
|
case bool:
|
||||||
|
fileMetadata[0].SetString(key, fmt.Sprintf("%t", val))
|
||||||
|
case int:
|
||||||
|
fileMetadata[0].SetInt(key, int64(val))
|
||||||
|
case int64:
|
||||||
|
fileMetadata[0].SetInt(key, val)
|
||||||
|
case float32:
|
||||||
|
fileMetadata[0].SetFloat(key, float64(val))
|
||||||
|
case float64:
|
||||||
|
fileMetadata[0].SetFloat(key, val)
|
||||||
|
// TODO: support more complex cases, e.g., arrays and nested objects
|
||||||
|
// (limitations in underlying library).
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("write PDF metadata with ExifTool: %w", gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exifTool.WriteMetadata(fileMetadata)
|
||||||
|
if fileMetadata[0].Err != nil {
|
||||||
|
return fmt.Errorf("write PDF metadata with ExifTool: %w", fileMetadata[0].Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*ExifTool)(nil)
|
||||||
|
_ gotenberg.Provisioner = (*ExifTool)(nil)
|
||||||
|
_ gotenberg.Validator = (*ExifTool)(nil)
|
||||||
|
_ gotenberg.PdfEngine = (*ExifTool)(nil)
|
||||||
|
)
|
||||||
322
pkg/modules/exiftool/exiftool_test.go
Normal file
322
pkg/modules/exiftool/exiftool_test.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package exiftool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExifTool_Descriptor(t *testing.T) {
|
||||||
|
descriptor := new(ExifTool).Descriptor()
|
||||||
|
|
||||||
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
|
expect := reflect.TypeOf(new(ExifTool))
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExifTool_Provision(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||||
|
|
||||||
|
err := engine.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExifTool_Validate(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
binPath string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "empty bin path",
|
||||||
|
binPath: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "bin path does not exist",
|
||||||
|
binPath: "/foo",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "validate success",
|
||||||
|
binPath: os.Getenv("EXIFTOOL_BIN_PATH"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
engine.binPath = tc.binPath
|
||||||
|
err := engine.Validate()
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExiftool_Merge(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExiftool_Convert(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExiftool_ReadMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
inputPath string
|
||||||
|
expectMetadata map[string]interface{}
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "invalid input path",
|
||||||
|
inputPath: "foo",
|
||||||
|
expectMetadata: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
expectMetadata: map[string]interface{}{
|
||||||
|
"FileName": "sample1.pdf",
|
||||||
|
"FileTypeExtension": "pdf",
|
||||||
|
"MIMEType": "application/pdf",
|
||||||
|
"PDFVersion": 1.4,
|
||||||
|
"PageCount": float64(3),
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Provision(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), tc.inputPath)
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectMetadata != nil && err == nil {
|
||||||
|
for k, v := range tc.expectMetadata {
|
||||||
|
if v2, ok := metadata[k]; !ok || v != v2 {
|
||||||
|
t.Errorf("expected entry %s with value %v to exists", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExiftool_WriteMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
createCopy bool
|
||||||
|
inputPath string
|
||||||
|
metadata map[string]interface{}
|
||||||
|
expectMetadata map[string]interface{}
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "invalid input path",
|
||||||
|
createCopy: false,
|
||||||
|
inputPath: "foo",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported",
|
||||||
|
createCopy: true,
|
||||||
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"Unsupported": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
createCopy: true,
|
||||||
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"Author": "Julien Neuhart",
|
||||||
|
"Copyright": "Julien Neuhart",
|
||||||
|
"CreationDate": "2006-09-18T16:27:50-04:00",
|
||||||
|
"Creator": "Gotenberg",
|
||||||
|
"Keywords": []string{
|
||||||
|
"first",
|
||||||
|
"second",
|
||||||
|
},
|
||||||
|
"Marked": true,
|
||||||
|
"ModDate": "2006-09-18T16:27:50-04:00",
|
||||||
|
"PDFVersion": 1.7,
|
||||||
|
"Producer": "Gotenberg",
|
||||||
|
"Subject": "Sample",
|
||||||
|
"Title": "Sample",
|
||||||
|
"Trapped": "Unknown",
|
||||||
|
// Those are not valid PDF metadata.
|
||||||
|
"int": 1,
|
||||||
|
"int64": int64(2),
|
||||||
|
"float32": float32(2.2),
|
||||||
|
"float64": 3.3,
|
||||||
|
},
|
||||||
|
expectMetadata: map[string]interface{}{
|
||||||
|
"Author": "Julien Neuhart",
|
||||||
|
"Copyright": "Julien Neuhart",
|
||||||
|
"CreationDate": "2006:09:18 16:27:50-04:00",
|
||||||
|
"Creator": "Gotenberg",
|
||||||
|
"Keywords": []interface{}{
|
||||||
|
"first",
|
||||||
|
"second",
|
||||||
|
},
|
||||||
|
"Marked": true,
|
||||||
|
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||||
|
"PDFVersion": 1.7,
|
||||||
|
"Producer": "Gotenberg",
|
||||||
|
"Subject": "Sample",
|
||||||
|
"Title": "Sample",
|
||||||
|
"Trapped": "Unknown",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Provision(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var destinationPath string
|
||||||
|
if tc.createCopy {
|
||||||
|
fs := gotenberg.NewFileSystem()
|
||||||
|
outputDir, err := fs.MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected error no but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = os.RemoveAll(fs.WorkingDirPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
destinationPath = fmt.Sprintf("%s/copy_temp.pdf", outputDir)
|
||||||
|
source, err := os.Open(tc.inputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open source file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(source *os.File) {
|
||||||
|
err := source.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close file: %v", err)
|
||||||
|
}
|
||||||
|
}(source)
|
||||||
|
|
||||||
|
destination, err := os.Create(destinationPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create destination file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(destination *os.File) {
|
||||||
|
err := destination.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close file: %v", err)
|
||||||
|
}
|
||||||
|
}(destination)
|
||||||
|
|
||||||
|
_, err = io.Copy(destination, source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("copy source into destination: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
destinationPath = tc.inputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.WriteMetadata(context.Background(), zap.NewNop(), tc.metadata, destinationPath)
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), destinationPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectMetadata != nil && err == nil {
|
||||||
|
for k, v := range tc.expectMetadata {
|
||||||
|
v2, ok := metadata[k]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected entry %s with value %v to exists, but got none", k, v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v2.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
for i, entry := range v.([]interface{}) {
|
||||||
|
if entry != v2.([]interface{})[i] {
|
||||||
|
t.Errorf("expected entry %s to contain value %v, but got %v", k, entry, v2.([]interface{})[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if v != v2 {
|
||||||
|
t.Errorf("expected entry %s with value %v to exists, but got %v", k, v, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -21,9 +21,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrInvalidPdfFormat happens if the PDF format option cannot be handled
|
// ErrInvalidPdfFormats happens if the PDF formats option cannot be handled
|
||||||
// by LibreOffice.
|
// by LibreOffice.
|
||||||
ErrInvalidPdfFormat = errors.New("invalid PDF format")
|
ErrInvalidPdfFormats = errors.New("invalid PDF formats")
|
||||||
|
|
||||||
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
||||||
// interpreted by LibreOffice.
|
// interpreted by LibreOffice.
|
||||||
@@ -47,10 +47,13 @@ type Options struct {
|
|||||||
Landscape bool
|
Landscape bool
|
||||||
|
|
||||||
// PageRanges allows to select the pages to convert.
|
// PageRanges allows to select the pages to convert.
|
||||||
// TODO: should prefer a method form PdfEngine.
|
|
||||||
// Optional.
|
// Optional.
|
||||||
PageRanges string
|
PageRanges string
|
||||||
|
|
||||||
|
// ExportFormFields allows to... export form fields in the resulting PDF.
|
||||||
|
// Optional.
|
||||||
|
ExportFormFields bool
|
||||||
|
|
||||||
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
|
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
|
||||||
// PDF/A-3b and PDF/UA.
|
// PDF/A-3b and PDF/UA.
|
||||||
// Optional.
|
// Optional.
|
||||||
@@ -80,22 +83,8 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
ID: "libreoffice-api",
|
ID: "libreoffice-api",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
||||||
|
|
||||||
// Deprecated flags.
|
|
||||||
fs.Duration("uno-listener-start-timeout", time.Duration(10)*time.Second, "Time limit for restarting the LibreOffice")
|
|
||||||
fs.Int64("uno-listener-restart-threshold", 10, "Conversions limit after which the LibreOffice listener is restarted - 0 means no restart")
|
|
||||||
fs.Bool("unoconv-disable-listener", false, "Do not start a long-running listener - save resources in detriment of unitary performance")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("uno-listener-start-timeout", "use the libreOffice-start-timeout property instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("uno-listener-restart-threshold", "use the libreOffice-restart-after property instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("unoconv-disable-listener", "use the libreOffice-auto-start property instead"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("create deprecated flags for the LibreOffice module: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature")
|
fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature")
|
||||||
|
fs.Int64("libreoffice-max-queue-size", 0, "Maximum request queue size for LibreOffice. Set to 0 to disable this feature")
|
||||||
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
|
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
|
||||||
fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
|
fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
|
||||||
|
|
||||||
@@ -123,7 +112,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
a.args = libreOfficeArguments{
|
a.args = libreOfficeArguments{
|
||||||
binPath: libreOfficeBinPath,
|
binPath: libreOfficeBinPath,
|
||||||
unoBinPath: unoBinPath,
|
unoBinPath: unoBinPath,
|
||||||
startTimeout: flags.MustDeprecatedDuration("uno-listener-start-timeout", "libreoffice-start-timeout"),
|
startTimeout: flags.MustDuration("libreoffice-start-timeout"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger.
|
// Logger.
|
||||||
@@ -139,7 +128,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
|
|
||||||
// Process.
|
// Process.
|
||||||
a.libreOffice = newLibreOfficeProcess(a.args)
|
a.libreOffice = newLibreOfficeProcess(a.args)
|
||||||
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustDeprecatedInt64("uno-listener-restart-threshold", "libreoffice-restart-after"))
|
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -204,46 +193,6 @@ func (a *Api) Stop(ctx context.Context) error {
|
|||||||
// Metrics returns the metrics.
|
// Metrics returns the metrics.
|
||||||
func (a *Api) Metrics() ([]gotenberg.Metric, error) {
|
func (a *Api) Metrics() ([]gotenberg.Metric, error) {
|
||||||
return []gotenberg.Metric{
|
return []gotenberg.Metric{
|
||||||
// TODO: remove deprecated.
|
|
||||||
{
|
|
||||||
Name: "unoconv_active_instances_count",
|
|
||||||
Description: "Current number of active unoconv instances - deprecated.",
|
|
||||||
Read: func() float64 {
|
|
||||||
return 1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
{
|
|
||||||
Name: "libreoffice_listener_active_instances_count",
|
|
||||||
Description: "Current number of active LibreOffice listener instances - deprecated.",
|
|
||||||
Read: func() float64 {
|
|
||||||
return 1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
{
|
|
||||||
Name: "unoconv_listener_active_instances_count",
|
|
||||||
Description: "Current number of active unoconv listener instances- deprecated.",
|
|
||||||
Read: func() float64 {
|
|
||||||
return 1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
{
|
|
||||||
Name: "libreoffice_listener_queue_length",
|
|
||||||
Description: "Current number of processes in the LibreOffice listener queue - deprecated, prefer libreoffice_requests_queue_size.",
|
|
||||||
Read: func() float64 {
|
|
||||||
return float64(a.supervisor.ReqQueueSize())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
{
|
|
||||||
Name: "unoconv_listener_queue_length",
|
|
||||||
Description: "Current number of processes in the queue - deprecated, prefer libreoffice_requests_queue_size.",
|
|
||||||
Read: func() float64 {
|
|
||||||
return float64(a.supervisor.ReqQueueSize())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "libreoffice_requests_queue_size",
|
Name: "libreoffice_requests_queue_size",
|
||||||
Description: "Current number of LibreOffice conversion requests waiting to be treated.",
|
Description: "Current number of LibreOffice conversion requests waiting to be treated.",
|
||||||
@@ -321,85 +270,136 @@ func (a *Api) Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath
|
|||||||
// FIXME: don't care, take all on the route level?
|
// FIXME: don't care, take all on the route level?
|
||||||
func (a *Api) Extensions() []string {
|
func (a *Api) Extensions() []string {
|
||||||
return []string{
|
return []string{
|
||||||
|
".123",
|
||||||
|
".602",
|
||||||
|
".abw",
|
||||||
".bib",
|
".bib",
|
||||||
".doc",
|
|
||||||
".xml",
|
|
||||||
".docx",
|
|
||||||
".fodt",
|
|
||||||
".html",
|
|
||||||
".ltx",
|
|
||||||
".txt",
|
|
||||||
".odt",
|
|
||||||
".ott",
|
|
||||||
".pdb",
|
|
||||||
".pdf",
|
|
||||||
".psw",
|
|
||||||
".rtf",
|
|
||||||
".sdw",
|
|
||||||
".stw",
|
|
||||||
".sxw",
|
|
||||||
".uot",
|
|
||||||
".vor",
|
|
||||||
".wps",
|
|
||||||
".epub",
|
|
||||||
".png",
|
|
||||||
".bmp",
|
".bmp",
|
||||||
|
".cdr",
|
||||||
|
".cgm",
|
||||||
|
".cmx",
|
||||||
|
".csv",
|
||||||
|
".cwk",
|
||||||
|
".dbf",
|
||||||
|
".dif",
|
||||||
|
".doc",
|
||||||
|
".docm",
|
||||||
|
".docx",
|
||||||
|
".dot",
|
||||||
|
".dotm",
|
||||||
|
".dotx",
|
||||||
|
".dxf",
|
||||||
".emf",
|
".emf",
|
||||||
".eps",
|
".eps",
|
||||||
|
".epub",
|
||||||
".fodg",
|
".fodg",
|
||||||
|
".fodp",
|
||||||
|
".fods",
|
||||||
|
".fodt",
|
||||||
|
".fopd",
|
||||||
".gif",
|
".gif",
|
||||||
".jpg",
|
".htm",
|
||||||
|
".html",
|
||||||
|
".hwp",
|
||||||
".jpeg",
|
".jpeg",
|
||||||
|
".jpg",
|
||||||
|
".key",
|
||||||
|
".ltx",
|
||||||
|
".lwp",
|
||||||
|
".mcw",
|
||||||
".met",
|
".met",
|
||||||
|
".mml",
|
||||||
|
".mw",
|
||||||
|
".numbers",
|
||||||
".odd",
|
".odd",
|
||||||
|
".odg",
|
||||||
|
".odm",
|
||||||
|
".odp",
|
||||||
|
".ods",
|
||||||
|
".odt",
|
||||||
".otg",
|
".otg",
|
||||||
|
".oth",
|
||||||
|
".otp",
|
||||||
|
".ots",
|
||||||
|
".ott",
|
||||||
|
".pages",
|
||||||
".pbm",
|
".pbm",
|
||||||
|
".pcd",
|
||||||
".pct",
|
".pct",
|
||||||
|
".pcx",
|
||||||
|
".pdb",
|
||||||
|
".pdf",
|
||||||
".pgm",
|
".pgm",
|
||||||
|
".png",
|
||||||
|
".pot",
|
||||||
|
".potm",
|
||||||
|
".potx",
|
||||||
".ppm",
|
".ppm",
|
||||||
|
".pps",
|
||||||
|
".ppt",
|
||||||
|
".pptm",
|
||||||
|
".pptx",
|
||||||
|
".psd",
|
||||||
|
".psw",
|
||||||
|
".pub",
|
||||||
|
".pwp",
|
||||||
|
".pxl",
|
||||||
".ras",
|
".ras",
|
||||||
|
".rtf",
|
||||||
|
".sda",
|
||||||
|
".sdc",
|
||||||
|
".sdd",
|
||||||
|
".sdp",
|
||||||
|
".sdw",
|
||||||
|
".sgl",
|
||||||
|
".slk",
|
||||||
|
".smf",
|
||||||
|
".stc",
|
||||||
".std",
|
".std",
|
||||||
|
".sti",
|
||||||
|
".stw",
|
||||||
".svg",
|
".svg",
|
||||||
".svm",
|
".svm",
|
||||||
".swf",
|
".swf",
|
||||||
|
".sxc",
|
||||||
".sxd",
|
".sxd",
|
||||||
|
".sxg",
|
||||||
|
".sxi",
|
||||||
|
".sxm",
|
||||||
".sxw",
|
".sxw",
|
||||||
|
".tga",
|
||||||
".tif",
|
".tif",
|
||||||
".tiff",
|
".tiff",
|
||||||
".xhtml",
|
".txt",
|
||||||
".xpm",
|
".uof",
|
||||||
".odp",
|
|
||||||
".fodp",
|
|
||||||
".potm",
|
|
||||||
".pot",
|
|
||||||
".pptx",
|
|
||||||
".pps",
|
|
||||||
".ppt",
|
|
||||||
".pwp",
|
|
||||||
".sda",
|
|
||||||
".sdd",
|
|
||||||
".sti",
|
|
||||||
".sxi",
|
|
||||||
".uop",
|
".uop",
|
||||||
".wmf",
|
|
||||||
".csv",
|
|
||||||
".dbf",
|
|
||||||
".dif",
|
|
||||||
".fods",
|
|
||||||
".ods",
|
|
||||||
".ots",
|
|
||||||
".pxl",
|
|
||||||
".sdc",
|
|
||||||
".slk",
|
|
||||||
".stc",
|
|
||||||
".sxc",
|
|
||||||
".uos",
|
".uos",
|
||||||
|
".uot",
|
||||||
|
".vdx",
|
||||||
|
".vor",
|
||||||
|
".vsd",
|
||||||
|
".vsdm",
|
||||||
|
".vsdx",
|
||||||
|
".wb2",
|
||||||
|
".wk1",
|
||||||
|
".wks",
|
||||||
|
".wmf",
|
||||||
|
".wpd",
|
||||||
|
".wpg",
|
||||||
|
".wps",
|
||||||
|
".xbm",
|
||||||
|
".xhtml",
|
||||||
".xls",
|
".xls",
|
||||||
".xlt",
|
".xlsb",
|
||||||
|
".xlsm",
|
||||||
".xlsx",
|
".xlsx",
|
||||||
".odg",
|
".xlt",
|
||||||
".dotx",
|
".xltm",
|
||||||
".xltx",
|
".xltx",
|
||||||
|
".xlw",
|
||||||
|
".xml",
|
||||||
|
".xpm",
|
||||||
|
".zabw",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApi_Descriptor(t *testing.T) {
|
func TestApi_Descriptor(t *testing.T) {
|
||||||
@@ -284,41 +284,16 @@ func TestApi_Metrics(t *testing.T) {
|
|||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(metrics) != 7 {
|
if len(metrics) != 2 {
|
||||||
t.Fatalf("expected %d metrics, but got %d", 7, len(metrics))
|
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
actual := metrics[0].Read()
|
||||||
if actual != float64(1) {
|
|
||||||
t.Errorf("expected %f for unoconv_active_instances_count, but got %f", float64(1), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[1].Read()
|
|
||||||
if actual != float64(1) {
|
|
||||||
t.Errorf("expected %f for libreoffice_listener_active_instances_count, but got %f", float64(1), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[2].Read()
|
|
||||||
if actual != float64(1) {
|
|
||||||
t.Errorf("expected %f for unoconv_listener_active_instances_count, but got %f", float64(1), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[3].Read()
|
|
||||||
if actual != float64(10) {
|
|
||||||
t.Errorf("expected %f for libreoffice_listener_queue_length, but got %f", float64(10), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[4].Read()
|
|
||||||
if actual != float64(10) {
|
|
||||||
t.Errorf("expected %f for unoconv_listener_queue_length, but got %f", float64(10), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[5].Read()
|
|
||||||
if actual != float64(10) {
|
if actual != float64(10) {
|
||||||
t.Errorf("expected %f for libreoffice_requests_queue_size, but got %f", float64(10), actual)
|
t.Errorf("expected %f for libreoffice_requests_queue_size, but got %f", float64(10), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = metrics[6].Read()
|
actual = metrics[1].Read()
|
||||||
if actual != float64(0) {
|
if actual != float64(0) {
|
||||||
t.Errorf("expected %f for libreoffice_restarts_count, but got %f", float64(0), actual)
|
t.Errorf("expected %f for libreoffice_restarts_count, but got %f", float64(0), actual)
|
||||||
}
|
}
|
||||||
@@ -475,7 +450,7 @@ func TestApi_Extensions(t *testing.T) {
|
|||||||
extensions := a.Extensions()
|
extensions := a.Extensions()
|
||||||
|
|
||||||
actual := len(extensions)
|
actual := len(extensions)
|
||||||
expect := 79
|
expect := 130
|
||||||
|
|
||||||
if actual != expect {
|
if actual != expect {
|
||||||
t.Errorf("expected %d extensions, but got %d", expect, actual)
|
t.Errorf("expected %d extensions, but got %d", expect, actual)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type libreOffice interface {
|
type libreOffice interface {
|
||||||
@@ -273,11 +273,12 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
|||||||
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !options.ExportFormFields {
|
||||||
|
args = append(args, "--export", "ExportFormFields=false")
|
||||||
|
}
|
||||||
|
|
||||||
switch options.PdfFormats.PdfA {
|
switch options.PdfFormats.PdfA {
|
||||||
case "":
|
case "":
|
||||||
case gotenberg.PdfA1a:
|
|
||||||
logger.Warn("PDF/A-1a is no more supported by LibreOffice (use PDF/A-1b instead)")
|
|
||||||
args = append(args, "--export", "SelectPdfVersion=1")
|
|
||||||
case gotenberg.PdfA1b:
|
case gotenberg.PdfA1b:
|
||||||
args = append(args, "--export", "SelectPdfVersion=1")
|
args = append(args, "--export", "SelectPdfVersion=1")
|
||||||
case gotenberg.PdfA2b:
|
case gotenberg.PdfA2b:
|
||||||
@@ -285,14 +286,20 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
|||||||
case gotenberg.PdfA3b:
|
case gotenberg.PdfA3b:
|
||||||
args = append(args, "--export", "SelectPdfVersion=3")
|
args = append(args, "--export", "SelectPdfVersion=3")
|
||||||
default:
|
default:
|
||||||
return ErrInvalidPdfFormat
|
return ErrInvalidPdfFormats
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PdfFormats.PdfUa {
|
if options.PdfFormats.PdfUa {
|
||||||
args = append(
|
args = append(
|
||||||
args,
|
args,
|
||||||
"--export", "EnableTextAccessForAccessibilityTools=true",
|
|
||||||
"--export", "UseTaggedPDF=true",
|
"--export", "UseTaggedPDF=true",
|
||||||
|
"--export", "EnableTextAccessForAccessibilityTools=true",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
args = append(
|
||||||
|
args,
|
||||||
|
"--export", "UseTaggedPDF=false",
|
||||||
|
"--export", "EnableTextAccessForAccessibilityTools=false",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLibreOfficeProcess_Start(t *testing.T) {
|
func TestLibreOfficeProcess_Start(t *testing.T) {
|
||||||
@@ -235,7 +235,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "ErrInvalidPdfFormat",
|
scenario: "ErrInvalidPdfFormats",
|
||||||
libreOffice: func() libreOffice {
|
libreOffice: func() libreOffice {
|
||||||
p := new(libreOfficeProcess)
|
p := new(libreOfficeProcess)
|
||||||
p.socketPort = 12345
|
p.socketPort = 12345
|
||||||
@@ -247,7 +247,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
cancelledCtx: false,
|
cancelledCtx: false,
|
||||||
start: false,
|
start: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectedError: ErrInvalidPdfFormat,
|
expectedError: ErrInvalidPdfFormats,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "ErrMalformedPageRanges",
|
scenario: "ErrMalformedPageRanges",
|
||||||
@@ -364,6 +364,35 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
|||||||
start: true,
|
start: true,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (disable form fields)",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
fs: func() *gotenberg.FileSystem {
|
||||||
|
fs := gotenberg.NewFileSystem()
|
||||||
|
|
||||||
|
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("DisableFormFields"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{ExportFormFields: false},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (page ranges)",
|
scenario: "success (page ranges)",
|
||||||
libreOffice: newLibreOfficeProcess(
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApiMock is a mock for the [Uno] interface.
|
// ApiMock is a mock for the [Uno] interface.
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
libeofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
libeofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
libreofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLibreOffice_Descriptor(t *testing.T) {
|
func TestLibreOffice_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// Package pdfengine provides a module which interacts with LibreOffice via the
|
// Package pdfengine provides a module which interacts with LibreOffice via the
|
||||||
// UNO (Universal Network Objects) API and implements the gotenberg.PdfEngine
|
// UNO (Universal Network Objects) API and implements the gotenberg.PdfEngine
|
||||||
// interface.
|
// interface. This package allows for:
|
||||||
|
//
|
||||||
|
// 1. The conversion to specific PDF formats.
|
||||||
package pdfengine
|
package pdfengine
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -64,13 +64,23 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, api.ErrInvalidPdfFormat) {
|
if errors.Is(err, api.ErrInvalidPdfFormats) {
|
||||||
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, gotenberg.ErrPdfFormatNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, gotenberg.ErrPdfFormatNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, err)
|
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMetadata is not available in this implementation.
|
||||||
|
func (engine *LibreOfficePdfEngine) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLibreOfficePdfEngine_Descriptor(t *testing.T) {
|
func TestLibreOfficePdfEngine_Descriptor(t *testing.T) {
|
||||||
@@ -137,7 +137,7 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
|||||||
scenario: "invalid PDF format",
|
scenario: "invalid PDF format",
|
||||||
api: &api.ApiMock{
|
api: &api.ApiMock{
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||||
return api.ErrInvalidPdfFormat
|
return api.ErrInvalidPdfFormats
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
@@ -166,3 +166,21 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(LibreOfficePdfEngine)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(LibreOfficePdfEngine)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package libreoffice
|
package libreoffice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
libreofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
||||||
@@ -24,70 +25,42 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
|
|
||||||
// Let's get the data from the form and validate them.
|
// Let's get the data from the form and validate them.
|
||||||
var (
|
var (
|
||||||
inputPaths []string
|
inputPaths []string
|
||||||
landscape bool
|
landscape bool
|
||||||
nativePageRanges string
|
nativePageRanges string
|
||||||
nativePdfA1aFormat bool
|
exportFormFields bool
|
||||||
nativePdfFormat string
|
pdfa string
|
||||||
pdfFormat string
|
pdfua bool
|
||||||
pdfa string
|
nativePdfFormats bool
|
||||||
pdfua bool
|
merge bool
|
||||||
nativePdfFormats bool
|
metadata map[string]interface{}
|
||||||
merge bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths(libreOffice.Extensions(), &inputPaths).
|
MandatoryPaths(libreOffice.Extensions(), &inputPaths).
|
||||||
Bool("landscape", &landscape, false).
|
Bool("landscape", &landscape, false).
|
||||||
String("nativePageRanges", &nativePageRanges, "").
|
String("nativePageRanges", &nativePageRanges, "").
|
||||||
Bool("nativePdfA1aFormat", &nativePdfA1aFormat, false).
|
Bool("exportFormFields", &exportFormFields, true).
|
||||||
String("nativePdfFormat", &nativePdfFormat, "").
|
|
||||||
String("pdfFormat", &pdfFormat, "").
|
|
||||||
String("pdfa", &pdfa, "").
|
String("pdfa", &pdfa, "").
|
||||||
Bool("pdfua", &pdfua, false).
|
Bool("pdfua", &pdfua, false).
|
||||||
Bool("nativePdfFormats", &nativePdfFormats, true).
|
Bool("nativePdfFormats", &nativePdfFormats, true).
|
||||||
Bool("merge", &merge, false).
|
Bool("merge", &merge, false).
|
||||||
|
Custom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
Validate()
|
Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: deprecated.
|
|
||||||
// pdfa > nativePdfFormat > pdfFormat > nativePdfA1aFormat.
|
|
||||||
var (
|
|
||||||
actualPdfArchive string
|
|
||||||
nativeFormats bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if nativePdfA1aFormat {
|
|
||||||
ctx.Log().Warn("'nativePdfA1aFormat' is deprecated; prefer the 'pdfa' form field instead")
|
|
||||||
actualPdfArchive = gotenberg.PdfA1a
|
|
||||||
nativeFormats = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if pdfFormat != "" {
|
|
||||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
|
||||||
actualPdfArchive = pdfFormat
|
|
||||||
nativeFormats = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if nativePdfFormat != "" {
|
|
||||||
ctx.Log().Warn("'nativePdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
|
||||||
actualPdfArchive = nativePdfFormat
|
|
||||||
nativeFormats = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if pdfa != "" {
|
|
||||||
actualPdfArchive = pdfa
|
|
||||||
nativeFormats = nativePdfFormats
|
|
||||||
}
|
|
||||||
|
|
||||||
if pdfua {
|
|
||||||
nativeFormats = nativePdfFormats
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfFormats := gotenberg.PdfFormats{
|
pdfFormats := gotenberg.PdfFormats{
|
||||||
PdfA: actualPdfArchive,
|
PdfA: pdfa,
|
||||||
PdfUa: pdfua,
|
PdfUa: pdfua,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,18 +68,28 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
outputPaths := make([]string, len(inputPaths))
|
outputPaths := make([]string, len(inputPaths))
|
||||||
for i, inputPath := range inputPaths {
|
for i, inputPath := range inputPaths {
|
||||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
options := libreofficeapi.Options{
|
options := libreofficeapi.Options{
|
||||||
Landscape: landscape,
|
Landscape: landscape,
|
||||||
PageRanges: nativePageRanges,
|
PageRanges: nativePageRanges,
|
||||||
|
ExportFormFields: exportFormFields,
|
||||||
}
|
}
|
||||||
|
|
||||||
if nativeFormats {
|
if nativePdfFormats {
|
||||||
options.PdfFormats = pdfFormats
|
options.PdfFormats = pdfFormats
|
||||||
}
|
}
|
||||||
|
|
||||||
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, libreofficeapi.ErrInvalidPdfFormats) {
|
||||||
|
return api.WrapError(
|
||||||
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("A PDF format in '%+v' is not supported", pdfFormats),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) {
|
if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
@@ -118,9 +101,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// So far so good, let's check if we have to merge the PDFs. Quick
|
// So far so good, let's check if we have to merge the PDFs.
|
||||||
// win: if there is only one PDF, skip this step.
|
|
||||||
|
|
||||||
if len(outputPaths) > 1 && merge {
|
if len(outputPaths) > 1 && merge {
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
@@ -129,47 +110,14 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
return fmt.Errorf("merge PDFs: %w", err)
|
return fmt.Errorf("merge PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, let's check if the client want to convert this result
|
// Only one output path.
|
||||||
// PDF to specific PDF formats.
|
outputPaths = []string{outputPath}
|
||||||
zeroValued := gotenberg.PdfFormats{}
|
|
||||||
if !nativeFormats && pdfFormats != zeroValued {
|
|
||||||
convertInputPath := outputPath
|
|
||||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Important: the output path is now the converted file.
|
|
||||||
outputPath = convertOutputPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last but not least, add the output path to the context so that
|
|
||||||
// the Uno is able to send it as a response to the client.
|
|
||||||
|
|
||||||
err = ctx.AddOutputPaths(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("add output path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ok, we don't have to merge the PDFs. Let's check if the client
|
// Let's check if the client want to convert each PDF to a specific
|
||||||
// want to convert each PDF to a specific PDF format.
|
// PDF format.
|
||||||
zeroValued := gotenberg.PdfFormats{}
|
zeroValued := gotenberg.PdfFormats{}
|
||||||
if !nativeFormats && pdfFormats != zeroValued {
|
if !nativePdfFormats && pdfFormats != zeroValued {
|
||||||
convertOutputPaths := make([]string, len(outputPaths))
|
convertOutputPaths := make([]string, len(outputPaths))
|
||||||
|
|
||||||
for i, outputPath := range outputPaths {
|
for i, outputPath := range outputPaths {
|
||||||
@@ -178,28 +126,40 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF: %w", err)
|
return fmt.Errorf("convert PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Important: the output paths are now the converted files.
|
// Important: the output paths are now the converted files.
|
||||||
outputPaths = convertOutputPaths
|
outputPaths = convertOutputPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last but not least, add the output paths to the context so that
|
// Writes and potentially overrides metadata entries, if any.
|
||||||
// the Uno is able to send them as a response to the client.
|
if len(metadata) > 0 {
|
||||||
|
for _, outputPath := range outputPaths {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outputPaths) > 1 {
|
||||||
|
// If .zip archive, document.docx -> document.docx.pdf.
|
||||||
|
for i, inputPath := range inputPaths {
|
||||||
|
outputPath := fmt.Sprintf("%s.pdf", inputPath)
|
||||||
|
|
||||||
|
err = ctx.Rename(outputPaths[i], outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rename output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPaths[i] = outputPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last but not least, add the output paths to the context so that
|
||||||
|
// the API is able to send them as a response to the client.
|
||||||
err = ctx.AddOutputPaths(outputPaths...)
|
err = ctx.AddOutputPaths(outputPaths...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add output paths: %w", err)
|
return fmt.Errorf("add output paths: %w", err)
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
libreofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertRoute(t *testing.T) {
|
func TestConvertRoute(t *testing.T) {
|
||||||
@@ -25,6 +26,7 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectHttpError bool
|
expectHttpError bool
|
||||||
expectHttpStatus int
|
expectHttpStatus int
|
||||||
expectOutputPathsCount int
|
expectOutputPathsCount int
|
||||||
|
expectOutputPaths []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "missing at least one mandatory file",
|
scenario: "missing at least one mandatory file",
|
||||||
@@ -37,6 +39,50 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrPdfFormatNotSupported (nativePdfFormats)",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||||
|
return libreofficeapi.ErrInvalidPdfFormats
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "ErrMalformedPageRanges",
|
scenario: "ErrMalformedPageRanges",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -44,6 +90,11 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
})
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfa": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
@@ -81,18 +132,16 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "ErrPdfFormatNotSupported (single file)",
|
scenario: "PDF engine merge error",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
|
"document2.docx": "/document2.docx",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfa": {
|
"merge": {
|
||||||
"foo",
|
"true",
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -106,17 +155,16 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
engine: &gotenberg.PdfEngineMock{
|
engine: &gotenberg.PdfEngineMock{
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
return gotenberg.ErrPdfFormatNotSupported
|
return errors.New("foo")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: false,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "PDF engine convert error (single file)",
|
scenario: "PDF engine convert error",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -150,7 +198,64 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot add output paths (single file)",
|
scenario: "PDF engine write metadata error",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot rename many files",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
"document2.docx": "/document2.docx",
|
||||||
|
"document2.doc": "/document2.doc",
|
||||||
|
})
|
||||||
|
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
|
return errors.New("cannot rename")
|
||||||
|
}})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx", ".doc"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot add output paths",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -176,7 +281,25 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
|
"document2.docx": "/document2.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"merge": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
"nativePdfFormats": {
|
||||||
|
"false",
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -188,6 +311,17 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
return []string{".docx"}
|
return []string{".docx"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
@@ -199,7 +333,25 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
"document2.docx": "/document2.docx",
|
"document2.docx": "/document2.docx",
|
||||||
|
"document2.doc": "/document2.doc",
|
||||||
})
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
"nativePdfFormats": {
|
||||||
|
"false",
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
|
return nil
|
||||||
|
}})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
@@ -207,15 +359,27 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
ExtensionsMock: func() []string {
|
ExtensionsMock: func() []string {
|
||||||
return []string{".docx"}
|
return []string{".docx", ".doc"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 2,
|
expectOutputPathsCount: 3,
|
||||||
|
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with non-native PDF/A & PDF/UA (single file)",
|
scenario: "success with native PDF/A & PDF/UA",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -228,9 +392,6 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"pdfua": {
|
"pdfua": {
|
||||||
"true",
|
"true",
|
||||||
},
|
},
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -242,325 +403,6 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
return []string{".docx"}
|
return []string{".docx"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with every non-native PDF/A & PDF/UA form fields (single file)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"nativePdfA1aFormat": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfFormat": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"nativePdfFormat": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "merge error",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "ErrPdfFormatNotSupported (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return gotenberg.ErrPdfFormatNotSupported
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: true,
|
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "PDF engine convert error (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "cannot add output paths (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
ctx.SetCancelled(true)
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with non-native PDF/A & PDF/UA (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with non-native PDF/A & PDF/UA (merge)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"document.docx": "/document.docx",
|
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"merge": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
"nativePdfFormats": {
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
libreOffice: &libreofficeapi.ApiMock{
|
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExtensionsMock: func() []string {
|
|
||||||
return []string{".docx"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
@@ -602,6 +444,12 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, path := range tc.expectOutputPaths {
|
||||||
|
if !slices.Contains(tc.ctx.OutputPaths(), path) {
|
||||||
|
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
"go.uber.org/zap/zaptest/observer"
|
"go.uber.org/zap/zaptest/observer"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLogging_Descriptor(t *testing.T) {
|
func TestLogging_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
|
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
|
||||||
// interface using the pdfcpu library. This package allows for the merging of
|
// interface using the pdfcpu library. This package allows for:
|
||||||
// PDF files but does not support conversion to specific PDF formats.
|
//
|
||||||
|
// 1. The merging of PDF files.
|
||||||
//
|
//
|
||||||
// See: https://github.com/pdfcpu/pdfcpu.
|
// See: https://github.com/pdfcpu/pdfcpu.
|
||||||
package pdfcpu
|
package pdfcpu
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
|
pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -54,6 +54,16 @@ func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats g
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMetadata is not available in this implementation.
|
||||||
|
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*PdfCpu)(nil)
|
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPdfCpu_Descriptor(t *testing.T) {
|
func TestPdfCpu_Descriptor(t *testing.T) {
|
||||||
@@ -102,3 +102,21 @@ func TestPdfCpu_Convert(t *testing.T) {
|
|||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfCpu)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfCpu)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package pdfengines
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type multiPdfEngines struct {
|
type multiPdfEngines struct {
|
||||||
@@ -70,6 +71,65 @@ func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, f
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type readMetadataResult struct {
|
||||||
|
metadata map[string]interface{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
var err error
|
||||||
|
var mu sync.Mutex // to safely append errors.
|
||||||
|
|
||||||
|
resultChan := make(chan readMetadataResult, len(multi.engines))
|
||||||
|
|
||||||
|
for _, engine := range multi.engines {
|
||||||
|
go func(engine gotenberg.PdfEngine) {
|
||||||
|
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
|
||||||
|
resultChan <- readMetadataResult{metadata: metadata, err: err}
|
||||||
|
}(engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range multi.engines {
|
||||||
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
if result.err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
err = multierr.Append(err, result.err)
|
||||||
|
mu.Unlock()
|
||||||
|
} else {
|
||||||
|
return result.metadata, nil
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("read PDF metadata with multi PDF engines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
var err error
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
for _, engine := range multi.engines {
|
||||||
|
go func(engine gotenberg.PdfEngine) {
|
||||||
|
errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
|
||||||
|
}(engine)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case writeMetadataErr := <-errChan:
|
||||||
|
errored := multierr.AppendInto(&err, writeMetadataErr)
|
||||||
|
if !errored {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMultiPdfEngines_Merge(t *testing.T) {
|
func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||||
@@ -177,3 +177,169 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
engine *multiPdfEngines
|
||||||
|
ctx context.Context
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "nominal behavior",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return make(map[string]interface{}), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "at least one engine does not return an error",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return make(map[string]interface{}), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "all engines return an error",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "context expired",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return make(map[string]interface{}), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
_, err := tc.engine.ReadMetadata(tc.ctx, zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
engine *multiPdfEngines
|
||||||
|
ctx context.Context
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "nominal behavior",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "at least one engine does not return an error",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "all engines return an error",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "context expired",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.engine.WriteMetadata(tc.ctx, zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -54,18 +54,6 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
names := flags.MustStringSlice("pdfengines-engines")
|
names := flags.MustStringSlice("pdfengines-engines")
|
||||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||||
|
|
||||||
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get logger provider: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(mod)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get logger: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = logger.Named("pdfengines")
|
|
||||||
|
|
||||||
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get PDF engines: %w", err)
|
return fmt.Errorf("get PDF engines: %w", err)
|
||||||
@@ -81,13 +69,13 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
// Selection from user.
|
// Selection from user.
|
||||||
mod.names = names
|
mod.names = names
|
||||||
|
|
||||||
for i, name := range names {
|
// Example in case of deprecated module name.
|
||||||
// FIXME: deprecated.
|
//for i, name := range names {
|
||||||
if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
// if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
||||||
logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
// logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
||||||
mod.names[i] = "libreoffice-pdfengine"
|
// mod.names[i] = "libreoffice-pdfengine"
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -180,6 +168,8 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
|||||||
return []api.Route{
|
return []api.Route{
|
||||||
mergeRoute(engine),
|
mergeRoute(engine),
|
||||||
convertRoute(engine),
|
convertRoute(engine),
|
||||||
|
readMetadataRoute(engine),
|
||||||
|
writeMetadataRoute(engine),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPdfEngines_Descriptor(t *testing.T) {
|
func TestPdfEngines_Descriptor(t *testing.T) {
|
||||||
@@ -34,16 +32,12 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
provider := &struct {
|
provider := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
gotenberg.LoggerProviderMock
|
|
||||||
}{}
|
}{}
|
||||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
return provider
|
return provider
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return zap.NewNop(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
engine := &struct {
|
engine := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
@@ -75,17 +69,12 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
provider := &struct {
|
provider := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
gotenberg.LoggerProviderMock
|
|
||||||
}{}
|
}{}
|
||||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
return provider
|
return provider
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return zap.NewNop(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
engine1 := &struct {
|
engine1 := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
gotenberg.ValidatorMock
|
gotenberg.ValidatorMock
|
||||||
@@ -130,108 +119,17 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
expectedPdfEngines: []string{"b", "a"},
|
expectedPdfEngines: []string{"b", "a"},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
scenario: "user select deprecated unoconv-pdfengine",
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
provider := &struct {
|
|
||||||
gotenberg.ModuleMock
|
|
||||||
gotenberg.LoggerProviderMock
|
|
||||||
}{}
|
|
||||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
|
||||||
return provider
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return zap.NewNop(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
engine := &struct {
|
|
||||||
gotenberg.ModuleMock
|
|
||||||
gotenberg.ValidatorMock
|
|
||||||
gotenberg.PdfEngineMock
|
|
||||||
}{}
|
|
||||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "libreoffice-pdfengine", New: func() gotenberg.Module { return engine }}
|
|
||||||
}
|
|
||||||
engine.ValidateMock = func() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := new(PdfEngines).Descriptor().FlagSet
|
|
||||||
err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: fs,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
provider.Descriptor(),
|
|
||||||
engine.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectedPdfEngines: []string{"libreoffice-pdfengine"},
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "no logger provider",
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "no logger from logger provider",
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
provider := &struct {
|
|
||||||
gotenberg.ModuleMock
|
|
||||||
gotenberg.LoggerProviderMock
|
|
||||||
}{}
|
|
||||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
|
||||||
return provider
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return nil, errors.New("foo")
|
|
||||||
}
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
provider.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
scenario: "no valid PDF engine",
|
scenario: "no valid PDF engine",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
provider := &struct {
|
provider := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
gotenberg.LoggerProviderMock
|
|
||||||
}{}
|
}{}
|
||||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
return provider
|
return provider
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return zap.NewNop(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
engine := &struct {
|
engine := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
gotenberg.ValidatorMock
|
gotenberg.ValidatorMock
|
||||||
@@ -414,7 +312,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 2,
|
expectRoutes: 4,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package pdfengines
|
package pdfengines
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mergeRoute returns an [api.Route] which can merge PDFs.
|
// mergeRoute returns an [api.Route] which can merge PDFs.
|
||||||
@@ -23,40 +25,35 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
// Let's get the data from the form and validate them.
|
// Let's get the data from the form and validate them.
|
||||||
var (
|
var (
|
||||||
inputPaths []string
|
inputPaths []string
|
||||||
pdfFormat string
|
|
||||||
pdfa string
|
pdfa string
|
||||||
pdfua bool
|
pdfua bool
|
||||||
|
metadata map[string]interface{}
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
String("pdfFormat", &pdfFormat, "").
|
|
||||||
String("pdfa", &pdfa, "").
|
String("pdfa", &pdfa, "").
|
||||||
Bool("pdfua", &pdfua, false).
|
Bool("pdfua", &pdfua, false).
|
||||||
|
Custom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
Validate()
|
Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var actualPdfArchive string
|
|
||||||
|
|
||||||
if pdfFormat != "" {
|
|
||||||
// FIXME: deprecated
|
|
||||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
|
||||||
actualPdfArchive = pdfFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
if pdfa != "" {
|
|
||||||
actualPdfArchive = pdfa
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfFormats := gotenberg.PdfFormats{
|
pdfFormats := gotenberg.PdfFormats{
|
||||||
PdfA: actualPdfArchive,
|
PdfA: pdfa,
|
||||||
PdfUa: pdfua,
|
PdfUa: pdfua,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alright, let's merge the PDFs.
|
// Alright, let's merge the PDFs.
|
||||||
|
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
||||||
@@ -73,18 +70,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF: %w", err)
|
return fmt.Errorf("convert PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,9 +78,16 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
outputPath = convertOutputPath
|
outputPath = convertOutputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes and potentially overrides metadata entries, if any.
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Last but not least, add the output path to the context so that
|
// Last but not least, add the output path to the context so that
|
||||||
// the API is able to send it as a response to the client.
|
// the API is able to send it as a response to the client.
|
||||||
|
|
||||||
err = ctx.AddOutputPaths(outputPath)
|
err = ctx.AddOutputPaths(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add output path: %w", err)
|
return fmt.Errorf("add output path: %w", err)
|
||||||
@@ -105,8 +98,8 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertRoute returns an [api.Route] which can convert a PDF to a specific
|
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
||||||
// PDF format.
|
// format.
|
||||||
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
@@ -118,14 +111,12 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
// Let's get the data from the form and validate them.
|
// Let's get the data from the form and validate them.
|
||||||
var (
|
var (
|
||||||
inputPaths []string
|
inputPaths []string
|
||||||
pdfFormat string
|
|
||||||
pdfa string
|
pdfa string
|
||||||
pdfua bool
|
pdfua bool
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
String("pdfFormat", &pdfFormat, "").
|
|
||||||
String("pdfa", &pdfa, "").
|
String("pdfa", &pdfa, "").
|
||||||
Bool("pdfua", &pdfua, false).
|
Bool("pdfua", &pdfua, false).
|
||||||
Validate()
|
Validate()
|
||||||
@@ -133,20 +124,8 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var actualPdfArchive string
|
|
||||||
|
|
||||||
if pdfFormat != "" {
|
|
||||||
// FIXME: deprecated.
|
|
||||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
|
||||||
actualPdfArchive = pdfFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
if pdfa != "" {
|
|
||||||
actualPdfArchive = pdfa
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfFormats := gotenberg.PdfFormats{
|
pdfFormats := gotenberg.PdfFormats{
|
||||||
PdfA: actualPdfArchive,
|
PdfA: pdfa,
|
||||||
PdfUa: pdfua,
|
PdfUa: pdfua,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,30 +142,27 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
|
|
||||||
// Alright, let's convert the PDFs.
|
// Alright, let's convert the PDFs.
|
||||||
outputPaths := make([]string, len(inputPaths))
|
outputPaths := make([]string, len(inputPaths))
|
||||||
|
|
||||||
for i, inputPath := range inputPaths {
|
for i, inputPath := range inputPaths {
|
||||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
return fmt.Errorf("convert PDF: %w", err)
|
||||||
return api.WrapError(
|
}
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
if len(outputPaths) > 1 {
|
||||||
http.StatusBadRequest,
|
// If .zip archive, keep the original filename.
|
||||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
err = ctx.Rename(outputPaths[i], inputPath)
|
||||||
),
|
if err != nil {
|
||||||
)
|
return fmt.Errorf("rename output path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF: %w", err)
|
outputPaths[i] = inputPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last but not least, add the output paths to the context so that
|
// Last but not least, add the output paths to the context so that
|
||||||
// the API is able to send them as a response to the client.
|
// the API is able to send them as a response to the client.
|
||||||
|
|
||||||
err = ctx.AddOutputPaths(outputPaths...)
|
err = ctx.AddOutputPaths(outputPaths...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add output paths: %w", err)
|
return fmt.Errorf("add output paths: %w", err)
|
||||||
@@ -196,3 +172,98 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readMetadataRoute returns an [api.Route] which returns the metadata of PDFs.
|
||||||
|
func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/pdfengines/metadata/read",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
// Let's get the data from the form and validate them.
|
||||||
|
var inputPaths []string
|
||||||
|
|
||||||
|
err := ctx.FormData().
|
||||||
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alright, let's read the metadata.
|
||||||
|
res := make(map[string]map[string]interface{}, len(inputPaths))
|
||||||
|
for _, inputPath := range inputPaths {
|
||||||
|
metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res[filepath.Base(inputPath)] = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.JSON(http.StatusOK, res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("return JSON response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.ErrNoOutputFile
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMetadataRoute returns an [api.Route] which can write metadata into
|
||||||
|
// PDFs.
|
||||||
|
func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/pdfengines/metadata/write",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
// Let's get the data from the form and validate them.
|
||||||
|
var (
|
||||||
|
inputPaths []string
|
||||||
|
metadata map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
err := ctx.FormData().
|
||||||
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
MandatoryCustom("metadata", func(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
err := json.Unmarshal([]byte(value), &metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return errors.New("no metadata")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alright, let's convert the PDFs.
|
||||||
|
for _, inputPath := range inputPaths {
|
||||||
|
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last but not least, add the output paths to the context so that
|
||||||
|
// the API is able to send them as a response to the client.
|
||||||
|
err = ctx.AddOutputPaths(inputPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add output paths: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMergeHandler(t *testing.T) {
|
func TestMergeHandler(t *testing.T) {
|
||||||
@@ -32,7 +35,27 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "error from PDF engine",
|
scenario: "invalid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine merge error",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -50,6 +73,60 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine convert error",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine write metadata error",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot add output paths",
|
scenario: "cannot add output paths",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -72,25 +149,6 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success",
|
scenario: "success",
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"file.pdf": "/file.pdf",
|
|
||||||
"file2.pdf": "/file2.pdf",
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "ErrPdfFormatNotSupported",
|
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -101,6 +159,9 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1b,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -109,67 +170,9 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return gotenberg.ErrPdfFormatNotSupported
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: true,
|
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "error from PDF engine (convert)",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"file.pdf": "/file.pdf",
|
|
||||||
"file2.pdf": "/file2.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with every PDF/A & PDF/UA form fields",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"file.pdf": "/file.pdf",
|
|
||||||
"file2.pdf": "/file2.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
gotenberg.PdfA1a,
|
|
||||||
},
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -227,6 +230,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectHttpError bool
|
expectHttpError bool
|
||||||
expectHttpStatus int
|
expectHttpStatus int
|
||||||
expectOutputPathsCount int
|
expectOutputPathsCount int
|
||||||
|
expectOutputPaths []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "missing at least one mandatory file",
|
scenario: "missing at least one mandatory file",
|
||||||
@@ -250,30 +254,6 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
scenario: "ErrPdfFormatNotSupported",
|
|
||||||
ctx: func() *api.ContextMock {
|
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"file.pdf": "/file.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: &gotenberg.PdfEngineMock{
|
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
|
||||||
return gotenberg.ErrPdfFormatNotSupported
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expectHttpError: true,
|
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
|
||||||
expectOutputPathsCount: 0,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
scenario: "error from PDF engine",
|
scenario: "error from PDF engine",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -322,16 +302,13 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with every PDF/A & PDF/UA form fields (single file)",
|
scenario: "success with PDF/A & PDF/UA form fields (single file)",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"file.pdf": "/file.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
|
||||||
gotenberg.PdfA1a,
|
|
||||||
},
|
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1b,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
@@ -351,7 +328,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with every PDF/A & PDF/UA form fields (many files)",
|
scenario: "cannot rename many files",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -359,9 +336,6 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
"file2.pdf": "/file2.pdf",
|
"file2.pdf": "/file2.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
|
||||||
gotenberg.PdfA1a,
|
|
||||||
},
|
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1b,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
@@ -369,6 +343,39 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
"true",
|
"true",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
|
return errors.New("cannot rename")
|
||||||
|
}})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success with PDF/A & PDF/UA form fields (many files)",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
|
return nil
|
||||||
|
}})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: &gotenberg.PdfEngineMock{
|
engine: &gotenberg.PdfEngineMock{
|
||||||
@@ -379,6 +386,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 2,
|
expectOutputPathsCount: 2,
|
||||||
|
expectOutputPaths: []string{"/file.pdf", "/file2.pdf"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
@@ -417,6 +425,304 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, path := range tc.expectOutputPaths {
|
||||||
|
if !slices.Contains(tc.ctx.OutputPaths(), path) {
|
||||||
|
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMetadataHandler(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *api.ContextMock
|
||||||
|
engine gotenberg.PdfEngine
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
|
expectedJson string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing at least one mandatory file",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "error from PDF engine",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectedError: api.ErrNoOutputFile,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectedJson: `{"file.pdf":{"bar":"foo","foo":"bar"}}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/forms/pdfengines/metadata/read", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c := echo.New().NewContext(req, rec)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
|
err := readMetadataRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpErr api.HttpError
|
||||||
|
isHttpError := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHttpError && !isHttpError {
|
||||||
|
t.Errorf("expected an HTTP error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHttpError && isHttpError {
|
||||||
|
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHttpError && isHttpError {
|
||||||
|
status, _ := httpErr.HttpError()
|
||||||
|
if status != tc.expectHttpStatus {
|
||||||
|
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedJson != "" && tc.expectedJson != strings.TrimSpace(rec.Body.String()) {
|
||||||
|
t.Errorf("expected '%s' as HTTP response but got '%s'", tc.expectedJson, strings.TrimSpace(rec.Body.String()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteMetadataHandler(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *api.ContextMock
|
||||||
|
engine gotenberg.PdfEngine
|
||||||
|
expectError bool
|
||||||
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
|
expectOutputPathsCount int
|
||||||
|
expectOutputPaths []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing at least one mandatory file",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid metadata form field",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no metadata",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"document.docx": "/document.docx",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{}",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "error from PDF engine",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot add output paths",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetCancelled(true)
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
c := echo.New().NewContext(nil, nil)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
|
err := writeMetadataRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpErr api.HttpError
|
||||||
|
isHttpError := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHttpError && !isHttpError {
|
||||||
|
t.Errorf("expected an HTTP error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHttpError && isHttpError {
|
||||||
|
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHttpError && isHttpError {
|
||||||
|
status, _ := httpErr.HttpError()
|
||||||
|
if status != tc.expectHttpStatus {
|
||||||
|
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||||
|
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range tc.expectOutputPaths {
|
||||||
|
if !slices.Contains(tc.ctx.OutputPaths(), path) {
|
||||||
|
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Package pdftk provides an implementation of the gotenberg.PdfEngine
|
// Package pdftk provides an implementation of the gotenberg.PdfEngine
|
||||||
// interface using the PDFtk command-line tool. This package allows for the
|
// interface using the PDFtk command-line tool. This package allows for:
|
||||||
// merging of PDF files but does not support conversion to specific PDF
|
//
|
||||||
// formats. The path to the PDFtk binary must be specified using the
|
// 1. The merging of PDF files.
|
||||||
// PDFTK_BIN_PATH environment variable.
|
//
|
||||||
|
// The path to the PDFtk binary must be specified using the PDFTK_BIN_PATH
|
||||||
|
// environment variable.
|
||||||
//
|
//
|
||||||
// See: https://gitlab.com/pdftk-java/pdftk.
|
// See: https://gitlab.com/pdftk-java/pdftk.
|
||||||
package pdftk
|
package pdftk
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -52,23 +51,6 @@ func (engine *PdfTk) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics returns the metrics.
|
|
||||||
func (engine *PdfTk) Metrics() ([]gotenberg.Metric, error) {
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
return []gotenberg.Metric{
|
|
||||||
{
|
|
||||||
Name: "pdftk_active_instances_count",
|
|
||||||
Description: "Current number of active PDFtk instances - deprecated.",
|
|
||||||
Read: func() float64 {
|
|
||||||
activeInstancesCountMu.RLock()
|
|
||||||
defer activeInstancesCountMu.RUnlock()
|
|
||||||
|
|
||||||
return activeInstancesCount
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge combines multiple PDFs into a single PDF.
|
// Merge combines multiple PDFs into a single PDF.
|
||||||
func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
var args []string
|
var args []string
|
||||||
@@ -80,16 +62,7 @@ func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths [
|
|||||||
return fmt.Errorf("create command: %w", err)
|
return fmt.Errorf("create command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount += 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
_, err = cmd.Exec()
|
_, err = cmd.Exec()
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount -= 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -102,16 +75,20 @@ func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats go
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// ReadMetadata is not available in this implementation.
|
||||||
activeInstancesCount float64
|
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
activeInstancesCountMu sync.RWMutex
|
return nil, fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*PdfTk)(nil)
|
_ gotenberg.Module = (*PdfTk)(nil)
|
||||||
_ gotenberg.Provisioner = (*PdfTk)(nil)
|
_ gotenberg.Provisioner = (*PdfTk)(nil)
|
||||||
_ gotenberg.Validator = (*PdfTk)(nil)
|
_ gotenberg.Validator = (*PdfTk)(nil)
|
||||||
_ gotenberg.MetricsProvider = (*PdfTk)(nil)
|
_ gotenberg.PdfEngine = (*PdfTk)(nil)
|
||||||
_ gotenberg.PdfEngine = (*PdfTk)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPdfTk_Descriptor(t *testing.T) {
|
func TestPdfTk_Descriptor(t *testing.T) {
|
||||||
@@ -71,22 +71,6 @@ func TestPdfTk_Validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPdfTk_Metrics(t *testing.T) {
|
|
||||||
metrics, err := new(PdfTk).Metrics()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(metrics) != 1 {
|
|
||||||
t.Fatalf("expected %d metrics, but got %d", 1, len(metrics))
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
|
||||||
if actual != 0 {
|
|
||||||
t.Errorf("expected %d PDFtk instances, but got %f", 0, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPdfTk_Merge(t *testing.T) {
|
func TestPdfTk_Merge(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
@@ -164,3 +148,21 @@ func TestPdfTk_Convert(t *testing.T) {
|
|||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfTk)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(PdfTk)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrometheus_Descriptor(t *testing.T) {
|
func TestPrometheus_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Package qpdf provides an implementation of the gotenberg.PdfEngine
|
// Package qpdf provides an implementation of the gotenberg.PdfEngine
|
||||||
// interface using the QPDF command-line tool. This package allows for the
|
// interface using the QPDF command-line tool. This package allows for:
|
||||||
// merging of PDF files but does not support conversion to specific PDF
|
//
|
||||||
// formats. The path to the QPDF binary must be specified using the
|
// 1. The merging of PDF files.
|
||||||
// QPDK_BIN_PATH environment variable.
|
//
|
||||||
|
// The path to the QPDF binary must be specified using the QPDK_BIN_PATH
|
||||||
|
// environment variable.
|
||||||
//
|
//
|
||||||
// See: https://github.com/qpdf/qpdf.
|
// See: https://github.com/qpdf/qpdf.
|
||||||
package qpdf
|
package qpdf
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -52,23 +51,6 @@ func (engine *QPdf) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics returns the metrics.
|
|
||||||
func (engine *QPdf) Metrics() ([]gotenberg.Metric, error) {
|
|
||||||
// TODO: remove deprecated.
|
|
||||||
return []gotenberg.Metric{
|
|
||||||
{
|
|
||||||
Name: "qpdf_active_instances_count",
|
|
||||||
Description: "Current number of active QPDF instances - deprecated.",
|
|
||||||
Read: func() float64 {
|
|
||||||
activeInstancesCountMu.RLock()
|
|
||||||
defer activeInstancesCountMu.RUnlock()
|
|
||||||
|
|
||||||
return activeInstancesCount
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge combines multiple PDFs into a single PDF.
|
// Merge combines multiple PDFs into a single PDF.
|
||||||
func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
var args []string
|
var args []string
|
||||||
@@ -82,16 +64,7 @@ func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
|
|||||||
return fmt.Errorf("create command: %w", err)
|
return fmt.Errorf("create command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount += 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
_, err = cmd.Exec()
|
_, err = cmd.Exec()
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount -= 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -104,15 +77,19 @@ func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats got
|
|||||||
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// ReadMetadata is not available in this implementation.
|
||||||
activeInstancesCount float64
|
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
activeInstancesCountMu sync.RWMutex
|
return nil, fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
// WriteMetadata is not available in this implementation.
|
||||||
|
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*QPdf)(nil)
|
_ gotenberg.Module = (*QPdf)(nil)
|
||||||
_ gotenberg.Provisioner = (*QPdf)(nil)
|
_ gotenberg.Provisioner = (*QPdf)(nil)
|
||||||
_ gotenberg.Validator = (*QPdf)(nil)
|
_ gotenberg.Validator = (*QPdf)(nil)
|
||||||
_ gotenberg.MetricsProvider = (*QPdf)(nil)
|
_ gotenberg.PdfEngine = (*QPdf)(nil)
|
||||||
_ gotenberg.PdfEngine = (*QPdf)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQPdf_Descriptor(t *testing.T) {
|
func TestQPdf_Descriptor(t *testing.T) {
|
||||||
@@ -71,22 +71,6 @@ func TestQPdf_Validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func TestQPdf_Merge(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
@@ -164,3 +148,21 @@ func TestQPdf_Convert(t *testing.T) {
|
|||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||||
|
engine := new(QPdf)
|
||||||
|
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||||
|
engine := new(QPdf)
|
||||||
|
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,7 +16,8 @@ import (
|
|||||||
"github.com/hashicorp/go-retryablehttp"
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func webhookMiddleware(w *Webhook) api.Middleware {
|
func webhookMiddleware(w *Webhook) api.Middleware {
|
||||||
@@ -44,38 +44,19 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's check if the webhook URLs are acceptable according to our
|
deadline, ok := ctx.Deadline()
|
||||||
// allowed/denied lists.
|
if !ok {
|
||||||
filter := func(URL, header string, allowList, denyList *regexp.Regexp) error {
|
return errors.New("context has no deadline")
|
||||||
if !allowList.MatchString(URL) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("'%s' does not match the expression from the allowed list", URL),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusForbidden,
|
|
||||||
fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if denyList.String() != "" && denyList.MatchString(URL) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("'%s' matches the expression from the denied list", URL),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusForbidden,
|
|
||||||
fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := filter(webhookUrl, "Gotenberg-Webhook-Url", w.allowList, w.denyList)
|
// Let's check if the webhook URLs are acceptable according to our
|
||||||
|
// allowed/denied lists.
|
||||||
|
err := gotenberg.FilterDeadline(w.allowList, w.denyList, webhookUrl, deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("filter webhook URL: %w", err)
|
return fmt.Errorf("filter webhook URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList)
|
err = gotenberg.FilterDeadline(w.errorAllowList, w.errorDenyList, webhookErrorUrl, deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("filter webhook error URL: %w", err)
|
return fmt.Errorf("filter webhook error URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebhookMiddlewareGuards(t *testing.T) {
|
func TestWebhookMiddlewareGuards(t *testing.T) {
|
||||||
@@ -47,10 +47,10 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
|
|
||||||
buildWebhookModule := func() *Webhook {
|
buildWebhookModule := func() *Webhook {
|
||||||
return &Webhook{
|
return &Webhook{
|
||||||
allowList: regexp.MustCompile(""),
|
allowList: regexp2.MustCompile("", 0),
|
||||||
denyList: regexp.MustCompile(""),
|
denyList: regexp2.MustCompile("", 0),
|
||||||
errorAllowList: regexp.MustCompile(""),
|
errorAllowList: regexp2.MustCompile("", 0),
|
||||||
errorDenyList: regexp.MustCompile(""),
|
errorDenyList: regexp2.MustCompile("", 0),
|
||||||
maxRetry: 0,
|
maxRetry: 0,
|
||||||
retryMinWait: 0,
|
retryMinWait: 0,
|
||||||
retryMaxWait: 0,
|
retryMaxWait: 0,
|
||||||
@@ -63,6 +63,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
request *http.Request
|
request *http.Request
|
||||||
mod *Webhook
|
mod *Webhook
|
||||||
next echo.HandlerFunc
|
next echo.HandlerFunc
|
||||||
|
noDeadline bool
|
||||||
expectError bool
|
expectError bool
|
||||||
expectHttpError bool
|
expectHttpError bool
|
||||||
expectHttpStatus int
|
expectHttpStatus int
|
||||||
@@ -76,6 +77,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
},
|
},
|
||||||
@@ -87,10 +89,23 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: true,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "context has no deadline",
|
||||||
|
request: func() *http.Request {
|
||||||
|
req := buildMultipartFormDataRequest()
|
||||||
|
req.Header.Set("Gotenberg-Webhook-Url", "foo")
|
||||||
|
req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: true,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "webhook URL is not allowed",
|
scenario: "webhook URL is not allowed",
|
||||||
request: func() *http.Request {
|
request: func() *http.Request {
|
||||||
@@ -101,12 +116,11 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.allowList = regexp.MustCompile("bar")
|
mod.allowList = regexp2.MustCompile("bar", 0)
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
expectError: true,
|
noDeadline: false,
|
||||||
expectHttpError: true,
|
expectError: true,
|
||||||
expectHttpStatus: http.StatusForbidden,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "webhook URL is denied",
|
scenario: "webhook URL is denied",
|
||||||
@@ -118,12 +132,11 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.denyList = regexp.MustCompile("foo")
|
mod.denyList = regexp2.MustCompile("foo", 0)
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
expectError: true,
|
noDeadline: false,
|
||||||
expectHttpError: true,
|
expectError: true,
|
||||||
expectHttpStatus: http.StatusForbidden,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "webhook error URL is not allowed",
|
scenario: "webhook error URL is not allowed",
|
||||||
@@ -135,12 +148,11 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.errorAllowList = regexp.MustCompile("foo")
|
mod.errorAllowList = regexp2.MustCompile("foo", 0)
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
expectError: true,
|
noDeadline: false,
|
||||||
expectHttpError: true,
|
expectError: true,
|
||||||
expectHttpStatus: http.StatusForbidden,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "webhook error URL is denied",
|
scenario: "webhook error URL is denied",
|
||||||
@@ -152,12 +164,11 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.errorDenyList = regexp.MustCompile("bar")
|
mod.errorDenyList = regexp2.MustCompile("bar", 0)
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
expectError: true,
|
noDeadline: false,
|
||||||
expectHttpError: true,
|
expectError: true,
|
||||||
expectHttpStatus: http.StatusForbidden,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "invalid webhook method (GET)",
|
scenario: "invalid webhook method (GET)",
|
||||||
@@ -169,6 +180,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: true,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
@@ -183,6 +195,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: true,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
@@ -198,6 +211,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: true,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
@@ -213,6 +227,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: true,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
@@ -228,6 +243,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: true,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
@@ -242,6 +258,7 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
mod: buildWebhookModule(),
|
mod: buildWebhookModule(),
|
||||||
|
noDeadline: false,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: true,
|
expectHttpError: true,
|
||||||
expectHttpStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
@@ -254,15 +271,20 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
|
|
||||||
c := srv.NewContext(tc.request, httptest.NewRecorder())
|
c := srv.NewContext(tc.request, httptest.NewRecorder())
|
||||||
|
|
||||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
if tc.noDeadline {
|
||||||
ctx.SetEchoContext(c)
|
ctx := &api.ContextMock{Context: &api.Context{Context: context.Background()}}
|
||||||
|
ctx.SetEchoContext(c)
|
||||||
c.Set("context", ctx.Context)
|
c.Set("context", ctx.Context)
|
||||||
c.Set("cancel", func() context.CancelFunc {
|
c.Set("cancel", func() context.CancelFunc {
|
||||||
return func() {
|
return nil
|
||||||
return
|
}())
|
||||||
}
|
} else {
|
||||||
}())
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
|
||||||
|
ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}}
|
||||||
|
ctx.SetEchoContext(c)
|
||||||
|
c.Set("context", ctx.Context)
|
||||||
|
c.Set("cancel", cancel)
|
||||||
|
}
|
||||||
|
|
||||||
err := webhookMiddleware(tc.mod).Handler(tc.next)(c)
|
err := webhookMiddleware(tc.mod).Handler(tc.next)(c)
|
||||||
|
|
||||||
@@ -320,10 +342,10 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
|
|
||||||
buildWebhookModule := func() *Webhook {
|
buildWebhookModule := func() *Webhook {
|
||||||
return &Webhook{
|
return &Webhook{
|
||||||
allowList: regexp.MustCompile(""),
|
allowList: regexp2.MustCompile("", 0),
|
||||||
denyList: regexp.MustCompile(""),
|
denyList: regexp2.MustCompile("", 0),
|
||||||
errorAllowList: regexp.MustCompile(""),
|
errorAllowList: regexp2.MustCompile("", 0),
|
||||||
errorDenyList: regexp.MustCompile(""),
|
errorDenyList: regexp2.MustCompile("", 0),
|
||||||
maxRetry: 0,
|
maxRetry: 0,
|
||||||
retryMinWait: 0,
|
retryMinWait: 0,
|
||||||
retryMaxWait: 0,
|
retryMaxWait: 0,
|
||||||
@@ -426,22 +448,17 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
c.Set("trace", "foo")
|
c.Set("trace", "foo")
|
||||||
c.Set("startTime", time.Now())
|
c.Set("startTime", time.Now())
|
||||||
|
|
||||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
|
||||||
|
ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}}
|
||||||
ctx.SetLogger(zap.NewNop())
|
ctx.SetLogger(zap.NewNop())
|
||||||
ctx.SetEchoContext(c)
|
ctx.SetEchoContext(c)
|
||||||
|
|
||||||
c.Set("context", ctx.Context)
|
c.Set("context", ctx.Context)
|
||||||
c.Set("cancel", func() context.CancelFunc {
|
c.Set("cancel", cancel)
|
||||||
return func() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}())
|
|
||||||
|
|
||||||
webhook := echo.New()
|
webhook := echo.New()
|
||||||
webhook.HideBanner = true
|
webhook.HideBanner = true
|
||||||
webhook.HidePort = true
|
webhook.HidePort = true
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
webhookPort := rand.Intn(65535-1025+1) + 1025
|
webhookPort := rand.Intn(65535-1025+1) + 1025
|
||||||
|
|
||||||
c.Request().Header.Set("Gotenberg-Webhook-Url", fmt.Sprintf("http://localhost:%d/", webhookPort))
|
c.Request().Header.Set("Gotenberg-Webhook-Url", fmt.Sprintf("http://localhost:%d/", webhookPort))
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package webhook
|
package webhook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"go.uber.org/multierr"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -19,10 +17,10 @@ func init() {
|
|||||||
// Webhook is a module which provides a middleware for uploading output files
|
// Webhook is a module which provides a middleware for uploading output files
|
||||||
// to any destinations in an asynchronous fashion.
|
// to any destinations in an asynchronous fashion.
|
||||||
type Webhook struct {
|
type Webhook struct {
|
||||||
allowList *regexp.Regexp
|
allowList *regexp2.Regexp
|
||||||
denyList *regexp.Regexp
|
denyList *regexp2.Regexp
|
||||||
errorAllowList *regexp.Regexp
|
errorAllowList *regexp2.Regexp
|
||||||
errorDenyList *regexp.Regexp
|
errorDenyList *regexp2.Regexp
|
||||||
maxRetry int
|
maxRetry int
|
||||||
retryMinWait time.Duration
|
retryMinWait time.Duration
|
||||||
retryMaxWait time.Duration
|
retryMaxWait time.Duration
|
||||||
@@ -36,31 +34,6 @@ func (w *Webhook) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
ID: "webhook",
|
ID: "webhook",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("webhook", flag.ExitOnError)
|
fs := flag.NewFlagSet("webhook", flag.ExitOnError)
|
||||||
// Deprecated flags.
|
|
||||||
fs.String("api-webhook-allow-list", "", "Set the allowed URLs for the webhook feature using a regular expression")
|
|
||||||
fs.String("api-webhook-deny-list", "", "Set the denied URLs for the webhook feature using a regular expression")
|
|
||||||
fs.String("api-webhook-error-allow-list", "", "Set the allowed URLs in case of an error for the webhook feature using a regular expression")
|
|
||||||
fs.String("api-webhook-error-deny-list", "", "Set the denied URLs in case of an error for the webhook feature using a regular expression")
|
|
||||||
fs.Int("api-webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
|
|
||||||
fs.Duration("api-webhook-retry-min-wait", time.Duration(1)*time.Second, "Set the minimum duration to wait before trying to call the webhook again")
|
|
||||||
fs.Duration("api-webhook-retry-max-wait", time.Duration(30)*time.Second, "Set the maximum duration to wait before trying to call the webhook again")
|
|
||||||
fs.Bool("api-disable-webhook", false, "Disable the webhook feature")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-webhook-allow-list", "use webhook-allow-list instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-webhook-deny-list", "use webhook-deny-list instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-webhook-error-allow-list", "use webhook-error-allow-list instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-webhook-error-deny-list", "use webhook-error-deny-list instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-webhook-max-retry", "use webhook-max-retry instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-webhook-retry-min-wait", "use webhook-retry-min-wait instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-webhook-retry-max-wait", "use webhook-retry-max-wait instead"))
|
|
||||||
err = multierr.Append(err, fs.MarkDeprecated("api-disable-webhook", "use webhook-disable instead"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("create deprecated flags for the webhook module: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// New flags.
|
|
||||||
fs.String("webhook-allow-list", "", "Set the allowed URLs for the webhook feature using a regular expression")
|
fs.String("webhook-allow-list", "", "Set the allowed URLs for the webhook feature using a regular expression")
|
||||||
fs.String("webhook-deny-list", "", "Set the denied URLs for the webhook feature using a regular expression")
|
fs.String("webhook-deny-list", "", "Set the denied URLs for the webhook feature using a regular expression")
|
||||||
fs.String("webhook-error-allow-list", "", "Set the allowed URLs in case of an error for the webhook feature using a regular expression")
|
fs.String("webhook-error-allow-list", "", "Set the allowed URLs in case of an error for the webhook feature using a regular expression")
|
||||||
@@ -80,15 +53,15 @@ func (w *Webhook) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
// Provision sets the module properties.
|
// Provision sets the module properties.
|
||||||
func (w *Webhook) Provision(ctx *gotenberg.Context) error {
|
func (w *Webhook) Provision(ctx *gotenberg.Context) error {
|
||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
w.allowList = flags.MustDeprecatedRegexp("api-webhook-allow-list", "webhook-allow-list")
|
w.allowList = flags.MustRegexp("webhook-allow-list")
|
||||||
w.denyList = flags.MustDeprecatedRegexp("api-webhook-deny-list", "webhook-deny-list")
|
w.denyList = flags.MustRegexp("webhook-deny-list")
|
||||||
w.errorAllowList = flags.MustDeprecatedRegexp("api-webhook-error-allow-list", "webhook-error-allow-list")
|
w.errorAllowList = flags.MustRegexp("webhook-error-allow-list")
|
||||||
w.errorDenyList = flags.MustDeprecatedRegexp("api-webhook-error-deny-list", "webhook-error-deny-list")
|
w.errorDenyList = flags.MustRegexp("webhook-error-deny-list")
|
||||||
w.maxRetry = flags.MustDeprecatedInt("api-webhook-max-retry", "webhook-max-retry")
|
w.maxRetry = flags.MustInt("webhook-max-retry")
|
||||||
w.retryMinWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-min-wait")
|
w.retryMinWait = flags.MustDuration("webhook-retry-min-wait")
|
||||||
w.retryMaxWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-max-wait")
|
w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")
|
||||||
w.clientTimeout = flags.MustDuration("webhook-client-timeout")
|
w.clientTimeout = flags.MustDuration("webhook-client-timeout")
|
||||||
w.disable = flags.MustDeprecatedBool("api-disable-webhook", "webhook-disable")
|
w.disable = flags.MustBool("webhook-disable")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebhook_Descriptor(t *testing.T) {
|
func TestWebhook_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ package standard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
// Standard Gotenberg modules.
|
// Standard Gotenberg modules.
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/chromium"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/chromium"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/exiftool"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/logging"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/pdfengine"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfcpu"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/logging"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/pdfcpu"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/pdftk"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/qpdf"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/prometheus"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/qpdf"
|
||||||
|
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,4 +3,8 @@
|
|||||||
set -x
|
set -x
|
||||||
|
|
||||||
go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./...
|
go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./...
|
||||||
go tool cover -html=coverage.txt -o /tests/coverage.html
|
RESULT=$?
|
||||||
|
|
||||||
|
go tool cover -html=coverage.txt -o /tests/coverage.html
|
||||||
|
|
||||||
|
exit $RESULT
|
||||||
4
test/testdata/chromium/html/footer.html
vendored
4
test/testdata/chromium/html/footer.html
vendored
@@ -2,8 +2,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-size: 30rem;
|
font-size: 12px;
|
||||||
margin: 4rem auto;
|
margin: auto 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
4
test/testdata/chromium/html/header.html
vendored
4
test/testdata/chromium/html/header.html
vendored
@@ -2,8 +2,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-size: 8rem;
|
font-size: 12px;
|
||||||
margin: 4rem auto;
|
margin: auto 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
8
test/testdata/chromium/html/index.html
vendored
8
test/testdata/chromium/html/index.html
vendored
@@ -59,6 +59,14 @@
|
|||||||
<p id="javascript" style="display: none">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
<p id="javascript" style="display: none">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="page-break-after">
|
||||||
|
<h2>/etc/passwd</h2>
|
||||||
|
<iframe src="/etc/passwd"></iframe>
|
||||||
|
|
||||||
|
<h2>\\localhost/etc/passwd</h2>
|
||||||
|
<iframe src="\\localhost/etc/passwd"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
var globalVar = 'notReady'
|
var globalVar = 'notReady'
|
||||||
|
|
||||||
|
|||||||
4
test/testdata/chromium/markdown/footer.html
vendored
4
test/testdata/chromium/markdown/footer.html
vendored
@@ -2,8 +2,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-size: 8rem;
|
font-size: 12px;
|
||||||
margin: 4rem auto;
|
margin: auto 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
4
test/testdata/chromium/markdown/header.html
vendored
4
test/testdata/chromium/markdown/header.html
vendored
@@ -2,8 +2,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-size: 8rem;
|
font-size: 12px;
|
||||||
margin: 4rem auto;
|
margin: auto 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user