mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 19:32:15 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b4d435c3 | ||
|
|
a8bde9d396 | ||
|
|
5c6f29095f | ||
|
|
0d3942848c | ||
|
|
9256a203ef | ||
|
|
f13e045e7f | ||
|
|
a60cb33ff1 | ||
|
|
dec32d981f | ||
|
|
3046615c3c | ||
|
|
130b94aa8c |
15
.github/workflows/continuous_integration.yml
vendored
15
.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.22'
|
go-version: '1.21'
|
||||||
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@v6
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
version: v1.59.0
|
version: v1.54.2
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
needs:
|
needs:
|
||||||
@@ -43,12 +43,9 @@ jobs:
|
|||||||
- name: Build testing environment
|
- name: Build testing environment
|
||||||
run: make build build-tests
|
run: make build build-tests
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make tests-once
|
run: |
|
||||||
- name: Upload to Codecov
|
make tests-once
|
||||||
uses: codecov/codecov-action@v4
|
bash <(curl -s https://codecov.io/bash)
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
multiarch_build:
|
multiarch_build:
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,3 @@
|
|||||||
/coverage.html
|
/coverage.html
|
||||||
/coverage.txt
|
/coverage.txt
|
||||||
.idea
|
/TODO.txt
|
||||||
.vscode
|
|
||||||
.DS_Store
|
|
||||||
@@ -3,54 +3,35 @@ linters-settings:
|
|||||||
sections:
|
sections:
|
||||||
- standard
|
- standard
|
||||||
- default
|
- default
|
||||||
- prefix(github.com/gotenberg/gotenberg/v8)
|
- prefix(github.com/gotenberg/gotenberg/v7)
|
||||||
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
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- tenv
|
|
||||||
- testableexamples
|
|
||||||
- tparallel
|
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- wastedassign
|
|
||||||
- whitespace
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 5m
|
deadline: 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) 2024 Julien Neuhart
|
Copyright (c) 2023 Julien Neuhart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
34
Makefile
34
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.22
|
GOLANG_VERSION=1.21
|
||||||
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.042 # See https://github.com/googlefonts/noto-emoji/releases.
|
NOTO_COLOR_EMOJI_VERSION=v2.040 # 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.59.0 # See https://github.com/golangci/golangci-lint/releases.
|
GOLANGCI_LINT_VERSION=v1.54.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
|
||||||
@@ -33,12 +33,8 @@ API_START_TIMEOUT=30s
|
|||||||
API_TIMEOUT=30s
|
API_TIMEOUT=30s
|
||||||
API_ROOT_PATH=/
|
API_ROOT_PATH=/
|
||||||
API_TRACE_HEADER=Gotenberg-Trace
|
API_TRACE_HEADER=Gotenberg-Trace
|
||||||
API_ENABLE_BASIC_AUTH=false
|
|
||||||
GOTENBERG_API_BASIC_AUTH_USERNAME=
|
|
||||||
GOTENBERG_API_BASIC_AUTH_PASSWORD=
|
|
||||||
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
|
||||||
@@ -49,13 +45,10 @@ 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
|
||||||
@@ -82,8 +75,6 @@ WEBHOOK_DISABLE=false
|
|||||||
run: ## Start a Gotenberg container
|
run: ## Start a Gotenberg container
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-p $(API_PORT):$(API_PORT) \
|
-p $(API_PORT):$(API_PORT) \
|
||||||
-e GOTENBERG_API_BASIC_AUTH_USERNAME=$(GOTENBERG_API_BASIC_AUTH_USERNAME) \
|
|
||||||
-e GOTENBERG_API_BASIC_AUTH_PASSWORD=$(GOTENBERG_API_BASIC_AUTH_PASSWORD) \
|
|
||||||
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
|
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION) \
|
||||||
gotenberg \
|
gotenberg \
|
||||||
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||||
@@ -93,11 +84,9 @@ run: ## Start a Gotenberg container
|
|||||||
--api-timeout=$(API_TIMEOUT) \
|
--api-timeout=$(API_TIMEOUT) \
|
||||||
--api-root-path=$(API_ROOT_PATH) \
|
--api-root-path=$(API_ROOT_PATH) \
|
||||||
--api-trace-header=$(API_TRACE_HEADER) \
|
--api-trace-header=$(API_TRACE_HEADER) \
|
||||||
--api-enable-basic-auth=$(API_ENABLE_BASIC_AUTH) \
|
|
||||||
--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) \
|
||||||
@@ -106,14 +95,11 @@ 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) \
|
||||||
@@ -126,8 +112,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) \
|
||||||
@@ -165,13 +151,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/v8)" --skip-generated --skip-vendor --custom-order .
|
gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v7)" --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/v8)
|
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v7)
|
||||||
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:8
|
docker run --rm -p 3000:3000 gotenberg/gotenberg:7
|
||||||
```
|
```
|
||||||
|
|
||||||
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:8
|
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:7
|
||||||
```
|
```
|
||||||
|
|
||||||
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/v8)
|
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
|
||||||
[](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/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v7/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
||||||
|
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
# Final stage
|
# Final stage
|
||||||
@@ -164,20 +164,19 @@ RUN \
|
|||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
# Install PDFtk, QPDF & ExifTool (PDF engines).
|
# Install PDFtk & QPDF (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 exiftool &&\
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
|
||||||
# See https://github.com/nextcloud/docker/issues/380.
|
# See https://github.com/nextcloud/docker/issues/380.
|
||||||
mkdir -p /usr/share/man/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/*
|
||||||
|
|
||||||
@@ -196,7 +195,6 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/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.
|
||||||
@@ -35,7 +35,6 @@ var Version = "snapshot"
|
|||||||
// Run starts the Gotenberg application. Call this in the main of your program.
|
// Run starts the Gotenberg application. Call this in the main of your program.
|
||||||
func Run() {
|
func Run() {
|
||||||
fmt.Printf(banner, Version)
|
fmt.Printf(banner, Version)
|
||||||
gotenberg.Version = Version
|
|
||||||
|
|
||||||
// Create the root FlagSet and adds the modules flags to it.
|
// Create the root FlagSet and adds the modules flags to it.
|
||||||
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
||||||
@@ -76,6 +75,7 @@ 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/v8/cmd"
|
gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd"
|
||||||
// Gotenberg modules.
|
// Gotenberg modules.
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/standard"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
744
docs/openapi.yaml
Normal file
744
docs/openapi.yaml
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
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
|
||||||
62
go.mod
62
go.mod
@@ -1,54 +1,50 @@
|
|||||||
module github.com/gotenberg/gotenberg/v8
|
module github.com/gotenberg/gotenberg/v7
|
||||||
|
|
||||||
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.1.0 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
github.com/barasher/go-exiftool v1.10.0
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61
|
||||||
github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881
|
github.com/chromedp/chromedp v0.9.3
|
||||||
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.6.0
|
github.com/google/uuid v1.5.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.7
|
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||||
github.com/klauspost/compress v1.17.8 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/labstack/echo/v4 v4.12.0
|
github.com/labstack/echo/v4 v4.11.3
|
||||||
github.com/labstack/gommon v0.4.2
|
github.com/labstack/gommon v0.4.1
|
||||||
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.8.0
|
github.com/pdfcpu/pdfcpu v0.6.0
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
||||||
github.com/prometheus/client_golang v1.19.1
|
github.com/prometheus/client_golang v1.17.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.12 // 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.27.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.16.0 // indirect
|
||||||
golang.org/x/image v0.16.0 // indirect
|
golang.org/x/image v0.14.0 // indirect
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/net v0.19.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.5.0
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
golang.org/x/term v0.20.0
|
golang.org/x/term v0.15.0
|
||||||
golang.org/x/text v0.15.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
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/gobwas/ws v1.3.1 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // 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
|
||||||
@@ -56,15 +52,15 @@ 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.6.1 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.54.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.4 // 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
|
||||||
golang.org/x/time v0.5.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
143
go.sum
143
go.sum
@@ -1,58 +1,50 @@
|
|||||||
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.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.3.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-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881 h1:RAUqkPvbEDGPgCYVc4GefBqAorWJAjKpVHgsRZyJmGE=
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61 h1:XD280QPATe9jaz20dylKe3vBsNcH1w3mkssGY0lidn8=
|
||||||
github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
||||||
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
||||||
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=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
|
||||||
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.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=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||||
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||||
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||||
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
|
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
|
||||||
@@ -61,8 +53,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.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
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=
|
||||||
@@ -71,10 +63,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.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
|
||||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk=
|
||||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM=
|
||||||
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=
|
||||||
@@ -86,6 +78,8 @@ 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=
|
||||||
@@ -95,76 +89,73 @@ 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.8.0 h1:SuEB4uVsPFz1nb802r38YpFpj9TtZh/oB0bGG34IRZw=
|
github.com/pdfcpu/pdfcpu v0.6.0 h1:z4kARP5bcWa39TTYMcN/kjBnm7MvhTWjXgeYmkdAGMI=
|
||||||
github.com/pdfcpu/pdfcpu v0.8.0/go.mod h1:jj03y/KKrwigt5xCi8t7px2mATcKuOzkIOoCX62yMho=
|
github.com/pdfcpu/pdfcpu v0.6.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.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.19/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.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
|
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||||
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
|
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.4/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
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=
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/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.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
|
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||||
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
|
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.5.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.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|
||||||
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.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
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=
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StringEnv retrieves the value of the environment variable named by the key.
|
|
||||||
// If the variable is present in the environment and not empty, the value is
|
|
||||||
// returned.
|
|
||||||
func StringEnv(key string) (string, error) {
|
|
||||||
val, ok := os.LookupEnv(key)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("environment variable '%s' does not exist", key)
|
|
||||||
}
|
|
||||||
if val == "" {
|
|
||||||
return "", fmt.Errorf("environment variable '%s' is empty", key)
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntEnv relies on [StringEnv] and converts the values if it exists and is not
|
|
||||||
// empty.
|
|
||||||
func IntEnv(key string) (int, error) {
|
|
||||||
val, err := StringEnv(key)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
intVal, err := strconv.Atoi(val)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("get int value of environment variable '%s': %w", key, err)
|
|
||||||
}
|
|
||||||
return intVal, nil
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStringEnv(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
scenario string
|
|
||||||
key string
|
|
||||||
setEnv func()
|
|
||||||
expectVal string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
scenario: "non-existing environment variable",
|
|
||||||
key: "NON_EXISTING",
|
|
||||||
expectVal: "",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "empty environment variable",
|
|
||||||
key: "EMPTY_STRING",
|
|
||||||
setEnv: func() {
|
|
||||||
err := os.Setenv("EMPTY_STRING", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectVal: "",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success",
|
|
||||||
key: "EXISTING_STRING_VALUE",
|
|
||||||
setEnv: func() {
|
|
||||||
err := os.Setenv("EXISTING_STRING_VALUE", "foo")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectVal: "foo",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
|
||||||
if tc.setEnv != nil {
|
|
||||||
tc.setEnv()
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := StringEnv(tc.key)
|
|
||||||
|
|
||||||
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.expectVal != val {
|
|
||||||
t.Errorf("expected value '%s' but got '%s'", tc.expectVal, val)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntEnv(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
scenario string
|
|
||||||
key string
|
|
||||||
setEnv func()
|
|
||||||
expectVal int
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
scenario: "empty environment variable",
|
|
||||||
key: "EMPTY_INT",
|
|
||||||
setEnv: func() {
|
|
||||||
err := os.Setenv("EMPTY_INT", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectVal: 0,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "non-integer value",
|
|
||||||
key: "NON_INTEGER",
|
|
||||||
setEnv: func() {
|
|
||||||
err := os.Setenv("NON_INTEGER", "foo")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectVal: 0,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success",
|
|
||||||
key: "EXISTING_INT_VALUE",
|
|
||||||
setEnv: func() {
|
|
||||||
err := os.Setenv("EXISTING_INT_VALUE", "123")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectVal: 123,
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
|
||||||
if tc.setEnv != nil {
|
|
||||||
tc.setEnv()
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := IntEnv(tc.key)
|
|
||||||
|
|
||||||
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.expectVal != val {
|
|
||||||
t.Errorf("expected value %d but got %d", tc.expectVal, val)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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) *regexp2.Regexp {
|
func (f *ParsedFlags) MustRegexp(name string) *regexp.Regexp {
|
||||||
val, err := f.GetString(name)
|
val, err := f.GetString(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return regexp2.MustCompile(val, 0)
|
return regexp.MustCompile(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) *regexp2.Regexp {
|
func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp.Regexp {
|
||||||
if f.Changed(deprecated) {
|
if f.Changed(deprecated) {
|
||||||
return f.MustRegexp(deprecated)
|
return f.MustRegexp(deprecated)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,11 +51,3 @@ 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,10 +35,8 @@ 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 {
|
||||||
@@ -49,14 +47,6 @@ 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)
|
||||||
@@ -137,15 +127,6 @@ 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,12 +55,6 @@ 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, "")
|
||||||
@@ -72,16 +66,6 @@ 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) {
|
||||||
@@ -204,16 +188,3 @@ 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/v8/cmd/gotenberg),
|
// Then, in the main command (github.com/gotenberg/gotenberg/v7/cmd/gotenberg),
|
||||||
// import the module:
|
// import the module:
|
||||||
//
|
//
|
||||||
// imports (
|
// imports (
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ 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 (
|
||||||
@@ -68,12 +64,6 @@ 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].
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
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)
|
|
||||||
)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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,10 +13,6 @@ 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.
|
||||||
@@ -78,7 +74,6 @@ 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
|
||||||
@@ -88,13 +83,12 @@ type processSupervisor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessSupervisor initializes a new [ProcessSupervisor].
|
// NewProcessSupervisor initializes a new [ProcessSupervisor].
|
||||||
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit, maxQueueSize int64) ProcessSupervisor {
|
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit 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)
|
||||||
@@ -173,11 +167,6 @@ 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, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 5).(*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, 0)
|
ps := NewProcessSupervisor(logger, process, 5)
|
||||||
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, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 5).(*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, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
||||||
if tc.initiallyStarted {
|
if tc.initiallyStarted {
|
||||||
ps.firstStart.Store(true)
|
ps.firstStart.Store(true)
|
||||||
}
|
}
|
||||||
@@ -249,8 +249,6 @@ 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",
|
||||||
@@ -350,34 +348,6 @@ 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()
|
||||||
@@ -402,16 +372,13 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit, tc.maxQueueSize).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit).(*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
|
||||||
@@ -484,13 +451,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, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 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 {
|
||||||
@@ -518,7 +485,7 @@ func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
|
||||||
|
|
||||||
// Simulating a lock.
|
// Simulating a lock.
|
||||||
ps.mutexChan <- struct{}{}
|
ps.mutexChan <- struct{}{}
|
||||||
@@ -619,7 +586,7 @@ func TestProcessSupervisor_RestartsCount(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
|
ps := NewProcessSupervisor(logger, process, 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++ {
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
// Version is the... version of the Gotenberg application.
|
|
||||||
var Version = "snapshot"
|
|
||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -28,14 +30,12 @@ func init() {
|
|||||||
// middlewares or health checks.
|
// middlewares or health checks.
|
||||||
type Api struct {
|
type Api struct {
|
||||||
port int
|
port int
|
||||||
tlsCertFile string
|
readTimeout time.Duration
|
||||||
tlsKeyFile string
|
writeTimeout time.Duration
|
||||||
startTimeout time.Duration
|
startTimeout time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
rootPath string
|
rootPath string
|
||||||
traceHeader string
|
traceHeader string
|
||||||
basicAuthUsername string
|
|
||||||
basicAuthPassword string
|
|
||||||
disableHealthCheckLogging bool
|
disableHealthCheckLogging bool
|
||||||
|
|
||||||
routes []Route
|
routes []Route
|
||||||
@@ -161,14 +161,24 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
||||||
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.String("api-tls-cert-file", "", "Path to the TLS/SSL certificate file - for HTTPS support")
|
|
||||||
fs.String("api-tls-key-file", "", "Path to the TLS/SSL key file - for HTTPS support")
|
|
||||||
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-enable-basic-auth", false, "Enable basic authentication - will look for the GOTENBERG_API_BASIC_AUTH_USERNAME and GOTENBERG_API_BASIC_AUTH_PASSWORD environment variables")
|
|
||||||
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) },
|
||||||
@@ -179,10 +189,10 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
a.port = flags.MustInt("api-port")
|
a.port = flags.MustInt("api-port")
|
||||||
a.tlsCertFile = flags.MustString("api-tls-cert-file")
|
|
||||||
a.tlsKeyFile = flags.MustString("api-tls-key-file")
|
|
||||||
a.startTimeout = flags.MustDuration("api-start-timeout")
|
a.startTimeout = flags.MustDuration("api-start-timeout")
|
||||||
a.timeout = flags.MustDuration("api-timeout")
|
a.readTimeout = flags.MustDeprecatedDuration("api-read-timeout", "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")
|
||||||
@@ -190,26 +200,22 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
|||||||
// Port from env?
|
// Port from env?
|
||||||
portEnvVar := flags.MustString("api-port-from-env")
|
portEnvVar := flags.MustString("api-port-from-env")
|
||||||
if portEnvVar != "" {
|
if portEnvVar != "" {
|
||||||
port, err := gotenberg.IntEnv(portEnvVar)
|
val, ok := os.LookupEnv(portEnvVar)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get API port from env: %w", err)
|
|
||||||
}
|
|
||||||
a.port = port
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable basic auth?
|
if !ok {
|
||||||
enableBasicAuth := flags.MustBool("api-enable-basic-auth")
|
return fmt.Errorf("environment variable '%s' does not exist", portEnvVar)
|
||||||
if enableBasicAuth {
|
|
||||||
basicAuthUsername, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_USERNAME")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get basic auth username from env: %w", err)
|
|
||||||
}
|
}
|
||||||
basicAuthPassword, err := gotenberg.StringEnv("GOTENBERG_API_BASIC_AUTH_PASSWORD")
|
|
||||||
if err != nil {
|
if val == "" {
|
||||||
return fmt.Errorf("get basic auth password from env: %w", err)
|
return fmt.Errorf("environment variable '%s' is empty", portEnvVar)
|
||||||
}
|
}
|
||||||
a.basicAuthUsername = basicAuthUsername
|
|
||||||
a.basicAuthPassword = basicAuthPassword
|
port, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get int value of environment variable '%s': %w", portEnvVar, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.port = port
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get routes from modules.
|
// Get routes from modules.
|
||||||
@@ -307,12 +313,6 @@ func (a *Api) Validate() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.tlsCertFile != "" && a.tlsKeyFile == "") || (a.tlsCertFile == "" && a.tlsKeyFile != "") {
|
|
||||||
err = multierr.Append(err,
|
|
||||||
errors.New("both TLS certificate and key files must be set"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(a.rootPath, "/") {
|
if !strings.HasPrefix(a.rootPath, "/") {
|
||||||
err = multierr.Append(err,
|
err = multierr.Append(err,
|
||||||
errors.New("root path must start with /"),
|
errors.New("root path must start with /"),
|
||||||
@@ -335,9 +335,8 @@ func (a *Api) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
routesMap := make(map[string]string, len(a.routes)+2)
|
routesMap := make(map[string]string, len(a.routes)+1)
|
||||||
routesMap["/health"] = "/health"
|
routesMap["/health"] = "/health"
|
||||||
routesMap["/version"] = "/version"
|
|
||||||
|
|
||||||
for _, route := range a.routes {
|
for _, route := range a.routes {
|
||||||
if route.Path == "" {
|
if route.Path == "" {
|
||||||
@@ -381,10 +380,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.timeout
|
a.srv.Server.ReadTimeout = a.readTimeout
|
||||||
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.timeout + a.timeout
|
a.srv.Server.WriteTimeout = a.writeTimeout + a.writeTimeout
|
||||||
a.srv.HTTPErrorHandler = httpErrorHandler()
|
a.srv.HTTPErrorHandler = httpErrorHandler()
|
||||||
|
|
||||||
// Let's prepare the modules' routes.
|
// Let's prepare the modules' routes.
|
||||||
@@ -411,13 +410,6 @@ func (a *Api) Start() error {
|
|||||||
loggerMiddleware(a.logger, disableLoggingForPaths),
|
loggerMiddleware(a.logger, disableLoggingForPaths),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Basic auth?
|
|
||||||
if a.basicAuthUsername != "" {
|
|
||||||
a.srv.Pre(
|
|
||||||
basicAuthMiddleware(a.basicAuthUsername, a.basicAuthPassword),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the modules' middlewares in their respective stacks.
|
// Add the modules' middlewares in their respective stacks.
|
||||||
var externalMultipartMiddlewares []Middleware
|
var externalMultipartMiddlewares []Middleware
|
||||||
for _, externalMiddleware := range a.externalMiddlewares {
|
for _, externalMiddleware := range a.externalMiddlewares {
|
||||||
@@ -426,7 +418,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)
|
||||||
case DefaultStack:
|
default:
|
||||||
a.srv.Use(externalMiddleware.Handler)
|
a.srv.Use(externalMiddleware.Handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,7 +447,7 @@ func (a *Api) Start() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's not forget the health check route...
|
// Let's not forget the health check route.
|
||||||
a.srv.GET(
|
a.srv.GET(
|
||||||
fmt.Sprintf("%s%s", a.rootPath, "health"),
|
fmt.Sprintf("%s%s", a.rootPath, "health"),
|
||||||
func() echo.HandlerFunc {
|
func() echo.HandlerFunc {
|
||||||
@@ -466,14 +458,6 @@ func (a *Api) Start() error {
|
|||||||
hardTimeoutMiddleware(hardTimeout),
|
hardTimeoutMiddleware(hardTimeout),
|
||||||
)
|
)
|
||||||
|
|
||||||
// ...and the version route.
|
|
||||||
a.srv.GET(
|
|
||||||
fmt.Sprintf("%s%s", a.rootPath, "version"),
|
|
||||||
func(c echo.Context) error {
|
|
||||||
return c.String(http.StatusOK, gotenberg.Version)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all modules to be ready.
|
// Wait for all modules to be ready.
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), a.startTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), a.startTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -490,15 +474,8 @@ func (a *Api) Start() error {
|
|||||||
|
|
||||||
// As the following code is blocking, run it in a goroutine.
|
// As the following code is blocking, run it in a goroutine.
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
server := &http2.Server{}
|
||||||
if a.tlsCertFile != "" && a.tlsKeyFile != "" {
|
err := a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server)
|
||||||
// Start an HTTPS server (supports HTTP/2).
|
|
||||||
err = a.srv.StartTLS(fmt.Sprintf(":%d", a.port), a.tlsCertFile, a.tlsKeyFile)
|
|
||||||
} else {
|
|
||||||
// Start an HTTP/2 Cleartext (non-HTTPS) server.
|
|
||||||
server := &http2.Server{}
|
|
||||||
err = a.srv.StartH2CServer(fmt.Sprintf(":%d", a.port), server)
|
|
||||||
}
|
|
||||||
if !errors.Is(err, http.ErrServerClosed) {
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
a.logger.Fatal(err.Error())
|
a.logger.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApi_Descriptor(t *testing.T) {
|
func TestApi_Descriptor(t *testing.T) {
|
||||||
@@ -58,28 +58,10 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_USERNAME environment variable",
|
scenario: "port from env: empty environment variable",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
fs := new(Api).Descriptor().FlagSet
|
fs := new(Api).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--api-enable-basic-auth=true"})
|
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: fs,
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_PASSWORD environment variable",
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
fs := new(Api).Descriptor().FlagSet
|
|
||||||
err := fs.Parse([]string{"--api-enable-basic-auth=true"})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -92,7 +74,7 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
setEnv: func() {
|
setEnv: func() {
|
||||||
err := os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
|
err := os.Setenv("PORT", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -379,7 +361,7 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs := new(Api).Descriptor().FlagSet
|
fs := new(Api).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--api-port-from-env=PORT", "--api-enable-basic-auth=true"})
|
err := fs.Parse([]string{"--api-port-from-env=PORT"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -401,14 +383,6 @@ func TestApi_Provision(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
err = os.Setenv("GOTENBERG_API_BASIC_AUTH_PASSWORD", "bar")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
expectPort: 1337,
|
expectPort: 1337,
|
||||||
expectMiddlewares: []Middleware{
|
expectMiddlewares: []Middleware{
|
||||||
@@ -462,8 +436,6 @@ func TestApi_Validate(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
port int
|
port int
|
||||||
tlsCertFile string
|
|
||||||
tlsKeyFile string
|
|
||||||
rootPath string
|
rootPath string
|
||||||
traceHeader string
|
traceHeader string
|
||||||
routes []Route
|
routes []Route
|
||||||
@@ -488,26 +460,6 @@ func TestApi_Validate(t *testing.T) {
|
|||||||
middlewares: nil,
|
middlewares: nil,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
scenario: "invalid TLS files: only cert file provided",
|
|
||||||
port: 10,
|
|
||||||
tlsCertFile: "cert.pem",
|
|
||||||
rootPath: "/foo/",
|
|
||||||
traceHeader: "foo",
|
|
||||||
routes: nil,
|
|
||||||
middlewares: nil,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "invalid TLS files: only key file provided",
|
|
||||||
port: 10,
|
|
||||||
tlsKeyFile: "key.pem",
|
|
||||||
rootPath: "/foo/",
|
|
||||||
traceHeader: "foo",
|
|
||||||
routes: nil,
|
|
||||||
middlewares: nil,
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
scenario: "invalid root path: missing / prefix",
|
scenario: "invalid root path: missing / prefix",
|
||||||
port: 10,
|
port: 10,
|
||||||
@@ -669,33 +621,10 @@ func TestApi_Validate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
scenario: "success with TLS",
|
|
||||||
port: 10,
|
|
||||||
tlsCertFile: "cert.pem",
|
|
||||||
tlsKeyFile: "key.pem",
|
|
||||||
rootPath: "/foo/",
|
|
||||||
traceHeader: "foo",
|
|
||||||
routes: []Route{
|
|
||||||
{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
Path: "/foo",
|
|
||||||
Handler: func(_ echo.Context) error { return nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
Path: "/forms/foo",
|
|
||||||
Handler: func(_ echo.Context) error { return nil },
|
|
||||||
IsMultipart: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod := Api{
|
mod := Api{
|
||||||
port: tc.port,
|
port: tc.port,
|
||||||
tlsCertFile: tc.tlsCertFile,
|
|
||||||
tlsKeyFile: tc.tlsKeyFile,
|
|
||||||
rootPath: tc.rootPath,
|
rootPath: tc.rootPath,
|
||||||
traceHeader: tc.traceHeader,
|
traceHeader: tc.traceHeader,
|
||||||
routes: tc.routes,
|
routes: tc.routes,
|
||||||
@@ -718,8 +647,6 @@ func TestApi_Start(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
readyFn []func() error
|
readyFn []func() error
|
||||||
tlsCertFile string
|
|
||||||
tlsKeyFile string
|
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -738,26 +665,12 @@ func TestApi_Start(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
scenario: "success with TLS",
|
|
||||||
readyFn: []func() error{
|
|
||||||
func() error { return nil },
|
|
||||||
func() error { return nil },
|
|
||||||
},
|
|
||||||
tlsCertFile: "/tests/test/testdata/api/cert.pem",
|
|
||||||
tlsKeyFile: "/tests/test/testdata/api/key.pem",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod := new(Api)
|
mod := new(Api)
|
||||||
mod.port = 3000
|
mod.port = 3000
|
||||||
mod.tlsCertFile = tc.tlsCertFile
|
|
||||||
mod.tlsKeyFile = tc.tlsKeyFile
|
|
||||||
mod.startTimeout = time.Duration(30) * time.Second
|
mod.startTimeout = time.Duration(30) * time.Second
|
||||||
mod.rootPath = "/"
|
mod.rootPath = "/"
|
||||||
mod.basicAuthUsername = "foo"
|
|
||||||
mod.basicAuthPassword = "bar"
|
|
||||||
mod.disableHealthCheckLogging = true
|
mod.disableHealthCheckLogging = true
|
||||||
mod.routes = []Route{
|
mod.routes = []Route{
|
||||||
{
|
{
|
||||||
@@ -842,22 +755,12 @@ func TestApi_Start(t *testing.T) {
|
|||||||
// health request.
|
// health request.
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
healthRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
healthRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
|
||||||
|
|
||||||
mod.srv.ServeHTTP(recorder, healthRequest)
|
mod.srv.ServeHTTP(recorder, healthRequest)
|
||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// version request.
|
|
||||||
versionRequest := httptest.NewRequest(http.MethodGet, "/version", nil)
|
|
||||||
versionRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
|
||||||
|
|
||||||
mod.srv.ServeHTTP(recorder, versionRequest)
|
|
||||||
if recorder.Code != http.StatusOK {
|
|
||||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// "multipart/form-data" request.
|
// "multipart/form-data" request.
|
||||||
multipartRequest := func(url string) *http.Request {
|
multipartRequest := func(url string) *http.Request {
|
||||||
body := &bytes.Buffer{}
|
body := &bytes.Buffer{}
|
||||||
@@ -888,7 +791,6 @@ func TestApi_Start(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, url, body)
|
req := httptest.NewRequest(http.MethodPost, url, body)
|
||||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||||
req.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -34,24 +34,18 @@ 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
|
|
||||||
|
|
||||||
logger *zap.Logger
|
cancelled bool
|
||||||
echoCtx echo.Context
|
logger *zap.Logger
|
||||||
pathRename gotenberg.PathRename
|
echoCtx echo.Context
|
||||||
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)
|
||||||
@@ -61,7 +55,6 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +86,7 @@ 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),
|
||||||
@@ -170,6 +164,7 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -196,20 +191,10 @@ func (ctx *Context) FormData() *FormData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePath generates a path within the context's working directory.
|
// GeneratePath generates a path within the context's working directory. It
|
||||||
// It generates a new UUID-based filename. It does not create a file.
|
// 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().String(), extension)
|
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), 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
|
||||||
@@ -284,8 +269,3 @@ func (ctx *Context) OutputFilename(outputPath string) string {
|
|||||||
|
|
||||||
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
|
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard.
|
|
||||||
var (
|
|
||||||
_ gotenberg.PathRename = (*osPathRename)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,77 +3,21 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/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
|
||||||
@@ -247,46 +191,12 @@ 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,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,8 +11,6 @@ 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
|
||||||
@@ -146,91 +143,6 @@ 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
|
||||||
@@ -392,7 +304,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.Sort(gotenberg.AlphanumericSort(*target))
|
sort.Strings(*target)
|
||||||
|
|
||||||
return form
|
return form
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -769,333 +768,6 @@ 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
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,21 +10,14 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// ErrAsyncProcess happens when a handler or middleware handles a request in an
|
||||||
// ErrAsyncProcess happens when a handler or middleware handles a request
|
// asynchronous fashion.
|
||||||
// in an asynchronous fashion.
|
var ErrAsyncProcess = errors.New("async process")
|
||||||
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.
|
||||||
@@ -40,22 +32,6 @@ 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()
|
||||||
@@ -114,6 +90,7 @@ func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
|
|||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
c.Set("rootPath", rootPath)
|
c.Set("rootPath", rootPath)
|
||||||
|
|
||||||
// Call the next middleware in the chain.
|
// Call the next middleware in the chain.
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
@@ -218,17 +195,6 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// basicAuthMiddleware manages basic authentication.
|
|
||||||
func basicAuthMiddleware(username, password string) echo.MiddlewareFunc {
|
|
||||||
return middleware.BasicAuth(func(u string, p string, e echo.Context) (bool, error) {
|
|
||||||
if subtle.ConstantTimeCompare([]byte(u), []byte(username)) == 1 &&
|
|
||||||
subtle.ConstantTimeCompare([]byte(p), []byte(password)) == 1 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
|
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
|
||||||
// [Context] and related context.CancelFunc in the [echo.Context] under
|
// [Context] and related context.CancelFunc in the [echo.Context] under
|
||||||
// "context" and "cancel". If the process is synchronous, it also handles the
|
// "context" and "cancel". If the process is synchronous, it also handles the
|
||||||
@@ -264,13 +230,6 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseError(t *testing.T) {
|
func TestParseError(t *testing.T) {
|
||||||
@@ -29,24 +29,9 @@ func TestParseError(t *testing.T) {
|
|||||||
expectMessage: http.StatusText(http.StatusInternalServerError),
|
expectMessage: http.StatusText(http.StatusInternalServerError),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
err: gotenberg.ErrFiltered,
|
err: context.DeadlineExceeded,
|
||||||
expectStatus: http.StatusForbidden,
|
expectStatus: http.StatusServiceUnavailable,
|
||||||
expectMessage: http.StatusText(http.StatusForbidden),
|
expectMessage: http.StatusText(http.StatusServiceUnavailable),
|
||||||
},
|
|
||||||
{
|
|
||||||
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(
|
||||||
@@ -236,56 +221,6 @@ func TestTraceMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicAuthMiddleware(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
scenario string
|
|
||||||
request *http.Request
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
scenario: "invalid basic auth",
|
|
||||||
request: func() *http.Request {
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
req.SetBasicAuth("invalid", "invalid")
|
|
||||||
return req
|
|
||||||
}(),
|
|
||||||
username: "foo",
|
|
||||||
password: "bar",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "valid basic auth",
|
|
||||||
request: func() *http.Request {
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
req.SetBasicAuth("foo", "bar")
|
|
||||||
return req
|
|
||||||
}(),
|
|
||||||
username: "foo",
|
|
||||||
password: "bar",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
srv := echo.New()
|
|
||||||
srv.HideBanner = true
|
|
||||||
srv.HidePort = true
|
|
||||||
c := srv.NewContext(tc.request, recorder)
|
|
||||||
err := basicAuthMiddleware(tc.username, tc.password)(func(c echo.Context) error {
|
|
||||||
return nil
|
|
||||||
})(c)
|
|
||||||
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 TestLoggerMiddleware(t *testing.T) {
|
func TestLoggerMiddleware(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
request *http.Request
|
request *http.Request
|
||||||
@@ -385,15 +320,6 @@ 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,8 +4,6 @@ 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.
|
||||||
@@ -78,7 +76,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)
|
||||||
@@ -86,14 +84,6 @@ 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,19 +117,6 @@ 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,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -14,16 +15,14 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type browser interface {
|
type browser interface {
|
||||||
gotenberg.Process
|
gotenberg.Process
|
||||||
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
||||||
screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type browserArguments struct {
|
type browserArguments struct {
|
||||||
@@ -39,10 +38,8 @@ type browserArguments struct {
|
|||||||
wsUrlReadTimeout time.Duration
|
wsUrlReadTimeout time.Duration
|
||||||
|
|
||||||
// Tasks specific.
|
// Tasks specific.
|
||||||
allowList *regexp2.Regexp
|
allowList *regexp.Regexp
|
||||||
denyList *regexp2.Regexp
|
denyList *regexp.Regexp
|
||||||
clearCache bool
|
|
||||||
clearCookies bool
|
|
||||||
disableJavaScript bool
|
disableJavaScript bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +87,6 @@ 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 {
|
||||||
@@ -166,7 +161,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 deleting it.
|
// of time before re-deleting it.
|
||||||
<-time.After(10 * time.Second)
|
<-time.After(10 * time.Second)
|
||||||
|
|
||||||
err := os.RemoveAll(userProfileDirPath)
|
err := os.RemoveAll(userProfileDirPath)
|
||||||
@@ -216,56 +211,18 @@ 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 PdfOptions) error {
|
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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),
|
|
||||||
setCookiesActionFunc(logger, options.Cookies),
|
|
||||||
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),
|
|
||||||
setCookiesActionFunc(logger, options.Cookies),
|
|
||||||
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.
|
|
||||||
setDeviceMetricsOverride(logger, options.Width, options.Height),
|
|
||||||
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 tasks")
|
return errors.New("browser not started, cannot handle PDF conversion")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
@@ -273,12 +230,6 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
|||||||
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()
|
||||||
|
|
||||||
@@ -292,16 +243,6 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
|||||||
// 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
|
||||||
@@ -312,7 +253,22 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
|||||||
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = chromedp.Run(taskCtx, tasks...)
|
tasks := chromedp.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()
|
||||||
|
|
||||||
@@ -328,15 +284,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
|||||||
return ErrRpccMessageTooLarge
|
return ErrRpccMessageTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("handle tasks: %w", err)
|
return fmt.Errorf("print to PDF: %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
@@ -8,12 +8,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
"github.com/chromedp/cdproto/network"
|
|
||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -21,6 +21,14 @@ 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")
|
||||||
@@ -29,32 +37,22 @@ 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
|
||||||
@@ -70,44 +68,28 @@ type Chromium struct {
|
|||||||
engine gotenberg.PdfEngine
|
engine gotenberg.PdfEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options are the common options for all conversions.
|
// Options are the available expectedOptions for converting HTML document to PDF.
|
||||||
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.
|
// converting it to PDF.
|
||||||
// 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.
|
// converting an HTML document to PDF.
|
||||||
// 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 until it returns true
|
// converting an HTML document to PDF until it returns true
|
||||||
// Optional.
|
// Optional.
|
||||||
WaitForExpression string
|
WaitForExpression string
|
||||||
|
|
||||||
// Cookies are the cookies to put in the Chromium cookies' jar.
|
|
||||||
// Optional
|
|
||||||
Cookies []Cookie
|
|
||||||
|
|
||||||
// ExtraHttpHeaders are the HTTP headers to send by Chromium while loading
|
// ExtraHttpHeaders are the HTTP headers to send by Chromium while loading
|
||||||
// the HTML document.
|
// the HTML document.
|
||||||
// Optional.
|
// Optional.
|
||||||
@@ -118,32 +100,6 @@ 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: "",
|
|
||||||
Cookies: nil,
|
|
||||||
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
|
||||||
@@ -152,15 +108,15 @@ type PdfOptions 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
|
||||||
@@ -213,109 +169,35 @@ type PdfOptions struct {
|
|||||||
PreferCssPageSize bool
|
PreferCssPageSize bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultPdfOptions returns the default values for PdfOptions.
|
// DefaultOptions returns the default values for Options.
|
||||||
func DefaultPdfOptions() PdfOptions {
|
func DefaultOptions() Options {
|
||||||
return PdfOptions{
|
return Options{
|
||||||
Options: DefaultOptions(),
|
FailOnConsoleExceptions: false,
|
||||||
Landscape: false,
|
WaitDelay: 0,
|
||||||
PrintBackground: false,
|
WaitWindowStatus: "",
|
||||||
Scale: 1.0,
|
WaitForExpression: "",
|
||||||
SinglePage: false,
|
ExtraHttpHeaders: nil,
|
||||||
PaperWidth: 8.5,
|
EmulatedMediaType: "",
|
||||||
PaperHeight: 11,
|
Landscape: false,
|
||||||
MarginTop: 0.39,
|
PrintBackground: false,
|
||||||
MarginBottom: 0.39,
|
OmitBackground: false,
|
||||||
MarginLeft: 0.39,
|
Scale: 1.0,
|
||||||
MarginRight: 0.39,
|
PaperWidth: 8.5,
|
||||||
PageRanges: "",
|
PaperHeight: 11,
|
||||||
HeaderTemplate: "<html><head></head><body></body></html>",
|
MarginTop: 0.39,
|
||||||
FooterTemplate: "<html><head></head><body></body></html>",
|
MarginBottom: 0.39,
|
||||||
PreferCssPageSize: false,
|
MarginLeft: 0.39,
|
||||||
|
MarginRight: 0.39,
|
||||||
|
PageRanges: "",
|
||||||
|
HeaderTemplate: "<html><head></head><body></body></html>",
|
||||||
|
FooterTemplate: "<html><head></head><body></body></html>",
|
||||||
|
PreferCssPageSize: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScreenshotOptions are the available options for capturing a screenshot from
|
|
||||||
// an HTML document.
|
|
||||||
type ScreenshotOptions struct {
|
|
||||||
Options
|
|
||||||
|
|
||||||
// Width is the device screen width in pixels.
|
|
||||||
// Optional.
|
|
||||||
Width int
|
|
||||||
|
|
||||||
// Height is the device screen height in pixels.
|
|
||||||
// Optional.
|
|
||||||
Height int
|
|
||||||
|
|
||||||
// Clip defines whether to clip the screenshot according to the device
|
|
||||||
// dimensions.
|
|
||||||
// Optional.
|
|
||||||
Clip bool
|
|
||||||
|
|
||||||
// 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(),
|
|
||||||
Width: 800,
|
|
||||||
Height: 600,
|
|
||||||
Clip: false,
|
|
||||||
Format: "png",
|
|
||||||
Quality: 100,
|
|
||||||
OptimizeForSpeed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cookie gathers the available entries for setting a cookie in the Chromium
|
|
||||||
// cookies' jar.
|
|
||||||
type Cookie struct {
|
|
||||||
// Name is the cookie name.
|
|
||||||
// Required.
|
|
||||||
Name string `json:"name"`
|
|
||||||
|
|
||||||
// Value is the cookie value.
|
|
||||||
// Required.
|
|
||||||
Value string `json:"value"`
|
|
||||||
|
|
||||||
// Domain is the cookie domain.
|
|
||||||
// Required.
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
|
|
||||||
// Path is the cookie path.
|
|
||||||
// Optional.
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
|
|
||||||
// Secure sets the cookie secure if true.
|
|
||||||
// Optional.
|
|
||||||
Secure bool `json:"secure,omitempty"`
|
|
||||||
|
|
||||||
// HttpOnly sets the cookie as HTTP-only if true.
|
|
||||||
// Optional.
|
|
||||||
HttpOnly bool `json:"httpOnly,omitempty"`
|
|
||||||
|
|
||||||
// SameSite is cookie 'Same-Site' status.
|
|
||||||
// Optional.
|
|
||||||
SameSite network.CookieSameSite `json:"sameSite,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 PdfOptions) error
|
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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]
|
||||||
@@ -335,8 +217,19 @@ 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")
|
||||||
@@ -347,9 +240,7 @@ 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")
|
||||||
|
|
||||||
@@ -383,8 +274,6 @@ 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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +290,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"), flags.MustInt64("chromium-max-queue-size"))
|
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"))
|
||||||
|
|
||||||
// PDF Engine.
|
// PDF Engine.
|
||||||
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))
|
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||||
@@ -470,6 +359,22 @@ 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.",
|
||||||
@@ -545,31 +450,19 @@ 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 PdfOptions) error {
|
func (mod *Chromium) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||||
// Note: no error wrapping because it leaks on errors we want to display to
|
// FIXME: no error wrapping because it leaks on console exceptions output.
|
||||||
// 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultOptions(t *testing.T) {
|
func TestDefaultOptions(t *testing.T) {
|
||||||
actual := DefaultPdfOptions()
|
actual := DefaultOptions()
|
||||||
notExpect := PdfOptions{}
|
notExpect := Options{}
|
||||||
|
|
||||||
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,16 +334,26 @@ 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) != 2 {
|
if len(metrics) != 4 {
|
||||||
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
t.Fatalf("expected %d metrics, but got %d", 4, 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[1].Read()
|
actual = metrics[3].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)
|
||||||
}
|
}
|
||||||
@@ -461,7 +471,7 @@ func TestChromium_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 6,
|
expectRoutes: 3,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -495,14 +505,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 PdfOptions) error {
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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 PdfOptions) error {
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||||
return errors.New("PDF task error")
|
return errors.New("PDF task error")
|
||||||
}},
|
}},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
@@ -515,49 +525,7 @@ func TestChromium_Pdf(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
mod.browser = tc.browser
|
mod.browser = tc.browser
|
||||||
|
|
||||||
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
|
|
||||||
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"
|
||||||
"slices"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
@@ -12,17 +12,14 @@ 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 *regexp2.Regexp, denyList *regexp2.Regexp) {
|
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp.Regexp, denyList *regexp.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:
|
||||||
@@ -30,15 +27,13 @@ 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
|
||||||
|
|
||||||
deadline, ok := ctx.Deadline()
|
if !allowList.MatchString(e.Request.URL) {
|
||||||
if !ok {
|
logger.Warn(fmt.Sprintf("'%s' does not match the expression from the allowed list", e.Request.URL))
|
||||||
logger.Error("context has no deadline, cannot filter URL")
|
allow = false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline)
|
if denyList.String() != "" && denyList.MatchString(e.Request.URL) {
|
||||||
if err != nil {
|
logger.Warn(fmt.Sprintf("'%s' matches the expression from the denied list", e.Request.URL))
|
||||||
logger.Warn(err.Error())
|
|
||||||
allow = false
|
allow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,15 +42,16 @@ 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))
|
||||||
}
|
}
|
||||||
@@ -64,37 +60,6 @@ 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,38 +5,28 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/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 PdfOptions) error
|
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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 PdfOptions) error {
|
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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 PdfOptions) error
|
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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 PdfOptions) error {
|
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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,42 +9,26 @@ 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 PdfOptions) error {
|
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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(), "", "", PdfOptions{})
|
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
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 PdfOptions) error {
|
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) 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(), "", "", PdfOptions{})
|
err := mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
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,7 +9,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,119 +17,24 @@ import (
|
|||||||
"github.com/russross/blackfriday/v2"
|
"github.com/russross/blackfriday/v2"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FormDataChromiumOptions creates [Options] from the form data. Fallback to
|
// FormDataChromiumPdfOptions 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 FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||||
defaultOptions := DefaultOptions()
|
defaultOptions := DefaultOptions()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
skipNetworkIdleEvent bool
|
failOnConsoleExceptions bool
|
||||||
failOnHttpStatusCodes []int64
|
waitDelay time.Duration
|
||||||
failOnConsoleExceptions bool
|
waitWindowStatus string
|
||||||
waitDelay time.Duration
|
waitForExpression string
|
||||||
waitWindowStatus string
|
userAgent string
|
||||||
waitForExpression string
|
extraHttpHeaders map[string]string
|
||||||
cookies []Cookie
|
emulatedMediaType string
|
||||||
extraHttpHeaders map[string]string
|
landscape, printBackground, omitBackground bool
|
||||||
emulatedMediaType string
|
|
||||||
omitBackground bool
|
|
||||||
)
|
|
||||||
|
|
||||||
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).
|
|
||||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
|
||||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
|
||||||
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
|
||||||
Custom("cookies", func(value string) error {
|
|
||||||
if value == "" {
|
|
||||||
cookies = defaultOptions.Cookies
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(value), &cookies)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshal cookies: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, cookie := range cookies {
|
|
||||||
if strings.TrimSpace(cookie.Name) == "" || strings.TrimSpace(cookie.Value) == "" || strings.TrimSpace(cookie.Domain) == "" {
|
|
||||||
err = multierr.Append(err, fmt.Errorf("cookie %d must have its name, value and domain set", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}).
|
|
||||||
Custom("extraHttpHeaders", func(value string) error {
|
|
||||||
if value == "" {
|
|
||||||
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshal extraHttpHeaders: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}).
|
|
||||||
Custom("emulatedMediaType", func(value string) error {
|
|
||||||
if value == "" {
|
|
||||||
emulatedMediaType = defaultOptions.EmulatedMediaType
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if value != "screen" && value != "print" {
|
|
||||||
return errors.New("wrong value, expected either 'screen', 'print' or empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
emulatedMediaType = value
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}).
|
|
||||||
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground)
|
|
||||||
|
|
||||||
options := Options{
|
|
||||||
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
|
||||||
FailOnHttpStatusCodes: failOnHttpStatusCodes,
|
|
||||||
FailOnConsoleExceptions: failOnConsoleExceptions,
|
|
||||||
WaitDelay: waitDelay,
|
|
||||||
WaitWindowStatus: waitWindowStatus,
|
|
||||||
WaitForExpression: waitForExpression,
|
|
||||||
Cookies: cookies,
|
|
||||||
ExtraHttpHeaders: extraHttpHeaders,
|
|
||||||
EmulatedMediaType: emulatedMediaType,
|
|
||||||
OmitBackground: omitBackground,
|
|
||||||
}
|
|
||||||
|
|
||||||
return form, options
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormDataChromiumPdfOptions creates [PdfOptions] from the form data. Fallback to
|
|
||||||
// default value if the considered key is not present.
|
|
||||||
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
|
||||||
form, options := FormDataChromiumOptions(ctx)
|
|
||||||
defaultPdfOptions := DefaultPdfOptions()
|
|
||||||
|
|
||||||
var (
|
|
||||||
landscape, printBackground, singlePage bool
|
|
||||||
scale, paperWidth, paperHeight float64
|
scale, paperWidth, paperHeight float64
|
||||||
marginTop, marginBottom, marginLeft, marginRight float64
|
marginTop, marginBottom, marginLeft, marginRight float64
|
||||||
pageRanges string
|
pageRanges string
|
||||||
@@ -138,145 +42,126 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
|||||||
preferCssPageSize bool
|
preferCssPageSize bool
|
||||||
)
|
)
|
||||||
|
|
||||||
form.
|
form := ctx.FormData().
|
||||||
Bool("landscape", &landscape, defaultPdfOptions.Landscape).
|
Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
|
||||||
Bool("printBackground", &printBackground, defaultPdfOptions.PrintBackground).
|
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||||
Float64("scale", &scale, defaultPdfOptions.Scale).
|
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||||
Bool("singlePage", &singlePage, defaultPdfOptions.SinglePage).
|
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||||
Inches("paperWidth", &paperWidth, defaultPdfOptions.PaperWidth).
|
String("userAgent", &userAgent, ""). // FIXME: deprecated.
|
||||||
Inches("paperHeight", &paperHeight, defaultPdfOptions.PaperHeight).
|
Custom("extraHttpHeaders", func(value string) error {
|
||||||
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 (
|
|
||||||
width, height int
|
|
||||||
clip bool
|
|
||||||
format string
|
|
||||||
quality int
|
|
||||||
optimizeForSpeed bool
|
|
||||||
)
|
|
||||||
|
|
||||||
form.
|
|
||||||
Int("width", &width, defaultScreenshotOptions.Width).
|
|
||||||
Int("height", &height, defaultScreenshotOptions.Height).
|
|
||||||
Bool("clip", &clip, defaultScreenshotOptions.Clip).
|
|
||||||
Custom("format", func(value string) error {
|
|
||||||
if value == "" {
|
if value == "" {
|
||||||
format = defaultScreenshotOptions.Format
|
extraHttpHeaders = defaultOptions.ExtraHttpHeaders
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if value != "png" && value != "jpeg" && value != "webp" {
|
err := json.Unmarshal([]byte(value), &extraHttpHeaders)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unmarshal extra HTTP headers: %w", 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
|
return nil
|
||||||
}).
|
}).
|
||||||
Bool("optimizeForSpeed", &optimizeForSpeed, defaultScreenshotOptions.OptimizeForSpeed)
|
Custom("emulatedMediaType", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
emulatedMediaType = defaultOptions.EmulatedMediaType
|
||||||
|
|
||||||
screenshotOptions := ScreenshotOptions{
|
return nil
|
||||||
Options: options,
|
}
|
||||||
Width: width,
|
|
||||||
Height: height,
|
if value != "screen" && value != "print" {
|
||||||
Clip: clip,
|
return fmt.Errorf("wrong value, expected either 'screen', 'print' or empty")
|
||||||
Format: format,
|
}
|
||||||
Quality: quality,
|
|
||||||
OptimizeForSpeed: optimizeForSpeed,
|
emulatedMediaType = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Bool("landscape", &landscape, defaultOptions.Landscape).
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return form, screenshotOptions
|
options := Options{
|
||||||
|
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||||
|
WaitDelay: waitDelay,
|
||||||
|
WaitWindowStatus: waitWindowStatus,
|
||||||
|
WaitForExpression: waitForExpression,
|
||||||
|
ExtraHttpHeaders: extraHttpHeaders,
|
||||||
|
EmulatedMediaType: emulatedMediaType,
|
||||||
|
Landscape: landscape,
|
||||||
|
PrintBackground: printBackground,
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
||||||
// data. Fallback to default value if the considered key is not present.
|
// data. Fallback to default value if the considered key is not present.
|
||||||
func FormDataChromiumPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
func FormDataChromiumPdfFormats(ctx *api.Context) gotenberg.PdfFormats {
|
||||||
var (
|
var (
|
||||||
pdfa string
|
pdfFormat string
|
||||||
pdfua bool
|
pdfa string
|
||||||
|
pdfua bool
|
||||||
)
|
)
|
||||||
|
|
||||||
form.
|
ctx.FormData().
|
||||||
|
String("pdfFormat", &pdfFormat, "").
|
||||||
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: pdfa,
|
PdfA: actualPdfArchive,
|
||||||
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{
|
||||||
@@ -286,8 +171,7 @@ 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(form)
|
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
||||||
metadata := FormDataPdfMetadata(form)
|
|
||||||
|
|
||||||
var url string
|
var url string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -297,7 +181,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, options, pdfFormats, metadata)
|
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -307,35 +191,6 @@ 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 {
|
||||||
@@ -346,8 +201,7 @@ 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(form)
|
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
||||||
metadata := FormDataPdfMetadata(form)
|
|
||||||
|
|
||||||
var inputPath string
|
var inputPath string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -358,7 +212,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, options, pdfFormats, metadata)
|
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -368,36 +222,6 @@ 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 {
|
||||||
@@ -408,8 +232,7 @@ 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(form)
|
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
||||||
metadata := FormDataPdfMetadata(form)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
inputPath string
|
inputPath string
|
||||||
@@ -424,12 +247,80 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := markdownToHtml(ctx, inputPath, markdownPaths)
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
return fmt.Errorf("parse template file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -439,126 +330,23 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// screenshotMarkdownRoute returns an [api.Route] which can take a screenshot
|
// convertUrl is a stub which is called by the other methods of this file.
|
||||||
// from markdown files.
|
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options Options) error {
|
||||||
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),
|
||||||
@@ -569,6 +357,23 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrInvalidEvaluationExpression) {
|
||||||
|
if options.WaitForExpression == "" {
|
||||||
|
// We do not expect the 'waitWindowStatus' form field to return
|
||||||
|
// an ErrInvalidEvaluationExpression error. In such a scenario,
|
||||||
|
// we return a 500.
|
||||||
|
return fmt.Errorf("convert to PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.WrapError(
|
||||||
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if errors.Is(err, ErrInvalidPrinterSettings) {
|
if errors.Is(err, ErrInvalidPrinterSettings) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
@@ -589,6 +394,16 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrConsoleExceptions) {
|
||||||
|
return api.WrapError(
|
||||||
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusConflict,
|
||||||
|
fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Errorf("convert to PDF: %w", err)
|
return fmt.Errorf("convert to PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,7 +416,18 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,14 +435,6 @@ 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)
|
||||||
@@ -624,66 +442,3 @@ 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
@@ -3,7 +3,6 @@ package chromium
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,44 +15,24 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
|
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options) 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(paperHeight).
|
WithPaperHeight(options.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(pageRanges).
|
WithPageRanges(options.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 != DefaultPdfOptions().HeaderTemplate ||
|
hasCustomHeaderFooter := options.HeaderTemplate != DefaultOptions().HeaderTemplate ||
|
||||||
options.FooterTemplate != DefaultPdfOptions().FooterTemplate
|
options.FooterTemplate != DefaultOptions().FooterTemplate
|
||||||
|
|
||||||
if !hasCustomHeaderFooter {
|
if !hasCustomHeaderFooter {
|
||||||
logger.Debug("no custom header nor footer")
|
logger.Debug("no custom header nor footer")
|
||||||
@@ -84,7 +63,7 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOpti
|
|||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
@@ -96,7 +75,7 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOpti
|
|||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
@@ -113,111 +92,12 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOpti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Clip {
|
|
||||||
captureScreenshot = captureScreenshot.WithClip(&page.Viewport{
|
|
||||||
Width: float64(options.Width),
|
|
||||||
Height: float64(options.Height),
|
|
||||||
Scale: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 setDeviceMetricsOverride(logger *zap.Logger, width, height int) chromedp.ActionFunc {
|
|
||||||
return func(ctx context.Context) error {
|
|
||||||
logger.Debug("set device metrics override")
|
|
||||||
|
|
||||||
err := emulation.SetDeviceMetricsOverride(int64(width), int64(height), 1.0, false).Do(ctx)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("set device metrics override: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,59 +112,11 @@ func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.Acti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCookiesActionFunc(logger *zap.Logger, cookies []Cookie) chromedp.ActionFunc {
|
|
||||||
return func(ctx context.Context) error {
|
|
||||||
if len(cookies) == 0 {
|
|
||||||
logger.Debug("no cookies to set")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
deadline, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
return errors.New("context has no deadline, cannot set cookies")
|
|
||||||
}
|
|
||||||
epochTime := cdp.TimeSinceEpoch(deadline)
|
|
||||||
|
|
||||||
cookiePretty := func(c *network.SetCookieParams) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"Name: '%s', Value: '%s', Domain: '%s', Path: '%s', Secure: %t, HTTPOnly: %t, SameSite: '%s', Expires: %s",
|
|
||||||
c.Name,
|
|
||||||
c.Value,
|
|
||||||
c.Domain,
|
|
||||||
c.Path,
|
|
||||||
c.Secure,
|
|
||||||
c.HTTPOnly,
|
|
||||||
c.SameSite.String(),
|
|
||||||
c.Expires.Time().String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cookie := range cookies {
|
|
||||||
cookieParams := network.
|
|
||||||
SetCookie(cookie.Name, cookie.Value).
|
|
||||||
WithDomain(cookie.Domain).
|
|
||||||
WithPath(cookie.Path).
|
|
||||||
WithSecure(cookie.Secure).
|
|
||||||
WithHTTPOnly(cookie.HttpOnly).
|
|
||||||
WithSameSite(cookie.SameSite).
|
|
||||||
WithExpires(&epochTime)
|
|
||||||
|
|
||||||
err := cookieParams.Do(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("set cookie %s: %w", cookiePretty(cookieParams), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(fmt.Sprintf("set cookie %s", cookiePretty(cookieParams)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +136,7 @@ func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleEvent bool) chromedp.ActionFunc {
|
func navigateActionFunc(logger *zap.Logger, url string) 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))
|
||||||
|
|
||||||
@@ -313,21 +145,12 @@ func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleEvent boo
|
|||||||
return fmt.Errorf("navigate to '%s': %w", url, err)
|
return fmt.Errorf("navigate to '%s': %w", url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitFunc := []func() error{
|
|
||||||
waitForEventDomContentEventFired(ctx, logger),
|
|
||||||
waitForEventLoadEventFired(ctx, logger),
|
|
||||||
waitForEventLoadingFinished(ctx, logger),
|
|
||||||
}
|
|
||||||
|
|
||||||
if !skipNetworkIdleEvent {
|
|
||||||
waitFunc = append(waitFunc, waitForEventNetworkIdle(ctx, logger))
|
|
||||||
} else {
|
|
||||||
logger.Debug("skipping network idle event")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = runBatch(
|
err = runBatch(
|
||||||
ctx,
|
ctx,
|
||||||
waitFunc...,
|
waitForEventDomContentEventFired(ctx, logger),
|
||||||
|
waitForEventLoadEventFired(ctx, logger),
|
||||||
|
waitForEventNetworkIdle(ctx, logger),
|
||||||
|
waitForEventLoadingFinished(ctx, logger),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -343,6 +166,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +225,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,11 +249,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,11 +276,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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)
|
|
||||||
)
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -21,9 +21,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrInvalidPdfFormats happens if the PDF formats option cannot be handled
|
// ErrInvalidPdfFormat happens if the PDF format option cannot be handled
|
||||||
// by LibreOffice.
|
// by LibreOffice.
|
||||||
ErrInvalidPdfFormats = errors.New("invalid PDF formats")
|
ErrInvalidPdfFormat = errors.New("invalid PDF format")
|
||||||
|
|
||||||
// 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,17 +47,10 @@ 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
|
|
||||||
|
|
||||||
// SinglePageSheets allows to output each sheet as a single page in the resulting PDF.
|
|
||||||
// Optional
|
|
||||||
SinglePageSheets 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.
|
||||||
@@ -87,8 +80,22 @@ 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")
|
||||||
|
|
||||||
@@ -116,7 +123,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.MustDuration("libreoffice-start-timeout"),
|
startTimeout: flags.MustDeprecatedDuration("uno-listener-start-timeout", "libreoffice-start-timeout"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger.
|
// Logger.
|
||||||
@@ -132,7 +139,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.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"))
|
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustDeprecatedInt64("uno-listener-restart-threshold", "libreoffice-restart-after"))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -197,6 +204,46 @@ 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.",
|
||||||
@@ -274,136 +321,85 @@ 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",
|
||||||
".bmp",
|
|
||||||
".cdr",
|
|
||||||
".cgm",
|
|
||||||
".cmx",
|
|
||||||
".csv",
|
|
||||||
".cwk",
|
|
||||||
".dbf",
|
|
||||||
".dif",
|
|
||||||
".doc",
|
".doc",
|
||||||
".docm",
|
".xml",
|
||||||
".docx",
|
".docx",
|
||||||
".dot",
|
|
||||||
".dotm",
|
|
||||||
".dotx",
|
|
||||||
".dxf",
|
|
||||||
".emf",
|
|
||||||
".eps",
|
|
||||||
".epub",
|
|
||||||
".fodg",
|
|
||||||
".fodp",
|
|
||||||
".fods",
|
|
||||||
".fodt",
|
".fodt",
|
||||||
".fopd",
|
|
||||||
".gif",
|
|
||||||
".htm",
|
|
||||||
".html",
|
".html",
|
||||||
".hwp",
|
|
||||||
".jpeg",
|
|
||||||
".jpg",
|
|
||||||
".key",
|
|
||||||
".ltx",
|
".ltx",
|
||||||
".lwp",
|
".txt",
|
||||||
".mcw",
|
|
||||||
".met",
|
|
||||||
".mml",
|
|
||||||
".mw",
|
|
||||||
".numbers",
|
|
||||||
".odd",
|
|
||||||
".odg",
|
|
||||||
".odm",
|
|
||||||
".odp",
|
|
||||||
".ods",
|
|
||||||
".odt",
|
".odt",
|
||||||
".otg",
|
|
||||||
".oth",
|
|
||||||
".otp",
|
|
||||||
".ots",
|
|
||||||
".ott",
|
".ott",
|
||||||
".pages",
|
|
||||||
".pbm",
|
|
||||||
".pcd",
|
|
||||||
".pct",
|
|
||||||
".pcx",
|
|
||||||
".pdb",
|
".pdb",
|
||||||
".pdf",
|
".pdf",
|
||||||
".pgm",
|
|
||||||
".png",
|
|
||||||
".pot",
|
|
||||||
".potm",
|
|
||||||
".potx",
|
|
||||||
".ppm",
|
|
||||||
".pps",
|
|
||||||
".ppt",
|
|
||||||
".pptm",
|
|
||||||
".pptx",
|
|
||||||
".psd",
|
|
||||||
".psw",
|
".psw",
|
||||||
".pub",
|
|
||||||
".pwp",
|
|
||||||
".pxl",
|
|
||||||
".ras",
|
|
||||||
".rtf",
|
".rtf",
|
||||||
".sda",
|
|
||||||
".sdc",
|
|
||||||
".sdd",
|
|
||||||
".sdp",
|
|
||||||
".sdw",
|
".sdw",
|
||||||
".sgl",
|
|
||||||
".slk",
|
|
||||||
".smf",
|
|
||||||
".stc",
|
|
||||||
".std",
|
|
||||||
".sti",
|
|
||||||
".stw",
|
".stw",
|
||||||
|
".sxw",
|
||||||
|
".uot",
|
||||||
|
".vor",
|
||||||
|
".wps",
|
||||||
|
".epub",
|
||||||
|
".png",
|
||||||
|
".bmp",
|
||||||
|
".emf",
|
||||||
|
".eps",
|
||||||
|
".fodg",
|
||||||
|
".gif",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".met",
|
||||||
|
".odd",
|
||||||
|
".otg",
|
||||||
|
".pbm",
|
||||||
|
".pct",
|
||||||
|
".pgm",
|
||||||
|
".ppm",
|
||||||
|
".ras",
|
||||||
|
".std",
|
||||||
".svg",
|
".svg",
|
||||||
".svm",
|
".svm",
|
||||||
".swf",
|
".swf",
|
||||||
".sxc",
|
|
||||||
".sxd",
|
".sxd",
|
||||||
".sxg",
|
|
||||||
".sxi",
|
|
||||||
".sxm",
|
|
||||||
".sxw",
|
".sxw",
|
||||||
".tga",
|
|
||||||
".tif",
|
".tif",
|
||||||
".tiff",
|
".tiff",
|
||||||
".txt",
|
|
||||||
".uof",
|
|
||||||
".uop",
|
|
||||||
".uos",
|
|
||||||
".uot",
|
|
||||||
".vdx",
|
|
||||||
".vor",
|
|
||||||
".vsd",
|
|
||||||
".vsdm",
|
|
||||||
".vsdx",
|
|
||||||
".wb2",
|
|
||||||
".wk1",
|
|
||||||
".wks",
|
|
||||||
".wmf",
|
|
||||||
".wpd",
|
|
||||||
".wpg",
|
|
||||||
".wps",
|
|
||||||
".xbm",
|
|
||||||
".xhtml",
|
".xhtml",
|
||||||
".xls",
|
|
||||||
".xlsb",
|
|
||||||
".xlsm",
|
|
||||||
".xlsx",
|
|
||||||
".xlt",
|
|
||||||
".xltm",
|
|
||||||
".xltx",
|
|
||||||
".xlw",
|
|
||||||
".xml",
|
|
||||||
".xpm",
|
".xpm",
|
||||||
".zabw",
|
".odp",
|
||||||
|
".fodp",
|
||||||
|
".potm",
|
||||||
|
".pot",
|
||||||
|
".pptx",
|
||||||
|
".pps",
|
||||||
|
".ppt",
|
||||||
|
".pwp",
|
||||||
|
".sda",
|
||||||
|
".sdd",
|
||||||
|
".sti",
|
||||||
|
".sxi",
|
||||||
|
".uop",
|
||||||
|
".wmf",
|
||||||
|
".csv",
|
||||||
|
".dbf",
|
||||||
|
".dif",
|
||||||
|
".fods",
|
||||||
|
".ods",
|
||||||
|
".ots",
|
||||||
|
".pxl",
|
||||||
|
".sdc",
|
||||||
|
".slk",
|
||||||
|
".stc",
|
||||||
|
".sxc",
|
||||||
|
".uos",
|
||||||
|
".xls",
|
||||||
|
".xlt",
|
||||||
|
".xlsx",
|
||||||
|
".odg",
|
||||||
|
".dotx",
|
||||||
|
".xltx",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApi_Descriptor(t *testing.T) {
|
func TestApi_Descriptor(t *testing.T) {
|
||||||
@@ -284,16 +284,41 @@ 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) != 2 {
|
if len(metrics) != 7 {
|
||||||
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
t.Fatalf("expected %d metrics, but got %d", 7, 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[1].Read()
|
actual = metrics[6].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)
|
||||||
}
|
}
|
||||||
@@ -450,7 +475,7 @@ func TestApi_Extensions(t *testing.T) {
|
|||||||
extensions := a.Extensions()
|
extensions := a.Extensions()
|
||||||
|
|
||||||
actual := len(extensions)
|
actual := len(extensions)
|
||||||
expect := 130
|
expect := 79
|
||||||
|
|
||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type libreOffice interface {
|
type libreOffice interface {
|
||||||
@@ -273,16 +273,11 @@ 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.SinglePageSheets {
|
|
||||||
args = append(args, "--export", "SinglePageSheets=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -290,20 +285,14 @@ 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 ErrInvalidPdfFormats
|
return ErrInvalidPdfFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PdfFormats.PdfUa {
|
if options.PdfFormats.PdfUa {
|
||||||
args = append(
|
args = append(
|
||||||
args,
|
args,
|
||||||
"--export", "UseTaggedPDF=true",
|
|
||||||
"--export", "EnableTextAccessForAccessibilityTools=true",
|
"--export", "EnableTextAccessForAccessibilityTools=true",
|
||||||
)
|
"--export", "UseTaggedPDF=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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/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: "ErrInvalidPdfFormats",
|
scenario: "ErrInvalidPdfFormat",
|
||||||
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: ErrInvalidPdfFormats,
|
expectedError: ErrInvalidPdfFormat,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "ErrMalformedPageRanges",
|
scenario: "ErrMalformedPageRanges",
|
||||||
@@ -364,64 +364,6 @@ 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 (single page sheets)",
|
|
||||||
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("SinglePageSheets"), 0o755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs
|
|
||||||
}(),
|
|
||||||
options: Options{SinglePageSheets: true},
|
|
||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
libeofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
libeofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
libreofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLibreOffice_Descriptor(t *testing.T) {
|
func TestLibreOffice_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// 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. This package allows for:
|
// interface.
|
||||||
//
|
|
||||||
// 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -64,23 +64,13 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, api.ErrInvalidPdfFormats) {
|
if errors.Is(err, api.ErrInvalidPdfFormat) {
|
||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
"github.com/gotenberg/gotenberg/v7/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.ErrInvalidPdfFormats
|
return api.ErrInvalidPdfFormat
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
@@ -166,21 +166,3 @@ 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,16 +1,15 @@
|
|||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
libreofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
||||||
@@ -25,44 +24,70 @@ 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
|
||||||
exportFormFields bool
|
nativePdfA1aFormat bool
|
||||||
singlePageSheets 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("exportFormFields", &exportFormFields, true).
|
Bool("nativePdfA1aFormat", &nativePdfA1aFormat, false).
|
||||||
Bool("singlePageSheets", &singlePageSheets, false).
|
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: pdfa,
|
PdfA: actualPdfArchive,
|
||||||
PdfUa: pdfua,
|
PdfUa: pdfua,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,29 +95,18 @@ 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,
|
|
||||||
SinglePageSheets: singlePageSheets,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if nativePdfFormats {
|
if nativeFormats {
|
||||||
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),
|
||||||
@@ -104,7 +118,9 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// So far so good, let's check if we have to merge the PDFs.
|
// So far so good, let's check if we have to merge the PDFs. Quick
|
||||||
|
// 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")
|
||||||
|
|
||||||
@@ -113,14 +129,47 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
return fmt.Errorf("merge PDFs: %w", err)
|
return fmt.Errorf("merge PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only one output path.
|
// Now, let's check if the client want to convert this result
|
||||||
outputPaths = []string{outputPath}
|
// PDF to specific PDF formats.
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's check if the client want to convert each PDF to a specific
|
// Ok, we don't have to merge the PDFs. Let's check if the client
|
||||||
// PDF format.
|
// want to convert each PDF to a specific PDF format.
|
||||||
zeroValued := gotenberg.PdfFormats{}
|
zeroValued := gotenberg.PdfFormats{}
|
||||||
if !nativePdfFormats && pdfFormats != zeroValued {
|
if !nativeFormats && pdfFormats != zeroValued {
|
||||||
convertOutputPaths := make([]string, len(outputPaths))
|
convertOutputPaths := make([]string, len(outputPaths))
|
||||||
|
|
||||||
for i, outputPath := range outputPaths {
|
for i, outputPath := range outputPaths {
|
||||||
@@ -129,40 +178,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes and potentially overrides metadata entries, if any.
|
|
||||||
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
|
// 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 Uno 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,15 +4,14 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
libreofficeapi "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertRoute(t *testing.T) {
|
func TestConvertRoute(t *testing.T) {
|
||||||
@@ -26,7 +25,6 @@ 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",
|
||||||
@@ -39,50 +37,6 @@ 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 {
|
||||||
@@ -90,11 +44,6 @@ 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{
|
||||||
@@ -132,16 +81,18 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "PDF engine merge error",
|
scenario: "ErrPdfFormatNotSupported (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{
|
||||||
"document.docx": "/document.docx",
|
"document.docx": "/document.docx",
|
||||||
"document2.docx": "/document2.docx",
|
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"merge": {
|
"pdfa": {
|
||||||
"true",
|
"foo",
|
||||||
|
},
|
||||||
|
"nativePdfFormats": {
|
||||||
|
"false",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -155,16 +106,17 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
engine: &gotenberg.PdfEngineMock{
|
engine: &gotenberg.PdfEngineMock{
|
||||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return errors.New("foo")
|
return gotenberg.ErrPdfFormatNotSupported
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectHttpError: false,
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "PDF engine convert error",
|
scenario: "PDF engine convert error (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{
|
||||||
@@ -198,64 +150,7 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "PDF engine write metadata error",
|
scenario: "cannot add output paths (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{
|
|
||||||
"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{
|
||||||
@@ -278,6 +173,312 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (single file)",
|
scenario: "success (single file)",
|
||||||
|
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 nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (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",
|
||||||
|
})
|
||||||
|
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"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success with non-native PDF/A & PDF/UA (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{
|
||||||
|
"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: "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: 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{
|
||||||
@@ -297,9 +498,6 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"nativePdfFormats": {
|
"nativePdfFormats": {
|
||||||
"false",
|
"false",
|
||||||
},
|
},
|
||||||
"metadata": {
|
|
||||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -312,13 +510,10 @@ 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 {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
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 {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -327,15 +522,17 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (many files)",
|
scenario: "success with non-native PDF/A & PDF/UA (merge)",
|
||||||
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",
|
"document2.docx": "/document2.docx",
|
||||||
"document2.doc": "/document2.doc",
|
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
|
"merge": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1b,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
@@ -345,53 +542,6 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"nativePdfFormats": {
|
"nativePdfFormats": {
|
||||||
"false",
|
"false",
|
||||||
},
|
},
|
||||||
"metadata": {
|
|
||||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
|
||||||
return nil
|
|
||||||
}})
|
|
||||||
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"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
expectHttpError: false,
|
|
||||||
expectOutputPathsCount: 3,
|
|
||||||
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scenario: "success with native PDF/A & PDF/UA",
|
|
||||||
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{
|
|
||||||
"pdfa": {
|
|
||||||
gotenberg.PdfA1b,
|
|
||||||
},
|
|
||||||
"pdfua": {
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -403,6 +553,14 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
return []string{".docx"}
|
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,
|
||||||
@@ -444,12 +602,6 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -174,7 +174,7 @@ func newLogEncoder(format string) (zapcore.Encoder, error) {
|
|||||||
// If interactive terminal, make output more human-readable by default.
|
// If interactive terminal, make output more human-readable by default.
|
||||||
// Credits: https://github.com/caddyserver/caddy/blob/v2.1.1/logging.go#L671.
|
// Credits: https://github.com/caddyserver/caddy/blob/v2.1.1/logging.go#L671.
|
||||||
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
|
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
|
||||||
encoder.AppendString(ts.Local().Format("2006/01/02 15:04:05.000"))
|
encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if format == textLoggingFormat || format == autoLoggingFormat {
|
if format == textLoggingFormat || format == autoLoggingFormat {
|
||||||
|
|||||||
@@ -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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLogging_Descriptor(t *testing.T) {
|
func TestLogging_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// 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:
|
// interface using the pdfcpu library. This package allows for the merging of
|
||||||
//
|
// 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -46,22 +46,12 @@ func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("merge PDFs with pdfcpu: %w", err)
|
return fmt.Errorf("merge PDFs with PDFcpu: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert is not available in this implementation.
|
// Convert is not available in this implementation.
|
||||||
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
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.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPdfCpu_Descriptor(t *testing.T) {
|
func TestPdfCpu_Descriptor(t *testing.T) {
|
||||||
@@ -102,21 +102,3 @@ 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,12 +3,11 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type multiPdfEngines struct {
|
type multiPdfEngines struct {
|
||||||
@@ -71,65 +70,6 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMultiPdfEngines_Merge(t *testing.T) {
|
func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||||
@@ -177,169 +177,3 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -54,6 +54,18 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
names := flags.MustStringSlice("pdfengines-engines")
|
names := flags.MustStringSlice("pdfengines-engines")
|
||||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||||
|
|
||||||
|
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(mod)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = logger.Named("pdfengines")
|
||||||
|
|
||||||
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get PDF engines: %w", err)
|
return fmt.Errorf("get PDF engines: %w", err)
|
||||||
@@ -69,13 +81,13 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
// Selection from user.
|
// Selection from user.
|
||||||
mod.names = names
|
mod.names = names
|
||||||
|
|
||||||
// Example in case of deprecated module name.
|
for i, name := range names {
|
||||||
//for i, name := range names {
|
// FIXME: deprecated.
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -168,8 +180,6 @@ 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,7 +6,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPdfEngines_Descriptor(t *testing.T) {
|
func TestPdfEngines_Descriptor(t *testing.T) {
|
||||||
@@ -32,12 +34,16 @@ 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
|
||||||
@@ -69,12 +75,17 @@ 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
|
||||||
@@ -120,16 +131,107 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "no valid PDF engine",
|
scenario: "user select deprecated unoconv-pdfengine",
|
||||||
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 {
|
||||||
|
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",
|
||||||
|
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 {
|
engine := &struct {
|
||||||
gotenberg.ModuleMock
|
gotenberg.ModuleMock
|
||||||
gotenberg.ValidatorMock
|
gotenberg.ValidatorMock
|
||||||
@@ -312,7 +414,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 4,
|
expectRoutes: 2,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mergeRoute returns an [api.Route] which can merge PDFs.
|
// mergeRoute returns an [api.Route] which can merge PDFs.
|
||||||
@@ -25,35 +23,40 @@ 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: pdfa,
|
PdfA: actualPdfArchive,
|
||||||
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)
|
||||||
@@ -70,7 +73,18 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,16 +92,9 @@ 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)
|
||||||
@@ -98,8 +105,8 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
// convertRoute returns an [api.Route] which can convert a PDF to a specific
|
||||||
// format.
|
// PDF 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,
|
||||||
@@ -111,12 +118,14 @@ 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()
|
||||||
@@ -124,8 +133,20 @@ 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: pdfa,
|
PdfA: actualPdfArchive,
|
||||||
PdfUa: pdfua,
|
PdfUa: pdfua,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,27 +163,30 @@ 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 {
|
|
||||||
return fmt.Errorf("convert PDF: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(outputPaths) > 1 {
|
if err != nil {
|
||||||
// If .zip archive, keep the original filename.
|
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||||
err = ctx.Rename(outputPaths[i], inputPath)
|
return api.WrapError(
|
||||||
if err != nil {
|
fmt.Errorf("convert PDF: %w", err),
|
||||||
return fmt.Errorf("rename output path: %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),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputPaths[i] = inputPath
|
return fmt.Errorf("convert PDF: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -172,98 +196,3 @@ 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,16 +4,13 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMergeHandler(t *testing.T) {
|
func TestMergeHandler(t *testing.T) {
|
||||||
@@ -35,27 +32,7 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "invalid metadata form field",
|
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",
|
|
||||||
"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{
|
||||||
@@ -73,60 +50,6 @@ 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 {
|
||||||
@@ -149,6 +72,25 @@ 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{
|
||||||
@@ -159,9 +101,6 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
"pdfa": {
|
"pdfa": {
|
||||||
gotenberg.PdfA1b,
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
"metadata": {
|
|
||||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -170,9 +109,67 @@ 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
|
||||||
},
|
},
|
||||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
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: "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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -230,7 +227,6 @@ 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",
|
||||||
@@ -254,6 +250,30 @@ 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 {
|
||||||
@@ -302,13 +322,16 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success with PDF/A & PDF/UA form fields (single file)",
|
scenario: "success with every 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,
|
||||||
},
|
},
|
||||||
@@ -328,7 +351,7 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot rename many files",
|
scenario: "success with every PDF/A & PDF/UA form fields (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{
|
||||||
@@ -336,6 +359,9 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -343,39 +369,6 @@ 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{
|
||||||
@@ -386,7 +379,6 @@ 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) {
|
||||||
@@ -425,304 +417,6 @@ 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,10 +1,8 @@
|
|||||||
// 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:
|
// interface using the PDFtk command-line tool. This package allows for the
|
||||||
//
|
// merging of PDF files but does not support conversion to specific PDF
|
||||||
// 1. The merging of PDF files.
|
// formats. The path to the PDFtk binary must be specified using the
|
||||||
//
|
// 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,10 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -51,6 +52,23 @@ 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
|
||||||
@@ -62,7 +80,16 @@ 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
|
||||||
}
|
}
|
||||||
@@ -75,20 +102,16 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadMetadata is not available in this implementation.
|
var (
|
||||||
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
activeInstancesCount float64
|
||||||
return nil, fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
activeInstancesCountMu sync.RWMutex
|
||||||
}
|
)
|
||||||
|
|
||||||
// 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.PdfEngine = (*PdfTk)(nil)
|
_ gotenberg.MetricsProvider = (*PdfTk)(nil)
|
||||||
|
_ gotenberg.PdfEngine = (*PdfTk)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPdfTk_Descriptor(t *testing.T) {
|
func TestPdfTk_Descriptor(t *testing.T) {
|
||||||
@@ -71,6 +71,22 @@ 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
|
||||||
@@ -148,21 +164,3 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrometheus_Descriptor(t *testing.T) {
|
func TestPrometheus_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
// 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:
|
// interface using the QPDF command-line tool. This package allows for the
|
||||||
//
|
// merging of PDF files but does not support conversion to specific PDF
|
||||||
// 1. The merging of PDF files.
|
// formats. The path to the QPDF binary must be specified using the
|
||||||
//
|
// 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,10 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -51,6 +52,23 @@ 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
|
||||||
@@ -64,7 +82,16 @@ 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
|
||||||
}
|
}
|
||||||
@@ -77,19 +104,15 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadMetadata is not available in this implementation.
|
var (
|
||||||
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
activeInstancesCount float64
|
||||||
return nil, fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
activeInstancesCountMu sync.RWMutex
|
||||||
}
|
)
|
||||||
|
|
||||||
// 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.PdfEngine = (*QPdf)(nil)
|
_ gotenberg.MetricsProvider = (*QPdf)(nil)
|
||||||
|
_ gotenberg.PdfEngine = (*QPdf)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQPdf_Descriptor(t *testing.T) {
|
func TestQPdf_Descriptor(t *testing.T) {
|
||||||
@@ -71,6 +71,22 @@ 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
|
||||||
@@ -148,21 +164,3 @@ 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,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,8 +17,7 @@ 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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func webhookMiddleware(w *Webhook) api.Middleware {
|
func webhookMiddleware(w *Webhook) api.Middleware {
|
||||||
@@ -44,19 +44,38 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
deadline, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
return errors.New("context has no deadline")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's check if the webhook URLs are acceptable according to our
|
// Let's check if the webhook URLs are acceptable according to our
|
||||||
// allowed/denied lists.
|
// allowed/denied lists.
|
||||||
err := gotenberg.FilterDeadline(w.allowList, w.denyList, webhookUrl, deadline)
|
filter := func(URL, header string, allowList, denyList *regexp.Regexp) error {
|
||||||
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("filter webhook URL: %w", err)
|
return fmt.Errorf("filter webhook URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = gotenberg.FilterDeadline(w.errorAllowList, w.errorDenyList, webhookErrorUrl, deadline)
|
err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("filter webhook error URL: %w", err)
|
return fmt.Errorf("filter webhook error URL: %w", err)
|
||||||
}
|
}
|
||||||
@@ -159,7 +178,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
echo.HeaderContentType: echo.MIMEApplicationJSON,
|
echo.HeaderContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||||
c.Get("traceHeader").(string): c.Get("trace").(string),
|
c.Get("traceHeader").(string): c.Get("trace").(string),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/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: regexp2.MustCompile("", 0),
|
allowList: regexp.MustCompile(""),
|
||||||
denyList: regexp2.MustCompile("", 0),
|
denyList: regexp.MustCompile(""),
|
||||||
errorAllowList: regexp2.MustCompile("", 0),
|
errorAllowList: regexp.MustCompile(""),
|
||||||
errorDenyList: regexp2.MustCompile("", 0),
|
errorDenyList: regexp.MustCompile(""),
|
||||||
maxRetry: 0,
|
maxRetry: 0,
|
||||||
retryMinWait: 0,
|
retryMinWait: 0,
|
||||||
retryMaxWait: 0,
|
retryMaxWait: 0,
|
||||||
@@ -63,7 +63,6 @@ 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
|
||||||
@@ -77,7 +76,6 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
noDeadline: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
},
|
},
|
||||||
@@ -89,23 +87,10 @@ 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 {
|
||||||
@@ -116,11 +101,12 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.allowList = regexp2.MustCompile("bar", 0)
|
mod.allowList = regexp.MustCompile("bar")
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
noDeadline: false,
|
expectError: true,
|
||||||
expectError: true,
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "webhook URL is denied",
|
scenario: "webhook URL is denied",
|
||||||
@@ -132,11 +118,12 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.denyList = regexp2.MustCompile("foo", 0)
|
mod.denyList = regexp.MustCompile("foo")
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
noDeadline: false,
|
expectError: true,
|
||||||
expectError: true,
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "webhook error URL is not allowed",
|
scenario: "webhook error URL is not allowed",
|
||||||
@@ -148,11 +135,12 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.errorAllowList = regexp2.MustCompile("foo", 0)
|
mod.errorAllowList = regexp.MustCompile("foo")
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
noDeadline: false,
|
expectError: true,
|
||||||
expectError: true,
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "webhook error URL is denied",
|
scenario: "webhook error URL is denied",
|
||||||
@@ -164,11 +152,12 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
mod: func() *Webhook {
|
mod: func() *Webhook {
|
||||||
mod := buildWebhookModule()
|
mod := buildWebhookModule()
|
||||||
mod.errorDenyList = regexp2.MustCompile("bar", 0)
|
mod.errorDenyList = regexp.MustCompile("bar")
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
noDeadline: false,
|
expectError: true,
|
||||||
expectError: true,
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "invalid webhook method (GET)",
|
scenario: "invalid webhook method (GET)",
|
||||||
@@ -180,7 +169,6 @@ 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,
|
||||||
@@ -195,7 +183,6 @@ 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,
|
||||||
@@ -211,7 +198,6 @@ 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,
|
||||||
@@ -227,7 +213,6 @@ 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,
|
||||||
@@ -243,7 +228,6 @@ 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,
|
||||||
@@ -258,7 +242,6 @@ 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,
|
||||||
@@ -271,20 +254,15 @@ func TestWebhookMiddlewareGuards(t *testing.T) {
|
|||||||
|
|
||||||
c := srv.NewContext(tc.request, httptest.NewRecorder())
|
c := srv.NewContext(tc.request, httptest.NewRecorder())
|
||||||
|
|
||||||
if tc.noDeadline {
|
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
ctx := &api.ContextMock{Context: &api.Context{Context: context.Background()}}
|
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", func() context.CancelFunc {
|
||||||
return nil
|
return func() {
|
||||||
}())
|
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)
|
||||||
|
|
||||||
@@ -342,10 +320,10 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
|
|
||||||
buildWebhookModule := func() *Webhook {
|
buildWebhookModule := func() *Webhook {
|
||||||
return &Webhook{
|
return &Webhook{
|
||||||
allowList: regexp2.MustCompile("", 0),
|
allowList: regexp.MustCompile(""),
|
||||||
denyList: regexp2.MustCompile("", 0),
|
denyList: regexp.MustCompile(""),
|
||||||
errorAllowList: regexp2.MustCompile("", 0),
|
errorAllowList: regexp.MustCompile(""),
|
||||||
errorDenyList: regexp2.MustCompile("", 0),
|
errorDenyList: regexp.MustCompile(""),
|
||||||
maxRetry: 0,
|
maxRetry: 0,
|
||||||
retryMinWait: 0,
|
retryMinWait: 0,
|
||||||
retryMaxWait: 0,
|
retryMaxWait: 0,
|
||||||
@@ -376,7 +354,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
return errors.New("foo")
|
return errors.New("foo")
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
expectWebhookContentType: echo.MIMEApplicationJSON,
|
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||||
expectWebhookMethod: http.MethodPost,
|
expectWebhookMethod: http.MethodPost,
|
||||||
expectWebhookErrorStatus: http.StatusInternalServerError,
|
expectWebhookErrorStatus: http.StatusInternalServerError,
|
||||||
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
|
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
|
||||||
@@ -390,7 +368,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
return api.NewSentinelHttpError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
|
return api.NewSentinelHttpError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
expectWebhookContentType: echo.MIMEApplicationJSON,
|
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||||
expectWebhookMethod: http.MethodPost,
|
expectWebhookMethod: http.MethodPost,
|
||||||
expectWebhookErrorStatus: http.StatusBadRequest,
|
expectWebhookErrorStatus: http.StatusBadRequest,
|
||||||
expectWebhookErrorMessage: http.StatusText(http.StatusBadRequest),
|
expectWebhookErrorMessage: http.StatusText(http.StatusBadRequest),
|
||||||
@@ -430,7 +408,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
returnedError: echo.ErrInternalServerError,
|
returnedError: echo.ErrInternalServerError,
|
||||||
expectWebhookContentType: echo.MIMEApplicationJSON,
|
expectWebhookContentType: echo.MIMEApplicationJSONCharsetUTF8,
|
||||||
expectWebhookMethod: http.MethodPost,
|
expectWebhookMethod: http.MethodPost,
|
||||||
expectWebhookErrorStatus: http.StatusInternalServerError,
|
expectWebhookErrorStatus: http.StatusInternalServerError,
|
||||||
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
|
expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError),
|
||||||
@@ -448,17 +426,22 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
c.Set("trace", "foo")
|
c.Set("trace", "foo")
|
||||||
c.Set("startTime", time.Now())
|
c.Set("startTime", time.Now())
|
||||||
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
|
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
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", cancel)
|
c.Set("cancel", func() context.CancelFunc {
|
||||||
|
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))
|
||||||
@@ -493,7 +476,7 @@ func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if contentType == echo.MIMEApplicationJSON {
|
if contentType == echo.MIMEApplicationJSONCharsetUTF8 {
|
||||||
body, err := io.ReadAll(c.Request().Body)
|
body, err := io.ReadAll(c.Request().Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errChan <- err
|
errChan <- err
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
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/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -17,10 +19,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 *regexp2.Regexp
|
allowList *regexp.Regexp
|
||||||
denyList *regexp2.Regexp
|
denyList *regexp.Regexp
|
||||||
errorAllowList *regexp2.Regexp
|
errorAllowList *regexp.Regexp
|
||||||
errorDenyList *regexp2.Regexp
|
errorDenyList *regexp.Regexp
|
||||||
maxRetry int
|
maxRetry int
|
||||||
retryMinWait time.Duration
|
retryMinWait time.Duration
|
||||||
retryMaxWait time.Duration
|
retryMaxWait time.Duration
|
||||||
@@ -34,6 +36,31 @@ 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")
|
||||||
@@ -53,15 +80,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.MustRegexp("webhook-allow-list")
|
w.allowList = flags.MustDeprecatedRegexp("api-webhook-allow-list", "webhook-allow-list")
|
||||||
w.denyList = flags.MustRegexp("webhook-deny-list")
|
w.denyList = flags.MustDeprecatedRegexp("api-webhook-deny-list", "webhook-deny-list")
|
||||||
w.errorAllowList = flags.MustRegexp("webhook-error-allow-list")
|
w.errorAllowList = flags.MustDeprecatedRegexp("api-webhook-error-allow-list", "webhook-error-allow-list")
|
||||||
w.errorDenyList = flags.MustRegexp("webhook-error-deny-list")
|
w.errorDenyList = flags.MustDeprecatedRegexp("api-webhook-error-deny-list", "webhook-error-deny-list")
|
||||||
w.maxRetry = flags.MustInt("webhook-max-retry")
|
w.maxRetry = flags.MustDeprecatedInt("api-webhook-max-retry", "webhook-max-retry")
|
||||||
w.retryMinWait = flags.MustDuration("webhook-retry-min-wait")
|
w.retryMinWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-min-wait")
|
||||||
w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")
|
w.retryMaxWait = flags.MustDeprecatedDuration("api-webhook-retry-min-wait", "webhook-retry-max-wait")
|
||||||
w.clientTimeout = flags.MustDuration("webhook-client-timeout")
|
w.clientTimeout = flags.MustDuration("webhook-client-timeout")
|
||||||
w.disable = flags.MustBool("webhook-disable")
|
w.disable = flags.MustDeprecatedBool("api-disable-webhook", "webhook-disable")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebhook_Descriptor(t *testing.T) {
|
func TestWebhook_Descriptor(t *testing.T) {
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ package standard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
// Standard Gotenberg modules.
|
// Standard Gotenberg modules.
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/chromium"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/chromium"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/exiftool"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/api"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/pdfengine"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/logging"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/logging"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfcpu"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/pdfcpu"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/pdftk"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/prometheus"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/qpdf"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/qpdf"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook"
|
||||||
_ "github.com/gotenberg/gotenberg/v8/pkg/modules/webhook"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
# Credits: https://github.com/thecodingmachine/docker-images-php.
|
# Credits: https://github.com/thecodingmachine/docker-images-php.
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
mkdir -p testing_file_system_rights.foo
|
mkdir testing_file_system_rights.foo
|
||||||
chmod 700 testing_file_system_rights.foo
|
chmod 700 testing_file_system_rights.foo
|
||||||
su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1"
|
su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1"
|
||||||
HAS_CONSISTENT_RIGHTS=$?
|
HAS_CONSISTENT_RIGHTS=$?
|
||||||
|
|
||||||
if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then
|
if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then
|
||||||
# If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!).
|
# If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!).
|
||||||
DOCKER_USER=$(stat -c '%u' "$(pwd)")
|
DOCKER_USER=$(stat -c '%U' "$(pwd)")
|
||||||
else
|
else
|
||||||
# macOs or Windows.
|
# macOs or Windows.
|
||||||
# Note: in most cases, we don't care about the rights (they are not respected).
|
# Note: in most cases, we don't care about the rights (they are not respected).
|
||||||
FILE_OWNER=$(stat -c '%u' "testing_file_system_rights.foo/foo")
|
FILE_OWNER=$(stat -c '%U' "testing_file_system_rights.foo/foo")
|
||||||
if [[ "$FILE_OWNER" == "0" ]]; then
|
if [[ "$FILE_OWNER" == "root" ]]; then
|
||||||
# If root, we are likely on a Windows host.
|
# If root, we are likely on a Windows host.
|
||||||
# All files will belong to root, but it does not matter as everybody can write/delete
|
# All files will belong to root, but it does not matter as everybody can write/delete
|
||||||
# those (0777 access rights).
|
# those (0777 access rights).
|
||||||
|
|||||||
@@ -3,8 +3,4 @@
|
|||||||
set -x
|
set -x
|
||||||
|
|
||||||
go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./...
|
go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./...
|
||||||
RESULT=$?
|
|
||||||
|
|
||||||
go tool cover -html=coverage.txt -o /tests/coverage.html
|
go tool cover -html=coverage.txt -o /tests/coverage.html
|
||||||
|
|
||||||
exit $RESULT
|
|
||||||
14
test/testdata/api/README.md
vendored
14
test/testdata/api/README.md
vendored
@@ -1,14 +0,0 @@
|
|||||||
To generate a valid certificate and private key use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In OpenSSL ≥ 1.1.1
|
|
||||||
openssl req -x509 -newkey rsa:4096 -sha256 -days 9999 -nodes \
|
|
||||||
-keyout key.pem -out cert.pem -subj "/CN=localhost" \
|
|
||||||
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1"
|
|
||||||
```
|
|
||||||
|
|
||||||
To check a certificate use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl x509 -in cert.pem -text
|
|
||||||
```
|
|
||||||
30
test/testdata/api/cert.pem
vendored
30
test/testdata/api/cert.pem
vendored
@@ -1,30 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFOjCCAyKgAwIBAgIUZaht2QoZwWDK8wEuwm+wmip9sqwwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI0MDUyNjA1MDExNFoYDzIwNTEx
|
|
||||||
MDExMDUwMTE0WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
|
|
||||||
AQUAA4ICDwAwggIKAoICAQCm3TNckD55ae6pxXS03iV2P3X/edZT4vRa8D15elsi
|
|
||||||
CSRf6mdU2TVAQyg22GEcMXgsbba7sotJVg7559LkWAgb3Dj9rbPputdS29ebncat
|
|
||||||
x1nzo1QHMCS/v7nWoPe9fQVlDEf3ebM198f+5vhccSYFxFa+YvsUmSw9yMF8z1nA
|
|
||||||
6POZTpm6ErByAY9EGvC8EcNOQd9xLOfBZc0e+mY/rMRxmL+0N+sSRE/ACU+2zxHN
|
|
||||||
KcCiFX5WQdLDYW1ZjX5xAVatCPNlxSQpj97/ktGsnqwGnJWSTeMUOPv7L0LrQasf
|
|
||||||
k1VXBvfTW2/ZqhI9WA8qfdC5XOuzVwfU14gv+6vv9AGZnUFmOjeo9XG4xWBxlIKj
|
|
||||||
AQAzeNwAGFltPkp5a3yPczxMjKY4xLsMGcDfpaNtv1B24+7fV77Nyty/gLLT9pUa
|
|
||||||
FtXLuRfIPSaUK3kFhAuYzGUL/sO9O9jJkNlxxygxQ+VyIgmdw0zWrUi3q+Lfmdya
|
|
||||||
FFjHdM7LnLrLCjSv3dEoMINngR3+IejsY+98Ehd25O3oW+ZnGIQX0VwykV8sOQWq
|
|
||||||
rpnnZ+2iHMtF6Vsj40z/Y9y7yVE18AIJFC+Yr/dD/nQyg6SE70wdNcOx0Ay1xHbP
|
|
||||||
WuV3fqKOfhONR7NEBSO2UbHi6GZAiYMMwwul+4yKAQ4+lHOEXG4JQcKvtZasxq9v
|
|
||||||
GwIDAQABo4GBMH8wHQYDVR0OBBYEFPPMaH3C0rAkD/E0gtBZwM0x8N9MMB8GA1Ud
|
|
||||||
IwQYMBaAFPPMaH3C0rAkD/E0gtBZwM0x8N9MMA8GA1UdEwEB/wQFMAMBAf8wLAYD
|
|
||||||
VR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG
|
|
||||||
SIb3DQEBCwUAA4ICAQB0DXz59JfQgfJvRJfraa+Cv/3nG4NNmHYtNGhrid5sagH3
|
|
||||||
LM8jkbA+JKRAn34SmsqUZb1f2XFI64TwrScgMfAvfi+5ibRqBjM/YL06bTlG61Ua
|
|
||||||
t2IQchV2kcX4zQnaMCIYG0dUj6JoXIspiFULMHyGDs7G9BKVcZxlaLrEvPXBqQjd
|
|
||||||
nwF80KHS/rCConmICU9uWHPgHxS1VmhQiwYGbrakETilau1ppRS0bpH2k/stVyjc
|
|
||||||
plttIr1DUD6xaRV4mZ0NZVlIXl2nDf78iQvreJeMrGl6AcNHzV+/rGHpY/gOrziP
|
|
||||||
PQkB1VtSiHtBe/VMKXcVB69SEzsoGEknqs4GPXuVYm47POo3/U5HmGU+FadSATNP
|
|
||||||
qtorRECUMbVn0IfDzJX2jBLUjZlhPI9gqO9DhiAT8FWHRV2yPsZKuiBBg5/hUvyb
|
|
||||||
Qsswtmi/YV5e6AnXEuvylwE3wYzlqDzxwlZM08bWFyZYnSf38d3hAjVwUHAugiaX
|
|
||||||
4sgE6GPAiONW5FkTZ4Z2oXYiN213QDIIcQeFAl04qSEFkGBV8x2hM0uXgFH1S++j
|
|
||||||
JePdKBbV/CjxegyyYsmnln+eO1kC8pCalB6k6pEtYMqNYOnmimC8UKrJATrH9EVq
|
|
||||||
eAehGBiIe514Oz06Ws2Vm4PTiY2x0KVbqphwkuz85ohSF9UXOHUcMryusr1zlg==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
52
test/testdata/api/key.pem
vendored
52
test/testdata/api/key.pem
vendored
@@ -1,52 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCm3TNckD55ae6p
|
|
||||||
xXS03iV2P3X/edZT4vRa8D15elsiCSRf6mdU2TVAQyg22GEcMXgsbba7sotJVg75
|
|
||||||
59LkWAgb3Dj9rbPputdS29ebncatx1nzo1QHMCS/v7nWoPe9fQVlDEf3ebM198f+
|
|
||||||
5vhccSYFxFa+YvsUmSw9yMF8z1nA6POZTpm6ErByAY9EGvC8EcNOQd9xLOfBZc0e
|
|
||||||
+mY/rMRxmL+0N+sSRE/ACU+2zxHNKcCiFX5WQdLDYW1ZjX5xAVatCPNlxSQpj97/
|
|
||||||
ktGsnqwGnJWSTeMUOPv7L0LrQasfk1VXBvfTW2/ZqhI9WA8qfdC5XOuzVwfU14gv
|
|
||||||
+6vv9AGZnUFmOjeo9XG4xWBxlIKjAQAzeNwAGFltPkp5a3yPczxMjKY4xLsMGcDf
|
|
||||||
paNtv1B24+7fV77Nyty/gLLT9pUaFtXLuRfIPSaUK3kFhAuYzGUL/sO9O9jJkNlx
|
|
||||||
xygxQ+VyIgmdw0zWrUi3q+LfmdyaFFjHdM7LnLrLCjSv3dEoMINngR3+IejsY+98
|
|
||||||
Ehd25O3oW+ZnGIQX0VwykV8sOQWqrpnnZ+2iHMtF6Vsj40z/Y9y7yVE18AIJFC+Y
|
|
||||||
r/dD/nQyg6SE70wdNcOx0Ay1xHbPWuV3fqKOfhONR7NEBSO2UbHi6GZAiYMMwwul
|
|
||||||
+4yKAQ4+lHOEXG4JQcKvtZasxq9vGwIDAQABAoICAA3s7URkT/0XTMZOUZcRPanI
|
|
||||||
p12oGq2Vhd8GUvBjDbKhdgUAgu8fJsUzgD+o1Ic4eaxQ26x+Z1wPb+CF6vZeLQ8Q
|
|
||||||
1JDB1lSYiy/BuRSmQ4EX30yTQjKiCe8Ws7j9u1nM/Tju9XKUbX2rgr1QeURTDQvz
|
|
||||||
fr8UyO9xzly5oawmLHoKTDTBzS0s3Md8Pys9NFSoXCxDwxSm3WXTy8fcuHvH51Je
|
|
||||||
SmFik/OfpSgu9BVwN9lvX+wm7wwzzMBmzmwS1Y0zbLsJbDFUv9fYHJF8/JKeC1fh
|
|
||||||
6Xme67yG+kB+3o7TmXxhHKt5TBAPfsjOMLIojzDJ9IlY/91PqLpSjRhetdKorRlf
|
|
||||||
gvTUGLPY8lHXwz6UBpdHmMhNHTBBvgHcOVAMD46Y81SdqMhH6TAYSh7XQVuA06Ql
|
|
||||||
MTWzGqzKriaQDWmJ6Nx8eZCRDZGXDq4rKiJFvRFUrGYYr818qU8yC+qv5uLrOF1w
|
|
||||||
YZV0WnZ7DtcaFliBrJVUfeTmrSJBa3yoIKbeaTDuh3TMHqgAbTx0lBdGoI6noRxx
|
|
||||||
v7fCGho/aSOJ2rtFOjRoLQ5K8onZ226arJEyfkTh16wztQgh9DC430OkL4cQkfA0
|
|
||||||
BxN8tGh4P0lqwcBnCOyzi4NV1NkwaaVv+pt757Cz+Cy6e2DQOtDkrFEh4p56NqDr
|
|
||||||
KcJz88LYbbChoFKlb/kBAoIBAQDhcuI0KWTErojWu/dB2Okth8OFaTdvAPEvo+qO
|
|
||||||
ItJgxyYL8JIdWUzS9vKro6vMctwIXvNa3K2AAwdZ1kHvaxDZaikQbnjnSjOqRlZa
|
|
||||||
4rVnI6SSKrYj6xmKcsxxTi9YP8b2msjdLuGhPfO8Dw4EQgOt6xg0rq4msnv6bYki
|
|
||||||
G7vsk/InnKLm8SEhR1XsmtVrpC7KbXniLQEehgs1Kp0nMLptSsOTnA1kGoZDVlXc
|
|
||||||
u+kBBai6MM0MMDpsJgM1IFkUousQl3ItBcTXD2ZCMcg5psQJbdEhu7oXiGkHdKlL
|
|
||||||
1UEIdSGNlzHiVM62uNxhNfwh7I6SPesDksJbE0Yd//jz/7FRAoIBAQC9ee+pDsSs
|
|
||||||
yqunQjMWImTpD5h9fSRTzWT8h0dXsJyYi7s9Dj4mQIuoarscMI6L7njeTqHE8ptc
|
|
||||||
b9DzIEWL+233nTOS59grCTD84Kyrn+Hw6EebgKYmtpZnvMw5cccoZ3qUsxVGurhA
|
|
||||||
lzVaKpHtPKWT1fduNmnqt6RkZ7aRBLZre/i9UntZ7VSZYRKMOBvJLpLdqmiSPkvM
|
|
||||||
w8ra+cedHwDegqLkoX0sxdI1yNYO1GSkJ+w1I1m1rzuCd9X5QXvyQhPezVB9wUwr
|
|
||||||
anNGb6/YgvthzvMQAXYqv4pmb769przieMJ6geQL3O8QEtvpDAV4SvrwnA+H/wfs
|
|
||||||
BvwI4MTHNJ6rAoIBADEfnzpOaq6QeTCQ9GdcpDJPisLfEj0Vr3f593nRDJYZzqh7
|
|
||||||
WtsaQisVs/rCRwTdYiRQzCXYP9XM0yU4ElXgtmMpRplV+PIssOVBPj+/dldq5MkU
|
|
||||||
lWtuJrqMGQ/3QUmxW+EQCQRo2rEPlrQ7c4pp9/NFbpjMFxZHfcwrYd4UvPXnprQK
|
|
||||||
5VP85oMh0A8mDarOs4NYJ16o/71u57JF/sgzTevShpr7Pc+6F9dEUKEwMK0QVpt7
|
|
||||||
VrJ5L8Gw5rIEwmREu4N5/F7jCujxag6yWjZ1p/GGBDOdfAb0qi39tYXYibsWCqGX
|
|
||||||
eD8gy4n07dAguzeJG+expiu5JrBBmt2ekNhZtoECggEAQ90VoraAaOcaPgju5UGi
|
|
||||||
ZPtKAcA9r6pEPlJxkg3WuKhrgr3EMs0xeDvqEnmtxJ7AOdduB21hieUesMXhQvbl
|
|
||||||
9ycd3sMdYKSlIB2Ums/kC/YWF7MnT3xcOIGdoAGJdIZDmFjgCPo9nhzKZiYTKCZt
|
|
||||||
o5qiSZ5Bp2jo/3FKKXzHnY3ElIKjXZ0sjNcA3qoGAbOltLEhTSf03D1QPcfy3+rH
|
|
||||||
0+p0T3ErtUqRNORfDuaxuT33Uzz7F6DQrrUfNWxn6WgTV9yvtKr0DiTNbu/3ypN3
|
|
||||||
WBvYYOY1HbCG8JObfK/ovTDFbz1woXMbSOWIUFsW5nPslxs+UNZeTwJdaQygTj0h
|
|
||||||
iwKCAQBuXOPtjWGZINgfjo/n67Q8FuzUXxDK/IuXzuDXhi+B3/O3/PcVEDHFByhI
|
|
||||||
KHOh2mTD25vgFWh+8dNg1R+m5uIeQ5zbZ5U0205XzmrZgBXXCAHKNruTwqZfQ6er
|
|
||||||
np+8wHhcCbBuy0Z0ponzBGlrDYmbxO9anXGxoe/rE3+pWbzMneZ+cHgNjXCtfZCY
|
|
||||||
XGfLeJWW3+B8y5Lff39ADawmjm8VWVEdd8DP5o2zulbXhgMMnAsSVsR/XgwBqpbf
|
|
||||||
82k1bqvN2v08ID5nmTRdzD+D51Hjw80Uoqhm5DJL4IwaoK/YstCu2TCbmLGiyqSk
|
|
||||||
lrSOZMPTLX1tN/cWa+Ii+/WALnkI
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
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: 12px;
|
font-size: 30rem;
|
||||||
margin: auto 20px;
|
margin: 4rem auto;
|
||||||
}
|
}
|
||||||
</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: 12px;
|
font-size: 8rem;
|
||||||
margin: auto 20px;
|
margin: 4rem auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
8
test/testdata/chromium/html/index.html
vendored
8
test/testdata/chromium/html/index.html
vendored
@@ -59,14 +59,6 @@
|
|||||||
<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: 12px;
|
font-size: 8rem;
|
||||||
margin: auto 20px;
|
margin: 4rem auto;
|
||||||
}
|
}
|
||||||
</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: 12px;
|
font-size: 8rem;
|
||||||
margin: auto 20px;
|
margin: 4rem auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user