mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1122f09093 | ||
|
|
1123af15c9 | ||
|
|
b2f6e865a7 | ||
|
|
93b9ac2d13 | ||
|
|
64af65b0e7 | ||
|
|
390c8437c5 | ||
|
|
cc65ca9b6a | ||
|
|
4bd7cba247 | ||
|
|
92e2f7f8b7 | ||
|
|
98054ddff6 | ||
|
|
4e5c1aba2e | ||
|
|
5eb44320ae | ||
|
|
f751269289 | ||
|
|
0e836622ec | ||
|
|
a6ac0a813d | ||
|
|
21c47f60f2 | ||
|
|
66142b1dd8 | ||
|
|
3a28f4a0cb | ||
|
|
71eeca96ee | ||
|
|
b56cde47ca | ||
|
|
793e65bac0 | ||
|
|
4d1a569269 | ||
|
|
5c56317d50 | ||
|
|
32491ae2ad | ||
|
|
28aeb11565 | ||
|
|
61eb3ff908 | ||
|
|
ff5455881e | ||
|
|
143b7ce678 | ||
|
|
9c3dfc78df | ||
|
|
017872c5ef | ||
|
|
b9ad67d147 | ||
|
|
62ea3b1a1a | ||
|
|
325b668d00 | ||
|
|
9ce6bf8e14 | ||
|
|
052448c59a | ||
|
|
c8b318b315 | ||
|
|
54daac329e | ||
|
|
b5a59e4de0 | ||
|
|
258876d13f | ||
|
|
9cc8e16d64 | ||
|
|
85d1254638 | ||
|
|
45eb261dcd | ||
|
|
8b2ff5f269 | ||
|
|
79ab568c80 | ||
|
|
382b1737cb | ||
|
|
c9b3a83592 | ||
|
|
0b21ce7ce9 | ||
|
|
523bf8c689 | ||
|
|
8ead5a9d1f | ||
|
|
46e3199438 | ||
|
|
d94e614a54 | ||
|
|
9f459867bf | ||
|
|
f5ce319c24 | ||
|
|
6b9c02d42a | ||
|
|
e2e06a8f83 | ||
|
|
8402f73d52 | ||
|
|
84334ace6a | ||
|
|
43e2518fbf | ||
|
|
13098713e9 | ||
|
|
77c6e541d6 | ||
|
|
bbd49f9372 | ||
|
|
015be517bf | ||
|
|
8faa936a68 | ||
|
|
39922cca5e | ||
|
|
63dd68081a | ||
|
|
3694b19a97 | ||
|
|
c58b2a5e45 | ||
|
|
5b8562f5bd | ||
|
|
67020426f6 | ||
|
|
3840894201 | ||
|
|
888f3feaff | ||
|
|
2e339d7457 | ||
|
|
fd0ff633e6 | ||
|
|
02923fe9ce | ||
|
|
875f97813e | ||
|
|
6a29385762 | ||
|
|
fbc9f9dec9 | ||
|
|
4607ade8e9 | ||
|
|
e4a553c022 | ||
|
|
bc733bab0c | ||
|
|
1a522ddf48 | ||
|
|
1de0f4c4fa | ||
|
|
71eb5c25b5 | ||
|
|
ea34979a29 | ||
|
|
1f784ad44c | ||
|
|
f89726752f | ||
|
|
921b6b7ba4 | ||
|
|
6c5cc7a0ce | ||
|
|
f851e2f2cf | ||
|
|
b73207f29a | ||
|
|
8d8f0b55ea | ||
|
|
0e1e9b9cf3 | ||
|
|
ab4eed78af | ||
|
|
8a36c15d5c | ||
|
|
bd895286bc | ||
|
|
b76990ec16 | ||
|
|
9bc93c5cf5 | ||
|
|
1d8ae79f84 | ||
|
|
4269aa2422 | ||
|
|
c887dc1ec8 | ||
|
|
8bbacd691d | ||
|
|
d8f3f85fb7 | ||
|
|
159c0b3350 | ||
|
|
45788f3235 | ||
|
|
4c85c6ccdf | ||
|
|
08408ae005 | ||
|
|
d470279fb7 | ||
|
|
8260458f8f | ||
|
|
0512dd1e86 | ||
|
|
4972019cb9 | ||
|
|
5e615cab52 | ||
|
|
64ea2b6d3b | ||
|
|
27018fa8e2 | ||
|
|
263d634801 | ||
|
|
c61c33f71c | ||
|
|
bfb30b8fed | ||
|
|
ca3e27937e | ||
|
|
50c34a4186 | ||
|
|
84875aaf82 | ||
|
|
b7a8b95820 | ||
|
|
5cad5ca11a | ||
|
|
7598de29dd | ||
|
|
af6ffe67c8 | ||
|
|
6944c4b3bf | ||
|
|
24817d5707 | ||
|
|
3d855712ee | ||
|
|
872263d253 | ||
|
|
1c491fb1c5 | ||
|
|
29374c579a | ||
|
|
206d7b6ca9 | ||
|
|
ab6ca8c894 | ||
|
|
e7f5fde051 | ||
|
|
030b358b0e | ||
|
|
4b1791d35c | ||
|
|
b788cf823d | ||
|
|
11cbc0288f | ||
|
|
2269c80b02 | ||
|
|
c922cc042e | ||
|
|
9cb6f44f72 | ||
|
|
f376cd5ab1 | ||
|
|
8b8a02b68c | ||
|
|
5759b1336a | ||
|
|
76f17cb6d7 | ||
|
|
7149c421cf | ||
|
|
353c599767 |
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
# Number of days of inactivity before an issue becomes stale
|
# Number of days of inactivity before an issue becomes stale
|
||||||
daysUntilStale: 60
|
daysUntilStale: 15
|
||||||
# Number of days of inactivity before a stale issue is closed
|
# Number of days of inactivity before a stale issue is closed
|
||||||
daysUntilClose: 7
|
daysUntilClose: 7
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
|
|||||||
11
.github/workflows/continuous_delivery.yml
vendored
11
.github/workflows/continuous_delivery.yml
vendored
@@ -5,23 +5,22 @@ on:
|
|||||||
types: [ published ]
|
types: [ published ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release Docker image
|
name: Release Docker image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Log in to Docker Hub Container Registry
|
- name: Log in to Docker Hub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image for release
|
||||||
run: |
|
run: |
|
||||||
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }}
|
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }}
|
||||||
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }} DOCKER_REPOSITORY=thecodingmachine
|
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }} DOCKER_REPOSITORY=thecodingmachine
|
||||||
|
|||||||
46
.github/workflows/continuous_integration.yml
vendored
46
.github/workflows/continuous_integration.yml
vendored
@@ -14,12 +14,17 @@ jobs:
|
|||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Setup Go
|
||||||
uses: actions/checkout@v2
|
uses: actions/setup-go@v5
|
||||||
- name: Run linters
|
|
||||||
uses: golangci/golangci-lint-action@v2
|
|
||||||
with:
|
with:
|
||||||
version: v1.43
|
go-version: '1.21'
|
||||||
|
cache: false
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Run linters
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: v1.54.2
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
needs:
|
needs:
|
||||||
@@ -30,14 +35,37 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- 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: |
|
run: |
|
||||||
make tests-once
|
make tests-once
|
||||||
bash <(curl -s https://codecov.io/bash)
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
multiarch_build:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs:
|
||||||
|
- Tests
|
||||||
|
name: Multi-arch build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Log in to Docker Hub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push Docker image for main branch
|
||||||
|
run: |
|
||||||
|
make release GOTENBERG_VERSION=edge
|
||||||
|
make release GOTENBERG_VERSION=edge DOCKER_REPOSITORY=thecodingmachine
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/coverage.html
|
/coverage.html
|
||||||
/coverage.txt
|
/coverage.txt
|
||||||
|
/TODO.txt
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
|
linters-settings:
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- default
|
||||||
|
- prefix(github.com/gotenberg/gotenberg/v8)
|
||||||
|
skip-generated: true
|
||||||
|
custom-order: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
|
||||||
- errcheck
|
- errcheck
|
||||||
- gofmt
|
- gci
|
||||||
- goimports
|
- gofumpt
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
@@ -13,14 +21,12 @@ linters:
|
|||||||
- misspell
|
- misspell
|
||||||
- prealloc
|
- prealloc
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
deadline: 5m
|
timeout: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: false
|
tests: false
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Julien Neuhart
|
Copyright (c) 2023 Julien Neuhart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
57
Makefile
57
Makefile
@@ -5,14 +5,14 @@ help: ## Show the help
|
|||||||
.PHONY: it
|
.PHONY: it
|
||||||
it: build build-tests ## Initialize the development environment
|
it: build build-tests ## Initialize the development environment
|
||||||
|
|
||||||
GOLANG_VERSION=1.17
|
GOLANG_VERSION=1.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.028 # See https://github.com/googlefonts/noto-emoji/releases.
|
NOTO_COLOR_EMOJI_VERSION=v2.040 # See https://github.com/googlefonts/noto-emoji/releases.
|
||||||
PDFTK_VERSION=1527259628 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||||
GOLANGCI_LINT_VERSION=v1.43.0 # See https://github.com/golangci/golangci-lint/releases.
|
GOLANGCI_LINT_VERSION=v1.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
|
||||||
@@ -29,24 +29,34 @@ build: ## Build the Gotenberg's Docker image
|
|||||||
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
||||||
API_PORT=3000
|
API_PORT=3000
|
||||||
API_PORT_FROM_ENV=
|
API_PORT_FROM_ENV=
|
||||||
API_READ_TIMEOUT=30s
|
API_START_TIMEOUT=30s
|
||||||
API_PROCESS_TIMEOUT=30s
|
API_TIMEOUT=30s
|
||||||
API_WRITE_TIMEOUT=30s
|
|
||||||
API_ROOT_PATH=/
|
API_ROOT_PATH=/
|
||||||
API_TRACE_HEADER=Gotenberg-Trace
|
API_TRACE_HEADER=Gotenberg-Trace
|
||||||
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||||
|
CHROMIUM_RESTART_AFTER=0
|
||||||
|
CHROMIUM_AUTO_START=false
|
||||||
|
CHROMIUM_START_TIMEOUT=20s
|
||||||
CHROMIUM_INCOGNITO=false
|
CHROMIUM_INCOGNITO=false
|
||||||
|
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
|
||||||
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
||||||
CHROMIUM_DISABLE_WEB_SECURITY=false
|
CHROMIUM_DISABLE_WEB_SECURITY=false
|
||||||
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
||||||
|
CHROMIUM_HOST_RESOLVER_RULES=
|
||||||
CHROMIUM_PROXY_SERVER=
|
CHROMIUM_PROXY_SERVER=
|
||||||
CHROMIUM_ALLOW_LIST=
|
CHROMIUM_ALLOW_LIST=
|
||||||
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
|
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
|
||||||
|
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_DISABLES_ROUTES=false
|
LIBREOFFICE_RESTART_AFTER=10
|
||||||
|
LIBREOFFICE_AUTO_START=false
|
||||||
|
LIBREOFFICE_START_TIMEOUT=20s
|
||||||
|
LIBREOFFICE_DISABLE_ROUTES=false
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
LOG_FORMAT=auto
|
LOG_FORMAT=auto
|
||||||
|
LOG_FIELDS_PREFIX=
|
||||||
PDFENGINES_ENGINES=
|
PDFENGINES_ENGINES=
|
||||||
PDFENGINES_DISABLE_ROUTES=false
|
PDFENGINES_DISABLE_ROUTES=false
|
||||||
PROMETHEUS_NAMESPACE=gotenberg
|
PROMETHEUS_NAMESPACE=gotenberg
|
||||||
@@ -60,6 +70,7 @@ WEBHOOK_ERROR_DENY_LIST=
|
|||||||
WEBHOOK_MAX_RETRY=4
|
WEBHOOK_MAX_RETRY=4
|
||||||
WEBHOOK_RETRY_MIN_WAIT=1s
|
WEBHOOK_RETRY_MIN_WAIT=1s
|
||||||
WEBHOOK_RETRY_MAX_WAIT=30s
|
WEBHOOK_RETRY_MAX_WAIT=30s
|
||||||
|
WEBHOOK_CLIENT_TIMEOUT=30s
|
||||||
WEBHOOK_DISABLE=false
|
WEBHOOK_DISABLE=false
|
||||||
|
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
@@ -71,24 +82,34 @@ run: ## Start a Gotenberg container
|
|||||||
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||||
--api-port=$(API_PORT) \
|
--api-port=$(API_PORT) \
|
||||||
--api-port-from-env=$(API_PORT_FROM_ENV) \
|
--api-port-from-env=$(API_PORT_FROM_ENV) \
|
||||||
--api-read-timeout=$(API_READ_TIMEOUT) \
|
--api-start-timeout=$(API_START_TIMEOUT) \
|
||||||
--api-process-timeout=$(API_PROCESS_TIMEOUT) \
|
--api-timeout=$(API_TIMEOUT) \
|
||||||
--api-write-timeout=$(API_WRITE_TIMEOUT) \
|
|
||||||
--api-root-path=$(API_ROOT_PATH) \
|
--api-root-path=$(API_ROOT_PATH) \
|
||||||
--api-trace-header=$(API_TRACE_HEADER) \
|
--api-trace-header=$(API_TRACE_HEADER) \
|
||||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||||
|
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||||
|
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||||
|
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
|
||||||
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
|
||||||
|
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
|
||||||
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
|
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
|
||||||
--chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \
|
--chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \
|
||||||
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
|
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
|
||||||
|
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
|
||||||
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
|
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
|
||||||
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
|
--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
|
||||||
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \
|
--chromium-deny-list=$(CHROMIUM_DENY_LIST) \
|
||||||
|
--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-disable-routes=$(LIBREOFFICE_DISABLES_ROUTES) \
|
--libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \
|
||||||
|
--libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \
|
||||||
|
--libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \
|
||||||
|
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \
|
||||||
--log-level=$(LOG_LEVEL) \
|
--log-level=$(LOG_LEVEL) \
|
||||||
--log-format=$(LOG_FORMAT) \
|
--log-format=$(LOG_FORMAT) \
|
||||||
|
--log-fields-prefix=$(LOG_FIELDS_PREFIX) \
|
||||||
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
||||||
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
|
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
|
||||||
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
|
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
|
||||||
@@ -102,6 +123,7 @@ run: ## Start a Gotenberg container
|
|||||||
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
|
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
|
||||||
--webhook-retry-min-wait=$(WEBHOOK_RETRY_MIN_WAIT) \
|
--webhook-retry-min-wait=$(WEBHOOK_RETRY_MIN_WAIT) \
|
||||||
--webhook-retry-max-wait=$(WEBHOOK_RETRY_MAX_WAIT) \
|
--webhook-retry-max-wait=$(WEBHOOK_RETRY_MAX_WAIT) \
|
||||||
|
--webhook-client-timeout=$(WEBHOOK_CLIENT_TIMEOUT) \
|
||||||
--webhook-disable=$(WEBHOOK_DISABLE)
|
--webhook-disable=$(WEBHOOK_DISABLE)
|
||||||
|
|
||||||
.PHONY: build-tests
|
.PHONY: build-tests
|
||||||
@@ -128,14 +150,18 @@ tests-once: ## Run the tests once (prefer the "tests" command while developing)
|
|||||||
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \
|
$(DOCKER_REPOSITORY)/gotenberg:$(GOTENBERG_VERSION)-tests \
|
||||||
gotest
|
gotest
|
||||||
|
|
||||||
|
# go install mvdan.cc/gofumpt@latest
|
||||||
|
# go install github.com/daixiang0/gci@latest
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: ## Format the code and "optimize" the dependencies
|
fmt: ## Format the code and "optimize" the dependencies
|
||||||
go fmt ./...
|
gofumpt -l -w .
|
||||||
|
gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v8)" --skip-generated --skip-vendor --custom-order .
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
|
# go install golang.org/x/tools/cmd/godoc@latest
|
||||||
.PHONY: godoc
|
.PHONY: godoc
|
||||||
godoc: ## Run a webserver with Gotenberg godoc (go get golang.org/x/tools/cmd/godoc)
|
godoc: ## Run a webserver with Gotenberg godoc
|
||||||
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v7)
|
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v8)
|
||||||
godoc -http=:6060
|
godoc -http=:6060
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
@@ -145,5 +171,6 @@ release: ## Build the Gotenberg's Docker image for many platforms, then push it
|
|||||||
$(GOTENBERG_VERSION) \
|
$(GOTENBERG_VERSION) \
|
||||||
$(GOTENBERG_USER_GID) \
|
$(GOTENBERG_USER_GID) \
|
||||||
$(GOTENBERG_USER_UID) \
|
$(GOTENBERG_USER_UID) \
|
||||||
|
$(NOTO_COLOR_EMOJI_VERSION) \
|
||||||
$(PDFTK_VERSION) \
|
$(PDFTK_VERSION) \
|
||||||
$(DOCKER_REPOSITORY)
|
$(DOCKER_REPOSITORY)
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -1,32 +1,32 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
|
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
|
||||||
<h3 align="center">Gotenberg</h3>
|
<h3 align="center">Gotenberg</h3>
|
||||||
<p align="center">A Docker-powered stateless API for PDF files</p>
|
<p align="center">A Docker-powered stateless API for PDF files</p>
|
||||||
<p align="center"><a href="https://gotenberg.dev/docs/about">Documentation</a> · 🔥 <a href="https://gotenberg.dev/docs/get-started/live-demo">Live Demo</a></p>
|
<p align="center"><a href="https://gotenberg.dev/docs/getting-started/introduction">Documentation</a> · <a href="https://gotenberg.dev/docs/getting-started/installation#live-demo-">Live Demo</a> 🔥</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Gotenberg provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice to convert many
|
Gotenberg provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice for converting
|
||||||
documents (HTML, Markdown, Word, Excel, etc.) to PDF, transform them, merge them, and more!
|
numerous document formats (HTML, Markdown, Word, Excel, etc.) into PDF files, and more!
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
Open a terminal and run the following command:
|
Open a terminal and run the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --rm -p 3000:3000 gotenberg/gotenberg:7
|
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, using the historic Docker repository from our sponsor [TheCodingMachine](https://www.thecodingmachine.com):
|
Alternatively, using the historic Docker repository from our sponsor [TheCodingMachine](https://www.thecodingmachine.com):
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:7
|
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:8
|
||||||
```
|
```
|
||||||
|
|
||||||
The API is now available on your host at http://localhost:3000.
|
The API is now available on your host at http://localhost:3000.
|
||||||
|
|
||||||
Head to the [documentation](https://gotenberg.dev/docs/about) to learn how to interact with it 🚀
|
Head to the [documentation](https://gotenberg.dev/docs/getting-started/introduction) to learn how to interact with it 🚀
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
@@ -41,6 +41,5 @@ Head to the [documentation](https://gotenberg.dev/docs/about) to learn how to in
|
|||||||
[](https://hub.docker.com/r/gotenberg/gotenberg)
|
[](https://hub.docker.com/r/gotenberg/gotenberg)
|
||||||
[](https://hub.docker.com/r/thecodingmachine/gotenberg)
|
[](https://hub.docker.com/r/thecodingmachine/gotenberg)
|
||||||
[](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
|
[](https://github.com/gotenberg/gotenberg/actions/workflows/continuous_integration.yml)
|
||||||
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v7)
|
[](https://pkg.go.dev/github.com/gotenberg/gotenberg/v8)
|
||||||
[](https://codecov.io/gh/gotenberg/gotenberg)
|
[](https://codecov.io/gh/gotenberg/gotenberg)
|
||||||
[](https://goreportcard.com/report/gotenberg/gotenberg)
|
|
||||||
|
|||||||
41
SECURITY.md
Normal file
41
SECURITY.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Please ensure to keep your environment up-to-date and use only the latest version of Gotenberg.
|
||||||
|
Security updates and patches will be applied only to the most recent version.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Your help in identifying vulnerabilities in our project is much appreciated.
|
||||||
|
We take all reports regarding security seriously.
|
||||||
|
|
||||||
|
If you discover a security vulnerability, please refrain from publishing it publicly.
|
||||||
|
Instead, kindly send us the details via email to *neuhart [dot] julien [at] gmail [dot] com*.
|
||||||
|
|
||||||
|
In the subject of your email, please indicate that it's a security vulnerability report for Gotenberg.
|
||||||
|
In your message, please include:
|
||||||
|
|
||||||
|
* A detailed description of the vulnerability.
|
||||||
|
* The steps to reproduce the issue.
|
||||||
|
* Any potential impact of the vulnerability on the users or system.
|
||||||
|
|
||||||
|
Please remember that this process is done in a *'best-effort'* manner.
|
||||||
|
This means we strive to respond and act as quickly as possible, but the speed may vary depending on the severity of
|
||||||
|
the issue and our resources.
|
||||||
|
|
||||||
|
Thank you in advance for helping to keep our project safe!
|
||||||
|
|
||||||
|
## Disclosure Policy
|
||||||
|
|
||||||
|
Once we have received your vulnerability report, we will work to validate and reproduce the issue.
|
||||||
|
If we can confirm the vulnerability, we will proceed to:
|
||||||
|
|
||||||
|
* Work on a fix and a release timeline.
|
||||||
|
* Notify you when the fix has been implemented and released.
|
||||||
|
* Credit you for discovering the vulnerability (unless you request anonymity).
|
||||||
|
* Please note that we will do our best to keep you informed about the progress towards resolving the issue.
|
||||||
|
|
||||||
|
## Comments on this Policy
|
||||||
|
|
||||||
|
If you have suggestions on how this process could be improved, please submit a pull request.
|
||||||
187
build/Dockerfile
187
build/Dockerfile
@@ -1,11 +1,14 @@
|
|||||||
# Note: ARG instructions do not create additional layers.
|
# ARG instructions do not create additional layers. Instead, next layers will
|
||||||
# Instead, next layers will concatenate them.
|
# concatenate them. Also, we have to repeat ARG instructions in each build
|
||||||
|
# stage that uses them.
|
||||||
ARG GOLANG_VERSION
|
ARG GOLANG_VERSION
|
||||||
# Note: we have to repeat ARG instructions in each build stage that uses them.
|
|
||||||
|
# ----------------------------------------------
|
||||||
|
# Gotenberg binary build stage
|
||||||
|
# ----------------------------------------------
|
||||||
|
FROM golang:$GOLANG_VERSION AS binary-stage
|
||||||
|
|
||||||
ARG GOTENBERG_VERSION
|
ARG GOTENBERG_VERSION
|
||||||
|
|
||||||
FROM golang:$GOLANG_VERSION AS builder
|
|
||||||
|
|
||||||
ENV CGO_ENABLED 0
|
ENV CGO_ENABLED 0
|
||||||
|
|
||||||
# Define the working directory outside of $GOPATH (we're using go modules).
|
# Define the working directory outside of $GOPATH (we're using go modules).
|
||||||
@@ -21,40 +24,26 @@ RUN go mod download &&\
|
|||||||
COPY cmd ./cmd
|
COPY cmd ./cmd
|
||||||
COPY pkg ./pkg
|
COPY pkg ./pkg
|
||||||
|
|
||||||
# Build the binary.
|
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
||||||
ARG GOTENBERG_VERSION
|
|
||||||
|
|
||||||
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v7/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
# ----------------------------------------------
|
||||||
|
# Final stage
|
||||||
FROM debian:11-slim
|
# ----------------------------------------------
|
||||||
|
FROM debian:12-slim
|
||||||
|
|
||||||
ARG GOTENBERG_VERSION
|
ARG GOTENBERG_VERSION
|
||||||
|
|
||||||
LABEL author="Julien Neuhart" \
|
|
||||||
description="A Docker-powered stateless API for PDF files." \
|
|
||||||
github="https://github.com/gotenberg/gotenberg" \
|
|
||||||
version="$GOTENBERG_VERSION" \
|
|
||||||
website="https://gotenberg.dev"
|
|
||||||
|
|
||||||
# Improve fonts subpixel hinting and smoothing.
|
|
||||||
# Credits:
|
|
||||||
# https://github.com/arachnys/athenapdf/issues/69.
|
|
||||||
# https://github.com/arachnys/athenapdf/commit/ba25a8d80a25d08d58865519c4cd8756dc9a336d.
|
|
||||||
COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
|
||||||
|
|
||||||
# Simple wrapper around Java and PDFtk.
|
|
||||||
COPY build/pdftk.sh /usr/bin/pdftk
|
|
||||||
|
|
||||||
# Setup the Docker image.
|
|
||||||
ARG GOTENBERG_USER_GID
|
ARG GOTENBERG_USER_GID
|
||||||
ARG GOTENBERG_USER_UID
|
ARG GOTENBERG_USER_UID
|
||||||
ARG NOTO_COLOR_EMOJI_VERSION
|
ARG NOTO_COLOR_EMOJI_VERSION
|
||||||
ARG PDFTK_VERSION
|
ARG PDFTK_VERSION
|
||||||
|
ARG TMP_CHOMIUM_VERSION_ARMHF="116.0.5845.180-1~deb12u1"
|
||||||
|
|
||||||
# Script for installing either Google Chrome stable on amd64 architecture or
|
LABEL org.opencontainers.image.title="Gotenberg" \
|
||||||
# Chromium on other architectures.
|
org.opencontainers.image.description="A Docker-powered stateless API for PDF files." \
|
||||||
# See https://github.com/gotenberg/gotenberg/issues/328.
|
org.opencontainers.image.version="$GOTENBERG_VERSION" \
|
||||||
COPY build/install-chromium.sh /tmp/install-chromium.sh
|
org.opencontainers.image.authors="Julien Neuhart <neuhart.julien@gmail.com>" \
|
||||||
|
org.opencontainers.image.documentation="https://gotenberg.dev" \
|
||||||
|
org.opencontainers.image.source="https://github.com/gotenberg/gotenberg"
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
# Create a non-root user.
|
# Create a non-root user.
|
||||||
@@ -62,19 +51,28 @@ RUN \
|
|||||||
groupadd --gid "$GOTENBERG_USER_GID" gotenberg &&\
|
groupadd --gid "$GOTENBERG_USER_GID" gotenberg &&\
|
||||||
useradd --uid "$GOTENBERG_USER_UID" --gid gotenberg --shell /bin/bash --home /home/gotenberg --no-create-home gotenberg &&\
|
useradd --uid "$GOTENBERG_USER_UID" --gid gotenberg --shell /bin/bash --home /home/gotenberg --no-create-home gotenberg &&\
|
||||||
mkdir /home/gotenberg &&\
|
mkdir /home/gotenberg &&\
|
||||||
chown gotenberg: /home/gotenberg &&\
|
chown gotenberg: /home/gotenberg
|
||||||
# Install dependencies required for the next instructions or debugging.
|
|
||||||
# Note: procps for "top" command (useful when debugging processes).
|
RUN \
|
||||||
|
# Install system dependencies required for the next instructions or debugging.
|
||||||
# Note: tini is a helper for reaping zombie processes.
|
# Note: tini is a helper for reaping zombie processes.
|
||||||
apt-get update -qq &&\
|
apt-get update -qq &&\
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg procps tini python3 default-jre-headless &&\
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg tini python3 default-jre-headless &&\
|
||||||
|
# Cleanup.
|
||||||
|
# Note: the Debian image does automatically a clean after each install thanks to a hook.
|
||||||
|
# Therefore, there is no need for apt-get clean.
|
||||||
|
# See https://stackoverflow.com/a/24417119/3248473.
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN \
|
||||||
# Install fonts.
|
# Install fonts.
|
||||||
# Credits:
|
# Credits:
|
||||||
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile.
|
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile.
|
||||||
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html.
|
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html.
|
||||||
curl -o ./ttf-mscorefonts-installer_3.8_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8_all.deb &&\
|
curl -o ./ttf-mscorefonts-installer_3.8.1_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||||
|
apt-get update -qq &&\
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||||
./ttf-mscorefonts-installer_3.8_all.deb \
|
./ttf-mscorefonts-installer_3.8.1_all.deb \
|
||||||
culmus \
|
culmus \
|
||||||
fonts-beng \
|
fonts-beng \
|
||||||
fonts-hosny-amiri \
|
fonts-hosny-amiri \
|
||||||
@@ -103,55 +101,100 @@ RUN \
|
|||||||
fonts-liberation \
|
fonts-liberation \
|
||||||
fonts-liberation2 \
|
fonts-liberation2 \
|
||||||
fonts-linuxlibertine \
|
fonts-linuxlibertine \
|
||||||
|
fonts-noto-cjk \
|
||||||
fonts-noto-core \
|
fonts-noto-core \
|
||||||
fonts-noto-mono \
|
fonts-noto-mono \
|
||||||
fonts-noto-ui-core \
|
fonts-noto-ui-core \
|
||||||
fonts-sil-gentium \
|
fonts-sil-gentium \
|
||||||
fonts-sil-gentium-basic &&\
|
fonts-sil-gentium-basic &&\
|
||||||
rm -f ./ttf-mscorefonts-installer_3.8_all.deb &&\
|
rm -f ./ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||||
# Add Color and Black-and-White Noto emoji font.
|
# Add Color and Black-and-White Noto emoji font.
|
||||||
# Credits:
|
# Credits:
|
||||||
# https://github.com/gotenberg/gotenberg/pull/325.
|
# https://github.com/gotenberg/gotenberg/pull/325.
|
||||||
# https://github.com/googlefonts/noto-emoji.
|
# https://github.com/googlefonts/noto-emoji.
|
||||||
curl -Ls "https://github.com/googlefonts/noto-emoji/raw/$NOTO_COLOR_EMOJI_VERSION/fonts/NotoColorEmoji.ttf" -o /usr/local/share/fonts/NotoColorEmoji.ttf &&\
|
curl -Ls "https://github.com/googlefonts/noto-emoji/raw/$NOTO_COLOR_EMOJI_VERSION/fonts/NotoColorEmoji.ttf" -o /usr/local/share/fonts/NotoColorEmoji.ttf &&\
|
||||||
# Install Google Chrome / Chromium.
|
|
||||||
/tmp/install-chromium.sh &&\
|
|
||||||
# Install LibreOffice.
|
|
||||||
# Note: we use the sid distribution to get the latest LibreOffice version.
|
|
||||||
# See https://github.com/gotenberg/gotenberg/pull/322.
|
|
||||||
echo "deb https://httpredir.debian.org/debian/ sid main contrib non-free" >> /etc/apt/sources.list &&\
|
|
||||||
apt-get update -qq &&\
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t sid libreoffice &&\
|
|
||||||
# Download unoconv (Python script).
|
|
||||||
curl -Ls https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv -o /usr/bin/unoconv &&\
|
|
||||||
chmod +x /usr/bin/unoconv &&\
|
|
||||||
# unoconv will look for the Python binary, which has to be at version 3.
|
|
||||||
ln -s /usr/bin/python3 /usr/bin/python &&\
|
|
||||||
# Download PDFtk.
|
|
||||||
# See https://github.com/gotenberg/gotenberg/pull/273.
|
|
||||||
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/pdftk-java/pdftk/-/jobs/$PDFTK_VERSION/artifacts/raw/build/libs/pdftk-all.jar" &&\
|
|
||||||
chmod a+x /usr/bin/pdftk-all.jar &&\
|
|
||||||
# See https://github.com/nextcloud/docker/issues/380.
|
|
||||||
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
|
||||||
# Cleanup.
|
# Cleanup.
|
||||||
# Note: the Debian image does automatically a clean after each install thanks to a hook.
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
# Therefore, there is no need for apt-get clean.
|
|
||||||
# See https://stackoverflow.com/a/24417119/3248473.
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
|
|
||||||
# Print versions of main dependencies.
|
|
||||||
chromium --version &&\
|
|
||||||
libreoffice --version &&\
|
|
||||||
unoconv --version &&\
|
|
||||||
pdftk --version
|
|
||||||
|
|
||||||
# Copy the Gotenberg binary from the builder stage.
|
RUN \
|
||||||
COPY --from=builder /home/gotenberg /usr/bin/
|
# Install either Google Chrome stable on amd64 architecture or
|
||||||
|
# Chromium on other architectures.
|
||||||
|
# See https://github.com/gotenberg/gotenberg/issues/328.
|
||||||
|
# FIXME:
|
||||||
|
# armhf is currently not working with the latest version of Chromium.
|
||||||
|
# See: https://github.com/gotenberg/gotenberg/issues/709.
|
||||||
|
/bin/bash -c \
|
||||||
|
'set -e &&\
|
||||||
|
if [[ "$(dpkg --print-architecture)" == "amd64" ]]; then \
|
||||||
|
curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||||
|
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
||||||
|
apt-get update -qq &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends --allow-unauthenticated google-chrome-stable &&\
|
||||||
|
mv /usr/bin/google-chrome-stable /usr/bin/chromium; \
|
||||||
|
elif [[ "$(dpkg --print-architecture)" == "armhf" ]]; then \
|
||||||
|
apt-get update -qq &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends devscripts &&\
|
||||||
|
debsnap chromium-common "$TMP_CHOMIUM_VERSION_ARMHF" -v --force --binary --architecture armhf &&\
|
||||||
|
debsnap chromium "$TMP_CHOMIUM_VERSION_ARMHF" -v --force --binary --architecture armhf &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install --fix-broken -y -qq --no-install-recommends "./binary-chromium-common/chromium-common_${TMP_CHOMIUM_VERSION_ARMHF}_armhf.deb" "./binary-chromium/chromium_${TMP_CHOMIUM_VERSION_ARMHF}_armhf.deb" &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq devscripts &&\
|
||||||
|
rm -rf ./binary-chromium-common/* ./binary-chromium/*; \
|
||||||
|
else \
|
||||||
|
apt-get update -qq &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium; \
|
||||||
|
fi' &&\
|
||||||
|
# Verify installation.
|
||||||
|
chromium --version &&\
|
||||||
|
# Cleanup.
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Install LibreOffice & unoconverter.
|
||||||
|
echo "deb http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list &&\
|
||||||
|
apt-get update -qq &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bookworm-backports libreoffice &&\
|
||||||
|
curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.0.1/unoconv -o /usr/bin/unoconverter &&\
|
||||||
|
chmod +x /usr/bin/unoconverter &&\
|
||||||
|
# unoconverter will look for the Python binary, which has to be at version 3.
|
||||||
|
ln -s /usr/bin/python3 /usr/bin/python &&\
|
||||||
|
# Verify installations.
|
||||||
|
libreoffice --version &&\
|
||||||
|
unoconverter --version &&\
|
||||||
|
# Cleanup.
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
# Install PDFtk & QPDF (PDF engines).
|
||||||
|
# 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" &&\
|
||||||
|
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||||
|
echo '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
||||||
|
chmod +x /usr/bin/pdftk &&\
|
||||||
|
apt-get update -qq &&\
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
|
||||||
|
# See https://github.com/nextcloud/docker/issues/380.
|
||||||
|
mkdir -p /usr/share/man/man1 &&\
|
||||||
|
# Verify installations.
|
||||||
|
pdftk --version &&\
|
||||||
|
qpdf --version &&\
|
||||||
|
# Cleanup.
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# Improve fonts subpixel hinting and smoothing.
|
||||||
|
# Credits:
|
||||||
|
# https://github.com/arachnys/athenapdf/issues/69.
|
||||||
|
# https://github.com/arachnys/athenapdf/commit/ba25a8d80a25d08d58865519c4cd8756dc9a336d.
|
||||||
|
COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||||
|
|
||||||
|
# Copy the Gotenberg binary from the binary stage.
|
||||||
|
COPY --from=binary-stage /home/gotenberg /usr/bin/
|
||||||
|
|
||||||
# Environment variables required by modules or else.
|
# Environment variables required by modules or else.
|
||||||
ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
|
|
||||||
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
|
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
|
||||||
ENV UNOCONV_BIN_PATH /usr/bin/unoconv
|
ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin
|
||||||
|
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
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
@@ -160,4 +203,4 @@ WORKDIR /home/gotenberg
|
|||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/bin/tini", "--" ]
|
ENTRYPOINT [ "/usr/bin/tini", "--" ]
|
||||||
CMD [ "gotenberg" ]
|
CMD [ "gotenberg" ]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Note: ARG instructions do not create additional layers.
|
|
||||||
# Instead, next layers will concatenate them.
|
|
||||||
ARG DOCKER_REPOSITORY
|
ARG DOCKER_REPOSITORY
|
||||||
ARG GOTENBERG_VERSION
|
ARG GOTENBERG_VERSION
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
ARCH=$(dpkg --print-architecture)
|
|
||||||
|
|
||||||
if [[ "$ARCH" == "amd64" ]]; then
|
|
||||||
curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
|
|
||||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list
|
|
||||||
apt-get update -qq
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends --allow-unauthenticated google-chrome-stable
|
|
||||||
mv /usr/bin/google-chrome-stable /usr/bin/chromium
|
|
||||||
else
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium
|
|
||||||
fi
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
exec java -jar /usr/bin/pdftk-all.jar "$@"
|
|
||||||
@@ -5,10 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.
|
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.
|
||||||
@@ -33,7 +36,7 @@ var Version = "snapshot"
|
|||||||
func Run() {
|
func Run() {
|
||||||
fmt.Printf(banner, Version)
|
fmt.Printf(banner, Version)
|
||||||
|
|
||||||
// Creates the roo` FlagSet and adds the modules flags to it.
|
// Create the root FlagSet and adds the modules flags to it.
|
||||||
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
||||||
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
|
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
|
||||||
|
|
||||||
@@ -46,14 +49,14 @@ func Run() {
|
|||||||
|
|
||||||
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
|
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
|
||||||
|
|
||||||
// Parses the flags...
|
// Parse the flags...
|
||||||
err := fs.Parse(os.Args[1:])
|
err := fs.Parse(os.Args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...and creates a wrapper around those.
|
// ...and create a wrapper around those.
|
||||||
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
|
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
// Get the graceful shutdown duration.
|
// Get the graceful shutdown duration.
|
||||||
@@ -61,7 +64,7 @@ func Run() {
|
|||||||
|
|
||||||
ctx := gotenberg.NewContext(parsedFlags, descriptors)
|
ctx := gotenberg.NewContext(parsedFlags, descriptors)
|
||||||
|
|
||||||
// Starts application modules.
|
// Start application modules.
|
||||||
apps, err := ctx.Modules(new(gotenberg.App))
|
apps, err := ctx.Modules(new(gotenberg.App))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[FATAL] %s\n", err)
|
fmt.Printf("[FATAL] %s\n", err)
|
||||||
@@ -81,19 +84,34 @@ func Run() {
|
|||||||
startupMessage := app.StartupMessage()
|
startupMessage := app.StartupMessage()
|
||||||
if startupMessage == "" {
|
if startupMessage == "" {
|
||||||
fmt.Printf("[SYSTEM] %s: application started\n", id)
|
fmt.Printf("[SYSTEM] %s: application started\n", id)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage)
|
fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage)
|
||||||
}(a.(gotenberg.App))
|
}(a.(gotenberg.App))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get modules that want to print system messages.
|
||||||
|
sysLoggers, err := ctx.Modules(new(gotenberg.SystemLogger))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[FATAL] %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range sysLoggers {
|
||||||
|
go func(logger gotenberg.SystemLogger) {
|
||||||
|
id := logger.(gotenberg.Module).Descriptor().ID
|
||||||
|
|
||||||
|
for _, message := range logger.SystemMessages() {
|
||||||
|
fmt.Printf("[SYSTEM] %s: %s\n", id, message)
|
||||||
|
}
|
||||||
|
}(l.(gotenberg.SystemLogger))
|
||||||
}
|
}
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
|
|
||||||
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C).
|
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) or SIGTERM (Kubernetes).
|
||||||
signal.Notify(quit, os.Interrupt)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
// Block until we receive our signal.
|
// Block until we receive our signal.
|
||||||
<-quit
|
<-quit
|
||||||
@@ -101,18 +119,39 @@ func Run() {
|
|||||||
gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration)
|
gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
forceQuit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(forceQuit, syscall.SIGINT)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// In case of force quit, cancel the context.
|
||||||
|
<-forceQuit
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
|
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
|
||||||
|
|
||||||
|
eg, _ := errgroup.WithContext(gracefulShutdownCtx)
|
||||||
|
|
||||||
for _, a := range apps {
|
for _, a := range apps {
|
||||||
id := a.(gotenberg.Module).Descriptor().ID
|
eg.Go(func(app gotenberg.App) func() error {
|
||||||
app := a.(gotenberg.App)
|
return func() error {
|
||||||
|
id := app.(gotenberg.Module).Descriptor().ID
|
||||||
|
|
||||||
err = app.Stop(gracefulShutdownCtx)
|
err = app.Stop(gracefulShutdownCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ERROR] stopping %s: %s\n", id, err)
|
return fmt.Errorf("stopping %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
|
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}(a.(gotenberg.App)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[FATAL] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
gotenbergcmd "github.com/gotenberg/gotenberg/v7/cmd"
|
gotenbergcmd "github.com/gotenberg/gotenberg/v8/cmd"
|
||||||
|
|
||||||
// Gotenberg modules.
|
// Gotenberg modules.
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/standard"
|
_ "github.com/gotenberg/gotenberg/v8/pkg/standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,730 +0,0 @@
|
|||||||
openapi: 3.0.3
|
|
||||||
info:
|
|
||||||
title: Gotenberg
|
|
||||||
version: 7.x
|
|
||||||
license:
|
|
||||||
name: MIT
|
|
||||||
url: 'https://github.com/gotenberg/gotenberg/blob/main/LICENSE'
|
|
||||||
contact:
|
|
||||||
url: 'https://github.com/gotenberg/gotenberg'
|
|
||||||
description: >-
|
|
||||||
A Docker-powered stateless API for PDF files.
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev
|
|
||||||
servers:
|
|
||||||
- url: 'http://localhost:3000'
|
|
||||||
description: Local server with the default Docker image and port
|
|
||||||
tags:
|
|
||||||
- name: chromium
|
|
||||||
description: Operations of the Chromium module
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/chromium
|
|
||||||
- name: libreoffice
|
|
||||||
description: Operations of the Libreoffice module
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/libreoffice
|
|
||||||
- name: pdfengines
|
|
||||||
description: Operations of the PDF Engines module
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/pdf-engines
|
|
||||||
paths:
|
|
||||||
/forms/chromium/convert/url:
|
|
||||||
post:
|
|
||||||
tags:
|
|
||||||
- chromium
|
|
||||||
summary: Convert the contents of a given URL to PDF
|
|
||||||
externalDocs:
|
|
||||||
url: https://gotenberg.dev/docs/modules/chromium
|
|
||||||
description: >-
|
|
||||||
Send a URL in your API request via the `url` form field
|
|
||||||
Send a remote URL in your API request via the `remoteURL` parameter, and
|
|
||||||
get the resulting PDF file. The API will fetch the given URL and render
|
|
||||||
the page to PDF using the underlying headless Chrome instance.
|
|
||||||
You can optionally include `header.html` and `footer.html` files as part of the request as well.
|
|
||||||
See externalDocs for more details.
|
|
||||||
parameters:
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Output-Filename
|
|
||||||
description: >-
|
|
||||||
By default, the API generates a UUID filename.
|
|
||||||
However, you may also specify the filename per request,
|
|
||||||
thanks to the Gotenberg-Output-Filename header.
|
|
||||||
Caution! The API adds the file extension automatically; you don't have to set it.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
- in: header
|
|
||||||
name: Gotenberg-Trace
|
|
||||||
description: >-
|
|
||||||
The trace, or request ID, identifies a request in the logs.
|
|
||||||
|
|
||||||
By default, the API generates a UUID trace for each request.
|
|
||||||
However, you may also specify the trace per request, thanks to the Gotenberg-Trace header.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
description: >-
|
|
||||||
The request must be `multipart/form-data` that includes a `url` form field.
|
|
||||||
The API uses a headless Chrome instance to do the conversion, therefore print
|
|
||||||
parameter such as margins and paper size are also accepted as optional parameters.
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/URLConvertRequestBody'
|
|
||||||
examples: { }
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
$ref: '#/components/responses/SuccessfulPDF'
|
|
||||||
'400':
|
|
||||||
description: Bad Request
|
|
||||||
|
|
||||||
/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
|
|
||||||
|
|
||||||
/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
|
|
||||||
|
|
||||||
/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
|
|
||||||
|
|
||||||
/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
|
|
||||||
|
|
||||||
/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
|
|
||||||
|
|
||||||
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
|
|
||||||
84
go.mod
84
go.mod
@@ -1,66 +1,66 @@
|
|||||||
module github.com/gotenberg/gotenberg/v7
|
module github.com/gotenberg/gotenberg/v8
|
||||||
|
|
||||||
go 1.17
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexliesenfeld/health v0.6.0
|
github.com/alexliesenfeld/health v0.8.0
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20211112223831-e508c7bf6446
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61
|
||||||
github.com/chromedp/chromedp v0.7.4
|
github.com/chromedp/chromedp v0.9.3
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.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.0
|
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||||
github.com/klauspost/compress v1.13.6 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/labstack/echo/v4 v4.6.1
|
github.com/labstack/echo/v4 v4.11.4
|
||||||
github.com/labstack/gommon v0.3.1
|
github.com/labstack/gommon v0.4.2
|
||||||
github.com/mattn/go-isatty v0.0.14 // 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.16
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/nwaples/rardecode v1.1.2 // indirect
|
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||||
github.com/pdfcpu/pdfcpu v0.3.12
|
github.com/pdfcpu/pdfcpu v0.6.0
|
||||||
github.com/pierrec/lz4/v4 v4.1.11 // indirect
|
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.18.0
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/multierr v1.11.0
|
||||||
go.uber.org/multierr v1.7.0
|
go.uber.org/zap v1.26.0
|
||||||
go.uber.org/zap v1.19.1
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
golang.org/x/image v0.14.0 // indirect
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
golang.org/x/net v0.19.0
|
||||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e
|
golang.org/x/sync v0.5.0
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
|
golang.org/x/term v0.15.0
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
golang.org/x/text v0.14.0
|
||||||
golang.org/x/text v0.3.7
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.1.0 // indirect
|
github.com/gobwas/ws v1.3.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||||
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect
|
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||||
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect
|
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/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.2.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.12.0 // 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.1 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
670
go.sum
670
go.sum
@@ -1,617 +1,163 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
|
||||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
|
||||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
|
||||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
|
||||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
|
||||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
|
||||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
|
||||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
|
||||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
|
||||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
|
||||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
|
||||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
|
||||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
|
||||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
|
||||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
|
||||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
|
||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
|
||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
|
||||||
github.com/alexliesenfeld/health v0.6.0 h1:HRBTCgybNSe4lqGEk7nU82c3bjwh9W+3b46W6UvD4CQ=
|
|
||||||
github.com/alexliesenfeld/health v0.6.0/go.mod h1:N4NDIeQtlWumG+6z1ne1v62eQxktz5ylEgGgH9emdMw=
|
|
||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61 h1:XD280QPATe9jaz20dylKe3vBsNcH1w3mkssGY0lidn8=
|
||||||
github.com/chromedp/cdproto v0.0.0-20210713064928-7d28b402946a/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
github.com/chromedp/cdproto v0.0.0-20231205062650-00455a960d61/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20211112223831-e508c7bf6446 h1:uG7ssld4pEDC5O+1UoKqMb0djjJb8qjuKZVnCskGY+s=
|
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
||||||
github.com/chromedp/cdproto v0.0.0-20211112223831-e508c7bf6446/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
||||||
github.com/chromedp/chromedp v0.7.4 h1:U+0d3WbB/Oj4mDuBOI0P7S3PJEued5UZIl5AJ3QulwU=
|
|
||||||
github.com/chromedp/chromedp v0.7.4/go.mod h1:dBj+SXuQHznp6ZPwZeDDEBZKwclUwDLbZ0hjMialMYs=
|
|
||||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
|
||||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
|
||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
|
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||||
github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
|
github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
|
||||||
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ=
|
github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc=
|
||||||
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
|
|
||||||
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94=
|
|
||||||
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok=
|
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
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 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
|
||||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||||
github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
|
||||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc=
|
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M=
|
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||||
github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/pdfcpu/pdfcpu v0.3.12 h1:B+MdKisilWNSk5OCO58Z9U6H93usH73xqk6hMOaZCls=
|
github.com/pdfcpu/pdfcpu v0.6.0 h1:z4kARP5bcWa39TTYMcN/kjBnm7MvhTWjXgeYmkdAGMI=
|
||||||
github.com/pdfcpu/pdfcpu v0.3.12/go.mod h1:8XVBtVxuuIuSZL4Ez15Q4QoC+H8zeAaGnuiOEwAk8jA=
|
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.11 h1:LVs17FAZJFOjgmJXl9Tf13WfLUvZq7/RjfEJrnwZ9OE=
|
github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
|
||||||
github.com/pierrec/lz4/v4 v4.1.11/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.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
|
||||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
|
||||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
|
||||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
|
||||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
|
||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
|
||||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
|
||||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8=
|
|
||||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
|
||||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
|
||||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
|
||||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
|
||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
|
||||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
|
||||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
|
||||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
|
||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
|
||||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
|
||||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
|
||||||
|
|||||||
@@ -13,51 +13,51 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cmd wraps an exec.Cmd.
|
// Cmd wraps an [exec.Cmd].
|
||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
process *exec.Cmd
|
process *exec.Cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command creates a Cmd without a context. It configures the internal
|
// Command creates a [Cmd] without a context. It configures the internal
|
||||||
// exec.Cmd of Cmd so that we may kill its unix process and all its children
|
// [exec.Cmd] of [Cmd] so that we may kill its unix process and all its
|
||||||
// without creating orphans.
|
// children without creating orphans.
|
||||||
//
|
//
|
||||||
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
||||||
func Command(logger *zap.Logger, binPath string, args ...string) Cmd {
|
func Command(logger *zap.Logger, binPath string, args ...string) *Cmd {
|
||||||
cmd := exec.Command(binPath, args...)
|
cmd := exec.Command(binPath, args...)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
return Cmd{
|
return &Cmd{
|
||||||
ctx: nil,
|
ctx: nil,
|
||||||
logger: logger.Named("cmd"),
|
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
|
||||||
process: cmd,
|
process: cmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandContext creates a Cmd with a context. It configures the internal
|
// CommandContext creates a [Cmd] with a context. It configures the internal
|
||||||
// exec.Cmd of Cmd so that we may kill its unix process and all its children
|
// [exec.Cmd] of [Cmd] so that we may kill its unix process and all its
|
||||||
// without creating orphans.
|
// children without creating orphans.
|
||||||
//
|
//
|
||||||
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
||||||
func CommandContext(ctx context.Context, logger *zap.Logger, binPath string, args ...string) (Cmd, error) {
|
func CommandContext(ctx context.Context, logger *zap.Logger, binPath string, args ...string) (*Cmd, error) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return Cmd{}, errors.New("nil context")
|
return nil, errors.New("nil context")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, binPath, args...)
|
cmd := exec.CommandContext(ctx, binPath, args...)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
return Cmd{
|
return &Cmd{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
logger: logger.Named("cmd"),
|
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
|
||||||
process: cmd,
|
process: cmd,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the command but does not wait for its completion.
|
// Start starts the command but does not wait for its completion.
|
||||||
func (cmd Cmd) Start() error {
|
func (cmd *Cmd) Start() error {
|
||||||
err := cmd.pipeOutput()
|
err := cmd.pipeOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("pipe unix process output: %w", err)
|
return fmt.Errorf("pipe unix process output: %w", err)
|
||||||
@@ -73,22 +73,37 @@ func (cmd Cmd) Start() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait waits for the command to complete. It should be called when using the
|
||||||
|
// Start method, so that the command does not leak zombies.
|
||||||
|
func (cmd *Cmd) Wait() error {
|
||||||
|
err := cmd.process.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for unix process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Exec executes the command and wait for its completion or until the context
|
// Exec executes the command and wait for its completion or until the context
|
||||||
// is done. In any case, it kills the unix process and all its children.
|
// is done. In any case, it kills the unix process and all its children.
|
||||||
func (cmd Cmd) Exec() error {
|
func (cmd *Cmd) Exec() (int, error) {
|
||||||
if cmd.ctx == nil {
|
if cmd.ctx == nil {
|
||||||
return errors.New("nil context")
|
return 10, errors.New("nil context")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("start command: %w", err)
|
if cmd.process.ProcessState == nil {
|
||||||
|
return 131, fmt.Errorf("start command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("start command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
errChan <- cmd.process.Wait()
|
errChan <- cmd.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -99,23 +114,27 @@ func (cmd Cmd) Exec() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("unix process error: %w", err)
|
if cmd.process.ProcessState == nil {
|
||||||
|
return 131, fmt.Errorf("unix process error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("unix process error: %w", err)
|
||||||
case <-cmd.ctx.Done():
|
case <-cmd.ctx.Done():
|
||||||
errProc := cmd.Kill()
|
errProc := cmd.Kill()
|
||||||
if errProc != nil {
|
if errProc != nil {
|
||||||
cmd.logger.Error(errProc.Error())
|
cmd.logger.Error(errProc.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("context done: %w", cmd.ctx.Err())
|
return 62, fmt.Errorf("context done: %w", cmd.ctx.Err())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pipeOutput creates logs entries according to the process stdout and stderr.
|
// pipeOutput creates logs entries according to the process stdout and stderr.
|
||||||
// It does nothing if the logging level is not debug.
|
// It does nothing if the logging level is not debug.
|
||||||
func (cmd Cmd) pipeOutput() error {
|
func (cmd *Cmd) pipeOutput() error {
|
||||||
checkedEntry := cmd.logger.Check(zap.DebugLevel, "check for debug level before piping unix process output")
|
checkedEntry := cmd.logger.Check(zap.DebugLevel, "check for debug level before piping unix process output")
|
||||||
if checkedEntry == nil {
|
if checkedEntry == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -135,11 +154,15 @@ func (cmd Cmd) pipeOutput() error {
|
|||||||
// (either stdout or stderr).
|
// (either stdout or stderr).
|
||||||
logCommandOutput := func(logger *zap.Logger, reader io.ReadCloser) {
|
logCommandOutput := func(logger *zap.Logger, reader io.ReadCloser) {
|
||||||
r := bufio.NewReader(reader)
|
r := bufio.NewReader(reader)
|
||||||
defer reader.Close()
|
defer func(reader io.ReadCloser) {
|
||||||
|
err := reader.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close reader: %s", err))
|
||||||
|
}
|
||||||
|
}(reader)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, _, err := r.ReadLine()
|
line, _, err := r.ReadLine()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
|
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
|
||||||
logger.Error(fmt.Sprintf("pipe unix process output error: %s", err))
|
logger.Error(fmt.Sprintf("pipe unix process output error: %s", err))
|
||||||
@@ -163,7 +186,7 @@ func (cmd Cmd) pipeOutput() error {
|
|||||||
// Kill kills the unix process and all its children without creating orphans.
|
// Kill kills the unix process and all its children without creating orphans.
|
||||||
//
|
//
|
||||||
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
|
||||||
func (cmd Cmd) Kill() error {
|
func (cmd *Cmd) Kill() error {
|
||||||
if cmd.process == nil {
|
if cmd.process == nil {
|
||||||
// We cannot use the logger here, because for whatever reason using it
|
// We cannot use the logger here, because for whatever reason using it
|
||||||
// result to a panic.
|
// result to a panic.
|
||||||
|
|||||||
@@ -10,211 +10,308 @@ import (
|
|||||||
|
|
||||||
func TestCommand(t *testing.T) {
|
func TestCommand(t *testing.T) {
|
||||||
cmd := Command(zap.NewNop(), "foo")
|
cmd := Command(zap.NewNop(), "foo")
|
||||||
|
|
||||||
if !cmd.process.SysProcAttr.Setpgid {
|
if !cmd.process.SysProcAttr.Setpgid {
|
||||||
t.Error("expected Setpgid to be true")
|
t.Error("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommandContext(t *testing.T) {
|
func TestCommandContext(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
ctx context.Context
|
scenario string
|
||||||
expectErr bool
|
ctx context.Context
|
||||||
|
expectCommandContextError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: nil,
|
scenario: "nominal behavior",
|
||||||
expectErr: true,
|
ctx: context.Background(),
|
||||||
|
expectCommandContextError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
scenario: "nil context",
|
||||||
|
ctx: nil,
|
||||||
|
expectCommandContextError: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
|
|
||||||
|
|
||||||
if err == nil && !cmd.process.SysProcAttr.Setpgid {
|
for _, tc := range tests {
|
||||||
t.Fatalf("test %d: expected Setpgid to be true", i)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
}
|
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if err == nil && !cmd.process.SysProcAttr.Setpgid {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectCommandContextError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.expectCommandContextError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_Start(t *testing.T) {
|
func TestCmd_Start(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
scenario string
|
||||||
expectErr bool
|
cmd *Cmd
|
||||||
|
expectStartError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "foo"),
|
scenario: "nominal behavior",
|
||||||
expectErr: true,
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
expectStartError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
scenario: "start error",
|
||||||
|
cmd: Command(zap.NewNop(), "foo"),
|
||||||
|
expectStartError: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
err := tc.cmd.Start()
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
}
|
err := tc.cmd.Start()
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectStartError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.expectStartError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_Wait(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
scenario string
|
||||||
|
cmd *Cmd
|
||||||
|
expectWaitError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "nominal behavior",
|
||||||
|
cmd: func() *Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectWaitError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "wait error",
|
||||||
|
cmd: func() *Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
err = cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectWaitError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.cmd.Wait()
|
||||||
|
|
||||||
|
if !tc.expectWaitError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectWaitError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_Exec(t *testing.T) {
|
func TestCmd_Exec(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
scenario string
|
||||||
timeout time.Duration
|
cmd *Cmd
|
||||||
expectErr bool
|
timeout time.Duration
|
||||||
|
expectExecError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "foo"),
|
scenario: "nominal behavior",
|
||||||
expectErr: true,
|
cmd: func() *Cmd {
|
||||||
|
cmd, err := CommandContext(context.Background(), zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectExecError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "foo"),
|
scenario: "nil context",
|
||||||
timeout: time.Duration(5) * time.Second,
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
expectErr: true,
|
expectExecError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
scenario: "start error",
|
||||||
timeout: time.Duration(5) * time.Second,
|
cmd: func() *Cmd {
|
||||||
|
cmd, err := CommandContext(context.Background(), zap.NewNop(), "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectExecError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "sleep", "3"),
|
scenario: "context done",
|
||||||
timeout: time.Duration(2) * time.Second,
|
cmd: Command(zap.NewNop(), "sleep", "2"),
|
||||||
expectErr: true,
|
timeout: time.Duration(1) * time.Second,
|
||||||
|
expectExecError: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
if tc.timeout > 0 {
|
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tc.cmd.ctx = ctx
|
for _, tc := range tests {
|
||||||
}
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
if tc.timeout > 0 {
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
err := tc.cmd.Exec()
|
tc.cmd.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
_, err := tc.cmd.Exec()
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectExecError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.expectExecError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_pipeOutput(t *testing.T) {
|
func TestCmd_pipeOutput(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
scenario string
|
||||||
run bool
|
cmd *Cmd
|
||||||
expectErr bool
|
run bool
|
||||||
|
expectPipeOutputError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
scenario: "nominal behavior",
|
||||||
|
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
|
||||||
|
run: true,
|
||||||
|
expectPipeOutputError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: func() Cmd {
|
scenario: "no debug, no pipe",
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
run: false,
|
||||||
|
expectPipeOutputError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "stdout already piped",
|
||||||
|
cmd: func() *Cmd {
|
||||||
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
_, err := cmd.process.StdoutPipe()
|
_, err := cmd.process.StdoutPipe()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
run: false,
|
||||||
|
expectPipeOutputError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: func() Cmd {
|
scenario: "stderr already piped",
|
||||||
|
cmd: func() *Cmd {
|
||||||
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
_, err := cmd.process.StderrPipe()
|
_, err := cmd.process.StderrPipe()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
run: false,
|
||||||
|
expectPipeOutputError: true,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
|
|
||||||
run: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
err := tc.cmd.pipeOutput()
|
|
||||||
|
|
||||||
if tc.run {
|
for _, tc := range tests {
|
||||||
errStart := tc.cmd.process.Start()
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.cmd.pipeOutput()
|
||||||
|
|
||||||
if errStart != nil {
|
if tc.run {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
errStart := tc.cmd.process.Start()
|
||||||
|
if errStart != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectPipeOutputError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectPipeOutputError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_Kill(t *testing.T) {
|
func TestCmd_Kill(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
scenario string
|
||||||
expectErr bool
|
cmd *Cmd
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Cmd{logger: zap.NewNop()},
|
scenario: "nominal behavior",
|
||||||
},
|
cmd: func() *Cmd {
|
||||||
{
|
|
||||||
cmd: func() Cmd {
|
|
||||||
cmd := Command(zap.NewNop(), "sleep", "60")
|
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||||
err := cmd.process.Start()
|
err := cmd.process.Start()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: func() Cmd {
|
scenario: "no process",
|
||||||
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
cmd: &Cmd{logger: zap.NewNop()},
|
||||||
err := cmd.process.Run()
|
},
|
||||||
|
{
|
||||||
|
scenario: "process already killed",
|
||||||
|
cmd: func() *Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||||
|
err := cmd.process.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
err = cmd.Kill()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
err := tc.cmd.Kill()
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
}
|
err := tc.cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
if !tc.expectErr && err != nil {
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context is a struct which helps initializing modules. When provisioning, a
|
// Context is a struct which helps to initialize modules. When provisioning, a
|
||||||
// module may use the context to get other modules that it needs internally.
|
// module may use the context to get other modules that it needs internally.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
flags ParsedFlags
|
flags ParsedFlags
|
||||||
@@ -13,8 +13,8 @@ type Context struct {
|
|||||||
moduleInstances map[string]interface{}
|
moduleInstances map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContext creates a Context.
|
// NewContext creates a [Context].
|
||||||
// In a module, prefer the Provisioner interface to get a Context.
|
// In a module, prefer the [Provisioner] interface to get a [Context].
|
||||||
func NewContext(
|
func NewContext(
|
||||||
flags ParsedFlags,
|
flags ParsedFlags,
|
||||||
descriptors []ModuleDescriptor,
|
descriptors []ModuleDescriptor,
|
||||||
@@ -32,7 +32,7 @@ func NewContext(
|
|||||||
// flags := ctx.ParsedFlags()
|
// flags := ctx.ParsedFlags()
|
||||||
// m.foo = flags.RequiredString("foo")
|
// m.foo = flags.RequiredString("foo")
|
||||||
// }
|
// }
|
||||||
func (ctx Context) ParsedFlags() ParsedFlags {
|
func (ctx *Context) ParsedFlags() ParsedFlags {
|
||||||
return ctx.flags
|
return ctx.flags
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ func (ctx Context) ParsedFlags() ParsedFlags {
|
|||||||
// initializes it. Otherwise, returns the already initialized instance.
|
// initializes it. Otherwise, returns the already initialized instance.
|
||||||
func (ctx *Context) Module(kind interface{}) (interface{}, error) {
|
func (ctx *Context) Module(kind interface{}) (interface{}, error) {
|
||||||
mods, err := ctx.Modules(kind)
|
mods, err := ctx.Modules(kind)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get module: %w", err)
|
return nil, fmt.Errorf("get module: %w", err)
|
||||||
}
|
}
|
||||||
@@ -101,7 +100,7 @@ func (ctx *Context) Modules(kind interface{}) ([]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadModule calls the Provision and/or Validate methods of the requested
|
// loadModule calls the Provision and/or Validate methods of the requested
|
||||||
// module if it satisfies the Provisioner and/or Validator interfaces.
|
// module if it satisfies the [Provisioner] and/or [Validator] interfaces.
|
||||||
func (ctx *Context) loadModule(id string, instance interface{}) error {
|
func (ctx *Context) loadModule(id string, instance interface{}) error {
|
||||||
if prov, ok := instance.(Provisioner); ok {
|
if prov, ok := instance.(Provisioner); ok {
|
||||||
// The instance can be provisioned.
|
// The instance can be provisioned.
|
||||||
|
|||||||
@@ -23,173 +23,212 @@ func TestContext_ParsedFlags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_Module(t *testing.T) {
|
func TestContext_Module(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
mods []ModuleDescriptor
|
scenario string
|
||||||
kind interface{}
|
mods []ModuleDescriptor
|
||||||
expectErr bool
|
kind interface{}
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "module with error on provision",
|
||||||
mods: func() []ModuleDescriptor {
|
mods: func() []ModuleDescriptor {
|
||||||
mod := struct{ ProtoProvisioner }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ProvisionerMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.provision = func(ctx *Context) error { return errors.New("foo") }
|
mod.ProvisionMock = func(ctx *Context) error { return errors.New("foo") }
|
||||||
|
|
||||||
return []ModuleDescriptor{mod.Descriptor()}
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
}(),
|
}(),
|
||||||
kind: new(Provisioner),
|
kind: new(Provisioner),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "two modules instead of one",
|
||||||
mods: func() []ModuleDescriptor {
|
mods: func() []ModuleDescriptor {
|
||||||
mod := struct{ ProtoProvisioner }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ProvisionerMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.provision = func(ctx *Context) error { return nil }
|
mod.ProvisionMock = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
|
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
|
||||||
}(),
|
}(),
|
||||||
kind: new(Provisioner),
|
kind: new(Provisioner),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "success",
|
||||||
mods: func() []ModuleDescriptor {
|
mods: func() []ModuleDescriptor {
|
||||||
mod := struct{ ProtoProvisioner }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ProvisionerMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.provision = func(ctx *Context) error { return nil }
|
mod.ProvisionMock = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
return []ModuleDescriptor{mod.Descriptor()}
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
}(),
|
}(),
|
||||||
kind: new(Provisioner),
|
kind: new(Provisioner),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
ctx := NewContext(ParsedFlags{}, tc.mods)
|
||||||
|
_, err := ctx.Module(tc.kind)
|
||||||
|
|
||||||
ctx := NewContext(ParsedFlags{}, tc.mods)
|
if !tc.expectError && err != nil {
|
||||||
_, err := ctx.Module(tc.kind)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_Modules(t *testing.T) {
|
func TestContext_Modules(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
mods []ModuleDescriptor
|
scenario string
|
||||||
kind interface{}
|
mods []ModuleDescriptor
|
||||||
expectErr bool
|
kind interface{}
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "module with error on provision",
|
||||||
mods: func() []ModuleDescriptor {
|
mods: func() []ModuleDescriptor {
|
||||||
mod := struct{ ProtoProvisioner }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ProvisionerMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.provision = func(ctx *Context) error { return errors.New("foo") }
|
mod.ProvisionMock = func(ctx *Context) error { return errors.New("foo") }
|
||||||
|
|
||||||
return []ModuleDescriptor{mod.Descriptor()}
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
}(),
|
}(),
|
||||||
kind: new(Provisioner),
|
kind: new(Provisioner),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "success (module)",
|
||||||
mods: func() []ModuleDescriptor {
|
mods: func() []ModuleDescriptor {
|
||||||
mod := struct{ ProtoProvisioner }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ProvisionerMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.provision = func(ctx *Context) error { return nil }
|
mod.ProvisionMock = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
|
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
|
||||||
}(),
|
}(),
|
||||||
kind: new(Provisioner),
|
kind: new(Provisioner),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "success (one module)",
|
||||||
mods: func() []ModuleDescriptor {
|
mods: func() []ModuleDescriptor {
|
||||||
mod := struct{ ProtoProvisioner }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ProvisionerMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.provision = func(ctx *Context) error { return nil }
|
mod.ProvisionMock = func(ctx *Context) error { return nil }
|
||||||
|
|
||||||
return []ModuleDescriptor{mod.Descriptor()}
|
return []ModuleDescriptor{mod.Descriptor()}
|
||||||
}(),
|
}(),
|
||||||
kind: new(Provisioner),
|
kind: new(Provisioner),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
ctx := NewContext(ParsedFlags{}, tc.mods)
|
||||||
|
_, err := ctx.Modules(tc.kind)
|
||||||
|
|
||||||
ctx := NewContext(ParsedFlags{}, tc.mods)
|
if !tc.expectError && err != nil {
|
||||||
_, err := ctx.Modules(tc.kind)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_loadModule(t *testing.T) {
|
func TestContext_loadModule(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
instance interface{}
|
scenario string
|
||||||
expectErr bool
|
instance interface{}
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "module with error on provision",
|
||||||
instance: func() interface{} {
|
instance: func() interface{} {
|
||||||
mod := struct{ ProtoProvisioner }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ProvisionerMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.provision = func(ctx *Context) error { return errors.New("foo") }
|
mod.ProvisionMock = func(ctx *Context) error { return errors.New("foo") }
|
||||||
|
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "module with error on validation",
|
||||||
instance: func() interface{} {
|
instance: func() interface{} {
|
||||||
mod := struct{ ProtoValidator }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ValidatorMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.validate = func() error { return errors.New("foo") }
|
mod.ValidateMock = func() error { return errors.New("foo") }
|
||||||
|
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "success",
|
||||||
instance: func() interface{} {
|
instance: func() interface{} {
|
||||||
mod := struct{ ProtoValidator }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() ModuleDescriptor {
|
ModuleMock
|
||||||
|
ValidatorMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() ModuleDescriptor {
|
||||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.validate = func() error { return nil }
|
mod.ValidateMock = func() error { return nil }
|
||||||
|
|
||||||
return mod
|
return mod
|
||||||
}(),
|
}(),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
ctx := NewContext(ParsedFlags{}, nil)
|
||||||
|
err := ctx.loadModule("foo", tc.instance)
|
||||||
|
|
||||||
ctx := NewContext(ParsedFlags{}, nil)
|
if !tc.expectError && err != nil {
|
||||||
err := ctx.loadModule("foo", tc.instance)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParsedFlags wraps a flag.FlagSet so that retrieving the typed values is
|
// ParsedFlags wraps a [flag.FlagSet] so that retrieving the typed values is
|
||||||
// easier.
|
// easier.
|
||||||
type ParsedFlags struct {
|
type ParsedFlags struct {
|
||||||
*flag.FlagSet
|
*flag.FlagSet
|
||||||
@@ -80,6 +80,28 @@ func (f *ParsedFlags) MustDeprecatedBool(deprecated string, newName string) bool
|
|||||||
return f.MustBool(newName)
|
return f.MustBool(newName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustInt64 returns the int64 value of a flag given by name.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustInt64(name string) int64 {
|
||||||
|
val, err := f.GetInt64(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustDeprecatedInt64 returns the int64 value of a deprecated flag if it was
|
||||||
|
// explicitly set or the int64 value of the new flag.
|
||||||
|
// It panics if an error occurs.
|
||||||
|
func (f *ParsedFlags) MustDeprecatedInt64(deprecated string, newName string) int64 {
|
||||||
|
if f.Changed(deprecated) {
|
||||||
|
return f.MustInt64(deprecated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.MustInt64(newName)
|
||||||
|
}
|
||||||
|
|
||||||
// MustInt returns the int value of a flag given by name.
|
// MustInt returns the int value of a flag given by name.
|
||||||
// It panics if an error occurs.
|
// It panics if an error occurs.
|
||||||
func (f *ParsedFlags) MustInt(name string) int {
|
func (f *ParsedFlags) MustInt(name string) int {
|
||||||
|
|||||||
@@ -20,23 +20,27 @@ func TestParsedFlags_MustString(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -44,49 +48,55 @@ func TestParsedFlags_MustString(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustString(tc.name)
|
parsedFlags.MustString(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedString(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedString(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue string
|
expectValue string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=foo"},
|
rawFlags: []string{"--foo=foo"},
|
||||||
expectValue: "foo",
|
expectValue: "foo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=bar"},
|
rawFlags: []string{"--bar=bar"},
|
||||||
expectValue: "bar",
|
expectValue: "bar",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=foo", "--bar=bar"},
|
rawFlags: []string{"--foo=foo", "--bar=bar"},
|
||||||
expectValue: "foo",
|
expectValue: "foo",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
fs.String("foo", "", "")
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.String("bar", "", "")
|
fs.String("foo", "", "")
|
||||||
|
fs.String("bar", "", "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedString("foo", "bar")
|
actual := parsedFlags.MustDeprecatedString("foo", "bar")
|
||||||
if actual != tc.expectValue {
|
if actual != tc.expectValue {
|
||||||
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectValue, actual)
|
t.Errorf("expected '%s' but got '%s'", tc.expectValue, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,23 +111,27 @@ func TestParsedFlags_MustStringSlice(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -125,49 +139,55 @@ func TestParsedFlags_MustStringSlice(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustStringSlice(tc.name)
|
parsedFlags.MustStringSlice(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedStringSlice(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedStringSlice(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue []string
|
expectValue []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=foo"},
|
rawFlags: []string{"--foo=foo"},
|
||||||
expectValue: []string{"foo"},
|
expectValue: []string{"foo"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=bar"},
|
rawFlags: []string{"--bar=bar"},
|
||||||
expectValue: []string{"bar"},
|
expectValue: []string{"bar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=foo", "--bar=bar"},
|
rawFlags: []string{"--foo=foo", "--bar=bar"},
|
||||||
expectValue: []string{"foo"},
|
expectValue: []string{"foo"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
fs.StringSlice("foo", make([]string, 0), "")
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.StringSlice("bar", make([]string, 0), "")
|
fs.StringSlice("foo", make([]string, 0), "")
|
||||||
|
fs.StringSlice("bar", make([]string, 0), "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedStringSlice("foo", "bar")
|
actual := parsedFlags.MustDeprecatedStringSlice("foo", "bar")
|
||||||
if !reflect.DeepEqual(actual, tc.expectValue) {
|
if !reflect.DeepEqual(actual, tc.expectValue) {
|
||||||
t.Errorf("test %d: expected %+v but got %+v", i, tc.expectValue, actual)
|
t.Errorf("expected %+v but got %+v", tc.expectValue, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,23 +202,27 @@ func TestParsedFlags_MustBool(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -206,48 +230,143 @@ func TestParsedFlags_MustBool(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustBool(tc.name)
|
parsedFlags.MustBool(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedBool(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedBool(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue bool
|
expectValue bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=true"},
|
rawFlags: []string{"--foo=true"},
|
||||||
expectValue: true,
|
expectValue: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=false"},
|
rawFlags: []string{"--bar=false"},
|
||||||
expectValue: false,
|
expectValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=true", "--bar=false"},
|
rawFlags: []string{"--foo=true", "--bar=false"},
|
||||||
expectValue: true,
|
expectValue: true,
|
||||||
},
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.Bool("foo", false, "")
|
||||||
|
fs.Bool("bar", true, "")
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := parsedFlags.MustDeprecatedBool("foo", "bar")
|
||||||
|
if actual != tc.expectValue {
|
||||||
|
t.Errorf("expected %v but got %v", tc.expectValue, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustInt64(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
|
fs.Int64("foo", 0, "")
|
||||||
|
|
||||||
|
err := fs.Parse([]string{"--foo=1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
name string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
|
name: "bar",
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
if tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Fatal("expected panic but got none")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPanic {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFlags.MustInt64(tc.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsedFlags_MustDeprecatedInt64(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
rawFlags []string
|
||||||
|
expectValue int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
|
rawFlags: []string{"--foo=1"},
|
||||||
|
expectValue: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
|
rawFlags: []string{"--bar=2"},
|
||||||
|
expectValue: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
|
rawFlags: []string{"--foo=1", "--bar=2"},
|
||||||
|
expectValue: 1,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.Bool("foo", false, "")
|
fs.Int64("foo", 0, "")
|
||||||
fs.Bool("bar", true, "")
|
fs.Int64("bar", 0, "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedBool("foo", "bar")
|
actual := parsedFlags.MustDeprecatedInt64("foo", "bar")
|
||||||
if actual != tc.expectValue {
|
if actual != tc.expectValue {
|
||||||
t.Errorf("test %d: expected %v but got %v", i, tc.expectValue, actual)
|
t.Errorf("expected %d but got %d", tc.expectValue, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,23 +382,27 @@ func TestParsedFlags_MustInt(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -287,49 +410,55 @@ func TestParsedFlags_MustInt(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustInt(tc.name)
|
parsedFlags.MustInt(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedInt(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedInt(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue int
|
expectValue int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1"},
|
rawFlags: []string{"--foo=1"},
|
||||||
expectValue: 1,
|
expectValue: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=2"},
|
rawFlags: []string{"--bar=2"},
|
||||||
expectValue: 2,
|
expectValue: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1", "--bar=2"},
|
rawFlags: []string{"--foo=1", "--bar=2"},
|
||||||
expectValue: 1,
|
expectValue: 1,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
fs.Int("foo", 0, "")
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.Int("bar", 0, "")
|
fs.Int("foo", 0, "")
|
||||||
|
fs.Int("bar", 0, "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedInt("foo", "bar")
|
actual := parsedFlags.MustDeprecatedInt("foo", "bar")
|
||||||
if actual != tc.expectValue {
|
if actual != tc.expectValue {
|
||||||
t.Errorf("test %d: expected %d but got %d", i, tc.expectValue, actual)
|
t.Errorf("expected %d but got %d", tc.expectValue, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,23 +473,27 @@ func TestParsedFlags_MustFloat64(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -368,49 +501,55 @@ func TestParsedFlags_MustFloat64(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustFloat64(tc.name)
|
parsedFlags.MustFloat64(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedFloat64(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedFloat64(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue float64
|
expectValue float64
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1.0"},
|
rawFlags: []string{"--foo=1.0"},
|
||||||
expectValue: 1.0,
|
expectValue: 1.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=2.0"},
|
rawFlags: []string{"--bar=2.0"},
|
||||||
expectValue: 2.0,
|
expectValue: 2.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1.0", "--bar=2.0"},
|
rawFlags: []string{"--foo=1.0", "--bar=2.0"},
|
||||||
expectValue: 1.0,
|
expectValue: 1.0,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
fs.Float64("foo", 0, "")
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.Float64("bar", 0, "")
|
fs.Float64("foo", 0, "")
|
||||||
|
fs.Float64("bar", 0, "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedFloat64("foo", "bar")
|
actual := parsedFlags.MustDeprecatedFloat64("foo", "bar")
|
||||||
if actual != tc.expectValue {
|
if actual != tc.expectValue {
|
||||||
t.Errorf("test %d: expected %f but got %f", i, tc.expectValue, actual)
|
t.Errorf("expected %f but got %f", tc.expectValue, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,23 +564,27 @@ func TestParsedFlags_MustDuration(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -449,49 +592,55 @@ func TestParsedFlags_MustDuration(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustDuration(tc.name)
|
parsedFlags.MustDuration(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedDuration(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedDuration(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue time.Duration
|
expectValue time.Duration
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1s"},
|
rawFlags: []string{"--foo=1s"},
|
||||||
expectValue: time.Duration(1) * time.Second,
|
expectValue: time.Duration(1) * time.Second,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=2s"},
|
rawFlags: []string{"--bar=2s"},
|
||||||
expectValue: time.Duration(2) * time.Second,
|
expectValue: time.Duration(2) * time.Second,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1s", "--bar=2s"},
|
rawFlags: []string{"--foo=1s", "--bar=2s"},
|
||||||
expectValue: time.Duration(1) * time.Second,
|
expectValue: time.Duration(1) * time.Second,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
fs.Duration("foo", 0, "")
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.Duration("bar", 0, "")
|
fs.Duration("foo", 0, "")
|
||||||
|
fs.Duration("bar", 0, "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedDuration("foo", "bar")
|
actual := parsedFlags.MustDeprecatedDuration("foo", "bar")
|
||||||
if actual != tc.expectValue {
|
if actual != tc.expectValue {
|
||||||
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectValue, actual)
|
t.Errorf("expected '%s' but got '%s'", tc.expectValue, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,27 +656,27 @@ func TestParsedFlags_MustHumanReadableBytesString(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "baz",
|
|
||||||
expectPanic: true,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -535,49 +684,55 @@ func TestParsedFlags_MustHumanReadableBytesString(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustHumanReadableBytesString(tc.name)
|
parsedFlags.MustHumanReadableBytesString(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedHumanReadableBytesString(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedHumanReadableBytesString(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue string
|
expectValue string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1MB"},
|
rawFlags: []string{"--foo=1MB"},
|
||||||
expectValue: "1MB",
|
expectValue: "1MB",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=2MB"},
|
rawFlags: []string{"--bar=2MB"},
|
||||||
expectValue: "2MB",
|
expectValue: "2MB",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=1MB", "--bar=2MB"},
|
rawFlags: []string{"--foo=1MB", "--bar=2MB"},
|
||||||
expectValue: "1MB",
|
expectValue: "1MB",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
fs.String("foo", "", "")
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.String("bar", "", "")
|
fs.String("foo", "", "")
|
||||||
|
fs.String("bar", "", "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedHumanReadableBytesString("foo", "bar")
|
actual := parsedFlags.MustDeprecatedHumanReadableBytesString("foo", "bar")
|
||||||
if actual != tc.expectValue {
|
if actual != tc.expectValue {
|
||||||
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectValue, actual)
|
t.Errorf("expected '%s' but got '%s'", tc.expectValue, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,27 +748,27 @@ func TestParsedFlags_MustRegexp(t *testing.T) {
|
|||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
name string
|
name string
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "foo",
|
scenario: "success",
|
||||||
|
name: "foo",
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-existing flag",
|
||||||
name: "bar",
|
name: "bar",
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "baz",
|
|
||||||
expectPanic: true,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Fatal("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -621,48 +776,54 @@ func TestParsedFlags_MustRegexp(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Fatalf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedFlags.MustRegexp(tc.name)
|
parsedFlags.MustRegexp(tc.name)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsedFlags_MustDeprecatedRegexp(t *testing.T) {
|
func TestParsedFlags_MustDeprecatedRegexp(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
rawFlags []string
|
rawFlags []string
|
||||||
expectValue *regexp.Regexp
|
expectValue *regexp.Regexp
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value",
|
||||||
rawFlags: []string{"--foo=foo"},
|
rawFlags: []string{"--foo=foo"},
|
||||||
expectValue: regexp.MustCompile("foo"),
|
expectValue: regexp.MustCompile("foo"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "non-deprecated flag value",
|
||||||
rawFlags: []string{"--bar=bar"},
|
rawFlags: []string{"--bar=bar"},
|
||||||
expectValue: regexp.MustCompile("bar"),
|
expectValue: regexp.MustCompile("bar"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "deprecated flag value > non-deprecated flag value",
|
||||||
rawFlags: []string{"--foo=foo", "--bar=bar"},
|
rawFlags: []string{"--foo=foo", "--bar=bar"},
|
||||||
expectValue: regexp.MustCompile("foo"),
|
expectValue: regexp.MustCompile("foo"),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
fs.String("foo", "", "")
|
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
|
||||||
fs.String("bar", "", "")
|
fs.String("foo", "", "")
|
||||||
|
fs.String("bar", "", "")
|
||||||
|
|
||||||
parsedFlags := ParsedFlags{FlagSet: fs}
|
parsedFlags := ParsedFlags{FlagSet: fs}
|
||||||
|
|
||||||
err := parsedFlags.Parse(tc.rawFlags)
|
err := parsedFlags.Parse(tc.rawFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := parsedFlags.MustDeprecatedRegexp("foo", "bar")
|
actual := parsedFlags.MustDeprecatedRegexp("foo", "bar")
|
||||||
if actual.String() != tc.expectValue.String() {
|
if actual.String() != tc.expectValue.String() {
|
||||||
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectValue.String(), actual.String())
|
t.Errorf("expected '%s' but got '%s'", tc.expectValue.String(), actual.String())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,24 +7,44 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TmpPath returns the default directory to use for temporary files and
|
// FileSystem provides utilities for managing temporary directories. It creates
|
||||||
// directories. Most if not all files and directories created by the
|
// unique directory names based on UUIDs to ensure isolation of temporary files
|
||||||
// application and its dependencies must be based on this default directory.
|
// for different modules.
|
||||||
func TmpPath() string {
|
type FileSystem struct {
|
||||||
return os.TempDir()
|
workingDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDirPath returns a random absolute path based on the temporary path.
|
// NewFileSystem initializes a new [FileSystem] instance with a unique working
|
||||||
func NewDirPath() string {
|
// directory.
|
||||||
return fmt.Sprintf("%s/%s", TmpPath(), uuid.New())
|
func NewFileSystem() *FileSystem {
|
||||||
|
return &FileSystem{
|
||||||
|
workingDir: uuid.NewString(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MkdirAll creates a random directory based on the temporary path and
|
// WorkingDir returns the unique name of the working directory.
|
||||||
// returns its absolute path.
|
func (fs *FileSystem) WorkingDir() string {
|
||||||
func MkdirAll() (string, error) {
|
return fs.workingDir
|
||||||
path := NewDirPath()
|
}
|
||||||
|
|
||||||
err := os.MkdirAll(path, 0755)
|
// WorkingDirPath constructs and returns the full path to the working directory
|
||||||
|
// inside the system's temporary directory.
|
||||||
|
func (fs *FileSystem) WorkingDirPath() string {
|
||||||
|
return fmt.Sprintf("%s/%s", os.TempDir(), fs.workingDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDirPath generates a new unique path for a directory inside the working
|
||||||
|
// directory.
|
||||||
|
func (fs *FileSystem) NewDirPath() string {
|
||||||
|
return fmt.Sprintf("%s/%s", fs.WorkingDirPath(), uuid.NewString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MkdirAll creates a new unique directory inside the working directory and
|
||||||
|
// returns its path. If the directory creation fails, an error is returned.
|
||||||
|
func (fs *FileSystem) MkdirAll() (string, error) {
|
||||||
|
path := fs.NewDirPath()
|
||||||
|
|
||||||
|
err := os.MkdirAll(path, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("create directory %s: %w", path, err)
|
return "", fmt.Errorf("create directory %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,55 @@
|
|||||||
package gotenberg
|
package gotenberg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTmpPath(t *testing.T) {
|
func TestFileSystem_WorkingDir(t *testing.T) {
|
||||||
osTempDir := os.TempDir()
|
fs := NewFileSystem()
|
||||||
tmpPath := TmpPath()
|
dirName := fs.WorkingDir()
|
||||||
|
|
||||||
if tmpPath != osTempDir {
|
if dirName == "" {
|
||||||
t.Errorf("expected path '%s' but got '%s'", osTempDir, tmpPath)
|
t.Error("expected directory name but got empty string")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewDirPath(t *testing.T) {
|
func TestFileSystem_WorkingDirPath(t *testing.T) {
|
||||||
newDirPath := NewDirPath()
|
fs := NewFileSystem()
|
||||||
tmpPath := TmpPath()
|
expectedPath := fmt.Sprintf("%s/%s", os.TempDir(), fs.WorkingDir())
|
||||||
|
|
||||||
if !strings.HasPrefix(newDirPath, tmpPath) {
|
if fs.WorkingDirPath() != expectedPath {
|
||||||
t.Fatalf("expected path '%s' to start with '%s'", newDirPath, tmpPath)
|
t.Errorf("expected path '%s' but got '%s'", expectedPath, fs.WorkingDirPath())
|
||||||
}
|
|
||||||
|
|
||||||
newDirPaths := make([]string, 1000)
|
|
||||||
for i := range newDirPaths {
|
|
||||||
newDirPaths[i] = NewDirPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, newDirPath := range newDirPaths {
|
|
||||||
for j, comparison := range newDirPaths {
|
|
||||||
if i == j {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if newDirPath == comparison {
|
|
||||||
t.Fatalf("expected path '%s' (index %d) to be unique, but found an identical path on index %d", newDirPath, i, j)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMkdirAll(t *testing.T) {
|
func TestFileSystem_NewDirPath(t *testing.T) {
|
||||||
path, err := MkdirAll()
|
fs := NewFileSystem()
|
||||||
|
newDir := fs.NewDirPath()
|
||||||
|
expectedPrefix := fs.WorkingDirPath()
|
||||||
|
|
||||||
|
if !strings.HasPrefix(newDir, expectedPrefix) {
|
||||||
|
t.Errorf("expected new directory to start with '%s' but got '%s'", expectedPrefix, newDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSystem_MkdirAll(t *testing.T) {
|
||||||
|
fs := NewFileSystem()
|
||||||
|
|
||||||
|
newPath, err := fs.MkdirAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpPath := TmpPath()
|
_, err = os.Stat(newPath)
|
||||||
if !strings.HasPrefix(path, tmpPath) {
|
if os.IsNotExist(err) {
|
||||||
t.Fatalf("expected path '%s' to start with '%s'", path, tmpPath)
|
t.Errorf("expected directory '%s' to exist but it doesn't", newPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = os.Stat(path)
|
err = os.RemoveAll(fs.WorkingDirPath())
|
||||||
if os.IsNotExist(err) {
|
if err != nil {
|
||||||
t.Errorf("expected path '%s' to exist but got: %v", path, err)
|
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
pkg/gotenberg/gc.go
Normal file
53
pkg/gotenberg/gc.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GarbageCollect scans the root path and deletes files or directories with
|
||||||
|
// names containing specific substrings.
|
||||||
|
func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr []string) error {
|
||||||
|
logger = logger.Named("gc")
|
||||||
|
|
||||||
|
// To make sure that the next Walk method stays on
|
||||||
|
// the root level of the considered path, we have to
|
||||||
|
// return a filepath.SkipDir error if the current path
|
||||||
|
// is a directory.
|
||||||
|
skipDirOrNil := func(info os.FileInfo) error {
|
||||||
|
if info.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Walk(rootPath, func(path string, info os.FileInfo, pathErr error) error {
|
||||||
|
if pathErr != nil {
|
||||||
|
return pathErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == rootPath {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, substr := range includeSubstr {
|
||||||
|
if strings.Contains(info.Name(), substr) || path == substr {
|
||||||
|
err := os.RemoveAll(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("garbage collect '%s': %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("'%s' removed", path))
|
||||||
|
|
||||||
|
return skipDirOrNil(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return skipDirOrNil(info)
|
||||||
|
})
|
||||||
|
}
|
||||||
98
pkg/gotenberg/gc_test.go
Normal file
98
pkg/gotenberg/gc_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGarbageCollect(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
rootPath string
|
||||||
|
includeSubstr []string
|
||||||
|
expectError bool
|
||||||
|
expectNotExists []string
|
||||||
|
expectExists []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "root path does not exist",
|
||||||
|
rootPath: uuid.NewString(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "remove include substrings",
|
||||||
|
rootPath: func() string {
|
||||||
|
path := fmt.Sprintf("%s/a_directory", os.TempDir())
|
||||||
|
|
||||||
|
err := os.MkdirAll(path, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file", path), []byte{1}, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fmt.Sprintf("%s/a_bar_file", path), []byte{1}, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fmt.Sprintf("%s/a_baz_file", path), []byte{1}, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}(),
|
||||||
|
includeSubstr: []string{"foo", fmt.Sprintf("%s/a_directory/a_bar_file", os.TempDir())},
|
||||||
|
expectError: false,
|
||||||
|
expectExists: []string{"a_baz_file"},
|
||||||
|
expectNotExists: []string{"a_foo_file", "a_bar_file"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(tc.rootPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := GarbageCollect(zap.NewNop(), tc.rootPath, tc.includeSubstr)
|
||||||
|
|
||||||
|
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.expectError && err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range tc.expectNotExists {
|
||||||
|
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
|
||||||
|
_, err = os.Stat(path)
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected '%s' not to exist but it does: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range tc.expectExists {
|
||||||
|
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
|
||||||
|
_, err = os.Stat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected '%s' to exist but it does not: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ package gotenberg
|
|||||||
|
|
||||||
import "go.uber.org/zap"
|
import "go.uber.org/zap"
|
||||||
|
|
||||||
// LoggerProvider is a module interface which exposes a method for creating a
|
// LoggerProvider is an interface for a module that supplies a method for
|
||||||
// zap.Logger for other modules.
|
// creating a [zap.Logger] instance for use by other modules.
|
||||||
//
|
//
|
||||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
// provider, _ := ctx.Module(new(gotenberg.LoggerProvider))
|
// provider, _ := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Metric struct {
|
|||||||
Read func() float64
|
Read func() float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetricsProvider is a module interface which provides a list of Metric.
|
// MetricsProvider is a module interface which provides a list of [Metric].
|
||||||
//
|
//
|
||||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
// provider, _ := ctx.Module(new(gotenberg.MetricsProvider))
|
// provider, _ := ctx.Module(new(gotenberg.MetricsProvider))
|
||||||
|
|||||||
140
pkg/gotenberg/mocks.go
Normal file
140
pkg/gotenberg/mocks.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModuleMock is a mock for the [Module] interface.
|
||||||
|
type ModuleMock struct {
|
||||||
|
DescriptorMock func() ModuleDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *ModuleMock) Descriptor() ModuleDescriptor {
|
||||||
|
return mod.DescriptorMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvisionerMock is a mock for the [Provisioner] interface.
|
||||||
|
type ProvisionerMock struct {
|
||||||
|
ProvisionMock func(*Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *ProvisionerMock) Provision(ctx *Context) error {
|
||||||
|
return mod.ProvisionMock(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatorMock is a mock for the [Validator] interface.
|
||||||
|
type ValidatorMock struct {
|
||||||
|
ValidateMock func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *ValidatorMock) Validate() error {
|
||||||
|
return mod.ValidateMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
||||||
|
type PdfEngineMock struct {
|
||||||
|
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||||
|
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
||||||
|
type PdfEngineProviderMock struct {
|
||||||
|
PdfEngineMock func() (PdfEngine, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *PdfEngineProviderMock) PdfEngine() (PdfEngine, error) {
|
||||||
|
return provider.PdfEngineMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessMock is a mock for the [Process] interface.
|
||||||
|
type ProcessMock struct {
|
||||||
|
StartMock func(logger *zap.Logger) error
|
||||||
|
StopMock func(logger *zap.Logger) error
|
||||||
|
HealthyMock func(logger *zap.Logger) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProcessMock) Start(logger *zap.Logger) error {
|
||||||
|
return p.StartMock(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProcessMock) Stop(logger *zap.Logger) error {
|
||||||
|
return p.StopMock(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProcessMock) Healthy(logger *zap.Logger) bool {
|
||||||
|
return p.HealthyMock(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessSupervisorMock is a mock for the [ProcessSupervisor] interface.
|
||||||
|
type ProcessSupervisorMock struct {
|
||||||
|
LaunchMock func() error
|
||||||
|
ShutdownMock func() error
|
||||||
|
HealthyMock func() bool
|
||||||
|
RunMock func(ctx context.Context, logger *zap.Logger, task func() error) error
|
||||||
|
ReqQueueSizeMock func() int64
|
||||||
|
RestartsCountMock func() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessSupervisorMock) Launch() error {
|
||||||
|
return s.LaunchMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessSupervisorMock) Shutdown() error {
|
||||||
|
return s.ShutdownMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessSupervisorMock) Healthy() bool {
|
||||||
|
return s.HealthyMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessSupervisorMock) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
|
return s.RunMock(ctx, logger, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessSupervisorMock) ReqQueueSize() int64 {
|
||||||
|
return s.ReqQueueSizeMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessSupervisorMock) RestartsCount() int64 {
|
||||||
|
return s.RestartsCountMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerProviderMock is a mock for the [LoggerProvider] interface.
|
||||||
|
type LoggerProviderMock struct {
|
||||||
|
LoggerMock func(mod Module) (*zap.Logger, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
|
||||||
|
return provider.LoggerMock(mod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricsProviderMock is a mock for the [MetricsProvider] interface.
|
||||||
|
type MetricsProviderMock struct {
|
||||||
|
MetricsMock func() ([]Metric, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *MetricsProviderMock) Metrics() ([]Metric, error) {
|
||||||
|
return provider.MetricsMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ Module = (*ModuleMock)(nil)
|
||||||
|
_ Validator = (*ValidatorMock)(nil)
|
||||||
|
_ PdfEngine = (*PdfEngineMock)(nil)
|
||||||
|
_ PdfEngineProvider = (*PdfEngineProviderMock)(nil)
|
||||||
|
_ Process = (*ProcessMock)(nil)
|
||||||
|
_ ProcessSupervisor = (*ProcessSupervisorMock)(nil)
|
||||||
|
_ LoggerProvider = (*LoggerProviderMock)(nil)
|
||||||
|
_ MetricsProvider = (*MetricsProviderMock)(nil)
|
||||||
|
)
|
||||||
190
pkg/gotenberg/mocks_test.go
Normal file
190
pkg/gotenberg/mocks_test.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModuleMock(t *testing.T) {
|
||||||
|
mock := &ModuleMock{
|
||||||
|
DescriptorMock: func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module {
|
||||||
|
return nil
|
||||||
|
}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if mock.Descriptor().ID != "foo" {
|
||||||
|
t.Errorf("expected ID '%s' from ModuleMock.Descriptor, but got '%s'", "foo", mock.Descriptor().ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerMock(t *testing.T) {
|
||||||
|
mock := &ProvisionerMock{
|
||||||
|
ProvisionMock: func(*Context) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Provision(&Context{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ProvisionerMock.Provision, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorMock(t *testing.T) {
|
||||||
|
mock := &ValidatorMock{
|
||||||
|
ValidateMock: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ValidatorMock.Validate, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPDFEngineMock(t *testing.T) {
|
||||||
|
mock := &PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineMock.Merge, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPDFEngineProviderMock(t *testing.T) {
|
||||||
|
mock := &PdfEngineProviderMock{
|
||||||
|
PdfEngineMock: func() (PdfEngine, error) {
|
||||||
|
return new(PdfEngineMock), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.PdfEngine()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineProviderMock.PdfEngine, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMock(t *testing.T) {
|
||||||
|
mock := &ProcessMock{
|
||||||
|
StartMock: func(logger *zap.Logger) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
StopMock: func(logger *zap.Logger) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Start(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ProcessMock.Start, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.Stop(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ProcessMock.Stop, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
healthy := mock.Healthy(zap.NewNop())
|
||||||
|
if !healthy {
|
||||||
|
t.Error("expected true from ProcessMock.Healthy, but got false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessSupervisorMock(t *testing.T) {
|
||||||
|
mock := &ProcessSupervisorMock{
|
||||||
|
LaunchMock: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ShutdownMock: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
HealthyMock: func() bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ReqQueueSizeMock: func() int64 {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
RestartsCountMock: func() int64 {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Launch()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ProcessSupervisorMock.Launch, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ProcessSupervisorMock.Shutdown, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
healthy := mock.Healthy()
|
||||||
|
if !healthy {
|
||||||
|
t.Error("expected true from ProcessSupervisorMock.Healthy, but got false")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.Run(context.TODO(), zap.NewNop(), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ProcessSupervisorMock.Run, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := mock.ReqQueueSize()
|
||||||
|
if size != 0 {
|
||||||
|
t.Errorf("expected 0 from ProcessSupervisorMock.ReqQueueSize, but got: %d", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
restarts := mock.RestartsCount()
|
||||||
|
if restarts != 0 {
|
||||||
|
t.Errorf("expected 0 from ProcessSupervisorMock.RestartsCount, but got: %d", restarts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerProviderMock(t *testing.T) {
|
||||||
|
mock := &LoggerProviderMock{
|
||||||
|
LoggerMock: func(mod Module) (*zap.Logger, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.Logger(new(ModuleMock))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from LoggerProviderMock.Logger, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetricsProviderMock(t *testing.T) {
|
||||||
|
mock := &MetricsProviderMock{
|
||||||
|
MetricsMock: func() ([]Metric, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.Metrics()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from MetricsProviderMock.Metrics, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,12 @@ type App interface {
|
|||||||
Stop(ctx context.Context) error
|
Stop(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemLogger is a module interface for modules which want to display
|
||||||
|
// messages on startup.
|
||||||
|
type SystemLogger interface {
|
||||||
|
SystemMessages() []string
|
||||||
|
}
|
||||||
|
|
||||||
// MustRegisterModule registers a module.
|
// MustRegisterModule registers a module.
|
||||||
//
|
//
|
||||||
// To register a module, create an init() method in the module main go file:
|
// To register a module, create an init() method in the module main go file:
|
||||||
@@ -77,7 +83,7 @@ type App interface {
|
|||||||
// gotenberg.MustRegisterModule(YourModule{})
|
// gotenberg.MustRegisterModule(YourModule{})
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Then, in the main command (github.com/gotenberg/gotenberg/v7/cmd/gotenberg),
|
// Then, in the main command (github.com/gotenberg/gotenberg/v8/cmd/gotenberg),
|
||||||
// import the module:
|
// import the module:
|
||||||
//
|
//
|
||||||
// imports (
|
// imports (
|
||||||
|
|||||||
@@ -5,32 +5,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoProvisioner struct {
|
|
||||||
ProtoModule
|
|
||||||
provision func(ctx *Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoProvisioner) Provision(ctx *Context) error {
|
|
||||||
return mod.provision(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoValidator struct {
|
|
||||||
ProtoModule
|
|
||||||
validate func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoValidator) Validate() error {
|
|
||||||
return mod.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMustRegisterModule(t *testing.T) {
|
func TestMustRegisterModule(t *testing.T) {
|
||||||
descriptorsMu.RLock()
|
descriptorsMu.RLock()
|
||||||
descriptors = map[string]ModuleDescriptor{
|
descriptors = map[string]ModuleDescriptor{
|
||||||
@@ -38,44 +12,51 @@ func TestMustRegisterModule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
descriptorsMu.RUnlock()
|
descriptorsMu.RUnlock()
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
ID string
|
ID string
|
||||||
New func() Module
|
New func() Module
|
||||||
expectPanic bool
|
expectPanic bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "no ID",
|
||||||
ID: "",
|
ID: "",
|
||||||
New: func() Module { return new(ProtoModule) },
|
New: func() Module { return new(ModuleMock) },
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "nil New method",
|
||||||
ID: "b",
|
ID: "b",
|
||||||
New: nil,
|
New: nil,
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "nil module",
|
||||||
ID: "b",
|
ID: "b",
|
||||||
New: func() Module { return nil },
|
New: func() Module { return nil },
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "existing module",
|
||||||
ID: "a",
|
ID: "a",
|
||||||
New: func() Module { return new(ProtoModule) },
|
New: func() Module { return new(ModuleMock) },
|
||||||
expectPanic: true,
|
expectPanic: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "b",
|
scenario: "success",
|
||||||
New: func() Module { return new(ProtoModule) },
|
ID: "b",
|
||||||
|
New: func() Module { return new(ModuleMock) },
|
||||||
|
expectPanic: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod := struct{ ProtoModule }{}
|
mod := &struct{ ModuleMock }{}
|
||||||
mod.descriptor = func() ModuleDescriptor { return ModuleDescriptor{ID: tc.ID, New: tc.New} }
|
mod.DescriptorMock = func() ModuleDescriptor { return ModuleDescriptor{ID: tc.ID, New: tc.New} }
|
||||||
|
|
||||||
if tc.expectPanic {
|
if tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Errorf("test %d: expected panic but got none", i)
|
t.Error("expected panic but got none")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -83,13 +64,13 @@ func TestMustRegisterModule(t *testing.T) {
|
|||||||
if !tc.expectPanic {
|
if !tc.expectPanic {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("test %d: expected no panic but got: %v", i, r)
|
t.Errorf("expected no panic but got: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
MustRegisterModule(mod)
|
MustRegisterModule(mod)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptorsMu.RLock()
|
descriptorsMu.RLock()
|
||||||
@@ -124,12 +105,3 @@ func TestGetModuleDescriptors(t *testing.T) {
|
|||||||
descriptors = make(map[string]ModuleDescriptor)
|
descriptors = make(map[string]ModuleDescriptor)
|
||||||
descriptorsMu.RUnlock()
|
descriptorsMu.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ Module = (*ProtoModule)(nil)
|
|
||||||
_ Provisioner = (*ProtoProvisioner)(nil)
|
|
||||||
_ Module = (*ProtoProvisioner)(nil)
|
|
||||||
_ Validator = (*ProtoValidator)(nil)
|
|
||||||
_ Module = (*ProtoValidator)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -8,45 +8,74 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrPDFEngineMethodNotAvailable happens if a PDFEngine method is not
|
// ErrPdfEngineMethodNotSupported is returned when a specific method of the
|
||||||
// available in the implementation.
|
// PdfEngine interface is not supported by its current implementation.
|
||||||
ErrPDFEngineMethodNotAvailable = errors.New("method not available")
|
ErrPdfEngineMethodNotSupported = errors.New("method not supported")
|
||||||
|
|
||||||
// ErrPDFFormatNotAvailable happens if a PDFEngine Convert's method does
|
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
||||||
// not handle a specific format.
|
// PdfEngine interface does not support a requested PDF format conversion.
|
||||||
ErrPDFFormatNotAvailable = errors.New("PDF format not available")
|
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FormatPDFA1a string = "PDF/A-1a"
|
// PdfA1a represents the PDF/A-1a format.
|
||||||
FormatPDFA1b string = "PDF/A-1b"
|
PdfA1a string = "PDF/A-1a"
|
||||||
FormatPDFA2a string = "PDF/A-2a"
|
|
||||||
FormatPDFA2b string = "PDF/A-2b"
|
// PdfA1b represents the PDF/A-1b format.
|
||||||
FormatPDFA2u string = "PDF/A-2u"
|
PdfA1b string = "PDF/A-1b"
|
||||||
FormatPDFA3a string = "PDF/A-3a"
|
|
||||||
FormatPDFA3b string = "PDF/A-3b"
|
// PdfA2a represents the PDF/A-2a format.
|
||||||
FormatPDFA3u string = "PDF/A-3u"
|
PdfA2a string = "PDF/A-2a"
|
||||||
|
|
||||||
|
// PdfA2b represents the PDF/A-2b format.
|
||||||
|
PdfA2b string = "PDF/A-2b"
|
||||||
|
|
||||||
|
// PdfA2u represents the PDF/A-2u format.
|
||||||
|
PdfA2u string = "PDF/A-2u"
|
||||||
|
|
||||||
|
// PdfA3a represents the PDF/A-3a format.
|
||||||
|
PdfA3a string = "PDF/A-3a"
|
||||||
|
|
||||||
|
// PdfA3b represents the PDF/A-3b format.
|
||||||
|
PdfA3b string = "PDF/A-3b"
|
||||||
|
|
||||||
|
// PdfA3u represents the PDF/A-3u format.
|
||||||
|
PdfA3u string = "PDF/A-3u"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PDFEngine is a module interface which exposes methods for manipulating one
|
// PdfFormats specifies the target formats for a PDF conversion.
|
||||||
// or more PDFs. Implementations may abstract powerful tools like PDFtk, or
|
type PdfFormats struct {
|
||||||
// fulfill those methods contracts in Golang directly.
|
// PdfA denotes the PDF/A standard format (e.g., PDF/A-1a).
|
||||||
type PDFEngine interface {
|
PdfA string
|
||||||
// Merge merges the given PDFs into a unique PDF. The pages' order reflects
|
|
||||||
// order of the given files.
|
// PdfUa indicates whether the PDF should comply
|
||||||
|
// with the PDF/UA (Universal Accessibility) standard.
|
||||||
|
PdfUa bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PdfEngine provides an interface for operations on PDFs. Implementations
|
||||||
|
// can utilize various tools like PDFtk, or implement functionality directly in
|
||||||
|
// Go.
|
||||||
|
type PdfEngine interface {
|
||||||
|
// Merge combines multiple PDFs into a single PDF. The resulting page order
|
||||||
|
// is determined by the order of files provided in inputPaths.
|
||||||
Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||||
|
|
||||||
// Convert converts the given PDF to a specific PDF format.
|
// Convert transforms a given PDF to the specified formats defined in
|
||||||
Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
// PdfFormats. If no format, it does nothing.
|
||||||
|
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDFEngineProvider is a module interface which exposes a method for creating a
|
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
||||||
// PDFEngine for other modules.
|
// This is used to decouple the creation of a [PdfEngine] from its consumers.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
//
|
//
|
||||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
// provider, _ := ctx.Module(new(gotenberg.PDFEngineProvider))
|
// provider, _ := ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||||
// pdfengines, _ := provider.(gotenberg.PDFEngineProvider).PDFEngine()
|
// engine, _ := provider.(gotenberg.PdfEngineProvider).PdfEngine()
|
||||||
// }
|
// }
|
||||||
type PDFEngineProvider interface {
|
type PdfEngineProvider interface {
|
||||||
PDFEngine() (PDFEngine, error)
|
// PdfEngine returns an instance of the [PdfEngine] interface for PDF operations.
|
||||||
|
PdfEngine() (PdfEngine, error)
|
||||||
}
|
}
|
||||||
|
|||||||
262
pkg/gotenberg/supervisor.go
Normal file
262
pkg/gotenberg/supervisor.go
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrProcessAlreadyRestarting happens if the [ProcessSupervisor] is trying
|
||||||
|
// to restart an already restarting [Process].
|
||||||
|
var ErrProcessAlreadyRestarting = errors.New("process already restarting")
|
||||||
|
|
||||||
|
// Process is an interface that represents an abstract process
|
||||||
|
// and provides methods for starting, stopping, and checking the health of the
|
||||||
|
// process.
|
||||||
|
//
|
||||||
|
// Implementations of this interface should handle the actual logic for
|
||||||
|
// starting, stopping, and ensuring the process's health.
|
||||||
|
type Process interface {
|
||||||
|
// Start initiates the process and returns an error if the process cannot
|
||||||
|
// be started.
|
||||||
|
Start(logger *zap.Logger) error
|
||||||
|
|
||||||
|
// Stop terminates the process and returns an error if the process cannot
|
||||||
|
// be stopped.
|
||||||
|
Stop(logger *zap.Logger) error
|
||||||
|
|
||||||
|
// Healthy checks the health of the process. It returns true if the process
|
||||||
|
// is healthy; otherwise, it returns false.
|
||||||
|
Healthy(logger *zap.Logger) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessSupervisor provides methods to manage a [Process], including
|
||||||
|
// starting, stopping, and ensuring its health.
|
||||||
|
//
|
||||||
|
// Additionally, it allows for the execution of tasks while managing the
|
||||||
|
// process's state and provides functionality for limiting the number of
|
||||||
|
// requests that can be handled by the process, as well as managing a request
|
||||||
|
// queue.
|
||||||
|
type ProcessSupervisor interface {
|
||||||
|
// Launch starts the managed [Process].
|
||||||
|
Launch() error
|
||||||
|
|
||||||
|
// Shutdown stops the managed [Process].
|
||||||
|
Shutdown() error
|
||||||
|
|
||||||
|
// Healthy checks and returns the health status of the managed [Process].
|
||||||
|
//
|
||||||
|
// If the process has not been started or is restarting, it is considered
|
||||||
|
// healthy and true is returned. Otherwise, it returns the health status of
|
||||||
|
// the actual process.
|
||||||
|
Healthy() bool
|
||||||
|
|
||||||
|
// Run executes a provided task while managing the state of the [Process].
|
||||||
|
//
|
||||||
|
// Run manages the request queue and may restart the process if it is not
|
||||||
|
// healthy or if the number of handled requests exceeds the maximum limit.
|
||||||
|
//
|
||||||
|
// It returns an error if the task cannot be run or if the process state
|
||||||
|
// cannot be managed properly.
|
||||||
|
Run(ctx context.Context, logger *zap.Logger, task func() error) error
|
||||||
|
|
||||||
|
// ReqQueueSize returns the current size of the request queue.
|
||||||
|
ReqQueueSize() int64
|
||||||
|
|
||||||
|
// RestartsCount returns the current number of restart.
|
||||||
|
RestartsCount() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type processSupervisor struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
process Process
|
||||||
|
maxReqLimit int64
|
||||||
|
mutexChan chan struct{}
|
||||||
|
firstStart atomic.Bool
|
||||||
|
reqCounter atomic.Int64
|
||||||
|
reqQueueSize atomic.Int64
|
||||||
|
restartsCounter atomic.Int64
|
||||||
|
isRestarting atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProcessSupervisor initializes a new [ProcessSupervisor].
|
||||||
|
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit int64) ProcessSupervisor {
|
||||||
|
b := &processSupervisor{
|
||||||
|
logger: logger,
|
||||||
|
process: process,
|
||||||
|
mutexChan: make(chan struct{}, 1),
|
||||||
|
maxReqLimit: maxReqLimit,
|
||||||
|
}
|
||||||
|
b.reqCounter.Store(0)
|
||||||
|
b.reqQueueSize.Store(0)
|
||||||
|
b.restartsCounter.Store(0)
|
||||||
|
b.isRestarting.Store(false)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) Launch() error {
|
||||||
|
s.logger.Debug("start process")
|
||||||
|
err := s.process.Start(s.logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.firstStart.Store(true)
|
||||||
|
s.logger.Debug("process successfully started")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) Shutdown() error {
|
||||||
|
s.logger.Debug("shutdown process")
|
||||||
|
err := s.process.Stop(s.logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutdown process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Debug("process successfully shutdown")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) restart() error {
|
||||||
|
if s.isRestarting.Load() {
|
||||||
|
s.logger.Debug("process already restarting, skip restart")
|
||||||
|
|
||||||
|
return ErrProcessAlreadyRestarting
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Debug("restart process")
|
||||||
|
s.isRestarting.Store(true)
|
||||||
|
defer s.isRestarting.Store(false)
|
||||||
|
|
||||||
|
err := s.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
// No big deal? Chances are it's already stopped.
|
||||||
|
s.logger.Debug(fmt.Sprintf("stop process before restart: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Launch()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("restart process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.reqCounter.Store(0)
|
||||||
|
s.restartsCounter.Add(1)
|
||||||
|
s.logger.Debug("process successfully restarted")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) Healthy() bool {
|
||||||
|
if !s.firstStart.Load() {
|
||||||
|
// A non-started process is always healthy.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.isRestarting.Load() {
|
||||||
|
// A restarting process is always healthy.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.process.Healthy(s.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
|
s.reqQueueSize.Add(1)
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := func() error {
|
||||||
|
select {
|
||||||
|
case s.mutexChan <- struct{}{}:
|
||||||
|
logger.Debug("process lock acquired")
|
||||||
|
s.reqQueueSize.Add(-1)
|
||||||
|
s.reqCounter.Add(1)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
logger.Debug("process lock released")
|
||||||
|
<-s.mutexChan
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !s.firstStart.Load() {
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.Launch()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process first start: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.Healthy() {
|
||||||
|
s.logger.Debug("process is unhealthy, cannot handle task, restarting...")
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.restart()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process restart before task: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.maxReqLimit > 0 && s.reqCounter.Load() >= s.maxReqLimit {
|
||||||
|
s.logger.Debug("max request limit reached, restarting...")
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.restart()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process restart before task: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: no error wrapping because it leaks on Chromium console exceptions output.
|
||||||
|
return s.runWithDeadline(ctx, task)
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Debug("failed to acquire process lock before deadline")
|
||||||
|
s.reqQueueSize.Add(-1)
|
||||||
|
|
||||||
|
return fmt.Errorf("acquire process lock: %w", ctx.Err())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if errors.Is(err, ErrProcessAlreadyRestarting) {
|
||||||
|
logger.Debug("process is already restarting, trying to acquire process lock again...")
|
||||||
|
s.reqQueueSize.Add(1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: no error wrapping because it leaks on Chromium console exceptions output.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) runWithDeadline(ctx context.Context, task func() error) error {
|
||||||
|
runChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
runChan <- task()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-runChan:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) ReqQueueSize() int64 {
|
||||||
|
return s.reqQueueSize.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *processSupervisor) RestartsCount() int64 {
|
||||||
|
return s.restartsCounter.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ ProcessSupervisor = (*processSupervisor)(nil)
|
||||||
|
)
|
||||||
602
pkg/gotenberg/supervisor_test.go
Normal file
602
pkg/gotenberg/supervisor_test.go
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessSupervisor_Launch(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
startError error
|
||||||
|
expectError bool
|
||||||
|
firstStartSet bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "successful launch",
|
||||||
|
startError: nil,
|
||||||
|
expectError: false,
|
||||||
|
firstStartSet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "failed launch",
|
||||||
|
startError: errors.New("start error"),
|
||||||
|
expectError: true,
|
||||||
|
firstStartSet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "process already started",
|
||||||
|
startError: nil,
|
||||||
|
expectError: false,
|
||||||
|
firstStartSet: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
process := &ProcessMock{
|
||||||
|
StartMock: func(logger *zap.Logger) error {
|
||||||
|
return tc.startError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
||||||
|
if tc.firstStartSet {
|
||||||
|
ps.firstStart.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ps.Launch()
|
||||||
|
|
||||||
|
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.firstStartSet && !ps.firstStart.Load() {
|
||||||
|
t.Error("expected firstStart to be set but it was not")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessSupervisor_Shutdown(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
stopError error
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "successful shutdown",
|
||||||
|
stopError: nil,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "failed shutdown",
|
||||||
|
stopError: errors.New("stop error"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
process := &ProcessMock{
|
||||||
|
StopMock: func(logger *zap.Logger) error {
|
||||||
|
return tc.stopError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewProcessSupervisor(logger, process, 5)
|
||||||
|
err := ps.Shutdown()
|
||||||
|
|
||||||
|
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 TestProcessSupervisor_restart(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
initiallyRestarting bool
|
||||||
|
startError error
|
||||||
|
stopError error
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "already restarting",
|
||||||
|
initiallyRestarting: true,
|
||||||
|
expectError: true,
|
||||||
|
expectedError: ErrProcessAlreadyRestarting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "successful restart",
|
||||||
|
startError: nil,
|
||||||
|
stopError: nil,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "failed to stop during restart",
|
||||||
|
startError: nil,
|
||||||
|
stopError: errors.New("stop error"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "failed to start during restart",
|
||||||
|
startError: errors.New("start error"),
|
||||||
|
stopError: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
process := &ProcessMock{
|
||||||
|
StartMock: func(logger *zap.Logger) error {
|
||||||
|
return tc.startError
|
||||||
|
},
|
||||||
|
StopMock: func(logger *zap.Logger) error {
|
||||||
|
return tc.stopError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
||||||
|
if tc.initiallyRestarting {
|
||||||
|
ps.isRestarting.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ps.restart()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessSupervisor_Healthy(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
initiallyStarted bool
|
||||||
|
initiallyRestarting bool
|
||||||
|
processHealthy bool
|
||||||
|
expectHealthy bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "non-started process is always healthy",
|
||||||
|
initiallyStarted: false,
|
||||||
|
expectHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "restarting process is always healthy",
|
||||||
|
initiallyStarted: true,
|
||||||
|
initiallyRestarting: true,
|
||||||
|
expectHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "process reports as healthy",
|
||||||
|
initiallyStarted: true,
|
||||||
|
processHealthy: true,
|
||||||
|
expectHealthy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "process reports as unhealthy",
|
||||||
|
initiallyStarted: true,
|
||||||
|
processHealthy: false,
|
||||||
|
expectHealthy: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
process := &ProcessMock{
|
||||||
|
HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return tc.processHealthy
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewProcessSupervisor(logger, process, 5).(*processSupervisor)
|
||||||
|
if tc.initiallyStarted {
|
||||||
|
ps.firstStart.Store(true)
|
||||||
|
}
|
||||||
|
if tc.initiallyRestarting {
|
||||||
|
ps.isRestarting.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
healthy := ps.Healthy()
|
||||||
|
|
||||||
|
if healthy != tc.expectHealthy {
|
||||||
|
t.Fatalf("expected healthy to be %v but got %v", tc.expectHealthy, healthy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessSupervisor_Run(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
initiallyStarted bool
|
||||||
|
isRestarting bool
|
||||||
|
startError error
|
||||||
|
processHealthy bool
|
||||||
|
maxReqLimit int64
|
||||||
|
tasksToRun int
|
||||||
|
taskError error
|
||||||
|
expectError bool
|
||||||
|
skipCallsCheck bool
|
||||||
|
expectedStartCalls int64
|
||||||
|
expectedHealthyCalls int64
|
||||||
|
expectedStopCalls int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "successfully run task on non-started process",
|
||||||
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: true,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: false,
|
||||||
|
expectedStartCalls: 1,
|
||||||
|
expectedHealthyCalls: 1,
|
||||||
|
expectedStopCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot launch non-started process",
|
||||||
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
|
startError: errors.New("launch error"),
|
||||||
|
processHealthy: true,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: true,
|
||||||
|
expectedStartCalls: 1,
|
||||||
|
expectedHealthyCalls: 0,
|
||||||
|
expectedStopCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "run task with unhealthy process causing restart",
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: false,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: false,
|
||||||
|
expectedStartCalls: 1,
|
||||||
|
expectedHealthyCalls: 1,
|
||||||
|
expectedStopCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot restart unhealthy process",
|
||||||
|
startError: errors.New("start error"),
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: false,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: true,
|
||||||
|
expectedStartCalls: 1,
|
||||||
|
expectedHealthyCalls: 1,
|
||||||
|
expectedStopCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrProcessAlreadyRestarting",
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: true,
|
||||||
|
processHealthy: false,
|
||||||
|
maxReqLimit: 1,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: true,
|
||||||
|
skipCallsCheck: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "run tasks reaching max request limit causing restart",
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: true,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 3,
|
||||||
|
expectError: false,
|
||||||
|
expectedStartCalls: 1,
|
||||||
|
expectedHealthyCalls: 3,
|
||||||
|
expectedStopCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot restart after reaching max request limit",
|
||||||
|
startError: errors.New("start error"),
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: true,
|
||||||
|
maxReqLimit: 2,
|
||||||
|
tasksToRun: 2,
|
||||||
|
expectError: true,
|
||||||
|
expectedStartCalls: 1,
|
||||||
|
expectedHealthyCalls: 2,
|
||||||
|
expectedStopCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "task error",
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
|
processHealthy: true,
|
||||||
|
maxReqLimit: 0,
|
||||||
|
tasksToRun: 1,
|
||||||
|
taskError: errors.New("task error"),
|
||||||
|
expectError: true,
|
||||||
|
expectedStartCalls: 0,
|
||||||
|
expectedHealthyCalls: 1,
|
||||||
|
expectedStopCalls: 0,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
var startCalls, healthyCalls, stopCalls atomic.Int64
|
||||||
|
startCalls.Store(0)
|
||||||
|
healthyCalls.Store(0)
|
||||||
|
stopCalls.Store(0)
|
||||||
|
|
||||||
|
process := &ProcessMock{
|
||||||
|
StartMock: func(logger *zap.Logger) error {
|
||||||
|
startCalls.Add(1)
|
||||||
|
return tc.startError
|
||||||
|
},
|
||||||
|
StopMock: func(logger *zap.Logger) error {
|
||||||
|
stopCalls.Add(1)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
healthyCalls.Add(1)
|
||||||
|
return tc.processHealthy
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit).(*processSupervisor)
|
||||||
|
if tc.initiallyStarted {
|
||||||
|
ps.firstStart.Store(true)
|
||||||
|
}
|
||||||
|
if tc.isRestarting {
|
||||||
|
ps.isRestarting.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
task := func() error {
|
||||||
|
return tc.taskError
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errorChan := make(chan error, tc.tasksToRun)
|
||||||
|
|
||||||
|
for i := 0; i < tc.tasksToRun; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := ps.Run(ctx, logger, task)
|
||||||
|
if err != nil {
|
||||||
|
errorChan <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errorChan)
|
||||||
|
|
||||||
|
for err := range errorChan {
|
||||||
|
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.skipCallsCheck {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if startCalls.Load() != tc.expectedStartCalls {
|
||||||
|
t.Errorf("expected %d process.Start calls, got %d", tc.expectedStartCalls, startCalls.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
if healthyCalls.Load() != tc.expectedHealthyCalls {
|
||||||
|
t.Errorf("expected %d process.Healthy calls, got %d", tc.expectedHealthyCalls, healthyCalls.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopCalls.Load() != tc.expectedStopCalls {
|
||||||
|
t.Errorf("expected %d process.Stop calls, got %d", tc.expectedStopCalls, stopCalls.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessSupervisor_runWithDeadline(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctxDone bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "task finished",
|
||||||
|
ctxDone: false,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "context expired",
|
||||||
|
ctxDone: true,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0).(*processSupervisor)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if tc.ctxDone {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ps.runWithDeadline(ctx, func() error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
process := &ProcessMock{
|
||||||
|
StartMock: func(logger *zap.Logger) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
|
||||||
|
|
||||||
|
// Simulating a lock.
|
||||||
|
ps.mutexChan <- struct{}{}
|
||||||
|
|
||||||
|
if ps.ReqQueueSize() != 0 {
|
||||||
|
t.Fatalf("expected queue size to be 0 but got %d", ps.ReqQueueSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errorChan := make(chan error, 10)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := ps.Run(ctx, logger, func() error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
errorChan <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to wait a little bit so that the request queue size may change.
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
if ps.ReqQueueSize() != 10 {
|
||||||
|
t.Fatalf("expected queue size to be 10 but got %d", ps.ReqQueueSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errorChan)
|
||||||
|
|
||||||
|
for err := range errorChan {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected a lock error but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ps.ReqQueueSize() != 0 {
|
||||||
|
t.Errorf("expected queue size to be 0 but got %d", ps.ReqQueueSize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessSupervisor_RestartsCount(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
initialRestartsCount int64
|
||||||
|
restartAttempts int
|
||||||
|
startError error
|
||||||
|
stopError error
|
||||||
|
expectedRestartsCount int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no restarts, counter remains 0",
|
||||||
|
initialRestartsCount: 0,
|
||||||
|
restartAttempts: 0,
|
||||||
|
expectedRestartsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "successful restart increases counter",
|
||||||
|
initialRestartsCount: 0,
|
||||||
|
restartAttempts: 1,
|
||||||
|
startError: nil,
|
||||||
|
stopError: nil,
|
||||||
|
expectedRestartsCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "failed to stop during restart, no impact",
|
||||||
|
initialRestartsCount: 0,
|
||||||
|
restartAttempts: 1,
|
||||||
|
startError: nil,
|
||||||
|
stopError: errors.New("stop error"),
|
||||||
|
expectedRestartsCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "multiple successful restarts",
|
||||||
|
initialRestartsCount: 0,
|
||||||
|
restartAttempts: 3,
|
||||||
|
startError: nil,
|
||||||
|
stopError: nil,
|
||||||
|
expectedRestartsCount: 3,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
process := &ProcessMock{
|
||||||
|
StartMock: func(logger *zap.Logger) error {
|
||||||
|
return tc.startError
|
||||||
|
},
|
||||||
|
StopMock: func(logger *zap.Logger) error {
|
||||||
|
return tc.stopError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewProcessSupervisor(logger, process, 0).(*processSupervisor)
|
||||||
|
ps.restartsCounter.Store(tc.initialRestartsCount)
|
||||||
|
|
||||||
|
for i := 0; i < tc.restartAttempts; i++ {
|
||||||
|
_ = ps.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
actualRestartsCount := ps.RestartsCount()
|
||||||
|
if actualRestartsCount != tc.expectedRestartsCount {
|
||||||
|
t.Fatalf("expected restarts count to be %d, but got %d", tc.expectedRestartsCount, actualRestartsCount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,26 +12,26 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alexliesenfeld/health"
|
"github.com/alexliesenfeld/health"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gotenberg.MustRegisterModule(API{})
|
gotenberg.MustRegisterModule(new(Api))
|
||||||
}
|
}
|
||||||
|
|
||||||
// API is a module which provides an HTTP server. Other modules may add routes,
|
// Api is a module which provides an HTTP server. Other modules may add routes,
|
||||||
// middlewares or health checks.
|
// middlewares or health checks.
|
||||||
type API struct {
|
type Api struct {
|
||||||
port int
|
port int
|
||||||
readTimeout time.Duration
|
startTimeout time.Duration
|
||||||
processTimeout time.Duration
|
timeout time.Duration
|
||||||
writeTimeout time.Duration
|
|
||||||
rootPath string
|
rootPath string
|
||||||
traceHeader string
|
traceHeader string
|
||||||
disableHealthCheckLogging bool
|
disableHealthCheckLogging bool
|
||||||
@@ -39,17 +39,18 @@ type API struct {
|
|||||||
routes []Route
|
routes []Route
|
||||||
externalMiddlewares []Middleware
|
externalMiddlewares []Middleware
|
||||||
healthChecks []health.CheckerOption
|
healthChecks []health.CheckerOption
|
||||||
gcGraceDuration time.Duration
|
readyFn []func() error
|
||||||
|
fs *gotenberg.FileSystem
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
srv *echo.Echo
|
srv *echo.Echo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router is a module interface which adds routes to the API.
|
// Router is a module interface which adds routes to the [Api].
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Routes() ([]Route, error)
|
Routes() ([]Route, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route represents a route from a Router.
|
// Route represents a route from a [Router].
|
||||||
type Route struct {
|
type Route struct {
|
||||||
// Method is the HTTP method of the route (i.e., GET, POST, etc.).
|
// Method is the HTTP method of the route (i.e., GET, POST, etc.).
|
||||||
// Required.
|
// Required.
|
||||||
@@ -72,13 +73,13 @@ type Route struct {
|
|||||||
Handler echo.HandlerFunc
|
Handler echo.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// MiddlewareProvider is a module interface which adds middlewares to the API.
|
// MiddlewareProvider is a module interface which adds middlewares to the [Api].
|
||||||
type MiddlewareProvider interface {
|
type MiddlewareProvider interface {
|
||||||
Middlewares() ([]Middleware, error)
|
Middlewares() ([]Middleware, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MiddlewareStack is a type which helps to determine in which stack the
|
// MiddlewareStack is a type which helps to determine in which stack the
|
||||||
// middlewares provided by the MiddlewareProvider modules should be located.
|
// middlewares provided by the [MiddlewareProvider] modules should be located.
|
||||||
type MiddlewareStack uint32
|
type MiddlewareStack uint32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -88,7 +89,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MiddlewarePriority is a type which helps to determine the execution order of
|
// MiddlewarePriority is a type which helps to determine the execution order of
|
||||||
// middlewares provided by the MiddlewareProvider modules in a stack.
|
// middlewares provided by the [MiddlewareProvider] modules in a stack.
|
||||||
type MiddlewarePriority uint32
|
type MiddlewarePriority uint32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -99,40 +100,40 @@ const (
|
|||||||
VeryHighPriority
|
VeryHighPriority
|
||||||
)
|
)
|
||||||
|
|
||||||
// Middleware is a middleware which can be added to the API's middlewares
|
// Middleware is a middleware which can be added to the [Api]'s middlewares
|
||||||
// chain.
|
// chain.
|
||||||
//
|
//
|
||||||
// middleware := Middleware{
|
// middleware := Middleware{
|
||||||
// Handler: func() echo.MiddlewareFunc {
|
// Handler: func() echo.MiddlewareFunc {
|
||||||
// return func(next echo.HandlerFunc) echo.HandlerFunc {
|
// return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
// return func(c echo.Context) error {
|
// return func(c echo.Context) error {
|
||||||
// rootPath := c.Get("rootPath").(string)
|
// rootPath := c.Get("rootPath").(string)
|
||||||
// healthURI := fmt.Sprintf("%shealth", rootPath)
|
// healthURI := fmt.Sprintf("%shealth", rootPath)
|
||||||
//
|
//
|
||||||
// // Skip the middleware if health check URI.
|
// // Skip the middleware if health check URI.
|
||||||
// if c.Request().RequestURI == healthURI {
|
// if c.Request().RequestURI == healthURI {
|
||||||
// // Call the next middleware in the chain.
|
// // Call the next middleware in the chain.
|
||||||
// return next(c)
|
// return next(c)
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// // Your middleware process.
|
// // Your middleware process.
|
||||||
// // ...
|
// // ...
|
||||||
//
|
//
|
||||||
// // Call the next middleware in the chain.
|
// // Call the next middleware in the chain.
|
||||||
// return next(c)
|
// return next(c)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }(),
|
// }(),
|
||||||
// }
|
// }
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
// Stack tells in which stack the middleware should be located.
|
// Stack tells in which stack the middleware should be located.
|
||||||
// Default to DefaultStack.
|
// Default to [DefaultStack].
|
||||||
// Optional.
|
// Optional.
|
||||||
Stack MiddlewareStack
|
Stack MiddlewareStack
|
||||||
|
|
||||||
// Priority tells if the middleware should be positioned high or not in
|
// Priority tells if the middleware should be positioned high or not in
|
||||||
// its stack.
|
// its stack.
|
||||||
// Default to VeryLowPriority.
|
// Default to [VeryLowPriority].
|
||||||
// Optional.
|
// Optional.
|
||||||
Priority MiddlewarePriority
|
Priority MiddlewarePriority
|
||||||
|
|
||||||
@@ -147,42 +148,35 @@ type Middleware struct {
|
|||||||
// See https://github.com/alexliesenfeld/health for more details.
|
// See https://github.com/alexliesenfeld/health for more details.
|
||||||
type HealthChecker interface {
|
type HealthChecker interface {
|
||||||
Checks() ([]health.CheckerOption, error)
|
Checks() ([]health.CheckerOption, error)
|
||||||
|
Ready() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GarbageCollectorGraceDurationIncrementer is a module interface for
|
// Descriptor returns an [Api]'s module descriptor.
|
||||||
// increasing the grace duration provided by the API for the garbage collector.
|
func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
type GarbageCollectorGraceDurationIncrementer interface {
|
|
||||||
AddGraceDuration() time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Descriptor returns an API's module descriptor.
|
|
||||||
func (API) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "api",
|
ID: "api",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
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.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-start-timeout", time.Duration(30)*time.Second, "Set the time limit for the API to start")
|
||||||
fs.Duration("api-process-timeout", time.Duration(30)*time.Second, "Set the maximum duration allowed to process a request")
|
fs.Duration("api-timeout", time.Duration(30)*time.Second, "Set the time limit for requests")
|
||||||
fs.Duration("api-write-timeout", time.Duration(30)*time.Second, "Set the maximum duration before timing out writes of the response")
|
|
||||||
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
fs.String("api-root-path", "/", "Set the root path of the API - for service discovery via URL paths")
|
||||||
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
fs.String("api-trace-header", "Gotenberg-Trace", "Set the header name to use for identifying requests")
|
||||||
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
New: func() gotenberg.Module { return new(API) },
|
New: func() gotenberg.Module { return new(Api) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets the module properties.
|
// Provision sets the module properties.
|
||||||
func (a *API) Provision(ctx *gotenberg.Context) error {
|
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
a.port = flags.MustInt("api-port")
|
a.port = flags.MustInt("api-port")
|
||||||
a.readTimeout = flags.MustDuration("api-read-timeout")
|
a.startTimeout = flags.MustDuration("api-start-timeout")
|
||||||
a.processTimeout = flags.MustDuration("api-process-timeout")
|
a.timeout = flags.MustDuration("api-timeout")
|
||||||
a.writeTimeout = flags.MustDuration("api-write-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")
|
||||||
@@ -271,20 +265,10 @@ func (a *API) Provision(ctx *gotenberg.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.healthChecks = append(a.healthChecks, checks...)
|
a.healthChecks = append(a.healthChecks, checks...)
|
||||||
|
a.readyFn = append(a.readyFn, healthChecker.Ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grace duration.
|
// Logger.
|
||||||
a.gcGraceDuration = a.readTimeout + a.processTimeout + a.writeTimeout
|
|
||||||
|
|
||||||
mods, err = ctx.Modules(new(GarbageCollectorGraceDurationIncrementer))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get garbage collector grace duration increments: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, incrementer := range mods {
|
|
||||||
a.gcGraceDuration += incrementer.(GarbageCollectorGraceDurationIncrementer).AddGraceDuration()
|
|
||||||
}
|
|
||||||
|
|
||||||
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get logger provider: %w", err)
|
return fmt.Errorf("get logger provider: %w", err)
|
||||||
@@ -297,11 +281,14 @@ func (a *API) Provision(ctx *gotenberg.Context) error {
|
|||||||
|
|
||||||
a.logger = logger
|
a.logger = logger
|
||||||
|
|
||||||
|
// File system.
|
||||||
|
a.fs = gotenberg.NewFileSystem()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the module properties.
|
// Validate validates the module properties.
|
||||||
func (a API) Validate() error {
|
func (a *Api) Validate() error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if a.port < 1 || a.port > 65535 {
|
if a.port < 1 || a.port > 65535 {
|
||||||
@@ -373,12 +360,14 @@ func (a API) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the HTTP server.
|
// Start starts the HTTP server.
|
||||||
func (a *API) Start() error {
|
func (a *Api) Start() error {
|
||||||
a.srv = echo.New()
|
a.srv = echo.New()
|
||||||
a.srv.HideBanner = true
|
a.srv.HideBanner = true
|
||||||
a.srv.HidePort = true
|
a.srv.HidePort = true
|
||||||
a.srv.Server.ReadTimeout = a.readTimeout
|
a.srv.Server.ReadTimeout = a.timeout
|
||||||
a.srv.Server.WriteTimeout = a.writeTimeout
|
a.srv.Server.IdleTimeout = a.timeout
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/396.
|
||||||
|
a.srv.Server.WriteTimeout = a.timeout + a.timeout
|
||||||
a.srv.HTTPErrorHandler = httpErrorHandler()
|
a.srv.HTTPErrorHandler = httpErrorHandler()
|
||||||
|
|
||||||
// Let's prepare the modules' routes.
|
// Let's prepare the modules' routes.
|
||||||
@@ -402,7 +391,6 @@ func (a *API) Start() error {
|
|||||||
latencyMiddleware(),
|
latencyMiddleware(),
|
||||||
rootPathMiddleware(a.rootPath),
|
rootPathMiddleware(a.rootPath),
|
||||||
traceMiddleware(a.traceHeader),
|
traceMiddleware(a.traceHeader),
|
||||||
timeoutsMiddleware(a.readTimeout, a.processTimeout, a.writeTimeout),
|
|
||||||
loggerMiddleware(a.logger, disableLoggingForPaths),
|
loggerMiddleware(a.logger, disableLoggingForPaths),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -419,14 +407,14 @@ func (a *API) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hardTimeout := a.processTimeout + (time.Duration(5) * time.Second)
|
hardTimeout := a.timeout + (time.Duration(5) * time.Second)
|
||||||
|
|
||||||
// Add the modules' routes and their specific middlewares.
|
// Add the modules' routes and their specific middlewares.
|
||||||
for _, route := range a.routes {
|
for _, route := range a.routes {
|
||||||
var middlewares []echo.MiddlewareFunc
|
var middlewares []echo.MiddlewareFunc
|
||||||
|
|
||||||
if route.IsMultipart {
|
if route.IsMultipart {
|
||||||
middlewares = append(middlewares, contextMiddleware(a.processTimeout))
|
middlewares = append(middlewares, contextMiddleware(a.fs, a.timeout))
|
||||||
|
|
||||||
for _, externalMultipartMiddleware := range externalMultipartMiddlewares {
|
for _, externalMultipartMiddleware := range externalMultipartMiddlewares {
|
||||||
middlewares = append(middlewares, externalMultipartMiddleware.Handler)
|
middlewares = append(middlewares, externalMultipartMiddleware.Handler)
|
||||||
@@ -447,14 +435,27 @@ func (a *API) Start() error {
|
|||||||
a.srv.GET(
|
a.srv.GET(
|
||||||
fmt.Sprintf("%s%s", a.rootPath, "health"),
|
fmt.Sprintf("%s%s", a.rootPath, "health"),
|
||||||
func() echo.HandlerFunc {
|
func() echo.HandlerFunc {
|
||||||
checks := append(a.healthChecks, health.WithTimeout(a.processTimeout))
|
checks := append(a.healthChecks, health.WithTimeout(a.timeout))
|
||||||
checker := health.NewChecker(checks...)
|
checker := health.NewChecker(checks...)
|
||||||
|
|
||||||
return echo.WrapHandler(health.NewHandler(checker))
|
return echo.WrapHandler(health.NewHandler(checker))
|
||||||
}(),
|
}(),
|
||||||
hardTimeoutMiddleware(hardTimeout),
|
hardTimeoutMiddleware(hardTimeout),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for all modules to be ready.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), a.startTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
for _, f := range a.readyFn {
|
||||||
|
eg.Go(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("waiting for modules readiness: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 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() {
|
||||||
server := &http2.Server{}
|
server := &http2.Server{}
|
||||||
@@ -468,26 +469,19 @@ func (a *API) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartupMessage returns a custom startup message.
|
// StartupMessage returns a custom startup message.
|
||||||
func (a API) StartupMessage() string {
|
func (a *Api) StartupMessage() string {
|
||||||
return fmt.Sprintf("server listening on port %d", a.port)
|
return fmt.Sprintf("server listening on port %d", a.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the HTTP server.
|
// Stop stops the HTTP server.
|
||||||
func (a API) Stop(ctx context.Context) error {
|
func (a *Api) Stop(ctx context.Context) error {
|
||||||
return a.srv.Shutdown(ctx)
|
return a.srv.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraceDuration updates the expiration time of files and directories parsed by
|
|
||||||
// the gc.GarbageCollector.
|
|
||||||
func (a API) GraceDuration() time.Duration {
|
|
||||||
return a.gcGraceDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*API)(nil)
|
_ gotenberg.Module = (*Api)(nil)
|
||||||
_ gotenberg.Provisioner = (*API)(nil)
|
_ gotenberg.Provisioner = (*Api)(nil)
|
||||||
_ gotenberg.Validator = (*API)(nil)
|
_ gotenberg.Validator = (*Api)(nil)
|
||||||
_ gotenberg.App = (*API)(nil)
|
_ gotenberg.App = (*Api)(nil)
|
||||||
_ gc.GarbageCollectorGraceDurationModifier = (*API)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,16 +12,14 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/mholt/archiver/v3"
|
"github.com/mholt/archiver/v3"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/text/runes"
|
|
||||||
"golang.org/x/text/transform"
|
|
||||||
"golang.org/x/text/unicode/norm"
|
"golang.org/x/text/unicode/norm"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -48,8 +46,8 @@ type Context struct {
|
|||||||
context.Context
|
context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, 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)
|
||||||
|
|
||||||
ctx := &Context{
|
ctx := &Context{
|
||||||
@@ -81,7 +79,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.logger.Debug(fmt.Sprintf("'%s' removed", ctx.dirPath))
|
ctx.logger.Debug(fmt.Sprintf("'%s' context's working directory removed", ctx.dirPath))
|
||||||
ctx.cancelled = true
|
ctx.cancelled = true
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -92,28 +90,28 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
|
|||||||
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),
|
||||||
NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: want 'multipart/form-data'"),
|
NewSentinelHttpError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: want 'multipart/form-data'"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, http.ErrMissingBoundary) {
|
if errors.Is(err, http.ErrMissingBoundary) {
|
||||||
return nil, cancel, WrapError(
|
return nil, cancel, WrapError(
|
||||||
fmt.Errorf("get multipart form: %w", err),
|
fmt.Errorf("get multipart form: %w", err),
|
||||||
NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: no boundary"),
|
NewSentinelHttpError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: no boundary"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(err.Error(), io.EOF.Error()) {
|
if strings.Contains(err.Error(), io.EOF.Error()) {
|
||||||
return nil, cancel, WrapError(
|
return nil, cancel, WrapError(
|
||||||
fmt.Errorf("get multipart form: %w", err),
|
fmt.Errorf("get multipart form: %w", err),
|
||||||
NewSentinelHTTPError(http.StatusBadRequest, "Malformed body: it does not match the 'Content-Type' header boundaries"),
|
NewSentinelHttpError(http.StatusBadRequest, "Malformed body: it does not match the 'Content-Type' header boundaries"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, cancel, fmt.Errorf("get multipart form: %w", err)
|
return nil, cancel, fmt.Errorf("get multipart form: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dirPath, err := gotenberg.MkdirAll()
|
dirPath, err := fs.MkdirAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cancel, fmt.Errorf("create working directory: %w", err)
|
return nil, cancel, fmt.Errorf("create working directory: %w", err)
|
||||||
}
|
}
|
||||||
@@ -123,15 +121,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
|
|||||||
ctx.files = make(map[string]string)
|
ctx.files = make(map[string]string)
|
||||||
|
|
||||||
copyToDisk := func(fh *multipart.FileHeader) error {
|
copyToDisk := func(fh *multipart.FileHeader) error {
|
||||||
// Avoid directory traversal and normalize filename.
|
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/104.
|
|
||||||
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
|
||||||
|
|
||||||
filename, _, err := transform.String(t, filepath.Base(fh.Filename))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("transform filename: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
in, err := fh.Open()
|
in, err := fh.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open multipart file: %w", err)
|
return fmt.Errorf("open multipart file: %w", err)
|
||||||
@@ -144,6 +133,10 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Avoid directory traversal and make sure filename characters are
|
||||||
|
// normalized.
|
||||||
|
// See: https://github.com/gotenberg/gotenberg/issues/662.
|
||||||
|
filename := norm.NFC.String(filepath.Base(fh.Filename))
|
||||||
path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
|
path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
|
||||||
|
|
||||||
out, err := os.Create(path)
|
out, err := os.Create(path)
|
||||||
@@ -178,19 +171,19 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Log().Debug(fmt.Sprintf("form data values: %+v", ctx.values))
|
ctx.Log().Debug(fmt.Sprintf("form fields: %+v", ctx.values))
|
||||||
ctx.Log().Debug(fmt.Sprintf("form data files: %+v", ctx.files))
|
ctx.Log().Debug(fmt.Sprintf("form files: %+v", ctx.files))
|
||||||
|
|
||||||
return ctx, cancel, err
|
return ctx, cancel, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request returns the http.Request.
|
// Request returns the [http.Request].
|
||||||
func (ctx Context) Request() *http.Request {
|
func (ctx *Context) Request() *http.Request {
|
||||||
return ctx.echoCtx.Request()
|
return ctx.echoCtx.Request()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormData return a FormData.
|
// FormData return a [FormData].
|
||||||
func (ctx Context) FormData() *FormData {
|
func (ctx *Context) FormData() *FormData {
|
||||||
return &FormData{
|
return &FormData{
|
||||||
values: ctx.values,
|
values: ctx.values,
|
||||||
files: ctx.files,
|
files: ctx.files,
|
||||||
@@ -200,7 +193,7 @@ func (ctx Context) FormData() *FormData {
|
|||||||
|
|
||||||
// GeneratePath generates a path within the context's working directory. It
|
// GeneratePath generates a path within the context's working directory. 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(), extension)
|
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,14 +215,14 @@ func (ctx *Context) AddOutputPaths(paths ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log returns the context zap.Logger.
|
// Log returns the context [zap.Logger].
|
||||||
func (ctx Context) Log() *zap.Logger {
|
func (ctx *Context) Log() *zap.Logger {
|
||||||
return ctx.logger
|
return ctx.logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildOutputFile builds the output file according to the output paths
|
// BuildOutputFile builds the output file according to the output paths
|
||||||
// registered in the context. If many output paths, an archive is created.
|
// registered in the context. If many output paths, an archive is created.
|
||||||
func (ctx Context) BuildOutputFile() (string, error) {
|
func (ctx *Context) BuildOutputFile() (string, error) {
|
||||||
if ctx.cancelled {
|
if ctx.cancelled {
|
||||||
return "", ErrContextAlreadyClosed
|
return "", ErrContextAlreadyClosed
|
||||||
}
|
}
|
||||||
@@ -267,7 +260,7 @@ func (ctx Context) BuildOutputFile() (string, error) {
|
|||||||
|
|
||||||
// OutputFilename returns the filename based on the given output path or the
|
// OutputFilename returns the filename based on the given output path or the
|
||||||
// "Gotenberg-Output-Filename" header's value.
|
// "Gotenberg-Output-Filename" header's value.
|
||||||
func (ctx Context) OutputFilename(outputPath string) string {
|
func (ctx *Context) OutputFilename(outputPath string) string {
|
||||||
filename := ctx.echoCtx.Request().Header.Get("Gotenberg-Output-Filename")
|
filename := ctx.echoCtx.Request().Header.Get("Gotenberg-Output-Filename")
|
||||||
|
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
@@ -276,71 +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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockContext is a helper for tests.
|
|
||||||
//
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
type MockContext struct {
|
|
||||||
*Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDirPath sets the context's working directory path.
|
|
||||||
//
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
// ctx.SetDirPath("/foo")
|
|
||||||
func (ctx *MockContext) SetDirPath(path string) {
|
|
||||||
ctx.dirPath = path
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetValues sets the values.
|
|
||||||
//
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
// ctx.SetValues(map[string][]string{
|
|
||||||
// "url": {
|
|
||||||
// "foo",
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
func (ctx *MockContext) SetValues(values map[string][]string) {
|
|
||||||
ctx.values = values
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFiles sets the files.
|
|
||||||
//
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
// ctx.SetFiles(map[string]string{
|
|
||||||
// "foo": "/foo",
|
|
||||||
// })
|
|
||||||
func (ctx *MockContext) SetFiles(files map[string]string) {
|
|
||||||
ctx.files = files
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCancelled sets if the context is cancelled or not.
|
|
||||||
//
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
// ctx.SetCancelled(true)
|
|
||||||
func (ctx *MockContext) SetCancelled(cancelled bool) {
|
|
||||||
ctx.cancelled = cancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutputPaths returns the registered output paths.
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
// outputPaths := ctx.OutputPaths()
|
|
||||||
func (ctx MockContext) OutputPaths() []string {
|
|
||||||
return ctx.outputPaths
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLogger sets the logger.
|
|
||||||
//
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
// ctx.SetLogger(zap.NewNop())
|
|
||||||
func (ctx *MockContext) SetLogger(logger *zap.Logger) {
|
|
||||||
ctx.logger = logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEchoContext sets the echo.Context.
|
|
||||||
//
|
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
// ctx.setEchoContext(c)
|
|
||||||
func (ctx *MockContext) SetEchoContext(c echo.Context) {
|
|
||||||
ctx.Context.echoCtx = c
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,144 +12,141 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewContext(t *testing.T) {
|
func TestNewContext(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
request *http.Request
|
request *http.Request
|
||||||
expectErr bool
|
expectError bool
|
||||||
expectHTTPErr bool
|
expectHttpError bool
|
||||||
expectHTTPStatus int
|
expectHttpStatus int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "http.ErrNotMultipart",
|
||||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
expectHTTPErr: true,
|
expectHttpError: true,
|
||||||
expectHTTPStatus: http.StatusUnsupportedMediaType,
|
expectHttpStatus: http.StatusUnsupportedMediaType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "http.ErrMissingBoundary",
|
||||||
request: func() *http.Request {
|
request: func() *http.Request {
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||||
req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm)
|
req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm)
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
expectHTTPErr: true,
|
expectHttpError: true,
|
||||||
expectHTTPStatus: http.StatusUnsupportedMediaType,
|
expectHttpStatus: http.StatusUnsupportedMediaType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "malformed body",
|
||||||
request: func() *http.Request {
|
request: func() *http.Request {
|
||||||
body := &bytes.Buffer{}
|
body := &bytes.Buffer{}
|
||||||
|
|
||||||
writer := multipart.NewWriter(body)
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := writer.Close()
|
err := writer.Close()
|
||||||
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 := writer.WriteField("foo", "foo")
|
err := writer.WriteField("foo", "foo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
expectHTTPErr: true,
|
expectHttpError: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "success",
|
||||||
request: func() *http.Request {
|
request: func() *http.Request {
|
||||||
body := &bytes.Buffer{}
|
body := &bytes.Buffer{}
|
||||||
|
|
||||||
writer := multipart.NewWriter(body)
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := writer.Close()
|
err := writer.Close()
|
||||||
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 := writer.WriteField("foo", "foo")
|
err := writer.WriteField("foo", "foo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
|
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
|
||||||
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 = part.Write([]byte("foo"))
|
_, err = part.Write([]byte("foo"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
handler := func(c echo.Context) error {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
_, cancel, err := newContext(c, zap.NewNop(), time.Duration(10)*time.Second)
|
handler := func(c echo.Context) error {
|
||||||
defer cancel()
|
_, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(), time.Duration(10)*time.Second)
|
||||||
// Context already cancelled.
|
defer cancel()
|
||||||
defer cancel()
|
// Context already cancelled.
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
recorder := httptest.NewRecorder()
|
||||||
}
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
srv := echo.New()
|
||||||
|
srv.HideBanner = true
|
||||||
|
srv.HidePort = true
|
||||||
|
|
||||||
srv := echo.New()
|
c := srv.NewContext(tc.request, recorder)
|
||||||
srv.HideBanner = true
|
err := handler(c)
|
||||||
srv.HidePort = true
|
|
||||||
|
|
||||||
c := srv.NewContext(tc.request, recorder)
|
if tc.expectError && err == nil {
|
||||||
err := handler(c)
|
t.Fatal("expected error but got none", err)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpErr HTTPError
|
|
||||||
isHTTPErr := errors.As(err, &httpErr)
|
|
||||||
|
|
||||||
if tc.expectHTTPErr && !isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected HTTP error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectHTTPErr && isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
|
||||||
status, _ := httpErr.HTTPError()
|
|
||||||
if status != tc.expectHTTPStatus {
|
|
||||||
t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpErr 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +155,7 @@ func TestContext_Request(t *testing.T) {
|
|||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
c := echo.New().NewContext(request, recorder)
|
c := echo.New().NewContext(request, recorder)
|
||||||
|
|
||||||
ctx := Context{
|
ctx := &Context{
|
||||||
echoCtx: c,
|
echoCtx: c,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +165,7 @@ func TestContext_Request(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_FormData(t *testing.T) {
|
func TestContext_FormData(t *testing.T) {
|
||||||
ctx := Context{
|
ctx := &Context{
|
||||||
values: map[string][]string{
|
values: map[string][]string{
|
||||||
"foo": {"foo"},
|
"foo": {"foo"},
|
||||||
},
|
},
|
||||||
@@ -189,7 +186,7 @@ func TestContext_FormData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_GeneratePath(t *testing.T) {
|
func TestContext_GeneratePath(t *testing.T) {
|
||||||
ctx := Context{
|
ctx := &Context{
|
||||||
dirPath: "/foo",
|
dirPath: "/foo",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,40 +198,49 @@ func TestContext_GeneratePath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_AddOutputPaths(t *testing.T) {
|
func TestContext_AddOutputPaths(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
ctx *Context
|
ctx *Context
|
||||||
path string
|
path string
|
||||||
expectCount int
|
expectCount int
|
||||||
expectErr bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: &Context{cancelled: true},
|
scenario: "ErrContextAlreadyClosed",
|
||||||
expectErr: true,
|
ctx: &Context{cancelled: true},
|
||||||
|
expectCount: 0,
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: &Context{dirPath: "/foo"},
|
scenario: "ErrOutOfBoundsOutputPath",
|
||||||
path: "/bar/foo.txt",
|
ctx: &Context{dirPath: "/foo"},
|
||||||
expectErr: true,
|
path: "/bar/foo.txt",
|
||||||
|
expectCount: 0,
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "success",
|
||||||
ctx: &Context{dirPath: "/foo"},
|
ctx: &Context{dirPath: "/foo"},
|
||||||
path: "/foo/foo.txt",
|
path: "/foo/foo.txt",
|
||||||
expectCount: 1,
|
expectCount: 1,
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
err := tc.ctx.AddOutputPaths(tc.path)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.ctx.AddOutputPaths(tc.path)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tc.ctx.outputPaths) != tc.expectCount {
|
if len(tc.ctx.outputPaths) != tc.expectCount {
|
||||||
t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectCount, len(tc.ctx.outputPaths))
|
t.Errorf("expected %d output paths but got %d", tc.expectCount, len(tc.ctx.outputPaths))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,76 +255,91 @@ func TestContext_Log(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_BuildOutputFile(t *testing.T) {
|
func TestContext_BuildOutputFile(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
ctx *Context
|
scenario string
|
||||||
expectErr bool
|
ctx *Context
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: &Context{cancelled: true},
|
scenario: "ErrContextAlreadyClosed",
|
||||||
expectErr: true,
|
ctx: &Context{cancelled: true},
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: &Context{},
|
scenario: "no output path",
|
||||||
expectErr: true,
|
ctx: &Context{},
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: &Context{outputPaths: []string{"foo.txt"}},
|
scenario: "success: one output path",
|
||||||
|
ctx: &Context{outputPaths: []string{"foo.txt"}},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: &Context{outputPaths: []string{"foo.txt", "foo.pdf"}},
|
scenario: "cannot archive: invalid output paths",
|
||||||
expectErr: true,
|
ctx: &Context{outputPaths: []string{"foo.txt", "foo.pdf"}},
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "success: many output paths",
|
||||||
ctx: &Context{
|
ctx: &Context{
|
||||||
outputPaths: []string{
|
outputPaths: []string{
|
||||||
"/tests/test/testdata/api/sample1.txt",
|
"/tests/test/testdata/api/sample1.txt",
|
||||||
"/tests/test/testdata/api/sample1.txt",
|
"/tests/test/testdata/api/sample1.txt",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
dirPath, err := gotenberg.MkdirAll()
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
if err != nil {
|
fs := gotenberg.NewFileSystem()
|
||||||
t.Fatalf("%d: expected no erro but got: %v", i, err)
|
dirPath, err := fs.MkdirAll()
|
||||||
}
|
if err != nil {
|
||||||
|
t.Fatalf("expected no erro but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
tc.ctx.dirPath = dirPath
|
defer func() {
|
||||||
tc.ctx.logger = zap.NewNop()
|
err := os.RemoveAll(fs.WorkingDirPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, err = tc.ctx.BuildOutputFile()
|
tc.ctx.dirPath = dirPath
|
||||||
|
tc.ctx.logger = zap.NewNop()
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
_, err = tc.ctx.BuildOutputFile()
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.RemoveAll(dirPath)
|
if !tc.expectError && err != nil {
|
||||||
if err != nil {
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
t.Fatalf("%d: expected no erro but got: %v", i, err)
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContext_OutputFilename(t *testing.T) {
|
func TestContext_OutputFilename(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
ctx *Context
|
ctx *Context
|
||||||
outputPath string
|
outputPath string
|
||||||
expectOutputFilename string
|
expectOutputFilename string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "with Gotenberg-Output-Filename header",
|
||||||
ctx: func() *Context {
|
ctx: func() *Context {
|
||||||
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
|
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
|
||||||
c.Request().Header.Set("Gotenberg-Output-Filename", "foo")
|
c.Request().Header.Set("Gotenberg-Output-Filename", "foo")
|
||||||
|
|
||||||
return &Context{echoCtx: c}
|
return &Context{echoCtx: c}
|
||||||
}(),
|
}(),
|
||||||
outputPath: "/foo/bar.txt",
|
outputPath: "/foo/bar.txt",
|
||||||
expectOutputFilename: "foo.txt",
|
expectOutputFilename: "foo.txt",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "without custom filename",
|
||||||
ctx: func() *Context {
|
ctx: func() *Context {
|
||||||
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
|
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
|
||||||
return &Context{echoCtx: c}
|
return &Context{echoCtx: c}
|
||||||
@@ -327,106 +348,12 @@ func TestContext_OutputFilename(t *testing.T) {
|
|||||||
expectOutputFilename: "foo.txt",
|
expectOutputFilename: "foo.txt",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
actual := tc.ctx.OutputFilename(tc.outputPath)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
actual := tc.ctx.OutputFilename(tc.outputPath)
|
||||||
|
|
||||||
if actual != tc.expectOutputFilename {
|
if actual != tc.expectOutputFilename {
|
||||||
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectOutputFilename, actual)
|
t.Errorf("expected '%s' but got '%s'", tc.expectOutputFilename, actual)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestMockContext_SetDirPath(t *testing.T) {
|
|
||||||
mock := &MockContext{&Context{}}
|
|
||||||
mock.SetDirPath("/foo")
|
|
||||||
|
|
||||||
actual := mock.dirPath
|
|
||||||
expect := "/foo"
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMockContext_SetValues(t *testing.T) {
|
|
||||||
mock := &MockContext{&Context{}}
|
|
||||||
mock.SetValues(map[string][]string{
|
|
||||||
"foo": {"foo"},
|
|
||||||
})
|
|
||||||
|
|
||||||
actual := mock.values
|
|
||||||
expect := map[string][]string{
|
|
||||||
"foo": {"foo"},
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expect) {
|
|
||||||
t.Errorf("expected %+v but got: %+v", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMockContext_SetFiles(t *testing.T) {
|
|
||||||
mock := &MockContext{&Context{}}
|
|
||||||
mock.SetFiles(map[string]string{
|
|
||||||
"foo": "/foo",
|
|
||||||
})
|
|
||||||
|
|
||||||
actual := mock.files
|
|
||||||
expect := map[string]string{
|
|
||||||
"foo": "/foo",
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expect) {
|
|
||||||
t.Errorf("expected %+v but got: %+v", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMockContext_SetCancelled(t *testing.T) {
|
|
||||||
mock := &MockContext{&Context{}}
|
|
||||||
mock.SetCancelled(true)
|
|
||||||
|
|
||||||
actual := mock.cancelled
|
|
||||||
|
|
||||||
if !actual {
|
|
||||||
t.Errorf("expected %t but got %t", true, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMockContext_OutputPaths(t *testing.T) {
|
|
||||||
mock := MockContext{
|
|
||||||
&Context{
|
|
||||||
outputPaths: []string{"/foo"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := mock.OutputPaths()
|
|
||||||
expect := []string{"/foo"}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expect) {
|
|
||||||
t.Errorf("expected %+v but got: %+v", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMockContext_SetLogger(t *testing.T) {
|
|
||||||
mock := MockContext{&Context{}}
|
|
||||||
|
|
||||||
expect := zap.NewNop()
|
|
||||||
mock.SetLogger(expect)
|
|
||||||
|
|
||||||
actual := mock.logger
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected %v but got %v", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMockContext_SetEchoContext(t *testing.T) {
|
|
||||||
mock := MockContext{&Context{}}
|
|
||||||
|
|
||||||
expect := echo.New().NewContext(nil, nil)
|
|
||||||
mock.SetEchoContext(expect)
|
|
||||||
|
|
||||||
actual := mock.echoCtx
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected %v but got %v", expect, actual)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,66 +2,66 @@ package api
|
|||||||
|
|
||||||
// Credits: https://www.joeshaw.org/error-handling-in-go-http-applications.
|
// Credits: https://www.joeshaw.org/error-handling-in-go-http-applications.
|
||||||
|
|
||||||
// HTTPError is an interface allowing to retrieve the HTTP details of an error.
|
// HttpError is an interface allowing to retrieve the HTTP details of an error.
|
||||||
type HTTPError interface {
|
type HttpError interface {
|
||||||
HTTPError() (int, string)
|
HttpError() (int, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SentinelHTTPError is the HTTP sidekick of an error.
|
// SentinelHttpError is the HTTP sidekick of an error.
|
||||||
type SentinelHTTPError struct {
|
type SentinelHttpError struct {
|
||||||
status int
|
status int
|
||||||
message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSentinelHTTPError creates a SentinelHTTPError. The message will be sent
|
// NewSentinelHttpError creates a [SentinelHttpError]. The message will be sent
|
||||||
// as the response's body if returned from a handler, so make sure to not leak
|
// as the response's body if returned from a handler, so make sure to not leak
|
||||||
// sensible information.
|
// sensible information.
|
||||||
func NewSentinelHTTPError(status int, message string) SentinelHTTPError {
|
func NewSentinelHttpError(status int, message string) SentinelHttpError {
|
||||||
return SentinelHTTPError{
|
return SentinelHttpError{
|
||||||
status: status,
|
status: status,
|
||||||
message: message,
|
message: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns the message.
|
// Error returns the message.
|
||||||
func (err SentinelHTTPError) Error() string {
|
func (err SentinelHttpError) Error() string {
|
||||||
return err.message
|
return err.message
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPError returns the status and message.
|
// HttpError returns the status and message.
|
||||||
func (err SentinelHTTPError) HTTPError() (int, string) {
|
func (err SentinelHttpError) HttpError() (int, string) {
|
||||||
return err.status, err.message
|
return err.status, err.message
|
||||||
}
|
}
|
||||||
|
|
||||||
// sentinelWrappedError contains both the error which will logged and the
|
// sentinelWrappedError contains both the error which will logged and the
|
||||||
// sidekick SentinelHTTPError.
|
// sidekick [SentinelHttpError].
|
||||||
type sentinelWrappedError struct {
|
type sentinelWrappedError struct {
|
||||||
error
|
error
|
||||||
sentinel SentinelHTTPError
|
sentinel SentinelHttpError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w sentinelWrappedError) Is(err error) bool {
|
func (w sentinelWrappedError) Is(err error) bool {
|
||||||
return w.sentinel == err
|
return w.sentinel == err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w sentinelWrappedError) HTTPError() (int, string) {
|
func (w sentinelWrappedError) HttpError() (int, string) {
|
||||||
return w.sentinel.HTTPError()
|
return w.sentinel.HttpError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapError wraps the given error with a SentinelHTTPError. The wrapped error
|
// WrapError wraps the given error with a [SentinelHttpError]. The wrapped
|
||||||
// will be displayed in a log, while the SentinelHTTPError will be sent in the
|
// error will be displayed in a log, while the [SentinelHttpError] will be sent
|
||||||
// response.
|
// in the response.
|
||||||
//
|
//
|
||||||
// return api.WrapError(
|
// return api.WrapError(
|
||||||
// // This first error will be logged.
|
// // This first error will be logged.
|
||||||
// fmt.Errorf("my action: %w", err),
|
// fmt.Errorf("my action: %w", err),
|
||||||
// // The HTTP error will be sent as a response.
|
// // The HTTP error will be sent as a response.
|
||||||
// api.NewSentinelHTTPError(
|
// api.NewSentinelHttpError(
|
||||||
// http.StatusForbidden,
|
// http.StatusForbidden,
|
||||||
// "Hey, you did something wrong!"
|
// "Hey, you did something wrong!"
|
||||||
// ),
|
// ),
|
||||||
// )
|
// )
|
||||||
func WrapError(err error, sentinel SentinelHTTPError) error {
|
func WrapError(err error, sentinel SentinelHttpError) error {
|
||||||
return sentinelWrappedError{
|
return sentinelWrappedError{
|
||||||
error: err,
|
error: err,
|
||||||
sentinel: sentinel,
|
sentinel: sentinel,
|
||||||
@@ -70,8 +70,8 @@ func WrapError(err error, sentinel SentinelHTTPError) error {
|
|||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ error = (*SentinelHTTPError)(nil)
|
_ error = (*SentinelHttpError)(nil)
|
||||||
_ HTTPError = (*SentinelHTTPError)(nil)
|
_ HttpError = (*SentinelHttpError)(nil)
|
||||||
_ error = (*sentinelWrappedError)(nil)
|
_ error = (*sentinelWrappedError)(nil)
|
||||||
_ HTTPError = (*sentinelWrappedError)(nil)
|
_ HttpError = (*sentinelWrappedError)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewSentinelHTTPError(t *testing.T) {
|
func TestNewSentinelHttpError(t *testing.T) {
|
||||||
actual := NewSentinelHTTPError(http.StatusInternalServerError, "foo")
|
actual := NewSentinelHttpError(http.StatusInternalServerError, "foo")
|
||||||
expect := SentinelHTTPError{
|
expect := SentinelHttpError{
|
||||||
status: http.StatusInternalServerError,
|
status: http.StatusInternalServerError,
|
||||||
message: "foo",
|
message: "foo",
|
||||||
}
|
}
|
||||||
@@ -19,8 +19,8 @@ func TestNewSentinelHTTPError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSentinelHTTPError_Error(t *testing.T) {
|
func TestSentinelHttpError_Error(t *testing.T) {
|
||||||
err := SentinelHTTPError{
|
err := SentinelHttpError{
|
||||||
message: "foo",
|
message: "foo",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,11 +32,11 @@ func TestSentinelHTTPError_Error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSentinelHTTPError_HTTPError(t *testing.T) {
|
func TestSentinelHttpError_HttpError(t *testing.T) {
|
||||||
actualStatus, actualMessage := SentinelHTTPError{
|
actualStatus, actualMessage := SentinelHttpError{
|
||||||
status: http.StatusInternalServerError,
|
status: http.StatusInternalServerError,
|
||||||
message: "foo",
|
message: "foo",
|
||||||
}.HTTPError()
|
}.HttpError()
|
||||||
|
|
||||||
expectStatus := http.StatusInternalServerError
|
expectStatus := http.StatusInternalServerError
|
||||||
expectMessage := "foo"
|
expectMessage := "foo"
|
||||||
@@ -51,7 +51,7 @@ func TestSentinelHTTPError_HTTPError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSentinelWrappedError_Is(t *testing.T) {
|
func TestSentinelWrappedError_Is(t *testing.T) {
|
||||||
errSentinel := SentinelHTTPError{}
|
errSentinel := SentinelHttpError{}
|
||||||
|
|
||||||
err := sentinelWrappedError{
|
err := sentinelWrappedError{
|
||||||
error: errors.New("foo"),
|
error: errors.New("foo"),
|
||||||
@@ -63,19 +63,19 @@ func TestSentinelWrappedError_Is(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSentinelWrappedError_HTTPError(t *testing.T) {
|
func TestSentinelWrappedError_HttpError(t *testing.T) {
|
||||||
expectStatus, expectMessage := SentinelHTTPError{
|
expectStatus, expectMessage := SentinelHttpError{
|
||||||
status: http.StatusInternalServerError,
|
status: http.StatusInternalServerError,
|
||||||
message: "foo",
|
message: "foo",
|
||||||
}.HTTPError()
|
}.HttpError()
|
||||||
|
|
||||||
actualStatus, actualMessage := sentinelWrappedError{
|
actualStatus, actualMessage := sentinelWrappedError{
|
||||||
error: errors.New("foo"),
|
error: errors.New("foo"),
|
||||||
sentinel: SentinelHTTPError{
|
sentinel: SentinelHttpError{
|
||||||
status: http.StatusInternalServerError,
|
status: http.StatusInternalServerError,
|
||||||
message: "foo",
|
message: "foo",
|
||||||
},
|
},
|
||||||
}.HTTPError()
|
}.HttpError()
|
||||||
|
|
||||||
if actualStatus != expectStatus {
|
if actualStatus != expectStatus {
|
||||||
t.Errorf("expected %d but got %d", expectStatus, actualStatus)
|
t.Errorf("expected %d but got %d", expectStatus, actualStatus)
|
||||||
@@ -91,13 +91,13 @@ func TestWrapError(t *testing.T) {
|
|||||||
|
|
||||||
expect := sentinelWrappedError{
|
expect := sentinelWrappedError{
|
||||||
error: errFoo,
|
error: errFoo,
|
||||||
sentinel: SentinelHTTPError{
|
sentinel: SentinelHttpError{
|
||||||
status: http.StatusInternalServerError,
|
status: http.StatusInternalServerError,
|
||||||
message: "foo",
|
message: "foo",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := WrapError(errFoo, SentinelHTTPError{
|
actual := WrapError(errFoo, SentinelHttpError{
|
||||||
status: http.StatusInternalServerError,
|
status: http.StatusInternalServerError,
|
||||||
message: "foo",
|
message: "foo",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -16,151 +16,151 @@ import (
|
|||||||
// FormData is a helper for validating and hydrating values from a
|
// FormData is a helper for validating and hydrating values from a
|
||||||
// "multipart/form-data" request.
|
// "multipart/form-data" request.
|
||||||
//
|
//
|
||||||
// form := ctx.FormData()
|
// form := ctx.FormData()
|
||||||
type FormData struct {
|
type FormData struct {
|
||||||
values map[string][]string
|
values map[string][]string
|
||||||
files map[string]string
|
files map[string]string
|
||||||
errors error
|
errors error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate returns nil or an error related to the FormData values, with a
|
// Validate returns nil or an error related to the [FormData] values, with a
|
||||||
// SentinelHTTPError (status code 400, errors' details as message) wrapped
|
// [SentinelHttpError] (status code 400, errors' details as message) wrapped
|
||||||
// inside.
|
// inside.
|
||||||
//
|
//
|
||||||
// var foo string
|
// var foo string
|
||||||
//
|
//
|
||||||
// err := ctx.FormData().
|
// err := ctx.FormData().
|
||||||
// MandatoryString("foo", &foo, "bar").
|
// MandatoryString("foo", &foo, "bar").
|
||||||
// Validate()
|
// Validate()
|
||||||
func (form FormData) Validate() error {
|
func (form *FormData) Validate() error {
|
||||||
if form.errors == nil {
|
if form.errors == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return WrapError(
|
return WrapError(
|
||||||
form.errors,
|
form.errors,
|
||||||
NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %s", form.errors)),
|
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %s", form.errors)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String binds a form data value to a string variable.
|
// String binds a form field to a string variable.
|
||||||
//
|
//
|
||||||
// var foo string
|
// var foo string
|
||||||
//
|
//
|
||||||
// ctx.FormData().String("foo", &foo, "bar")
|
// ctx.FormData().String("foo", &foo, "bar")
|
||||||
func (form *FormData) String(key string, target *string, defaultValue string) *FormData {
|
func (form *FormData) String(key string, target *string, defaultValue string) *FormData {
|
||||||
return form.mustValue(key, target, defaultValue)
|
return form.mustValue(key, target, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MandatoryString binds a form data value to a string variable. It populates
|
// MandatoryString binds a form field to a string variable. It populates
|
||||||
// an error if the value is empty or the "key" does not exist.
|
// an error if the value is empty or the "key" does not exist.
|
||||||
//
|
//
|
||||||
// var foo string
|
// var foo string
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryString("foo", &foo)
|
// ctx.FormData().MandatoryString("foo", &foo)
|
||||||
func (form *FormData) MandatoryString(key string, target *string) *FormData {
|
func (form *FormData) MandatoryString(key string, target *string) *FormData {
|
||||||
return form.mustMandatoryValue(key, target)
|
return form.mustMandatoryField(key, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bool binds a form data value to a bool variable. It populates an error if
|
// Bool binds a form field to a bool variable. It populates an error if
|
||||||
// the value is not bool.
|
// the value is not bool.
|
||||||
//
|
//
|
||||||
// var foo bool
|
// var foo bool
|
||||||
//
|
//
|
||||||
// ctx.FormData().Bool("foo", &foo, true)
|
// ctx.FormData().Bool("foo", &foo, true)
|
||||||
func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormData {
|
func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormData {
|
||||||
return form.mustValue(key, target, defaultValue)
|
return form.mustValue(key, target, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MandatoryBool binds a form data value to a bool variable. It populates an
|
// MandatoryBool binds a form field to a bool variable. It populates an
|
||||||
// error if the value is not bool, is empty, or the "key" does not exist.
|
// error if the value is not bool, is empty, or the "key" does not exist.
|
||||||
//
|
//
|
||||||
// var foo bool
|
// var foo bool
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryBool("foo", &foo)
|
// ctx.FormData().MandatoryBool("foo", &foo)
|
||||||
func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
|
func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
|
||||||
return form.mustMandatoryValue(key, target)
|
return form.mustMandatoryField(key, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int binds a form data value to an int variable. It populates an error if the
|
// Int binds a form field to an int variable. It populates an error if the
|
||||||
// value is not int.
|
// value is not int.
|
||||||
//
|
//
|
||||||
// var foo int
|
// var foo int
|
||||||
//
|
//
|
||||||
// ctx.FormData().Int("foo", &foo, 2)
|
// ctx.FormData().Int("foo", &foo, 2)
|
||||||
func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
|
func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
|
||||||
return form.mustValue(key, target, defaultValue)
|
return form.mustValue(key, target, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MandatoryInt binds a form data value to an int variable. It populates an
|
// MandatoryInt binds a form field to an int variable. It populates an
|
||||||
// error if the value is not int, is empty, or the "key" does not exist.
|
// error if the value is not int, is empty, or the "key" does not exist.
|
||||||
//
|
//
|
||||||
// var foo int
|
// var foo int
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryInt("foo", &foo)
|
// ctx.FormData().MandatoryInt("foo", &foo)
|
||||||
func (form *FormData) MandatoryInt(key string, target *int) *FormData {
|
func (form *FormData) MandatoryInt(key string, target *int) *FormData {
|
||||||
return form.mustMandatoryValue(key, target)
|
return form.mustMandatoryField(key, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Float64 binds a form data value to a float64 variable. It populates an error
|
// Float64 binds a form field to a float64 variable. It populates an error
|
||||||
// if the value is not float64.
|
// if the value is not float64.
|
||||||
//
|
//
|
||||||
// var foo float64
|
// var foo float64
|
||||||
//
|
//
|
||||||
// ctx.FormData().Float64("foo", &foo, 2.0)
|
// ctx.FormData().Float64("foo", &foo, 2.0)
|
||||||
func (form *FormData) Float64(key string, target *float64, defaultValue float64) *FormData {
|
func (form *FormData) Float64(key string, target *float64, defaultValue float64) *FormData {
|
||||||
return form.mustValue(key, target, defaultValue)
|
return form.mustValue(key, target, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MandatoryFloat64 binds a form data value to a float64 variable. It populates
|
// MandatoryFloat64 binds a form field to a float64 variable. It populates
|
||||||
// an error if the is not float64, is empty, or the "key" does not exist.
|
// an error if the is not float64, is empty, or the "key" does not exist.
|
||||||
//
|
//
|
||||||
// var foo float64
|
// var foo float64
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryFloat64("foo", &foo)
|
// ctx.FormData().MandatoryFloat64("foo", &foo)
|
||||||
func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData {
|
func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData {
|
||||||
return form.mustMandatoryValue(key, target)
|
return form.mustMandatoryField(key, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration binds a form data value to a time.Duration variable. It populates
|
// Duration binds a form field to a time.Duration variable. It populates
|
||||||
// an error if the form data value is not time.Duration.
|
// an error if the form field is not time.Duration.
|
||||||
//
|
//
|
||||||
// var foo time.Duration
|
// var foo time.Duration
|
||||||
//
|
//
|
||||||
// ctx.FormData().Duration("foo", &foo, time.Duration(2) * time.Second)
|
// ctx.FormData().Duration("foo", &foo, time.Duration(2) * time.Second)
|
||||||
func (form *FormData) Duration(key string, target *time.Duration, defaultValue time.Duration) *FormData {
|
func (form *FormData) Duration(key string, target *time.Duration, defaultValue time.Duration) *FormData {
|
||||||
return form.mustValue(key, target, defaultValue)
|
return form.mustValue(key, target, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MandatoryDuration binds a form data value to a time.Duration variable. It
|
// MandatoryDuration binds a form field to a time.Duration variable. It
|
||||||
// populates an error if the value is not time.Duration, is empty, or the "key"
|
// populates an error if the value is not time.Duration, is empty, or the "key"
|
||||||
// does not exist.
|
// does not exist.
|
||||||
//
|
//
|
||||||
// var foo time.Duration
|
// var foo time.Duration
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryDuration("foo", &foo)
|
// ctx.FormData().MandatoryDuration("foo", &foo)
|
||||||
func (form *FormData) MandatoryDuration(key string, target *time.Duration) *FormData {
|
func (form *FormData) MandatoryDuration(key string, target *time.Duration) *FormData {
|
||||||
return form.mustMandatoryValue(key, target)
|
return form.mustMandatoryField(key, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom helps to define a custom binding function for a form data value.
|
// Custom helps to define a custom binding function for a form field.
|
||||||
//
|
//
|
||||||
// var foo map[string]string
|
// var foo map[string]string
|
||||||
//
|
//
|
||||||
// ctx.FormData().Custom("foo", func(value string) error {
|
// ctx.FormData().Custom("foo", func(value string) error {
|
||||||
// if value == "" {
|
// if value == "" {
|
||||||
// foo = "bar"
|
// foo = "bar"
|
||||||
//
|
//
|
||||||
// return nil
|
// return nil
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// err := json.Unmarshal([]byte(value), &foo)
|
// err := json.Unmarshal([]byte(value), &foo)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// return fmt.Errorf("unmarshal foo: %w", err)
|
// return fmt.Errorf("unmarshal foo: %w", err)
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// return nil
|
// return nil
|
||||||
// })
|
// })
|
||||||
func (form *FormData) Custom(key string, assign func(value string) error) *FormData {
|
func (form *FormData) Custom(key string, assign func(value string) error) *FormData {
|
||||||
var value string
|
var value string
|
||||||
form.mustValue(key, &value, "")
|
form.mustValue(key, &value, "")
|
||||||
@@ -168,30 +168,29 @@ func (form *FormData) Custom(key string, assign func(value string) error) *FormD
|
|||||||
err := assign(value)
|
err := assign(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
form.append(
|
form.append(
|
||||||
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return form
|
return form
|
||||||
}
|
}
|
||||||
|
|
||||||
// MandatoryCustom helps to define a custom binding function for a form data
|
// MandatoryCustom helps to define a custom binding function for a form field.
|
||||||
// value. It populates an error if the value is empty or the "key" does not
|
// It populates an error if the value is empty or the "key" does not exist.
|
||||||
// exist.
|
|
||||||
//
|
//
|
||||||
// var foo map[string]string
|
// var foo map[string]string
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryCustom("foo", func(value string) error {
|
// ctx.FormData().MandatoryCustom("foo", func(value string) error {
|
||||||
// err := json.Unmarshal([]byte(value), &foo)
|
// err := json.Unmarshal([]byte(value), &foo)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// return fmt.Errorf("unmarshal foo: %w", err)
|
// return fmt.Errorf("unmarshal foo: %w", err)
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// return nil
|
// return nil
|
||||||
// })
|
// })
|
||||||
func (form *FormData) MandatoryCustom(key string, assign func(value string) error) *FormData {
|
func (form *FormData) MandatoryCustom(key string, assign func(value string) error) *FormData {
|
||||||
var value string
|
var value string
|
||||||
form.mustMandatoryValue(key, &value)
|
form.mustMandatoryField(key, &value)
|
||||||
|
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return form
|
return form
|
||||||
@@ -200,7 +199,7 @@ func (form *FormData) MandatoryCustom(key string, assign func(value string) erro
|
|||||||
err := assign(value)
|
err := assign(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
form.append(
|
form.append(
|
||||||
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,9 +208,9 @@ func (form *FormData) MandatoryCustom(key string, assign func(value string) erro
|
|||||||
|
|
||||||
// Path binds the absolute path of a form data file to a string variable.
|
// Path binds the absolute path of a form data file to a string variable.
|
||||||
//
|
//
|
||||||
// var path string
|
// var path string
|
||||||
//
|
//
|
||||||
// ctx.FormData().Path("foo.txt", &path)
|
// ctx.FormData().Path("foo.txt", &path)
|
||||||
func (form *FormData) Path(filename string, target *string) *FormData {
|
func (form *FormData) Path(filename string, target *string) *FormData {
|
||||||
return form.path(filename, target)
|
return form.path(filename, target)
|
||||||
}
|
}
|
||||||
@@ -219,18 +218,18 @@ func (form *FormData) Path(filename string, target *string) *FormData {
|
|||||||
// MandatoryPath binds the absolute path ofa form data file to a string
|
// MandatoryPath binds the absolute path ofa form data file to a string
|
||||||
// variable. It populates an error if the file does not exist.
|
// variable. It populates an error if the file does not exist.
|
||||||
//
|
//
|
||||||
// var path string
|
// var path string
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryPath("foo.txt", &path)
|
// ctx.FormData().MandatoryPath("foo.txt", &path)
|
||||||
func (form *FormData) MandatoryPath(filename string, target *string) *FormData {
|
func (form *FormData) MandatoryPath(filename string, target *string) *FormData {
|
||||||
return form.mandatoryPath(filename, target)
|
return form.mandatoryPath(filename, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content binds the content of a form data file to a string variable.
|
// Content binds the content of a form data file to a string variable.
|
||||||
//
|
//
|
||||||
// var content string
|
// var content string
|
||||||
//
|
//
|
||||||
// ctx.FormData().Content("foo.txt", &content, "bar")
|
// ctx.FormData().Content("foo.txt", &content, "bar")
|
||||||
func (form *FormData) Content(filename string, target *string, defaultValue string) *FormData {
|
func (form *FormData) Content(filename string, target *string, defaultValue string) *FormData {
|
||||||
var path string
|
var path string
|
||||||
form.path(filename, &path)
|
form.path(filename, &path)
|
||||||
@@ -247,9 +246,9 @@ func (form *FormData) Content(filename string, target *string, defaultValue stri
|
|||||||
// MandatoryContent binds the content of a form data file to a string variable.
|
// MandatoryContent binds the content of a form data file to a string variable.
|
||||||
// It populates an error if the file does not exist.
|
// It populates an error if the file does not exist.
|
||||||
//
|
//
|
||||||
// var content string
|
// var content string
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryContent("foo.txt", &content)
|
// ctx.FormData().MandatoryContent("foo.txt", &content)
|
||||||
func (form *FormData) MandatoryContent(filename string, target *string) *FormData {
|
func (form *FormData) MandatoryContent(filename string, target *string) *FormData {
|
||||||
var path string
|
var path string
|
||||||
form.mandatoryPath(filename, &path)
|
form.mandatoryPath(filename, &path)
|
||||||
@@ -264,9 +263,9 @@ func (form *FormData) MandatoryContent(filename string, target *string) *FormDat
|
|||||||
// Paths binds the absolute paths of form data files, according to a list of
|
// Paths binds the absolute paths of form data files, according to a list of
|
||||||
// file extensions, to a string slice variable.
|
// file extensions, to a string slice variable.
|
||||||
//
|
//
|
||||||
// var paths []string
|
// var paths []string
|
||||||
//
|
//
|
||||||
// ctx.FormData().Paths([]string{".txt"}, &paths)
|
// ctx.FormData().Paths([]string{".txt"}, &paths)
|
||||||
func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
|
func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
|
||||||
return form.paths(extensions, target)
|
return form.paths(extensions, target)
|
||||||
}
|
}
|
||||||
@@ -275,9 +274,9 @@ func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
|
|||||||
// list of file extensions, to a string slice variable. It populates an error
|
// list of file extensions, to a string slice variable. It populates an error
|
||||||
// if there is no file for given file extensions.
|
// if there is no file for given file extensions.
|
||||||
//
|
//
|
||||||
// var paths []string
|
// var paths []string
|
||||||
//
|
//
|
||||||
// ctx.FormData().MandatoryPaths([]string{".txt"}, &paths)
|
// ctx.FormData().MandatoryPaths([]string{".txt"}, &paths)
|
||||||
func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *FormData {
|
func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *FormData {
|
||||||
form.paths(extensions, target)
|
form.paths(extensions, target)
|
||||||
|
|
||||||
@@ -315,7 +314,7 @@ func (form *FormData) append(err error) {
|
|||||||
form.errors = multierr.Append(form.errors, err)
|
form.errors = multierr.Append(form.errors, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustValue binds the target interface with a form data value. If the value is
|
// mustValue binds the target interface with a form field. If the value is
|
||||||
// empty or the "key" does not exist, it binds the default value. Currently,
|
// empty or the "key" does not exist, it binds the default value. Currently,
|
||||||
// only the string, bool, int, float64 and time.Duration types are bindable.
|
// only the string, bool, int, float64 and time.Duration types are bindable.
|
||||||
func (form *FormData) mustValue(key string, target interface{}, defaultValue interface{}) *FormData {
|
func (form *FormData) mustValue(key string, target interface{}, defaultValue interface{}) *FormData {
|
||||||
@@ -343,16 +342,16 @@ func (form *FormData) mustValue(key string, target interface{}, defaultValue int
|
|||||||
return form.mustAssign(key, val[0], target)
|
return form.mustAssign(key, val[0], target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustMandatoryValue binds the target interface with a form data value. It
|
// mustMandatoryField binds the target interface with a form field. It
|
||||||
// populates an error if the value is empty or the "key" does not exist.
|
// populates an error if the value is empty or the "key" does not exist.
|
||||||
// Currently, only the string, bool, int, float64 and time.Duration types are
|
// Currently, only the string, bool, int, float64 and time.Duration types are
|
||||||
// bindable.
|
// bindable.
|
||||||
func (form *FormData) mustMandatoryValue(key string, target interface{}) *FormData {
|
func (form *FormData) mustMandatoryField(key string, target interface{}) *FormData {
|
||||||
val, ok := form.values[key]
|
val, ok := form.values[key]
|
||||||
|
|
||||||
if !ok || val[0] == "" {
|
if !ok || val[0] == "" {
|
||||||
form.append(
|
form.append(
|
||||||
fmt.Errorf("form value '%s' is required", key),
|
fmt.Errorf("form field '%s' is required", key),
|
||||||
)
|
)
|
||||||
|
|
||||||
return form
|
return form
|
||||||
@@ -386,7 +385,7 @@ func (form *FormData) mustAssign(key, value string, target interface{}) *FormDat
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
form.append(
|
form.append(
|
||||||
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +425,7 @@ func (form *FormData) mandatoryPath(filename string, target *string) *FormData {
|
|||||||
// readFile binds the content of a file to a string variable. It populates an
|
// readFile binds the content of a file to a string variable. It populates an
|
||||||
// error if it fails to read the file content.
|
// error if it fails to read the file content.
|
||||||
func (form *FormData) readFile(path, filename string, target *string) *FormData {
|
func (form *FormData) readFile(path, filename string, target *string) *FormData {
|
||||||
b, err := ioutil.ReadFile(path)
|
b, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
form.append(
|
form.append(
|
||||||
fmt.Errorf("form file '%s' is invalid (%w)", filename, err),
|
fmt.Errorf("form file '%s' is invalid (%w)", filename, err),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrAsyncProcess happens when a handler or middleware handles a request in an
|
// ErrAsyncProcess happens when a handler or middleware handles a request in an
|
||||||
@@ -19,7 +22,8 @@ var ErrAsyncProcess = errors.New("async process")
|
|||||||
// 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.
|
||||||
func ParseError(err error) (int, string) {
|
func ParseError(err error) (int, string) {
|
||||||
echoErr, ok := err.(*echo.HTTPError)
|
var echoErr *echo.HTTPError
|
||||||
|
ok := errors.As(err, &echoErr)
|
||||||
if ok {
|
if ok {
|
||||||
return echoErr.Code, http.StatusText(echoErr.Code)
|
return echoErr.Code, http.StatusText(echoErr.Code)
|
||||||
}
|
}
|
||||||
@@ -28,9 +32,9 @@ func ParseError(err error) (int, string) {
|
|||||||
return http.StatusServiceUnavailable, http.StatusText(http.StatusServiceUnavailable)
|
return http.StatusServiceUnavailable, http.StatusText(http.StatusServiceUnavailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpErr HTTPError
|
var httpErr HttpError
|
||||||
if errors.As(err, &httpErr) {
|
if errors.As(err, &httpErr) {
|
||||||
return httpErr.HTTPError()
|
return httpErr.HttpError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default 500 status code.
|
// Default 500 status code.
|
||||||
@@ -53,10 +57,10 @@ func httpErrorHandler() echo.HTTPErrorHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// latencyMiddleware sets the start time in the echo.Context under "startTime".
|
// latencyMiddleware sets the start time in the [echo.Context] under
|
||||||
// Its value will be used later to calculate a request latency.
|
// "startTime". Its value will be used later to calculate a request latency.
|
||||||
//
|
//
|
||||||
// startTime := c.Get("startTime").(time.Time)
|
// startTime := c.Get("startTime").(time.Time)
|
||||||
func latencyMiddleware() echo.MiddlewareFunc {
|
func latencyMiddleware() echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
@@ -70,18 +74,18 @@ func latencyMiddleware() echo.MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rootPathMiddleware sets the root path in the echo.Context under "rootPath".
|
// rootPathMiddleware sets the root path in the [echo.Context] under
|
||||||
// Its value may be used to skip a middleware execution based on a request
|
// "rootPath". Its value may be used to skip a middleware execution based on a
|
||||||
// URI.
|
// request URI.
|
||||||
//
|
//
|
||||||
// rootPath := c.Get("rootPath").(string)
|
// rootPath := c.Get("rootPath").(string)
|
||||||
// healthURI := fmt.Sprintf("%s/health", rootPath)
|
// healthURI := fmt.Sprintf("%s/health", rootPath)
|
||||||
//
|
//
|
||||||
// // Skip the middleware if health check URI.
|
// // Skip the middleware if health check URI.
|
||||||
// if c.Request().RequestURI == healthURI {
|
// if c.Request().RequestURI == healthURI {
|
||||||
// // Call the next middleware in the chain.
|
// // Call the next middleware in the chain.
|
||||||
// return next(c)
|
// return next(c)
|
||||||
// }
|
// }
|
||||||
func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
|
func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
@@ -93,12 +97,12 @@ func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// traceMiddleware sets the request identifier in the echo.Context under
|
// traceMiddleware sets the request identifier in the [echo.Context] under
|
||||||
// "trace". Its value is either retrieved from the trace header or generated if
|
// "trace". Its value is either retrieved from the trace header or generated if
|
||||||
// the header is not present / its value is empty.
|
// the header is not present / its value is empty.
|
||||||
//
|
//
|
||||||
// trace := c.Get("trace").(string)
|
// trace := c.Get("trace").(string)
|
||||||
// traceHeader := c.Get("traceHeader").(string).
|
// traceHeader := c.Get("traceHeader").(string).
|
||||||
func traceMiddleware(header string) echo.MiddlewareFunc {
|
func traceMiddleware(header string) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
@@ -119,38 +123,26 @@ func traceMiddleware(header string) echo.MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// timeoutsMiddleware sets the read, process and write timeouts in the
|
// loggerMiddleware sets the logger in the [echo.Context] under "logger" and
|
||||||
// echo.Context under "readTimeout", "processTimeout" and "writeTimeout".
|
// logs a synchronous request result.
|
||||||
//
|
//
|
||||||
// readTimeout := c.Get("readTimeout").(time.Duration)
|
// logger := c.Get("logger").(*zap.Logger)
|
||||||
// processTimeout := c.Get("processTimeout").(time.Duration)
|
|
||||||
// writeTimeout := c.Get("writeTimeout").(time.Duration)
|
|
||||||
func timeoutsMiddleware(readTimeout, processTimeout, writeTimeout time.Duration) echo.MiddlewareFunc {
|
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
c.Set("readTimeout", readTimeout)
|
|
||||||
c.Set("processTimeout", processTimeout)
|
|
||||||
c.Set("writeTimeout", writeTimeout)
|
|
||||||
|
|
||||||
// Call the next middleware in the chain.
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loggerMiddleware sets the logger in the echo.Context under "logger" and logs
|
|
||||||
// a synchronous request result.
|
|
||||||
//
|
|
||||||
// logger := c.Get("logger").(*zap.Logger)
|
|
||||||
func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.MiddlewareFunc {
|
func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
startTime := c.Get("startTime").(time.Time)
|
startTime := c.Get("startTime").(time.Time)
|
||||||
trace := c.Get("trace").(string)
|
trace := c.Get("trace").(string)
|
||||||
|
rootPath := c.Get("rootPath").(string)
|
||||||
|
|
||||||
// Create the request logger and add it to our locals.
|
// Create the request logger and add it to our locals.
|
||||||
reqLogger := logger.With(zap.String("trace", trace))
|
reqLogger := logger.With(zap.String("trace", trace))
|
||||||
c.Set("logger", reqLogger)
|
c.Set("logger", reqLogger.Named(func() string {
|
||||||
|
return strings.ReplaceAll(
|
||||||
|
strings.ReplaceAll(c.Request().URL.Path, rootPath, ""),
|
||||||
|
"/",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
}()))
|
||||||
|
|
||||||
// Call the next middleware in the chain.
|
// Call the next middleware in the chain.
|
||||||
err := next(c)
|
err := next(c)
|
||||||
@@ -159,7 +151,6 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range disableLoggingForPaths {
|
for _, path := range disableLoggingForPaths {
|
||||||
rootPath := c.Get("rootPath").(string)
|
|
||||||
URI := fmt.Sprintf("%s%s", rootPath, path)
|
URI := fmt.Sprintf("%s%s", rootPath, path)
|
||||||
|
|
||||||
if c.Request().RequestURI == URI {
|
if c.Request().RequestURI == URI {
|
||||||
@@ -205,20 +196,20 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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"
|
// [Context] and related context.CancelFunc in the [echo.Context] under
|
||||||
// and "cancel". If the process is synchronous, it also handles the result of a
|
// "context" and "cancel". If the process is synchronous, it also handles the
|
||||||
// "multipart/form-data" request.
|
// result of a "multipart/form-data" request.
|
||||||
//
|
//
|
||||||
// ctx := c.Get("context").(*api.Context)
|
// ctx := c.Get("context").(*api.Context)
|
||||||
// cancel := c.Get("cancel").(context.CancelFunc)
|
// cancel := c.Get("cancel").(context.CancelFunc)
|
||||||
func contextMiddleware(processTimeout time.Duration) echo.MiddlewareFunc {
|
func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
logger := c.Get("logger").(*zap.Logger)
|
logger := c.Get("logger").(*zap.Logger)
|
||||||
|
|
||||||
// We create a context with a timeout so that underlying processes are
|
// We create a context with a timeout so that underlying processes are
|
||||||
// able to stop early and handle correctly a timeout scenario.
|
// able to stop early and handle correctly a timeout scenario.
|
||||||
ctx, cancel, err := newContext(c, logger, processTimeout)
|
ctx, cancel, err := newContext(c, logger, fs, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseError(t *testing.T) {
|
func TestParseError(t *testing.T) {
|
||||||
@@ -34,7 +36,7 @@ func TestParseError(t *testing.T) {
|
|||||||
{
|
{
|
||||||
err: WrapError(
|
err: WrapError(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
NewSentinelHTTPError(http.StatusBadRequest, "foo"),
|
NewSentinelHttpError(http.StatusBadRequest, "foo"),
|
||||||
),
|
),
|
||||||
expectStatus: http.StatusBadRequest,
|
expectStatus: http.StatusBadRequest,
|
||||||
expectMessage: "foo",
|
expectMessage: "foo",
|
||||||
@@ -71,7 +73,7 @@ func TestHttpErrorHandler(t *testing.T) {
|
|||||||
{
|
{
|
||||||
err: WrapError(
|
err: WrapError(
|
||||||
errors.New("foo"),
|
errors.New("foo"),
|
||||||
NewSentinelHTTPError(http.StatusBadRequest, "foo"),
|
NewSentinelHttpError(http.StatusBadRequest, "foo"),
|
||||||
),
|
),
|
||||||
expectStatus: http.StatusBadRequest,
|
expectStatus: http.StatusBadRequest,
|
||||||
expectMessage: "foo",
|
expectMessage: "foo",
|
||||||
@@ -122,7 +124,6 @@ func TestLatencyMiddleware(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)(c)
|
)(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -150,7 +151,6 @@ func TestRootPathMiddleware(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)(c)
|
)(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,6 @@ func TestTraceMiddleware(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)(c)
|
)(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -222,47 +221,6 @@ func TestTraceMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeoutsMiddleware(t *testing.T) {
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
|
||||||
|
|
||||||
srv := echo.New()
|
|
||||||
srv.HideBanner = true
|
|
||||||
srv.HidePort = true
|
|
||||||
|
|
||||||
c := srv.NewContext(request, recorder)
|
|
||||||
|
|
||||||
expectReadTimeout := time.Duration(1) * time.Second
|
|
||||||
expectProcessTimeout := time.Duration(2) * time.Second
|
|
||||||
expectWriteTimeout := time.Duration(3) * time.Second
|
|
||||||
|
|
||||||
err := timeoutsMiddleware(expectReadTimeout, expectProcessTimeout, expectWriteTimeout)(
|
|
||||||
func(c echo.Context) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)(c)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
actualReadTimeout := c.Get("readTimeout").(time.Duration)
|
|
||||||
actualProcessTimeout := c.Get("processTimeout").(time.Duration)
|
|
||||||
actualWriteTimeout := c.Get("writeTimeout").(time.Duration)
|
|
||||||
|
|
||||||
if actualReadTimeout != expectReadTimeout {
|
|
||||||
t.Errorf("expected '%s' but got '%s", expectReadTimeout, actualReadTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actualProcessTimeout != expectProcessTimeout {
|
|
||||||
t.Errorf("expected '%s' but got '%s", expectProcessTimeout, actualProcessTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actualWriteTimeout != expectWriteTimeout {
|
|
||||||
t.Errorf("expected '%s' but got '%s", actualWriteTimeout, expectWriteTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoggerMiddleware(t *testing.T) {
|
func TestLoggerMiddleware(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
request *http.Request
|
request *http.Request
|
||||||
@@ -312,7 +270,6 @@ func TestLoggerMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c)
|
err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -430,7 +387,7 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
c.Set("trace", "foo")
|
c.Set("trace", "foo")
|
||||||
c.Set("startTime", time.Now())
|
c.Set("startTime", time.Now())
|
||||||
|
|
||||||
err := contextMiddleware(time.Duration(10) * time.Second)(tc.next)(c)
|
err := contextMiddleware(gotenberg.NewFileSystem(), time.Duration(10)*time.Second)(tc.next)(c)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectErr && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
|||||||
124
pkg/modules/api/mocks.go
Normal file
124
pkg/modules/api/mocks.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextMock is a helper for tests.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
type ContextMock struct {
|
||||||
|
*Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDirPath sets the context's working directory path.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.SetDirPath("/foo")
|
||||||
|
func (ctx *ContextMock) SetDirPath(path string) {
|
||||||
|
ctx.dirPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirPath returns the context's working directory path.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.SetDirPath("/foo")
|
||||||
|
// dirPath := ctx.DirPath()
|
||||||
|
func (ctx *ContextMock) DirPath() string {
|
||||||
|
return ctx.dirPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValues sets the values.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.SetValues(map[string][]string{
|
||||||
|
// "url": {
|
||||||
|
// "foo",
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
func (ctx *ContextMock) SetValues(values map[string][]string) {
|
||||||
|
ctx.values = values
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFiles sets the files.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.SetFiles(map[string]string{
|
||||||
|
// "foo": "/foo",
|
||||||
|
// })
|
||||||
|
func (ctx *ContextMock) SetFiles(files map[string]string) {
|
||||||
|
ctx.files = files
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCancelled sets if the context is cancelled or not.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.SetCancelled(true)
|
||||||
|
func (ctx *ContextMock) SetCancelled(cancelled bool) {
|
||||||
|
ctx.cancelled = cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputPaths returns the registered output paths.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// outputPaths := ctx.OutputPaths()
|
||||||
|
func (ctx *ContextMock) OutputPaths() []string {
|
||||||
|
return ctx.outputPaths
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger sets the logger.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.SetLogger(zap.NewNop())
|
||||||
|
func (ctx *ContextMock) SetLogger(logger *zap.Logger) {
|
||||||
|
ctx.logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEchoContext sets the echo.Context.
|
||||||
|
//
|
||||||
|
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||||
|
// ctx.setEchoContext(c)
|
||||||
|
func (ctx *ContextMock) SetEchoContext(c echo.Context) {
|
||||||
|
ctx.Context.echoCtx = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouterMock is a mock for the [Router] interface.
|
||||||
|
type RouterMock struct {
|
||||||
|
RoutesMock func() ([]Route, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router *RouterMock) Routes() ([]Route, error) {
|
||||||
|
return router.RoutesMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareProviderMock is a mock for the [MiddlewareProvider] interface.
|
||||||
|
type MiddlewareProviderMock struct {
|
||||||
|
MiddlewaresMock func() ([]Middleware, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *MiddlewareProviderMock) Middlewares() ([]Middleware, error) {
|
||||||
|
return provider.MiddlewaresMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheckerMock is mock for the [HealthChecker] interface.
|
||||||
|
type HealthCheckerMock struct {
|
||||||
|
ChecksMock func() ([]health.CheckerOption, error)
|
||||||
|
ReadyMock func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *HealthCheckerMock) Checks() ([]health.CheckerOption, error) {
|
||||||
|
return mod.ChecksMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *HealthCheckerMock) Ready() error {
|
||||||
|
return mod.ReadyMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ Router = (*RouterMock)(nil)
|
||||||
|
_ MiddlewareProvider = (*MiddlewareProviderMock)(nil)
|
||||||
|
_ HealthChecker = (*HealthCheckerMock)(nil)
|
||||||
|
)
|
||||||
165
pkg/modules/api/mocks_test.go
Normal file
165
pkg/modules/api/mocks_test.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContextMock_SetDirPath(t *testing.T) {
|
||||||
|
mock := &ContextMock{&Context{}}
|
||||||
|
mock.SetDirPath("/foo")
|
||||||
|
|
||||||
|
actual := mock.dirPath
|
||||||
|
expect := "/foo"
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMock_DirPath(t *testing.T) {
|
||||||
|
mock := &ContextMock{&Context{}}
|
||||||
|
mock.SetDirPath("/foo")
|
||||||
|
|
||||||
|
actual := mock.DirPath()
|
||||||
|
expect := "/foo"
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMock_SetValues(t *testing.T) {
|
||||||
|
mock := &ContextMock{&Context{}}
|
||||||
|
mock.SetValues(map[string][]string{
|
||||||
|
"foo": {"foo"},
|
||||||
|
})
|
||||||
|
|
||||||
|
actual := mock.values
|
||||||
|
expect := map[string][]string{
|
||||||
|
"foo": {"foo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actual, expect) {
|
||||||
|
t.Errorf("expected %+v but got: %+v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMock_SetFiles(t *testing.T) {
|
||||||
|
mock := &ContextMock{&Context{}}
|
||||||
|
mock.SetFiles(map[string]string{
|
||||||
|
"foo": "/foo",
|
||||||
|
})
|
||||||
|
|
||||||
|
actual := mock.files
|
||||||
|
expect := map[string]string{
|
||||||
|
"foo": "/foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actual, expect) {
|
||||||
|
t.Errorf("expected %+v but got: %+v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMock_SetCancelled(t *testing.T) {
|
||||||
|
mock := &ContextMock{&Context{}}
|
||||||
|
mock.SetCancelled(true)
|
||||||
|
|
||||||
|
actual := mock.cancelled
|
||||||
|
|
||||||
|
if !actual {
|
||||||
|
t.Errorf("expected %t but got %t", true, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMock_OutputPaths(t *testing.T) {
|
||||||
|
mock := ContextMock{
|
||||||
|
&Context{
|
||||||
|
outputPaths: []string{"/foo"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := mock.OutputPaths()
|
||||||
|
expect := []string{"/foo"}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actual, expect) {
|
||||||
|
t.Errorf("expected %+v but got: %+v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMock_SetLogger(t *testing.T) {
|
||||||
|
mock := ContextMock{&Context{}}
|
||||||
|
|
||||||
|
expect := zap.NewNop()
|
||||||
|
mock.SetLogger(expect)
|
||||||
|
|
||||||
|
actual := mock.logger
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected %v but got %v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextMock_SetEchoContext(t *testing.T) {
|
||||||
|
mock := ContextMock{&Context{}}
|
||||||
|
|
||||||
|
expect := echo.New().NewContext(nil, nil)
|
||||||
|
mock.SetEchoContext(expect)
|
||||||
|
|
||||||
|
actual := mock.echoCtx
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected %v but got %v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterMock(t *testing.T) {
|
||||||
|
mock := &RouterMock{
|
||||||
|
RoutesMock: func() ([]Route, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.Routes()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from RouterMock.Routes, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareProviderMock(t *testing.T) {
|
||||||
|
mock := &MiddlewareProviderMock{
|
||||||
|
MiddlewaresMock: func() ([]Middleware, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.Middlewares()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from MiddlewareProviderMock.Middlewares, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealthCheckerMock(t *testing.T) {
|
||||||
|
mock := &HealthCheckerMock{
|
||||||
|
ChecksMock: func() ([]health.CheckerOption, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
ReadyMock: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.Checks()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from HealthCheckerMock.Checks, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.Ready()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from HealthCheckerMock.Ready, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
355
pkg/modules/chromium/browser.go
Normal file
355
pkg/modules/chromium/browser.go
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
package chromium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/fetch"
|
||||||
|
"github.com/chromedp/cdproto/network"
|
||||||
|
"github.com/chromedp/cdproto/runtime"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type browser interface {
|
||||||
|
gotenberg.Process
|
||||||
|
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type browserArguments struct {
|
||||||
|
// Executor args.
|
||||||
|
binPath string
|
||||||
|
incognito bool
|
||||||
|
allowInsecureLocalhost bool
|
||||||
|
ignoreCertificateErrors bool
|
||||||
|
disableWebSecurity bool
|
||||||
|
allowFileAccessFromFiles bool
|
||||||
|
hostResolverRules string
|
||||||
|
proxyServer string
|
||||||
|
wsUrlReadTimeout time.Duration
|
||||||
|
|
||||||
|
// Tasks specific.
|
||||||
|
allowList *regexp.Regexp
|
||||||
|
denyList *regexp.Regexp
|
||||||
|
clearCache bool
|
||||||
|
clearCookies bool
|
||||||
|
disableJavaScript bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type chromiumBrowser struct {
|
||||||
|
initialCtx context.Context
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
userProfileDirPath string
|
||||||
|
ctxMu sync.RWMutex
|
||||||
|
isStarted atomic.Bool
|
||||||
|
|
||||||
|
arguments browserArguments
|
||||||
|
fs *gotenberg.FileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChromiumBrowser(arguments browserArguments) browser {
|
||||||
|
b := &chromiumBrowser{
|
||||||
|
initialCtx: context.Background(),
|
||||||
|
arguments: arguments,
|
||||||
|
fs: gotenberg.NewFileSystem(),
|
||||||
|
}
|
||||||
|
b.isStarted.Store(false)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) Start(logger *zap.Logger) error {
|
||||||
|
if b.isStarted.Load() {
|
||||||
|
return errors.New("browser is already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
debug := &debugLogger{logger: logger}
|
||||||
|
b.userProfileDirPath = b.fs.NewDirPath()
|
||||||
|
|
||||||
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
chromedp.CombinedOutput(debug),
|
||||||
|
chromedp.ExecPath(b.arguments.binPath),
|
||||||
|
chromedp.NoSandbox,
|
||||||
|
// See:
|
||||||
|
// https://github.com/gotenberg/gotenberg/issues/327
|
||||||
|
// https://github.com/chromedp/chromedp/issues/904
|
||||||
|
chromedp.DisableGPU,
|
||||||
|
// See:
|
||||||
|
// https://github.com/puppeteer/puppeteer/issues/661
|
||||||
|
// https://github.com/puppeteer/puppeteer/issues/2410
|
||||||
|
chromedp.Flag("font-render-hinting", "none"),
|
||||||
|
chromedp.UserDataDir(b.userProfileDirPath),
|
||||||
|
)
|
||||||
|
|
||||||
|
if b.arguments.incognito {
|
||||||
|
opts = append(opts, chromedp.Flag("incognito", b.arguments.incognito))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.arguments.allowInsecureLocalhost {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/488.
|
||||||
|
opts = append(opts, chromedp.Flag("allow-insecure-localhost", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.arguments.ignoreCertificateErrors {
|
||||||
|
opts = append(opts, chromedp.IgnoreCertErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.arguments.disableWebSecurity {
|
||||||
|
opts = append(opts, chromedp.Flag("disable-web-security", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.arguments.allowFileAccessFromFiles {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/356.
|
||||||
|
opts = append(opts, chromedp.Flag("allow-file-access-from-files", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.arguments.hostResolverRules != "" {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/488.
|
||||||
|
opts = append(opts, chromedp.Flag("host-resolver-rules", b.arguments.hostResolverRules))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.arguments.proxyServer != "" {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/376.
|
||||||
|
opts = append(opts, chromedp.ProxyServer(b.arguments.proxyServer))
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/524.
|
||||||
|
opts = append(opts, chromedp.WSURLReadTimeout(b.arguments.wsUrlReadTimeout))
|
||||||
|
|
||||||
|
allocatorCtx, allocatorCancel := chromedp.NewExecAllocator(b.initialCtx, opts...)
|
||||||
|
ctx, cancel := chromedp.NewContext(allocatorCtx, chromedp.WithDebugf(debug.Printf))
|
||||||
|
|
||||||
|
err := chromedp.Run(ctx)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
allocatorCancel()
|
||||||
|
return fmt.Errorf("run exec allocator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ctxMu.Lock()
|
||||||
|
defer b.ctxMu.Unlock()
|
||||||
|
|
||||||
|
// We have to keep the context around, as we need it to create new tabs
|
||||||
|
// later.
|
||||||
|
b.ctx = ctx
|
||||||
|
b.cancelFunc = func() {
|
||||||
|
cancel()
|
||||||
|
allocatorCancel()
|
||||||
|
}
|
||||||
|
b.isStarted.Store(true)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
|
||||||
|
if !b.isStarted.Load() {
|
||||||
|
// No big deal? Like calling cancel twice.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always remove the user profile directory created by Chromium.
|
||||||
|
copyUserProfileDirPath := b.userProfileDirPath
|
||||||
|
defer func(userProfileDirPath string) {
|
||||||
|
go func() {
|
||||||
|
// FIXME: Chromium seems to recreate the user profile directory
|
||||||
|
// right after its deletion if we do not wait a certain amount
|
||||||
|
// of time before re-deleting it.
|
||||||
|
<-time.After(10 * time.Second)
|
||||||
|
|
||||||
|
err := os.RemoveAll(userProfileDirPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("remove Chromium's user profile directory: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
|
||||||
|
}()
|
||||||
|
}(copyUserProfileDirPath)
|
||||||
|
|
||||||
|
b.ctxMu.Lock()
|
||||||
|
defer b.ctxMu.Unlock()
|
||||||
|
|
||||||
|
b.cancelFunc()
|
||||||
|
b.ctx = nil
|
||||||
|
b.userProfileDirPath = ""
|
||||||
|
b.isStarted.Store(false)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) Healthy(logger *zap.Logger) bool {
|
||||||
|
// Good to know: the supervisor does not call this method if no first start
|
||||||
|
// or if the process is restarting.
|
||||||
|
|
||||||
|
if !b.isStarted.Load() {
|
||||||
|
// Non-started browser but not restarting?
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ctxMu.RLock()
|
||||||
|
defer b.ctxMu.RUnlock()
|
||||||
|
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(b.ctx, time.Duration(10)*time.Second)
|
||||||
|
defer timeoutCancel()
|
||||||
|
|
||||||
|
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx)
|
||||||
|
defer taskCancel()
|
||||||
|
|
||||||
|
err := chromedp.Run(taskCtx, chromedp.Navigate("about:blank"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("browser health check failed: %s", err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
|
||||||
|
network.Enable(),
|
||||||
|
fetch.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||||
|
forceExactColorsActionFunc(),
|
||||||
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
// PDF specific.
|
||||||
|
printToPdfActionFunc(logger, outputPath, options),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
|
||||||
|
network.Enable(),
|
||||||
|
fetch.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||||
|
forceExactColorsActionFunc(),
|
||||||
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
// Screenshot specific.
|
||||||
|
captureScreenshotActionFunc(logger, outputPath, options),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string, options Options, tasks chromedp.Tasks) error {
|
||||||
|
if !b.isStarted.Load() {
|
||||||
|
return errors.New("browser not started, cannot handle tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We validate the "main" URL against our allow / deny lists.
|
||||||
|
if !b.arguments.allowList.MatchString(url) {
|
||||||
|
return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", url, ErrUrlNotAuthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.arguments.denyList.String() != "" && b.arguments.denyList.MatchString(url) {
|
||||||
|
return fmt.Errorf("'%s' matches the expression from the denied list: %w", url, ErrUrlNotAuthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline, ok := ctx.Deadline()
|
||||||
|
if !ok {
|
||||||
|
return errors.New("context has no deadline")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ctxMu.RLock()
|
||||||
|
defer b.ctxMu.RUnlock()
|
||||||
|
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(b.ctx, time.Until(deadline))
|
||||||
|
defer timeoutCancel()
|
||||||
|
|
||||||
|
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx)
|
||||||
|
defer taskCancel()
|
||||||
|
|
||||||
|
// We validate all others requests against our allow / deny lists.
|
||||||
|
// If a request does not pass the validation, we make it fail.
|
||||||
|
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 (
|
||||||
|
consoleExceptions error
|
||||||
|
consoleExceptionsMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
|
if options.FailOnConsoleExceptions && !b.arguments.disableJavaScript {
|
||||||
|
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := chromedp.Run(taskCtx, tasks...)
|
||||||
|
if err != nil {
|
||||||
|
errMessage := err.Error()
|
||||||
|
|
||||||
|
if strings.Contains(errMessage, "Show invalid printer settings error (-32000)") || strings.Contains(errMessage, "content area is empty (-32602)") {
|
||||||
|
return ErrInvalidPrinterSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(errMessage, "Page range syntax error") {
|
||||||
|
return ErrPageRangesSyntaxError
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(errMessage, "rpcc: message too large") {
|
||||||
|
return ErrRpccMessageTooLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("handle tasks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||||
|
invalidHttpStatusCodeMu.RLock()
|
||||||
|
defer invalidHttpStatusCodeMu.RUnlock()
|
||||||
|
|
||||||
|
if invalidHttpStatusCode != nil {
|
||||||
|
return fmt.Errorf("%v: %w", invalidHttpStatusCode, ErrInvalidHttpStatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
|
consoleExceptionsMu.RLock()
|
||||||
|
defer consoleExceptionsMu.RUnlock()
|
||||||
|
|
||||||
|
if consoleExceptions != nil {
|
||||||
|
return fmt.Errorf("%v: %w", consoleExceptions, ErrConsoleExceptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Process = (*chromiumBrowser)(nil)
|
||||||
|
_ browser = (*chromiumBrowser)(nil)
|
||||||
|
)
|
||||||
2015
pkg/modules/chromium/browser_test.go
Normal file
2015
pkg/modules/chromium/browser_test.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,58 +3,20 @@ package chromium
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/alexliesenfeld/health"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoAPI struct {
|
|
||||||
pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ Options) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoAPI) PDF(ctx context.Context, logger *zap.Logger, URL, outputPath string, options Options) error {
|
|
||||||
return mod.pdf(ctx, logger, URL, outputPath, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngineProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
pdfEngine func() (gotenberg.PDFEngine, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngineProvider) PDFEngine() (gotenberg.PDFEngine, error) {
|
|
||||||
return mod.pdfEngine()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngine struct {
|
|
||||||
merge func(_ context.Context, _ *zap.Logger, _ []string, _ string) error
|
|
||||||
convert func(_ context.Context, _ *zap.Logger, _, _, _ string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return mod.merge(ctx, logger, inputPaths, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
|
||||||
return mod.convert(ctx, logger, format, inputPath, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultOptions(t *testing.T) {
|
func TestDefaultOptions(t *testing.T) {
|
||||||
actual := DefaultOptions()
|
actual := DefaultPdfOptions()
|
||||||
notExpect := Options{}
|
notExpect := PdfOptions{}
|
||||||
|
|
||||||
if reflect.DeepEqual(actual, notExpect) {
|
if reflect.DeepEqual(actual, notExpect) {
|
||||||
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
||||||
@@ -62,7 +24,7 @@ func TestDefaultOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestChromium_Descriptor(t *testing.T) {
|
func TestChromium_Descriptor(t *testing.T) {
|
||||||
descriptor := Chromium{}.Descriptor()
|
descriptor := new(Chromium).Descriptor()
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
expect := reflect.TypeOf(new(Chromium))
|
expect := reflect.TypeOf(new(Chromium))
|
||||||
@@ -73,11 +35,13 @@ func TestChromium_Descriptor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestChromium_Provision(t *testing.T) {
|
func TestChromium_Provision(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
ctx *gotenberg.Context
|
scenario string
|
||||||
expectErr bool
|
ctx *gotenberg.Context
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "no logger provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
@@ -86,15 +50,19 @@ func TestChromium_Provision(t *testing.T) {
|
|||||||
[]gotenberg.ModuleDescriptor{},
|
[]gotenberg.ModuleDescriptor{},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "no logger from logger provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoPDFEngineProvider }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.pdfEngine = func() (gotenberg.PDFEngine, error) {
|
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
return nil, errors.New("foo")
|
return nil, errors.New("foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,16 +75,78 @@ func TestChromium_Provision(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "no PDF engine provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoPDFEngineProvider }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.pdfEngine = func() (gotenberg.PDFEngine, error) {
|
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
return struct{ ProtoPDFEngine }{}, nil
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(Chromium).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no PDF engine from PDF engine provider",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
gotenberg.PdfEngineProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(Chromium).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "provision success",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
gotenberg.PdfEngineProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||||
|
return new(gotenberg.PdfEngineMock), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -130,62 +160,287 @@ func TestChromium_Provision(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(Chromium)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
err := mod.Provision(tc.ctx)
|
mod := new(Chromium)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChromium_Validate(t *testing.T) {
|
func TestChromium_Validate(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
binPath string
|
scenario string
|
||||||
expectErr bool
|
binPath string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectErr: true,
|
scenario: "empty bin path",
|
||||||
|
binPath: "",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
binPath: "/foo",
|
scenario: "bin path does not exist",
|
||||||
expectErr: true,
|
binPath: "/foo",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
scenario: "validate success",
|
||||||
|
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(Chromium)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod.binPath = tc.binPath
|
mod := new(Chromium)
|
||||||
err := mod.Validate()
|
mod.args = browserArguments{
|
||||||
|
binPath: tc.binPath,
|
||||||
|
}
|
||||||
|
err := mod.Validate()
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChromium_Start(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
autoStart bool
|
||||||
|
supervisor *gotenberg.ProcessSupervisorMock
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no auto-start",
|
||||||
|
autoStart: false,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start success",
|
||||||
|
autoStart: true,
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start failed",
|
||||||
|
autoStart: true,
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
mod.autoStart = tc.autoStart
|
||||||
|
mod.supervisor = tc.supervisor
|
||||||
|
|
||||||
|
err := mod.Start()
|
||||||
|
|
||||||
|
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_StartupMessage(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
|
||||||
|
mod.autoStart = true
|
||||||
|
autoStartMsg := mod.StartupMessage()
|
||||||
|
|
||||||
|
mod.autoStart = false
|
||||||
|
noAutoStartMsg := mod.StartupMessage()
|
||||||
|
|
||||||
|
if autoStartMsg == noAutoStartMsg {
|
||||||
|
t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChromium_Stop(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
supervisor *gotenberg.ProcessSupervisorMock
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "stop success",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "stop failed",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
mod.logger = zap.NewNop()
|
||||||
|
mod.supervisor = tc.supervisor
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := mod.Stop(ctx)
|
||||||
|
|
||||||
|
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_Metrics(t *testing.T) {
|
func TestChromium_Metrics(t *testing.T) {
|
||||||
metrics, err := new(Chromium).Metrics()
|
mod := new(Chromium)
|
||||||
if err != nil {
|
mod.supervisor = &gotenberg.ProcessSupervisorMock{
|
||||||
t.Errorf("expected no error but got: %v", err)
|
ReqQueueSizeMock: func() int64 {
|
||||||
|
return 10
|
||||||
|
},
|
||||||
|
RestartsCountMock: func() int64 {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(metrics) != 1 {
|
metrics, err := mod.Metrics()
|
||||||
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(metrics) != 2 {
|
||||||
|
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
actual := metrics[0].Read()
|
||||||
if actual != 0 {
|
if actual != float64(10) {
|
||||||
t.Errorf("expected %d Chromium instances, but got %f", 0, actual)
|
t.Errorf("expected %f for chromium_requests_queue_size, but got %f", float64(10), actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual = metrics[1].Read()
|
||||||
|
if actual != float64(0) {
|
||||||
|
t.Errorf("expected %f for chromium_restarts_count, but got %f", float64(0), actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChromium_Checks(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
supervisor gotenberg.ProcessSupervisor
|
||||||
|
expectAvailabilityStatus health.AvailabilityStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "healthy module",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||||
|
return true
|
||||||
|
}},
|
||||||
|
expectAvailabilityStatus: health.StatusUp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "unhealthy module",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||||
|
return false
|
||||||
|
}},
|
||||||
|
expectAvailabilityStatus: health.StatusDown,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
mod.supervisor = tc.supervisor
|
||||||
|
|
||||||
|
checks, err := mod.Checks()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checker := health.NewChecker(checks...)
|
||||||
|
result := checker.Check(context.Background())
|
||||||
|
|
||||||
|
if result.Status != tc.expectAvailabilityStatus {
|
||||||
|
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChromium_Ready(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
autoStart bool
|
||||||
|
startTimeout time.Duration
|
||||||
|
browser browser
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no auto-start",
|
||||||
|
autoStart: false,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start: context done",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(200) * time.Millisecond,
|
||||||
|
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start success",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return true
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
mod.autoStart = tc.autoStart
|
||||||
|
mod.args = browserArguments{wsUrlReadTimeout: tc.startTimeout}
|
||||||
|
mod.browser = tc.browser
|
||||||
|
|
||||||
|
err := mod.Ready()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,293 +454,118 @@ func TestChromium_Chromium(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestChromium_Routes(t *testing.T) {
|
func TestChromium_Routes(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
expectRoutes int
|
expectRoutes int
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectRoutes: 3,
|
scenario: "routes not disabled",
|
||||||
|
expectRoutes: 6,
|
||||||
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "routes disabled",
|
||||||
|
expectRoutes: 0,
|
||||||
disableRoutes: true,
|
disableRoutes: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(Chromium)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod.disableRoutes = tc.disableRoutes
|
mod := new(Chromium)
|
||||||
|
mod.disableRoutes = tc.disableRoutes
|
||||||
|
|
||||||
routes, err := mod.Routes()
|
routes, err := mod.Routes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectRoutes != len(routes) {
|
if tc.expectRoutes != len(routes) {
|
||||||
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes))
|
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChromium_PDF(t *testing.T) {
|
func TestChromium_Pdf(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
timeout time.Duration
|
scenario string
|
||||||
cancel context.CancelFunc
|
supervisor gotenberg.ProcessSupervisor
|
||||||
URL string
|
browser browser
|
||||||
options Options
|
expectError bool
|
||||||
userAgent string
|
|
||||||
incognito bool
|
|
||||||
ignoreCertificateErrors bool
|
|
||||||
disableWebSecurity bool
|
|
||||||
allowFileAccessFromFiles bool
|
|
||||||
proxyServer string
|
|
||||||
allowList *regexp.Regexp
|
|
||||||
denyList *regexp.Regexp
|
|
||||||
disableJavaScript bool
|
|
||||||
expectErr bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
scenario: "PDF task success",
|
||||||
allowList: regexp.MustCompile("file:///tmp/*"),
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
expectErr: true,
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
scenario: "PDF task error",
|
||||||
denyList: regexp.MustCompile("file:///tests/*"),
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
expectErr: true,
|
return errors.New("PDF task error")
|
||||||
},
|
}},
|
||||||
{
|
expectError: true,
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
options: Options{
|
|
||||||
UserAgent: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample10/index.html",
|
|
||||||
options: Options{
|
|
||||||
FailOnConsoleExceptions: true,
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample9/index.html",
|
|
||||||
disableJavaScript: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
options: Options{
|
|
||||||
ExtraHTTPHeaders: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
|
|
||||||
options: Options{
|
|
||||||
ExtraLinkTags: []LinkTag{
|
|
||||||
{
|
|
||||||
Href: "font.woff",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Href: "style.css",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
|
||||||
options: Options{
|
|
||||||
EmulatedMediaType: "foo",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
|
||||||
options: Options{
|
|
||||||
EmulatedMediaType: "screen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample8/index.html",
|
|
||||||
options: Options{
|
|
||||||
EmulatedMediaType: "print",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample11/index.html",
|
|
||||||
options: Options{
|
|
||||||
ExtraScriptTags: []ScriptTag{
|
|
||||||
{
|
|
||||||
Src: "script.js",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
options: Options{
|
|
||||||
WaitDelay: time.Duration(1) * time.Nanosecond,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: time.Duration(3) * time.Second,
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
|
||||||
options: Options{
|
|
||||||
WaitWindowStatus: "foo",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: time.Duration(3) * time.Second,
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
|
||||||
options: Options{
|
|
||||||
WaitWindowStatus: "ready",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: time.Duration(3) * time.Second,
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
|
||||||
options: Options{
|
|
||||||
WaitForExpression: "window.status === 'foo'",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: time.Duration(3) * time.Second,
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample2/index.html",
|
|
||||||
options: Options{
|
|
||||||
WaitForExpression: "window.status === 'ready'",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
options: Options{
|
|
||||||
WaitForExpression: "return undefined",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
options: Options{
|
|
||||||
MarginBottom: 100,
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
options: Options{
|
|
||||||
PageRanges: "foo",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
userAgent: "foo",
|
|
||||||
incognito: true,
|
|
||||||
ignoreCertificateErrors: true,
|
|
||||||
disableWebSecurity: true,
|
|
||||||
allowFileAccessFromFiles: true,
|
|
||||||
proxyServer: "foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample1/index.html",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
|
|
||||||
allowList: regexp.MustCompile("file:///tests/*"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample3/index.html",
|
|
||||||
denyList: regexp.MustCompile("file:///etc/*"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample4/index.html",
|
|
||||||
options: Options{
|
|
||||||
HeaderTemplate: func() string {
|
|
||||||
b, err := ioutil.ReadFile("/tests/test/testdata/chromium/url/sample2/header.html")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(b)
|
|
||||||
}(),
|
|
||||||
FooterTemplate: func() string {
|
|
||||||
b, err := ioutil.ReadFile("/tests/test/testdata/chromium/url/sample2/footer.html")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(b)
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample5/index.html",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample6/index.html",
|
|
||||||
allowFileAccessFromFiles: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URL: "file:///tests/test/testdata/chromium/html/sample7/index.html",
|
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod := new(Chromium)
|
mod := new(Chromium)
|
||||||
mod.binPath = os.Getenv("CHROMIUM_BIN_PATH")
|
mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
mod.userAgent = tc.userAgent
|
return task()
|
||||||
mod.incognito = tc.incognito
|
}}
|
||||||
mod.ignoreCertificateErrors = tc.ignoreCertificateErrors
|
mod.browser = tc.browser
|
||||||
mod.disableWebSecurity = tc.disableWebSecurity
|
|
||||||
mod.allowFileAccessFromFiles = tc.allowFileAccessFromFiles
|
|
||||||
mod.proxyServer = tc.proxyServer
|
|
||||||
|
|
||||||
if tc.allowList == nil {
|
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
tc.allowList = regexp.MustCompile("")
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.denyList == nil {
|
if tc.expectError && err == nil {
|
||||||
tc.denyList = regexp.MustCompile("")
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
mod.allowList = tc.allowList
|
|
||||||
mod.denyList = tc.denyList
|
|
||||||
mod.disableJavaScript = tc.disableJavaScript
|
|
||||||
|
|
||||||
outputDir, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err := os.RemoveAll(outputDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if tc.timeout == 0 {
|
|
||||||
err = mod.PDF(context.Background(), zap.NewNop(), tc.URL, outputDir+"/foo.pdf", tc.options)
|
|
||||||
} else {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = mod.PDF(ctx, zap.NewNop(), tc.URL, outputDir+"/foo.pdf", tc.options)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
func TestChromium_Screenshot(t *testing.T) {
|
||||||
var (
|
for _, tc := range []struct {
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
scenario string
|
||||||
_ API = (*ProtoAPI)(nil)
|
supervisor gotenberg.ProcessSupervisor
|
||||||
_ gotenberg.PDFEngineProvider = (*ProtoPDFEngineProvider)(nil)
|
browser browser
|
||||||
_ gotenberg.Module = (*ProtoPDFEngineProvider)(nil)
|
expectError bool
|
||||||
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
|
}{
|
||||||
)
|
{
|
||||||
|
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 {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// debugLogger is wrapper around a zap.Logger which is used for debugging
|
// debugLogger is wrapper around a [zap.Logger] which is used for debugging
|
||||||
// Chromium.
|
// Chromium.
|
||||||
type debugLogger struct {
|
type debugLogger struct {
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write logs the bytes in a debug message.
|
// Write logs the bytes in a debug message.
|
||||||
func (debug debugLogger) Write(p []byte) (n int, err error) {
|
func (debug *debugLogger) Write(p []byte) (n int, err error) {
|
||||||
debug.logger.Debug(string(p))
|
debug.logger.Debug(string(p))
|
||||||
|
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printf logs a debug message.
|
// Printf logs a debug message.
|
||||||
func (debug debugLogger) Printf(format string, v ...interface{}) {
|
func (debug *debugLogger) Printf(format string, v ...interface{}) {
|
||||||
debug.logger.Debug(fmt.Sprintf(format, v...))
|
debug.logger.Debug(fmt.Sprintf(format, v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDebugLogger_Write(t *testing.T) {
|
func TestDebugLogger_Write(t *testing.T) {
|
||||||
actual, err := debugLogger{logger: zap.NewNop()}.Write([]byte("foo"))
|
actual, err := (&debugLogger{logger: zap.NewNop()}).Write([]byte("foo"))
|
||||||
expected := len([]byte("foo"))
|
expected := len([]byte("foo"))
|
||||||
|
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
@@ -20,5 +20,5 @@ func TestDebugLogger_Write(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugLogger_Printf(t *testing.T) {
|
func TestDebugLogger_Printf(t *testing.T) {
|
||||||
debugLogger{logger: zap.NewNop()}.Printf("%s", "foo")
|
(&debugLogger{logger: zap.NewNop()}).Printf("%s", "foo")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package chromium provides a module which adds routes for converting HTML
|
// Package chromium provides a module which adds routes for converting HTML
|
||||||
// documents to PDF. Other modules may also retrieve the API provided by this
|
// documents to PDF. Other modules may also retrieve the [Api] provided by this
|
||||||
// module.
|
// module.
|
||||||
package chromium
|
package chromium
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/cdp"
|
"github.com/chromedp/cdproto/cdp"
|
||||||
@@ -43,7 +44,6 @@ 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))
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,6 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
|||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
@@ -62,6 +61,37 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowL
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenForEventResponseReceived listens for an invalid HTTP status code is
|
||||||
|
// returned by the main page.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||||
|
func listenForEventResponseReceived(ctx context.Context, logger *zap.Logger, url string, failOnHttpStatusCodes []int64, invalidHttpStatusCode *error, invalidHttpStatusCodeMu *sync.RWMutex) {
|
||||||
|
for _, code := range []int64{199, 299, 399, 499, 599} {
|
||||||
|
if slices.Contains(failOnHttpStatusCodes, code) {
|
||||||
|
for i := code - 99; i <= code; i++ {
|
||||||
|
failOnHttpStatusCodes = append(failOnHttpStatusCodes, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||||
|
switch ev := ev.(type) {
|
||||||
|
case *network.EventResponseReceived:
|
||||||
|
if ev.Response.URL != url {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for main page: %+v", ev.Response))
|
||||||
|
|
||||||
|
if slices.Contains(failOnHttpStatusCodes, ev.Response.Status) {
|
||||||
|
invalidHttpStatusCodeMu.Lock()
|
||||||
|
defer invalidHttpStatusCodeMu.Unlock()
|
||||||
|
|
||||||
|
*invalidHttpStatusCode = fmt.Errorf("%d: %s", ev.Response.Status, ev.Response.StatusText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// listenForEventExceptionThrown listens for exceptions in the console and
|
// listenForEventExceptionThrown listens for exceptions in the console and
|
||||||
// appends those exceptions to the given error pointer.
|
// appends those exceptions to the given error pointer.
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/262.
|
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||||
@@ -181,7 +211,6 @@ func waitForEventLoadingFinished(ctx context.Context, logger *zap.Logger) func()
|
|||||||
// completed or an error is encountered.
|
// completed or an error is encountered.
|
||||||
func runBatch(ctx context.Context, fn ...func() error) error {
|
func runBatch(ctx context.Context, fn ...func() error) error {
|
||||||
eg, _ := errgroup.WithContext(ctx)
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
for _, f := range fn {
|
for _, f := range fn {
|
||||||
eg.Go(f)
|
eg.Go(f)
|
||||||
}
|
}
|
||||||
|
|||||||
44
pkg/modules/chromium/mocks.go
Normal file
44
pkg/modules/chromium/mocks.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package chromium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApiMock is a mock for the [Api] interface.
|
||||||
|
type ApiMock struct {
|
||||||
|
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
ScreenshotMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
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.
|
||||||
|
type browserMock struct {
|
||||||
|
gotenberg.ProcessMock
|
||||||
|
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
screenshotMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
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.
|
||||||
|
var (
|
||||||
|
_ Api = (*ApiMock)(nil)
|
||||||
|
_ browser = (*browserMock)(nil)
|
||||||
|
)
|
||||||
50
pkg/modules/chromium/mocks_test.go
Normal file
50
pkg/modules/chromium/mocks_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package chromium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApiMock(t *testing.T) {
|
||||||
|
mock := &ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
|
if err != nil {
|
||||||
|
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) {
|
||||||
|
mock := &browserMock{
|
||||||
|
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
117
pkg/modules/chromium/stream.go
Normal file
117
pkg/modules/chromium/stream.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package chromium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/cdp"
|
||||||
|
cdprotoio "github.com/chromedp/cdproto/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credits: https://raw.githubusercontent.com/mafredri/cdp/3c5eab7ffc5cbee667b0a813ce470ac423792811/protocol/io/stream_reader.go.
|
||||||
|
type streamReader struct {
|
||||||
|
ctx context.Context
|
||||||
|
handle cdprotoio.StreamHandle
|
||||||
|
r io.Reader
|
||||||
|
pos int
|
||||||
|
eof bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a chunk of the stream.
|
||||||
|
func (reader *streamReader) Read(p []byte) (n int, err error) {
|
||||||
|
if reader.r != nil {
|
||||||
|
// Continue reading from buffer.
|
||||||
|
return reader.read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reader.eof {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chromium might have an off-by-one when deciding the maximum size (at
|
||||||
|
// least for base64 encoded data), usually it will overflow. We subtract
|
||||||
|
// one to make sure it fits into p.
|
||||||
|
size := len(p) - 1
|
||||||
|
if size < 1 {
|
||||||
|
// Safety-check to avoid crashing Chrome (e.g. via SetSize(-1)).
|
||||||
|
size = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := reader.next(reader.pos, size)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.eof = reply.EOF
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case reply.Base64encoded:
|
||||||
|
b := []byte(reply.Data)
|
||||||
|
size := base64.StdEncoding.DecodedLen(len(b))
|
||||||
|
|
||||||
|
// Safety-check for fast-path to avoid panics.
|
||||||
|
if len(p) >= size {
|
||||||
|
n, err = base64.StdEncoding.Decode(p, b)
|
||||||
|
reader.pos += n
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.r = base64.NewDecoder(base64.StdEncoding, bytes.NewReader(b))
|
||||||
|
default:
|
||||||
|
reader.r = strings.NewReader(reply.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the stream, discard any temporary backing storage.
|
||||||
|
func (reader *streamReader) Close() error {
|
||||||
|
err := cdprotoio.Close(reader.handle).Do(reader.ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("close Chromium stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reader *streamReader) next(pos, size int) (cdprotoio.ReadReturns, error) {
|
||||||
|
params := cdprotoio.
|
||||||
|
Read(reader.handle).
|
||||||
|
WithOffset(int64(pos)).
|
||||||
|
WithSize(int64(size))
|
||||||
|
|
||||||
|
var res cdprotoio.ReadReturns
|
||||||
|
err := cdp.Execute(reader.ctx, cdprotoio.CommandRead, params, &res)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, fmt.Errorf("execute IO.read command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reader *streamReader) read(p []byte) (n int, err error) {
|
||||||
|
n, err = reader.r.Read(p)
|
||||||
|
reader.pos += n
|
||||||
|
|
||||||
|
if !reader.eof && err == io.EOF {
|
||||||
|
reader.r = nil
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ io.Reader = (*streamReader)(nil)
|
||||||
|
)
|
||||||
397
pkg/modules/chromium/tasks.go
Normal file
397
pkg/modules/chromium/tasks.go
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
package chromium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/cdp"
|
||||||
|
"github.com/chromedp/cdproto/emulation"
|
||||||
|
"github.com/chromedp/cdproto/network"
|
||||||
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
printToPdf := page.PrintToPDF().
|
||||||
|
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
|
||||||
|
WithLandscape(options.Landscape).
|
||||||
|
WithPrintBackground(options.PrintBackground).
|
||||||
|
WithScale(options.Scale).
|
||||||
|
WithPaperWidth(options.PaperWidth).
|
||||||
|
WithPaperHeight(options.PaperHeight).
|
||||||
|
WithMarginTop(options.MarginTop).
|
||||||
|
WithMarginBottom(options.MarginBottom).
|
||||||
|
WithMarginLeft(options.MarginLeft).
|
||||||
|
WithMarginRight(options.MarginRight).
|
||||||
|
WithPageRanges(options.PageRanges).
|
||||||
|
WithPreferCSSPageSize(options.PreferCssPageSize)
|
||||||
|
|
||||||
|
hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
|
||||||
|
options.FooterTemplate != DefaultPdfOptions().FooterTemplate
|
||||||
|
|
||||||
|
if !hasCustomHeaderFooter {
|
||||||
|
logger.Debug("no custom header nor footer")
|
||||||
|
|
||||||
|
printToPdf = printToPdf.WithDisplayHeaderFooter(false)
|
||||||
|
} else {
|
||||||
|
logger.Debug("with custom header and/or footer")
|
||||||
|
|
||||||
|
printToPdf = printToPdf.
|
||||||
|
WithDisplayHeaderFooter(true).
|
||||||
|
WithHeaderTemplate(options.HeaderTemplate).
|
||||||
|
WithFooterTemplate(options.FooterTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("print to PDF with: %+v", printToPdf))
|
||||||
|
|
||||||
|
_, stream, err := printToPdf.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("print to PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := &streamReader{
|
||||||
|
ctx: ctx,
|
||||||
|
handle: stream,
|
||||||
|
r: nil,
|
||||||
|
pos: 0,
|
||||||
|
eof: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = reader.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close reader: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 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))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
buffer := bufio.NewReader(reader)
|
||||||
|
|
||||||
|
_, err = buffer.WriteTo(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write result to output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureScreenshotActionFunc(logger *zap.Logger, outputPath string, options ScreenshotOptions) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
captureScreenshot := page.CaptureScreenshot().
|
||||||
|
WithCaptureBeyondViewport(true).
|
||||||
|
WithFromSurface(true).
|
||||||
|
WithOptimizeForSpeed(options.OptimizeForSpeed).
|
||||||
|
WithFormat(page.CaptureScreenshotFormat(options.Format))
|
||||||
|
|
||||||
|
if options.Format == "jpeg" {
|
||||||
|
captureScreenshot = captureScreenshot.
|
||||||
|
WithQuality(int64(options.Quality))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("capture screenshot with: %+v", captureScreenshot))
|
||||||
|
|
||||||
|
buffer, err := captureScreenshot.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("capture screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close output path: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = file.Write(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write result to output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearCacheActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/753.
|
||||||
|
if !clear {
|
||||||
|
logger.Debug("cache not cleared")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("clear cache")
|
||||||
|
|
||||||
|
err := network.ClearBrowserCache().Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("clear cache: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearCookiesActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/753.
|
||||||
|
if !clear {
|
||||||
|
logger.Debug("cookies not cleared")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("clear cookies")
|
||||||
|
|
||||||
|
err := network.ClearBrowserCookies().Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("clear cookies: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/175.
|
||||||
|
if !disable {
|
||||||
|
logger.Debug("JavaScript not disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("disable JavaScript")
|
||||||
|
|
||||||
|
err := emulation.SetScriptExecutionDisabled(true).Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("disable JavaScript: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extraHttpHeadersActionFunc(logger *zap.Logger, extraHttpHeaders map[string]string) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
if len(extraHttpHeaders) == 0 {
|
||||||
|
logger.Debug("no extra HTTP headers")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", extraHttpHeaders))
|
||||||
|
|
||||||
|
headers := make(network.Headers, len(extraHttpHeaders))
|
||||||
|
for key, value := range extraHttpHeaders {
|
||||||
|
headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
err := network.SetExtraHTTPHeaders(headers).Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("set extra HTTP headers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleEvent bool) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
logger.Debug(fmt.Sprintf("navigate to '%s'", url))
|
||||||
|
|
||||||
|
_, _, _, err := page.Navigate(url).Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
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(
|
||||||
|
ctx,
|
||||||
|
waitFunc...,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("wait for events: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackground, printBackground bool) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/226.
|
||||||
|
if !omitBackground {
|
||||||
|
logger.Debug("default white background not hidden")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !printBackground {
|
||||||
|
// See https://github.com/chromedp/chromedp/issues/1179#issuecomment-1284794416.
|
||||||
|
return fmt.Errorf("validate omit background: %w", ErrOmitBackgroundWithoutPrintBackground)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("hide default white background")
|
||||||
|
|
||||||
|
err := emulation.SetDefaultBackgroundColorOverride().WithColor(
|
||||||
|
&cdp.RGBA{
|
||||||
|
R: 0,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 0,
|
||||||
|
}).Do(ctx)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("hide default white background: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forceExactColorsActionFunc() chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// See:
|
||||||
|
// https://github.com/gotenberg/gotenberg/issues/354
|
||||||
|
// https://github.com/puppeteer/puppeteer/issues/2685
|
||||||
|
// https://github.com/chromedp/chromedp/issues/520
|
||||||
|
script := `
|
||||||
|
(() => {
|
||||||
|
const css = 'html { -webkit-print-color-adjust: exact !important; }';
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.type = 'text/css';
|
||||||
|
style.appendChild(document.createTextNode(css));
|
||||||
|
document.head.appendChild(style);
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
|
||||||
|
evaluate := chromedp.Evaluate(script, nil)
|
||||||
|
err := evaluate.Do(ctx)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("add CSS for exact colors: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
if mediaType == "" {
|
||||||
|
logger.Debug("no emulated media type")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if mediaType != "screen" && mediaType != "print" {
|
||||||
|
return fmt.Errorf("validate emulated media type '%s': %w", mediaType, ErrInvalidEmulatedMediaType)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("emulate media type '%s'", mediaType))
|
||||||
|
|
||||||
|
emulatedMedia := emulation.SetEmulatedMedia()
|
||||||
|
err := emulatedMedia.WithMedia(mediaType).Do(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("emulate media type '%s': %w", mediaType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitDelayBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool, delay time.Duration) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
if disableJavaScript {
|
||||||
|
logger.Debug("JavaScript disabled, skipping wait delay")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if delay <= 0 {
|
||||||
|
logger.Debug("no wait delay")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wait for a given amount of time so that JavaScript
|
||||||
|
// scripts have a chance to finish before printing the page.
|
||||||
|
logger.Debug(fmt.Sprintf("wait '%s' before print", delay))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("wait delay: %w", ctx.Err())
|
||||||
|
case <-time.After(delay):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool, expression string) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
if disableJavaScript {
|
||||||
|
logger.Debug("JavaScript disabled, skipping wait expression")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if expression == "" {
|
||||||
|
logger.Debug("no wait expression")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wait until the evaluation of the expression is true or
|
||||||
|
// until the context is done.
|
||||||
|
logger.Debug(fmt.Sprintf("wait until '%s' is true before print", expression))
|
||||||
|
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return fmt.Errorf("context done while evaluating '%s': %w", expression, ctx.Err())
|
||||||
|
case <-ticker.C:
|
||||||
|
var ok bool
|
||||||
|
evaluate := chromedp.Evaluate(expression, &ok)
|
||||||
|
|
||||||
|
err := evaluate.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate: %v: %w", err, ErrInvalidEvaluationExpression)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
ticker.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Package gc provides a module for removing files and directories that have
|
|
||||||
// expired.
|
|
||||||
package gc
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
package gc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
gotenberg.MustRegisterModule(GarbageCollector{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GarbageCollector is a module for removing files and directories that have
|
|
||||||
// expired. It allows us to make sure that the application does not leak files
|
|
||||||
// or directories when running.
|
|
||||||
type GarbageCollector struct {
|
|
||||||
rootPath string
|
|
||||||
graceDuration time.Duration
|
|
||||||
excludeSubstr []string
|
|
||||||
|
|
||||||
ticker *time.Ticker
|
|
||||||
done chan bool
|
|
||||||
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// GarbageCollectorGraceDurationModifier is a module interface which allows to
|
|
||||||
// update the expiration time of files and directories parsed by the garbage
|
|
||||||
// collector. For instance, if the grace duration is 30s, the garbage collector
|
|
||||||
// will remove paths that have a modification time older than 30s. If there are
|
|
||||||
// many GarbageCollectorGraceDurationModifier, only the longest grace duration
|
|
||||||
// is selected.
|
|
||||||
type GarbageCollectorGraceDurationModifier interface {
|
|
||||||
GraceDuration() time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// GarbageCollectorExcludeSubstrModifier is a module interface which adds the
|
|
||||||
// given substrings to the exclude list of the garbage collector. If a path
|
|
||||||
// contains one of those substrings, the garbage collector ignores it.
|
|
||||||
type GarbageCollectorExcludeSubstrModifier interface {
|
|
||||||
ExcludeSubstr() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Descriptor returns a GarbageCollector's module descriptor.
|
|
||||||
func (gc GarbageCollector) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{
|
|
||||||
ID: "gc",
|
|
||||||
New: func() gotenberg.Module { return new(GarbageCollector) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provision sets the module properties.
|
|
||||||
func (gc *GarbageCollector) Provision(ctx *gotenberg.Context) error {
|
|
||||||
gc.rootPath = gotenberg.TmpPath()
|
|
||||||
|
|
||||||
graceDurationModifiers, err := ctx.Modules(new(GarbageCollectorGraceDurationModifier))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get grace duration modifiers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, graceDurationModifier := range graceDurationModifiers {
|
|
||||||
modifier := graceDurationModifier.(GarbageCollectorGraceDurationModifier)
|
|
||||||
|
|
||||||
if gc.graceDuration < modifier.GraceDuration() {
|
|
||||||
gc.graceDuration = modifier.GraceDuration()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
excludeSubstrModifiers, err := ctx.Modules(new(GarbageCollectorExcludeSubstrModifier))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get exclude substr modifiers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.excludeSubstr = strings.Split(os.Getenv("GC_EXCLUDE_SUBSTR"), ",")
|
|
||||||
|
|
||||||
for _, excludeSubstrModifier := range excludeSubstrModifiers {
|
|
||||||
modifier := excludeSubstrModifier.(GarbageCollectorExcludeSubstrModifier)
|
|
||||||
|
|
||||||
gc.excludeSubstr = append(gc.excludeSubstr, modifier.ExcludeSubstr()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get logger provider: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(gc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get logger: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.logger = logger
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the garbage collector.
|
|
||||||
func (gc *GarbageCollector) Start() error {
|
|
||||||
gc.ticker = time.NewTicker(gc.graceDuration + time.Duration(1)*time.Second)
|
|
||||||
gc.done = make(chan bool, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
func() {
|
|
||||||
gcMu.RLock()
|
|
||||||
defer gcMu.RUnlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-gc.done:
|
|
||||||
return
|
|
||||||
case <-gc.ticker.C:
|
|
||||||
gc.collect(false)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect parses the root path of the garbage collector and removes files or
|
|
||||||
// directories that have expired. It ignores the expiration date if the "force"
|
|
||||||
// argument is set to true.
|
|
||||||
func (gc GarbageCollector) collect(force bool) {
|
|
||||||
expirationTime := time.Now().Add(-gc.graceDuration)
|
|
||||||
|
|
||||||
// To make sure that the next Walk method stays on
|
|
||||||
// the root level of the considered path, we have to
|
|
||||||
// return a filepath.SkipDir error if the current path
|
|
||||||
// is a directory.
|
|
||||||
skipDirOrNil := func(info os.FileInfo) error {
|
|
||||||
if info.IsDir() {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
removePath := func(path string) {
|
|
||||||
err := os.RemoveAll(path)
|
|
||||||
if err != nil {
|
|
||||||
gc.logger.Error(fmt.Sprintf("remove '%s': %s", path, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.logger.Debug(fmt.Sprintf("'%s' removed", path))
|
|
||||||
}
|
|
||||||
|
|
||||||
err := filepath.Walk(gc.rootPath, func(path string, info os.FileInfo, pathErr error) error {
|
|
||||||
if pathErr != nil {
|
|
||||||
// For whatever reasons, the Walk method failed
|
|
||||||
// to process the current path.
|
|
||||||
return pathErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if path == gc.rootPath {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, substr := range gc.excludeSubstr {
|
|
||||||
if strings.Contains(info.Name(), substr) {
|
|
||||||
return skipDirOrNil(info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if force {
|
|
||||||
removePath(path)
|
|
||||||
|
|
||||||
return skipDirOrNil(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.ModTime().Before(expirationTime) {
|
|
||||||
removePath(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return skipDirOrNil(info)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
gc.logger.Error(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartupMessage returns an empty string.
|
|
||||||
func (gc GarbageCollector) StartupMessage() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the garbage collector.
|
|
||||||
func (gc *GarbageCollector) Stop(ctx context.Context) error {
|
|
||||||
_, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
return errors.New("no context dead line")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block until the context is done so that other module may gracefully stop
|
|
||||||
// before we do a shutdown cleanup. We skip this step if we receive a
|
|
||||||
// SIGINT in the meantime.
|
|
||||||
gc.logger.Debug("wait for the end of grace duration")
|
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(quit, os.Interrupt)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.ticker.Stop()
|
|
||||||
gc.done <- true
|
|
||||||
|
|
||||||
gc.logger.Debug("shutdown cleanup...")
|
|
||||||
gc.collect(true)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var gcMu sync.RWMutex
|
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*GarbageCollector)(nil)
|
|
||||||
_ gotenberg.Provisioner = (*GarbageCollector)(nil)
|
|
||||||
_ gotenberg.App = (*GarbageCollector)(nil)
|
|
||||||
)
|
|
||||||
@@ -1,468 +0,0 @@
|
|||||||
package gc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoValidator struct {
|
|
||||||
ProtoModule
|
|
||||||
validate func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoValidator) Validate() error {
|
|
||||||
return mod.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoGarbageCollectorGraceDurationModifier struct {
|
|
||||||
ProtoValidator
|
|
||||||
graceDuration func() time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoGarbageCollectorGraceDurationModifier) GraceDuration() time.Duration {
|
|
||||||
return mod.graceDuration()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoGarbageCollectorExcludeSubstrModifier struct {
|
|
||||||
ProtoValidator
|
|
||||||
excludeSubstr func() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoGarbageCollectorExcludeSubstrModifier) ExcludeSubstr() []string {
|
|
||||||
return mod.excludeSubstr()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoLoggerProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
logger func(mod gotenberg.Module) (*zap.Logger, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory ProtoLoggerProvider) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return factory.logger(mod)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGarbageCollector_Descriptor(t *testing.T) {
|
|
||||||
descriptor := GarbageCollector{}.Descriptor()
|
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
|
||||||
expect := reflect.TypeOf(new(GarbageCollector))
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGarbageCollector_Provision(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
ctx *gotenberg.Context
|
|
||||||
expectGraceDuration time.Duration
|
|
||||||
expectExcludeSubstr []string
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoGarbageCollectorGraceDurationModifier
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.validate = func() error { return errors.New("foo") }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
})
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoGarbageCollectorExcludeSubstrModifier
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.validate = func() error { return errors.New("foo") }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
})
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: gotenberg.NewContext(gotenberg.ParsedFlags{}, make([]gotenberg.ModuleDescriptor, 0)),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return nil, errors.New("foo") }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
})
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
})
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod1 := struct {
|
|
||||||
ProtoGarbageCollectorGraceDurationModifier
|
|
||||||
}{}
|
|
||||||
mod1.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
|
|
||||||
}
|
|
||||||
mod1.graceDuration = func() time.Duration { return time.Duration(10) * time.Second }
|
|
||||||
mod1.validate = func() error { return nil }
|
|
||||||
|
|
||||||
mod2 := struct {
|
|
||||||
ProtoGarbageCollectorGraceDurationModifier
|
|
||||||
}{}
|
|
||||||
mod2.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
|
|
||||||
}
|
|
||||||
mod2.graceDuration = func() time.Duration { return time.Duration(20) * time.Second }
|
|
||||||
mod2.validate = func() error { return nil }
|
|
||||||
|
|
||||||
mod3 := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod3.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }}
|
|
||||||
}
|
|
||||||
mod3.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
|
|
||||||
mod1.Descriptor(),
|
|
||||||
mod2.Descriptor(),
|
|
||||||
mod3.Descriptor(),
|
|
||||||
})
|
|
||||||
}(),
|
|
||||||
expectGraceDuration: time.Duration(20) * time.Second,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod1 := struct {
|
|
||||||
ProtoGarbageCollectorExcludeSubstrModifier
|
|
||||||
}{}
|
|
||||||
mod1.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
|
|
||||||
}
|
|
||||||
mod1.excludeSubstr = func() []string { return []string{"foo"} }
|
|
||||||
mod1.validate = func() error { return nil }
|
|
||||||
|
|
||||||
mod2 := struct {
|
|
||||||
ProtoGarbageCollectorExcludeSubstrModifier
|
|
||||||
}{}
|
|
||||||
mod2.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
|
|
||||||
}
|
|
||||||
mod2.excludeSubstr = func() []string { return []string{"bar"} }
|
|
||||||
mod2.validate = func() error { return nil }
|
|
||||||
|
|
||||||
mod3 := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod3.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }}
|
|
||||||
}
|
|
||||||
mod3.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(gotenberg.ParsedFlags{}, []gotenberg.ModuleDescriptor{
|
|
||||||
mod1.Descriptor(),
|
|
||||||
mod2.Descriptor(),
|
|
||||||
mod3.Descriptor(),
|
|
||||||
})
|
|
||||||
}(),
|
|
||||||
expectExcludeSubstr: func() []string {
|
|
||||||
expect := strings.Split(os.Getenv("GC_EXCLUDE_SUBSTR"), ",")
|
|
||||||
return append(expect, "foo", "bar")
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
mod := new(GarbageCollector)
|
|
||||||
err := mod.Provision(tc.ctx)
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectGraceDuration != 0 && tc.expectGraceDuration != mod.graceDuration {
|
|
||||||
t.Errorf("test %d: expected grace duration of '%s' but got '%s'", i, tc.expectGraceDuration, mod.graceDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectExcludeSubstr != nil && !reflect.DeepEqual(tc.expectExcludeSubstr, mod.excludeSubstr) {
|
|
||||||
t.Errorf("test %d: expected exclude substr '%s' but got '%s'", i, tc.expectExcludeSubstr, mod.excludeSubstr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGarbageCollector_Start(t *testing.T) {
|
|
||||||
mod := new(GarbageCollector)
|
|
||||||
mod.logger = zap.NewNop()
|
|
||||||
|
|
||||||
path, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.rootPath = path
|
|
||||||
|
|
||||||
err = mod.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(2) * time.Second)
|
|
||||||
mod.ticker.Stop()
|
|
||||||
mod.done <- true
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGarbageCollector_collect(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
gc *GarbageCollector
|
|
||||||
expectNotExists []string
|
|
||||||
expectExists []string
|
|
||||||
force bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
gc: func() *GarbageCollector {
|
|
||||||
mod := new(GarbageCollector)
|
|
||||||
mod.logger = zap.NewNop()
|
|
||||||
|
|
||||||
path, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.rootPath = path
|
|
||||||
|
|
||||||
err = os.WriteFile(path+"/foo", []byte{1}, 0755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.excludeSubstr = []string{
|
|
||||||
"foo",
|
|
||||||
}
|
|
||||||
|
|
||||||
return mod
|
|
||||||
}(),
|
|
||||||
expectExists: []string{
|
|
||||||
"/foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
gc: func() *GarbageCollector {
|
|
||||||
mod := new(GarbageCollector)
|
|
||||||
mod.logger = zap.NewNop()
|
|
||||||
|
|
||||||
path, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.rootPath = path
|
|
||||||
|
|
||||||
err = os.WriteFile(path+"/foo", []byte{1}, 0755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.MkdirAll(path+"/bar", 0755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mod
|
|
||||||
}(),
|
|
||||||
expectNotExists: []string{
|
|
||||||
"/foo",
|
|
||||||
"/bar",
|
|
||||||
},
|
|
||||||
force: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
gc: func() *GarbageCollector {
|
|
||||||
mod := new(GarbageCollector)
|
|
||||||
mod.logger = zap.NewNop()
|
|
||||||
mod.graceDuration = time.Duration(10) * time.Second
|
|
||||||
|
|
||||||
path, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.rootPath = path
|
|
||||||
|
|
||||||
err = os.WriteFile(path+"/foo", []byte{1}, 0755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newTime := time.Now().Add(-time.Duration(20) * time.Second)
|
|
||||||
err = os.Chtimes(path+"/foo", newTime, newTime)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(path+"/bar", []byte{1}, 0755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newTime = time.Now().Add(time.Duration(10) * time.Second)
|
|
||||||
err = os.Chtimes(path+"/bar", newTime, newTime)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mod
|
|
||||||
}(),
|
|
||||||
expectNotExists: []string{
|
|
||||||
"/foo",
|
|
||||||
},
|
|
||||||
expectExists: []string{
|
|
||||||
"/bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
tc.gc.collect(tc.force)
|
|
||||||
|
|
||||||
for _, name := range tc.expectNotExists {
|
|
||||||
path := tc.gc.rootPath + name
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
t.Errorf("test %d: expected '%s' not to exist but got: %v", i, path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range tc.expectExists {
|
|
||||||
path := tc.gc.rootPath + name
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
t.Errorf("test %d: expected '%s' to exist but got: %v", i, path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.RemoveAll(tc.gc.rootPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGarbageCollector_StartupMessage(t *testing.T) {
|
|
||||||
actual := new(GarbageCollector).StartupMessage()
|
|
||||||
expect := ""
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGarbageCollector_Stop(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
timeout time.Duration
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: time.Duration(1) * time.Nanosecond,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
mod := new(GarbageCollector)
|
|
||||||
mod.logger = zap.NewNop()
|
|
||||||
|
|
||||||
path, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.rootPath = path
|
|
||||||
|
|
||||||
err = mod.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.timeout == 0 {
|
|
||||||
err = mod.Stop(context.TODO())
|
|
||||||
} else {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = mod.Stop(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoValidator)(nil)
|
|
||||||
_ GarbageCollectorGraceDurationModifier = (*ProtoGarbageCollectorGraceDurationModifier)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoGarbageCollectorGraceDurationModifier)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoGarbageCollectorGraceDurationModifier)(nil)
|
|
||||||
_ GarbageCollectorExcludeSubstrModifier = (*ProtoGarbageCollectorExcludeSubstrModifier)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoGarbageCollectorExcludeSubstrModifier)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoGarbageCollectorExcludeSubstrModifier)(nil)
|
|
||||||
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
|
|
||||||
)
|
|
||||||
360
pkg/modules/libreoffice/api/api.go
Normal file
360
pkg/modules/libreoffice/api/api.go
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gotenberg.MustRegisterModule(new(Api))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidPdfFormat happens if the PDF format option cannot be handled
|
||||||
|
// by LibreOffice.
|
||||||
|
ErrInvalidPdfFormat = errors.New("invalid PDF format")
|
||||||
|
|
||||||
|
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
||||||
|
// interpreted by LibreOffice.
|
||||||
|
ErrMalformedPageRanges = errors.New("page ranges are malformed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Api is a module which provides a [Uno] to interact with LibreOffice.
|
||||||
|
type Api struct {
|
||||||
|
autoStart bool
|
||||||
|
args libreOfficeArguments
|
||||||
|
|
||||||
|
logger *zap.Logger
|
||||||
|
libreOffice libreOffice
|
||||||
|
supervisor gotenberg.ProcessSupervisor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options gathers available options when converting a document to PDF.
|
||||||
|
type Options struct {
|
||||||
|
// Landscape allows to change the orientation of the resulting PDF.
|
||||||
|
// Optional.
|
||||||
|
Landscape bool
|
||||||
|
|
||||||
|
// PageRanges allows to select the pages to convert.
|
||||||
|
// Optional.
|
||||||
|
PageRanges string
|
||||||
|
|
||||||
|
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
|
||||||
|
// PDF/A-3b and PDF/UA.
|
||||||
|
// Optional.
|
||||||
|
PdfFormats gotenberg.PdfFormats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uno is an abstraction on top of the Universal Network Objects API.
|
||||||
|
type Uno interface {
|
||||||
|
Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||||
|
Extensions() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is a module interface which exposes a method for creating a
|
||||||
|
// [Uno] for other modules.
|
||||||
|
//
|
||||||
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
|
// provider, _ := ctx.Module(new(libreofficeapi.Provider))
|
||||||
|
// libreOffice, _ := provider.(api.Provider).LibreOffice()
|
||||||
|
// }
|
||||||
|
type Provider interface {
|
||||||
|
LibreOffice() (Uno, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor returns a [Api]'s module descriptor.
|
||||||
|
func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{
|
||||||
|
ID: "libreoffice-api",
|
||||||
|
FlagSet: func() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("api", flag.ExitOnError)
|
||||||
|
fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. 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.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
New: func() gotenberg.Module { return new(Api) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets the module properties.
|
||||||
|
func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||||
|
flags := ctx.ParsedFlags()
|
||||||
|
a.autoStart = flags.MustBool("libreoffice-auto-start")
|
||||||
|
|
||||||
|
libreOfficeBinPath, ok := os.LookupEnv("LIBREOFFICE_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("LIBREOFFICE_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
unoBinPath, ok := os.LookupEnv("UNOCONVERTER_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("UNOCONVERTER_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.args = libreOfficeArguments{
|
||||||
|
binPath: libreOfficeBinPath,
|
||||||
|
unoBinPath: unoBinPath,
|
||||||
|
startTimeout: flags.MustDuration("libreoffice-start-timeout"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger.
|
||||||
|
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger provider: %w", err)
|
||||||
|
}
|
||||||
|
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(a)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger: %w", err)
|
||||||
|
}
|
||||||
|
a.logger = logger.Named("libreoffice")
|
||||||
|
|
||||||
|
// Process.
|
||||||
|
a.libreOffice = newLibreOfficeProcess(a.args)
|
||||||
|
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustInt64("libreoffice-restart-after"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the module properties.
|
||||||
|
func (a *Api) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, statErr := os.Stat(a.args.binPath)
|
||||||
|
if os.IsNotExist(statErr) {
|
||||||
|
err = multierr.Append(err, fmt.Errorf("LibreOffice binary path does not exist: %w", statErr))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, statErr = os.Stat(a.args.unoBinPath)
|
||||||
|
if os.IsNotExist(statErr) {
|
||||||
|
err = multierr.Append(err, fmt.Errorf("unoconverter binary path does not exist: %w", statErr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start does nothing if auto-start is not enabled. Otherwise, it starts a
|
||||||
|
// LibreOffice instance.
|
||||||
|
func (a *Api) Start() error {
|
||||||
|
if !a.autoStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.supervisor.Launch()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("launch supervisor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartupMessage returns a custom startup message.
|
||||||
|
func (a *Api) StartupMessage() string {
|
||||||
|
if !a.autoStart {
|
||||||
|
return "LibreOffice ready to start"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "LibreOffice automatically started"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the current browser instance.
|
||||||
|
func (a *Api) Stop(ctx context.Context) error {
|
||||||
|
// Block until the context is done so that other module may gracefully stop
|
||||||
|
// before we do a shutdown.
|
||||||
|
a.logger.Debug("wait for the end of grace duration")
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
err := a.supervisor.Shutdown()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("stop LibreOffice: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics returns the metrics.
|
||||||
|
func (a *Api) Metrics() ([]gotenberg.Metric, error) {
|
||||||
|
return []gotenberg.Metric{
|
||||||
|
{
|
||||||
|
Name: "libreoffice_requests_queue_size",
|
||||||
|
Description: "Current number of LibreOffice conversion requests waiting to be treated.",
|
||||||
|
Read: func() float64 {
|
||||||
|
return float64(a.supervisor.ReqQueueSize())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "libreoffice_restarts_count",
|
||||||
|
Description: "Current number of LibreOffice restarts.",
|
||||||
|
Read: func() float64 {
|
||||||
|
return float64(a.supervisor.RestartsCount())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks adds a health check that verifies if LibreOffice is healthy.
|
||||||
|
func (a *Api) Checks() ([]health.CheckerOption, error) {
|
||||||
|
return []health.CheckerOption{
|
||||||
|
health.WithCheck(health.Check{
|
||||||
|
Name: "libreoffice",
|
||||||
|
Check: func(_ context.Context) error {
|
||||||
|
if a.supervisor.Healthy() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("LibreOffice is unhealthy")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ready returns no error if the module is ready.
|
||||||
|
func (a *Api) Ready() error {
|
||||||
|
if !a.autoStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), a.args.startTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return fmt.Errorf("context done while waiting for LibreOffice to be ready: %w", ctx.Err())
|
||||||
|
case <-ticker.C:
|
||||||
|
ok := a.libreOffice.Healthy(a.logger)
|
||||||
|
if ok {
|
||||||
|
ticker.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LibreOffice returns a [Uno] for interacting with LibreOffice.
|
||||||
|
func (a *Api) LibreOffice() (Uno, error) {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pdf converts a document to PDF.
|
||||||
|
func (a *Api) Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
return a.supervisor.Run(ctx, logger, func() error {
|
||||||
|
return a.libreOffice.pdf(ctx, logger, inputPath, outputPath, options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions returns the file extensions available for conversions.
|
||||||
|
// FIXME: don't care, take all on the route level?
|
||||||
|
func (a *Api) Extensions() []string {
|
||||||
|
return []string{
|
||||||
|
".bib",
|
||||||
|
".doc",
|
||||||
|
".xml",
|
||||||
|
".docx",
|
||||||
|
".fodt",
|
||||||
|
".html",
|
||||||
|
".ltx",
|
||||||
|
".txt",
|
||||||
|
".odt",
|
||||||
|
".ott",
|
||||||
|
".pdb",
|
||||||
|
".pdf",
|
||||||
|
".psw",
|
||||||
|
".rtf",
|
||||||
|
".sdw",
|
||||||
|
".stw",
|
||||||
|
".sxw",
|
||||||
|
".uot",
|
||||||
|
".vor",
|
||||||
|
".wps",
|
||||||
|
".epub",
|
||||||
|
".png",
|
||||||
|
".bmp",
|
||||||
|
".emf",
|
||||||
|
".eps",
|
||||||
|
".fodg",
|
||||||
|
".gif",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".met",
|
||||||
|
".odd",
|
||||||
|
".otg",
|
||||||
|
".pbm",
|
||||||
|
".pct",
|
||||||
|
".pgm",
|
||||||
|
".ppm",
|
||||||
|
".ras",
|
||||||
|
".std",
|
||||||
|
".svg",
|
||||||
|
".svm",
|
||||||
|
".swf",
|
||||||
|
".sxd",
|
||||||
|
".sxw",
|
||||||
|
".tif",
|
||||||
|
".tiff",
|
||||||
|
".xhtml",
|
||||||
|
".xpm",
|
||||||
|
".odp",
|
||||||
|
".fodp",
|
||||||
|
".potm",
|
||||||
|
".pot",
|
||||||
|
".pptx",
|
||||||
|
".pps",
|
||||||
|
".ppt",
|
||||||
|
".pwp",
|
||||||
|
".sda",
|
||||||
|
".sdd",
|
||||||
|
".sti",
|
||||||
|
".sxi",
|
||||||
|
".uop",
|
||||||
|
".wmf",
|
||||||
|
".csv",
|
||||||
|
".dbf",
|
||||||
|
".dif",
|
||||||
|
".fods",
|
||||||
|
".ods",
|
||||||
|
".ots",
|
||||||
|
".pxl",
|
||||||
|
".sdc",
|
||||||
|
".slk",
|
||||||
|
".stc",
|
||||||
|
".sxc",
|
||||||
|
".uos",
|
||||||
|
".xls",
|
||||||
|
".xlt",
|
||||||
|
".xlsx",
|
||||||
|
".odg",
|
||||||
|
".dotx",
|
||||||
|
".xltx",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*Api)(nil)
|
||||||
|
_ gotenberg.Provisioner = (*Api)(nil)
|
||||||
|
_ gotenberg.Validator = (*Api)(nil)
|
||||||
|
_ gotenberg.App = (*Api)(nil)
|
||||||
|
_ gotenberg.MetricsProvider = (*Api)(nil)
|
||||||
|
_ api.HealthChecker = (*Api)(nil)
|
||||||
|
_ Uno = (*Api)(nil)
|
||||||
|
_ Provider = (*Api)(nil)
|
||||||
|
)
|
||||||
458
pkg/modules/libreoffice/api/api_test.go
Normal file
458
pkg/modules/libreoffice/api/api_test.go
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApi_Descriptor(t *testing.T) {
|
||||||
|
descriptor := new(Api).Descriptor()
|
||||||
|
|
||||||
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
|
expect := reflect.TypeOf(new(Api))
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApi_Provision(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *gotenberg.Context
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no logger provider",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(Api).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no logger from logger provider",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(Api).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "provision success",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
mod := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
|
}
|
||||||
|
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(Api).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
mod.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
err := a.Provision(tc.ctx)
|
||||||
|
|
||||||
|
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 TestApi_Validate(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
binPath string
|
||||||
|
unoBinPath string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "empty LibreOffice bin path",
|
||||||
|
binPath: "",
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "LibreOffice bin path does not exist",
|
||||||
|
binPath: "/foo",
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "empty uno bin path",
|
||||||
|
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||||
|
unoBinPath: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "uno bin path does not exist",
|
||||||
|
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||||
|
unoBinPath: "/foo",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "validate success",
|
||||||
|
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.args = libreOfficeArguments{
|
||||||
|
binPath: tc.binPath,
|
||||||
|
unoBinPath: tc.unoBinPath,
|
||||||
|
}
|
||||||
|
err := a.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 TestApi_Start(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
autoStart bool
|
||||||
|
supervisor *gotenberg.ProcessSupervisorMock
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no auto-start",
|
||||||
|
autoStart: false,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start success",
|
||||||
|
autoStart: true,
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start failed",
|
||||||
|
autoStart: true,
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.autoStart = tc.autoStart
|
||||||
|
a.supervisor = tc.supervisor
|
||||||
|
|
||||||
|
err := a.Start()
|
||||||
|
|
||||||
|
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 TestApi_StartupMessage(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
|
||||||
|
a.autoStart = true
|
||||||
|
autoStartMsg := a.StartupMessage()
|
||||||
|
|
||||||
|
a.autoStart = false
|
||||||
|
noAutoStartMsg := a.StartupMessage()
|
||||||
|
|
||||||
|
if autoStartMsg == noAutoStartMsg {
|
||||||
|
t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApi_Stop(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
supervisor *gotenberg.ProcessSupervisorMock
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "stop success",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "stop failed",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.logger = zap.NewNop()
|
||||||
|
a.supervisor = tc.supervisor
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := a.Stop(ctx)
|
||||||
|
|
||||||
|
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 TestApi_Metrics(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.supervisor = &gotenberg.ProcessSupervisorMock{
|
||||||
|
ReqQueueSizeMock: func() int64 {
|
||||||
|
return 10
|
||||||
|
},
|
||||||
|
RestartsCountMock: func() int64 {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics, err := a.Metrics()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(metrics) != 2 {
|
||||||
|
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := metrics[0].Read()
|
||||||
|
if actual != float64(10) {
|
||||||
|
t.Errorf("expected %f for libreoffice_requests_queue_size, but got %f", float64(10), actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual = metrics[1].Read()
|
||||||
|
if actual != float64(0) {
|
||||||
|
t.Errorf("expected %f for libreoffice_restarts_count, but got %f", float64(0), actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApi_Checks(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
supervisor gotenberg.ProcessSupervisor
|
||||||
|
expectAvailabilityStatus health.AvailabilityStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "healthy module",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||||
|
return true
|
||||||
|
}},
|
||||||
|
expectAvailabilityStatus: health.StatusUp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "unhealthy module",
|
||||||
|
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||||
|
return false
|
||||||
|
}},
|
||||||
|
expectAvailabilityStatus: health.StatusDown,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.supervisor = tc.supervisor
|
||||||
|
|
||||||
|
checks, err := a.Checks()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checker := health.NewChecker(checks...)
|
||||||
|
result := checker.Check(context.Background())
|
||||||
|
|
||||||
|
if result.Status != tc.expectAvailabilityStatus {
|
||||||
|
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChromium_Ready(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
autoStart bool
|
||||||
|
startTimeout time.Duration
|
||||||
|
libreOffice libreOffice
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "no auto-start",
|
||||||
|
autoStart: false,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start: context done",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(200) * time.Millisecond,
|
||||||
|
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return false
|
||||||
|
}}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "auto-start success",
|
||||||
|
autoStart: true,
|
||||||
|
startTimeout: time.Duration(30) * time.Second,
|
||||||
|
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||||
|
return true
|
||||||
|
}}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.autoStart = tc.autoStart
|
||||||
|
a.args = libreOfficeArguments{startTimeout: tc.startTimeout}
|
||||||
|
a.libreOffice = tc.libreOffice
|
||||||
|
|
||||||
|
err := a.Ready()
|
||||||
|
|
||||||
|
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 TestApi_LibreOffice(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
|
||||||
|
_, err := a.LibreOffice()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApi_Pdf(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
supervisor gotenberg.ProcessSupervisor
|
||||||
|
libreOffice libreOffice
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "PDF task success",
|
||||||
|
libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF task error",
|
||||||
|
libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||||
|
return errors.New("PDF task error")
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
a.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
|
return task()
|
||||||
|
}}
|
||||||
|
a.libreOffice = tc.libreOffice
|
||||||
|
|
||||||
|
err := a.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 TestApi_Extensions(t *testing.T) {
|
||||||
|
a := new(Api)
|
||||||
|
extensions := a.Extensions()
|
||||||
|
|
||||||
|
actual := len(extensions)
|
||||||
|
expect := 79
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected %d extensions, but got %d", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
pkg/modules/libreoffice/api/doc.go
Normal file
3
pkg/modules/libreoffice/api/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package api provides a module which manages a LibreOffice instance and
|
||||||
|
// interacts with it via the UNO (Universal Network Objects) API.
|
||||||
|
package api
|
||||||
31
pkg/modules/libreoffice/api/freeport.go
Normal file
31
pkg/modules/libreoffice/api/freeport.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func freePort(logger *zap.Logger) (int, error) {
|
||||||
|
netListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := netListener.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close network listener: %s", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
addr := netListener.Addr().String()
|
||||||
|
|
||||||
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("get free port from host: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Atoi(portStr)
|
||||||
|
}
|
||||||
395
pkg/modules/libreoffice/api/libreoffice.go
Normal file
395
pkg/modules/libreoffice/api/libreoffice.go
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type libreOffice interface {
|
||||||
|
gotenberg.Process
|
||||||
|
pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type libreOfficeArguments struct {
|
||||||
|
binPath string
|
||||||
|
unoBinPath string
|
||||||
|
startTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type libreOfficeProcess struct {
|
||||||
|
socketPort int
|
||||||
|
userProfileDirPath string
|
||||||
|
cmd *gotenberg.Cmd
|
||||||
|
cfgMu sync.RWMutex
|
||||||
|
isStarted atomic.Bool
|
||||||
|
|
||||||
|
arguments libreOfficeArguments
|
||||||
|
fs *gotenberg.FileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLibreOfficeProcess(arguments libreOfficeArguments) libreOffice {
|
||||||
|
p := &libreOfficeProcess{
|
||||||
|
arguments: arguments,
|
||||||
|
fs: gotenberg.NewFileSystem(),
|
||||||
|
}
|
||||||
|
p.isStarted.Store(false)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *libreOfficeProcess) Start(logger *zap.Logger) error {
|
||||||
|
if p.isStarted.Load() {
|
||||||
|
return errors.New("LibreOffice is already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := freePort(logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get free port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userProfileDirPath := p.fs.NewDirPath()
|
||||||
|
args := []string{
|
||||||
|
"--headless",
|
||||||
|
"--invisible",
|
||||||
|
"--nocrashreport",
|
||||||
|
"--nodefault",
|
||||||
|
"--nologo",
|
||||||
|
"--nofirststartwizard",
|
||||||
|
"--norestore",
|
||||||
|
fmt.Sprintf("-env:UserInstallation=file://%s", userProfileDirPath),
|
||||||
|
fmt.Sprintf("--accept=socket,host=127.0.0.1,port=%d,tcpNoDelay=1;urp;StarOffice.ComponentContext", port),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), p.arguments.startTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.binPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create LibreOffice command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For whatever reason, LibreOffice requires a first start before being
|
||||||
|
// able to run as a daemon.
|
||||||
|
exitCode, err := cmd.Exec()
|
||||||
|
if err != nil && exitCode != 81 {
|
||||||
|
return fmt.Errorf("execute LibreOffice: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("got exit code 81, e.g., LibreOffice first start")
|
||||||
|
|
||||||
|
// Second start (daemon).
|
||||||
|
cmd = gotenberg.Command(logger, p.arguments.binPath, args...)
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start LibreOffice: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitChan := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// By waiting the process, we avoid the creation of a zombie process
|
||||||
|
// and make sure we catch an early exit if any.
|
||||||
|
waitChan <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
connChan := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// As the LibreOffice socket may take some time to be available, we
|
||||||
|
// have to ensure that it is indeed accepting connections.
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
connChan <- ctx.Err()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Duration(1)*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
connChan <- nil
|
||||||
|
err = conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("close connection after health checking the LibreOffice: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var success bool
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if success {
|
||||||
|
p.cfgMu.Lock()
|
||||||
|
defer p.cfgMu.Unlock()
|
||||||
|
|
||||||
|
p.socketPort = port
|
||||||
|
p.userProfileDirPath = userProfileDirPath
|
||||||
|
p.cmd = cmd
|
||||||
|
p.isStarted.Store(true)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's make sure the process is killed.
|
||||||
|
err = cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("kill LibreOffice process: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the user profile directory is deleted.
|
||||||
|
err = os.RemoveAll(userProfileDirPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("remove LibreOffice's user profile directory: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("'%s' LibreOffice's user profile directory removed", userProfileDirPath))
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Debug("waiting for the LibreOffice socket to be available...")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err = <-connChan:
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("LibreOffice socket not available: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("LibreOffice socket available")
|
||||||
|
success = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
case err = <-waitChan:
|
||||||
|
return fmt.Errorf("LibreOffice process exited: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *libreOfficeProcess) Stop(logger *zap.Logger) error {
|
||||||
|
if !p.isStarted.Load() {
|
||||||
|
// No big deal? Like calling cancel twice.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always remove the user profile directory created by LibreOffice.
|
||||||
|
copyUserProfileDirPath := p.userProfileDirPath
|
||||||
|
defer func(userProfileDirPath string) {
|
||||||
|
go func() {
|
||||||
|
err := os.RemoveAll(userProfileDirPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("remove LibreOffice's user profile directory: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("'%s' LibreOffice's user profile directory removed", userProfileDirPath))
|
||||||
|
|
||||||
|
// Also remove LibreOffice specific files in the temporary directory.
|
||||||
|
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{"OSL_PIPE", ".tmp"})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}(copyUserProfileDirPath)
|
||||||
|
|
||||||
|
p.cfgMu.Lock()
|
||||||
|
defer p.cfgMu.Unlock()
|
||||||
|
|
||||||
|
err := p.cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("kill LibreOffice process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.socketPort = 0
|
||||||
|
p.userProfileDirPath = ""
|
||||||
|
p.cmd = nil
|
||||||
|
p.isStarted.Store(false)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *libreOfficeProcess) Healthy(logger *zap.Logger) bool {
|
||||||
|
// Good to know: the supervisor does not call this method if no first start
|
||||||
|
// or if the process is restarting.
|
||||||
|
|
||||||
|
if !p.isStarted.Load() {
|
||||||
|
// Non-started browser but not restarting?
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cfgMu.RLock()
|
||||||
|
defer p.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", p.socketPort), time.Duration(10)*time.Second)
|
||||||
|
if err == nil {
|
||||||
|
err = conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("close connection after health checking LibreOffice: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
if !p.isStarted.Load() {
|
||||||
|
return errors.New("LibreOffice not started, cannot handle PDF conversion")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--no-launch",
|
||||||
|
"--format",
|
||||||
|
"pdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "--port", fmt.Sprintf("%d", p.socketPort))
|
||||||
|
|
||||||
|
checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
|
||||||
|
if checkedEntry != nil {
|
||||||
|
args = append(args, "-vvv")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Landscape {
|
||||||
|
args = append(args, "--printer", "PaperOrientation=landscape")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.PageRanges != "" {
|
||||||
|
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch options.PdfFormats.PdfA {
|
||||||
|
case "":
|
||||||
|
case gotenberg.PdfA1b:
|
||||||
|
args = append(args, "--export", "SelectPdfVersion=1")
|
||||||
|
case gotenberg.PdfA2b:
|
||||||
|
args = append(args, "--export", "SelectPdfVersion=2")
|
||||||
|
case gotenberg.PdfA3b:
|
||||||
|
args = append(args, "--export", "SelectPdfVersion=3")
|
||||||
|
default:
|
||||||
|
return ErrInvalidPdfFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.PdfFormats.PdfUa {
|
||||||
|
args = append(
|
||||||
|
args,
|
||||||
|
"--export", "EnableTextAccessForAccessibilityTools=true",
|
||||||
|
"--export", "UseTaggedPDF=true",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath, err := nonBasicLatinCharactersGuard(logger, inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("non-basic latin characters guard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "--output", outputPath, inputPath)
|
||||||
|
|
||||||
|
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.unoBinPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create uno command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("print to PDF with: %+v", options))
|
||||||
|
|
||||||
|
exitCode, err := cmd.Exec()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LibreOffice's errors are not explicit.
|
||||||
|
// That's why we have to make an educated guess according to the exit code
|
||||||
|
// and given inputs.
|
||||||
|
if exitCode == 5 && options.PageRanges != "" {
|
||||||
|
return ErrMalformedPageRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possible errors:
|
||||||
|
// 1. LibreOffice failed for some reason.
|
||||||
|
// 2. Context done.
|
||||||
|
//
|
||||||
|
// On the second scenario, LibreOffice might not have time to remove some
|
||||||
|
// of its temporary files, as it has been killed without warning. The
|
||||||
|
// garbage collector will delete them for us (if the module is loaded).
|
||||||
|
return fmt.Errorf("convert to PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LibreOffice cannot convert a file with a name containing non-basic Latin
|
||||||
|
// characters.
|
||||||
|
// See:
|
||||||
|
// https://github.com/gotenberg/gotenberg/issues/104
|
||||||
|
// https://github.com/gotenberg/gotenberg/issues/730
|
||||||
|
func nonBasicLatinCharactersGuard(logger *zap.Logger, inputPath string) (string, error) {
|
||||||
|
hasNonBasicLatinChars := func(str string) bool {
|
||||||
|
for _, r := range str {
|
||||||
|
// Check if the character is outside basic Latin.
|
||||||
|
if r != '.' && (r < ' ' || r > '~') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := filepath.Base(inputPath)
|
||||||
|
if !hasNonBasicLatinChars(filename) {
|
||||||
|
logger.Debug("no non-basic latin characters in filename, skip copy")
|
||||||
|
return inputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warn("non-basic latin characters in filename, copy to a file with a valid filename")
|
||||||
|
basePath := filepath.Dir(inputPath)
|
||||||
|
ext := filepath.Ext(inputPath)
|
||||||
|
newInputPath := filepath.Join(basePath, fmt.Sprintf("%s%s", uuid.NewString(), ext))
|
||||||
|
|
||||||
|
in, err := os.Open(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("open file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := in.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close file: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := os.Create(newInputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create new file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := out.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close new file: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("copy file to new file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newInputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Process = (*libreOfficeProcess)(nil)
|
||||||
|
_ libreOffice = (*libreOfficeProcess)(nil)
|
||||||
|
)
|
||||||
650
pkg/modules/libreoffice/api/libreoffice_test.go
Normal file
650
pkg/modules/libreoffice/api/libreoffice_test.go
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLibreOfficeProcess_Start(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
libreOffice libreOffice
|
||||||
|
expectError bool
|
||||||
|
cleanup bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "successful start",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectError: false,
|
||||||
|
cleanup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "LibreOffice already started",
|
||||||
|
libreOffice: func() libreOffice {
|
||||||
|
p := new(libreOfficeProcess)
|
||||||
|
p.isStarted.Store(true)
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
cleanup: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "non-exit code 81 on first start",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: "foo",
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectError: true,
|
||||||
|
cleanup: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
err := tc.libreOffice.Start(logger)
|
||||||
|
|
||||||
|
if tc.cleanup {
|
||||||
|
defer func(p libreOffice, logger *zap.Logger) {
|
||||||
|
err = p.Stop(logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||||
|
}
|
||||||
|
}(tc.libreOffice, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestLibreOfficeProcess_Stop(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
libreOffice libreOffice
|
||||||
|
setup func(libreOffice libreOffice, logger *zap.Logger) error
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "successful stop",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
setup: func(p libreOffice, logger *zap.Logger) error {
|
||||||
|
return p.Start(logger)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "LibreOffice already stopped",
|
||||||
|
libreOffice: func() libreOffice {
|
||||||
|
p := new(libreOfficeProcess)
|
||||||
|
p.isStarted.Store(false)
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
if tc.setup != nil {
|
||||||
|
err := tc.setup(tc.libreOffice, logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("setup error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tc.libreOffice.Stop(logger)
|
||||||
|
|
||||||
|
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 TestLibreOfficeProcess_Healthy(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
libreOffice libreOffice
|
||||||
|
setup func(libreOffice libreOffice, logger *zap.Logger) error
|
||||||
|
expectHealthy bool
|
||||||
|
cleanup bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "healthy LibreOffice",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
setup: func(p libreOffice, logger *zap.Logger) error {
|
||||||
|
return p.Start(logger)
|
||||||
|
},
|
||||||
|
expectHealthy: true,
|
||||||
|
cleanup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "LibreOffice not started",
|
||||||
|
libreOffice: func() libreOffice {
|
||||||
|
p := new(libreOfficeProcess)
|
||||||
|
p.isStarted.Store(false)
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
expectHealthy: false,
|
||||||
|
cleanup: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "unhealthy LibreOffice",
|
||||||
|
libreOffice: func() libreOffice {
|
||||||
|
p := new(libreOfficeProcess)
|
||||||
|
p.isStarted.Store(true)
|
||||||
|
p.socketPort = 12345
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
expectHealthy: false,
|
||||||
|
cleanup: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
|
||||||
|
if tc.setup != nil {
|
||||||
|
err := tc.setup(tc.libreOffice, logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("setup error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.cleanup {
|
||||||
|
defer func(p libreOffice, logger *zap.Logger) {
|
||||||
|
err := p.Stop(logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||||
|
}
|
||||||
|
}(tc.libreOffice, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
healthy := tc.libreOffice.Healthy(logger)
|
||||||
|
|
||||||
|
if !tc.expectHealthy && healthy {
|
||||||
|
t.Fatal("expected unhealthy LibreOffice but got an healthy one")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectHealthy && !healthy {
|
||||||
|
t.Fatal("expected a healthy LibreOffice but got an unhealthy one")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
libreOffice libreOffice
|
||||||
|
fs *gotenberg.FileSystem
|
||||||
|
options Options
|
||||||
|
cancelledCtx bool
|
||||||
|
start bool
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "LibreOffice not started",
|
||||||
|
libreOffice: func() libreOffice {
|
||||||
|
p := new(libreOfficeProcess)
|
||||||
|
p.isStarted.Store(false)
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
fs: gotenberg.NewFileSystem(),
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: false,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrInvalidPdfFormat",
|
||||||
|
libreOffice: func() libreOffice {
|
||||||
|
p := new(libreOfficeProcess)
|
||||||
|
p.socketPort = 12345
|
||||||
|
p.isStarted.Store(true)
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
fs: gotenberg.NewFileSystem(),
|
||||||
|
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: "foo"}},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: false,
|
||||||
|
expectError: true,
|
||||||
|
expectedError: ErrInvalidPdfFormat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrMalformedPageRanges",
|
||||||
|
libreOffice: newLibreOfficeProcess(
|
||||||
|
libreOfficeArguments{
|
||||||
|
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||||
|
startTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
options: Options{PageRanges: "foo"},
|
||||||
|
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("ErrMalformedPageRanges"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: true,
|
||||||
|
expectedError: ErrMalformedPageRanges,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "context done",
|
||||||
|
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("Context done"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
cancelledCtx: true,
|
||||||
|
start: true,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (default options)",
|
||||||
|
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("Success"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (landscape)",
|
||||||
|
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("Landscape"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{Landscape: true},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (page ranges)",
|
||||||
|
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("Landscape"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{PageRanges: "1-1"},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (PDF/A-1b)",
|
||||||
|
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("Landscape"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b}},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (PDF/A-2b)",
|
||||||
|
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("Landscape"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA2b}},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (PDF/A-3b)",
|
||||||
|
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("Landscape"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA3b}},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (PDF/UA)",
|
||||||
|
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("Landscape"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
options: Options{PdfFormats: gotenberg.PdfFormats{PdfUa: true}},
|
||||||
|
cancelledCtx: false,
|
||||||
|
start: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
// Force the debug level.
|
||||||
|
logger := zap.NewExample()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(tc.fs.WorkingDirPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if tc.start {
|
||||||
|
err := tc.libreOffice.Start(logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("setup error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(p libreOffice, logger *zap.Logger) {
|
||||||
|
err = p.Stop(logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||||
|
}
|
||||||
|
}(tc.libreOffice, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(5)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if tc.cancelledCtx {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tc.libreOffice.pdf(
|
||||||
|
ctx,
|
||||||
|
logger,
|
||||||
|
fmt.Sprintf("%s/document.txt", tc.fs.WorkingDirPath()),
|
||||||
|
fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()),
|
||||||
|
tc.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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonBasicLatinCharactersGuard(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
fs *gotenberg.FileSystem
|
||||||
|
filename string
|
||||||
|
expectSameInputPath bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "basic latin characters",
|
||||||
|
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("Basic latin characters"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
filename: "document.txt",
|
||||||
|
expectSameInputPath: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "non-basic latin characters",
|
||||||
|
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/éèßàùä.txt", fs.WorkingDirPath()), []byte("Non-basic latin characters"), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
filename: "éèßàùä.txt",
|
||||||
|
expectSameInputPath: false,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(tc.fs.WorkingDirPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
inputPath := fmt.Sprintf("%s/%s", tc.fs.WorkingDirPath(), tc.filename)
|
||||||
|
newInputPath, err := nonBasicLatinCharactersGuard(
|
||||||
|
zap.NewNop(),
|
||||||
|
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.expectSameInputPath && newInputPath != inputPath {
|
||||||
|
t.Fatalf("expected same input path, but got '%s'", newInputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectSameInputPath && newInputPath == inputPath {
|
||||||
|
t.Fatalf("expected different input path, but got same '%s'", newInputPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
49
pkg/modules/libreoffice/api/mocks.go
Normal file
49
pkg/modules/libreoffice/api/mocks.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApiMock is a mock for the [Uno] interface.
|
||||||
|
type ApiMock struct {
|
||||||
|
PdfMock func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||||
|
ExtensionsMock func() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
return api.PdfMock(ctx, logger, inputPath, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *ApiMock) Extensions() []string {
|
||||||
|
return api.ExtensionsMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProviderMock is a mock for the [Provider] interface.
|
||||||
|
type ProviderMock struct {
|
||||||
|
LibreOfficeMock func() (Uno, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ProviderMock) LibreOffice() (Uno, error) {
|
||||||
|
return provider.LibreOfficeMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// libreOfficeMock is a mock for the [libreOffice] interface.
|
||||||
|
type libreOfficeMock struct {
|
||||||
|
gotenberg.ProcessMock
|
||||||
|
pdfMock func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *libreOfficeMock) pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
return b.pdfMock(ctx, logger, inputPath, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ Uno = (*ApiMock)(nil)
|
||||||
|
_ Provider = (*ProviderMock)(nil)
|
||||||
|
_ libreOffice = (*libreOfficeMock)(nil)
|
||||||
|
)
|
||||||
55
pkg/modules/libreoffice/api/mocks_test.go
Normal file
55
pkg/modules/libreoffice/api/mocks_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApiMock(t *testing.T) {
|
||||||
|
mock := &ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := mock.Extensions()
|
||||||
|
if ext != nil {
|
||||||
|
t.Errorf("expected nil result from ApiMock.Extensions, but got: %v", ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderMock(t *testing.T) {
|
||||||
|
mock := &ProviderMock{
|
||||||
|
LibreOfficeMock: func() (Uno, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.LibreOffice()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ProviderMock.LibreOffice, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficeMock(t *testing.T) {
|
||||||
|
mock := &libreOfficeMock{
|
||||||
|
pdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from libreOfficeMock.pdf, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
// Package libreoffice provides a module which adds a route for converting
|
// Package libreoffice provides a module which adds a route for converting
|
||||||
// document to PDF with LibreOffice.
|
// documents to PDF with LibreOffice.
|
||||||
package libreoffice
|
package libreoffice
|
||||||
|
|||||||
@@ -3,26 +3,27 @@ package libreoffice
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
|
libeofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gotenberg.MustRegisterModule(LibreOffice{})
|
gotenberg.MustRegisterModule(new(LibreOffice))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LibreOffice is a module which provides a route for converting documents to
|
// LibreOffice is a module which provides a route for converting documents to
|
||||||
// PDF with LibreOffice.
|
// PDF with LibreOffice.
|
||||||
type LibreOffice struct {
|
type LibreOffice struct {
|
||||||
unoconv unoconv.API
|
api libeofficeapi.Uno
|
||||||
engine gotenberg.PDFEngine
|
engine gotenberg.PdfEngine
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a LibreOffice's module descriptor.
|
// Descriptor returns a [LibreOffice]'s module descriptor.
|
||||||
func (LibreOffice) Descriptor() gotenberg.ModuleDescriptor {
|
func (mod *LibreOffice) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "libreoffice",
|
ID: "libreoffice",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
@@ -40,24 +41,24 @@ func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
|
|||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
mod.disableRoutes = flags.MustBool("libreoffice-disable-routes")
|
mod.disableRoutes = flags.MustBool("libreoffice-disable-routes")
|
||||||
|
|
||||||
provider, err := ctx.Module(new(unoconv.Provider))
|
provider, err := ctx.Module(new(libeofficeapi.Provider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv provider: %w", err)
|
return fmt.Errorf("get LibreOffice Uno provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uno, err := provider.(unoconv.Provider).Unoconv()
|
libreOfficeApi, err := provider.(libeofficeapi.Provider).LibreOffice()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv API: %w", err)
|
return fmt.Errorf("get LibreOffice Uno: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mod.unoconv = uno
|
mod.api = libreOfficeApi
|
||||||
|
|
||||||
provider, err = ctx.Module(new(gotenberg.PDFEngineProvider))
|
provider, err = ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get PDF engine provider: %w", err)
|
return fmt.Errorf("get PDF engine provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine, err := provider.(gotenberg.PDFEngineProvider).PDFEngine()
|
engine, err := provider.(gotenberg.PdfEngineProvider).PdfEngine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get PDF engine: %w", err)
|
return fmt.Errorf("get PDF engine: %w", err)
|
||||||
}
|
}
|
||||||
@@ -68,13 +69,13 @@ func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Routes returns the HTTP routes.
|
// Routes returns the HTTP routes.
|
||||||
func (mod LibreOffice) Routes() ([]api.Route, error) {
|
func (mod *LibreOffice) Routes() ([]api.Route, error) {
|
||||||
if mod.disableRoutes {
|
if mod.disableRoutes {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return []api.Route{
|
return []api.Route{
|
||||||
convertRoute(mod.unoconv, mod.engine),
|
convertRoute(mod.api, mod.engine),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +1,16 @@
|
|||||||
package libreoffice
|
package libreoffice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
unoconv func() (unoconv.API, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvProvider) Unoconv() (unoconv.API, error) {
|
|
||||||
return mod.unoconv()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvAPI struct {
|
|
||||||
pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ unoconv.Options) error
|
|
||||||
extensions func() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error {
|
|
||||||
return mod.pdf(ctx, logger, inputPath, outputPath, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) Extensions() []string {
|
|
||||||
return mod.extensions()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngineProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
pdfEngine func() (gotenberg.PDFEngine, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngineProvider) PDFEngine() (gotenberg.PDFEngine, error) {
|
|
||||||
return mod.pdfEngine()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngine struct {
|
|
||||||
merge func(_ context.Context, _ *zap.Logger, _ []string, _ string) error
|
|
||||||
convert func(_ context.Context, _ *zap.Logger, _, _, _ string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return mod.merge(ctx, logger, inputPaths, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
|
||||||
return mod.convert(ctx, logger, format, inputPath, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLibreOffice_Descriptor(t *testing.T) {
|
func TestLibreOffice_Descriptor(t *testing.T) {
|
||||||
descriptor := LibreOffice{}.Descriptor()
|
descriptor := new(LibreOffice).Descriptor()
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
expect := reflect.TypeOf(new(LibreOffice))
|
expect := reflect.TypeOf(new(LibreOffice))
|
||||||
@@ -75,35 +21,34 @@ func TestLibreOffice_Descriptor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLibreOffice_Provision(t *testing.T) {
|
func TestLibreOffice_Provision(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
ctx *gotenberg.Context
|
scenario string
|
||||||
expectErr bool
|
ctx *gotenberg.Context
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "no LibreOffice API provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoModule }{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{},
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "no LibreOffice API from LibreOffice API provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
libreofficeapi.ProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||||
return nil, errors.New("foo")
|
return nil, errors.New("foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,16 +61,20 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "no PDF engine provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
mod := &struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
libreofficeapi.ProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
}
|
}
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return new(libreofficeapi.ApiMock), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -137,23 +86,23 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "no PDF engine from PDF engine provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod1 := struct{ ProtoUnoconvProvider }{}
|
mod := &struct {
|
||||||
mod1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
|
libreofficeapi.ProviderMock
|
||||||
|
gotenberg.PdfEngineProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
}
|
}
|
||||||
mod1.unoconv = func() (unoconv.API, error) {
|
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return new(libreofficeapi.ApiMock), nil
|
||||||
}
|
}
|
||||||
|
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||||
mod2 := struct{ ProtoPDFEngineProvider }{}
|
|
||||||
mod2.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
|
|
||||||
}
|
|
||||||
mod2.pdfEngine = func() (gotenberg.PDFEngine, error) {
|
|
||||||
return nil, errors.New("foo")
|
return nil, errors.New("foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,29 +111,28 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod1.Descriptor(),
|
mod.Descriptor(),
|
||||||
mod2.Descriptor(),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "provision success",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod1 := struct{ ProtoUnoconvProvider }{}
|
mod := &struct {
|
||||||
mod1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
|
libreofficeapi.ProviderMock
|
||||||
|
gotenberg.PdfEngineProviderMock
|
||||||
|
}{}
|
||||||
|
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||||
}
|
}
|
||||||
mod1.unoconv = func() (unoconv.API, error) {
|
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return new(libreofficeapi.ApiMock), nil
|
||||||
}
|
}
|
||||||
|
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||||
mod2 := struct{ ProtoPDFEngineProvider }{}
|
return new(gotenberg.PdfEngineMock), nil
|
||||||
mod2.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
|
|
||||||
}
|
|
||||||
mod2.pdfEngine = func() (gotenberg.PDFEngine, error) {
|
|
||||||
return struct{ ProtoPDFEngine }{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -192,59 +140,57 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod1.Descriptor(),
|
mod.Descriptor(),
|
||||||
mod2.Descriptor(),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(LibreOffice)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
err := mod.Provision(tc.ctx)
|
mod := new(LibreOffice)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLibreOffice_Routes(t *testing.T) {
|
func TestLibreOffice_Routes(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
expectRoutes int
|
expectRoutes int
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectRoutes: 1,
|
scenario: "routes not disabled",
|
||||||
|
expectRoutes: 1,
|
||||||
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "routes disabled",
|
||||||
|
expectRoutes: 0,
|
||||||
disableRoutes: true,
|
disableRoutes: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(LibreOffice)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod.disableRoutes = tc.disableRoutes
|
mod := new(LibreOffice)
|
||||||
|
mod.disableRoutes = tc.disableRoutes
|
||||||
|
|
||||||
routes, err := mod.Routes()
|
routes, err := mod.Routes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectRoutes != len(routes) {
|
if tc.expectRoutes != len(routes) {
|
||||||
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes))
|
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ unoconv.Provider = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ unoconv.API = (*ProtoUnoconvAPI)(nil)
|
|
||||||
_ gotenberg.PDFEngineProvider = (*ProtoPDFEngineProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoPDFEngineProvider)(nil)
|
|
||||||
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
// Package pdfengine provides a module which abstracts the CLI tool unoconv and
|
// Package pdfengine provides a module which interacts with LibreOffice via the
|
||||||
// implements the gotenberg.PDFEngine interface.
|
// UNO (Universal Network Objects) API and implements the gotenberg.PdfEngine
|
||||||
|
// interface.
|
||||||
package pdfengine
|
package pdfengine
|
||||||
|
|||||||
@@ -2,75 +2,78 @@ package pdfengine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gotenberg.MustRegisterModule(UnoconvPDFEngine{})
|
gotenberg.MustRegisterModule(new(LibreOfficePdfEngine))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnoconvPDFEngine abstracts the CLI tool unoconv and implements the
|
// LibreOfficePdfEngine interacts with the LibreOffice (Universal Network Objects) API
|
||||||
// gotenberg.PDFEngine interface.
|
// and implements the [gotenberg.PdfEngine] interface.
|
||||||
type UnoconvPDFEngine struct {
|
type LibreOfficePdfEngine struct {
|
||||||
unoconv unoconv.API
|
unoApi api.Uno
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a UnoconvPDFEngine's module descriptor.
|
// Descriptor returns a [LibreOfficePdfEngine]'s module descriptor.
|
||||||
func (UnoconvPDFEngine) Descriptor() gotenberg.ModuleDescriptor {
|
func (engine *LibreOfficePdfEngine) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "unoconv-pdfengine",
|
ID: "libreoffice-pdfengine",
|
||||||
New: func() gotenberg.Module { return new(UnoconvPDFEngine) },
|
New: func() gotenberg.Module { return new(LibreOfficePdfEngine) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets the module properties.
|
// Provision sets the module properties.
|
||||||
func (engine *UnoconvPDFEngine) Provision(ctx *gotenberg.Context) error {
|
func (engine *LibreOfficePdfEngine) Provision(ctx *gotenberg.Context) error {
|
||||||
provider, err := ctx.Module(new(unoconv.Provider))
|
provider, err := ctx.Module(new(api.Provider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv provider: %w", err)
|
return fmt.Errorf("get LibreOffice Uno provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uno, err := provider.(unoconv.Provider).Unoconv()
|
unoApi, err := provider.(api.Provider).LibreOffice()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv API: %w", err)
|
return fmt.Errorf("get LibreOffice Uno: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.unoconv = uno
|
engine.unoApi = unoApi
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge is not available for this PDF engine.
|
// Merge is not available in this implementation.
|
||||||
func (engine UnoconvPDFEngine) Merge(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
func (engine *LibreOfficePdfEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
return fmt.Errorf("merge PDFs with unoconv: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
|
return fmt.Errorf("merge PDFs with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
||||||
// PDF/A-1 format is available. If another PDF format is requested, it returns
|
// PDF/A-1b, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
|
||||||
// a gotenberg.ErrPDFFormatNotAvailable error.
|
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
|
||||||
func (engine UnoconvPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
// error.
|
||||||
if format != gotenberg.FormatPDFA1a {
|
func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, gotenberg.ErrPDFFormatNotAvailable)
|
err := engine.unoApi.Pdf(ctx, logger, inputPath, outputPath, api.Options{
|
||||||
}
|
PdfFormats: formats,
|
||||||
|
|
||||||
err := engine.unoconv.PDF(ctx, logger, inputPath, outputPath, unoconv.Options{
|
|
||||||
PDFArchive: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err)
|
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, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*UnoconvPDFEngine)(nil)
|
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
||||||
_ gotenberg.Provisioner = (*UnoconvPDFEngine)(nil)
|
_ gotenberg.Provisioner = (*LibreOfficePdfEngine)(nil)
|
||||||
_ gotenberg.PDFEngine = (*UnoconvPDFEngine)(nil)
|
_ gotenberg.PdfEngine = (*LibreOfficePdfEngine)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,192 +6,163 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
func TestLibreOfficePdfEngine_Descriptor(t *testing.T) {
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
descriptor := new(LibreOfficePdfEngine).Descriptor()
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
unoconv func() (unoconv.API, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvProvider) Unoconv() (unoconv.API, error) {
|
|
||||||
return mod.unoconv()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvAPI struct {
|
|
||||||
pdf func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error {
|
|
||||||
return mod.pdf(ctx, logger, inputPath, outputPath, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) Extensions() []string {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Descriptor(t *testing.T) {
|
|
||||||
descriptor := UnoconvPDFEngine{}.Descriptor()
|
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
expect := reflect.TypeOf(new(UnoconvPDFEngine))
|
expect := reflect.TypeOf(new(LibreOfficePdfEngine))
|
||||||
|
|
||||||
if actual != expect {
|
if actual != expect {
|
||||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Provision(t *testing.T) {
|
func TestLibreOfficePdfEngine_Provider(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
ctx *gotenberg.Context
|
scenario string
|
||||||
expectErr bool
|
ctx *gotenberg.Context
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: func() *gotenberg.Context {
|
scenario: "no LibreOffice API provider",
|
||||||
mod := struct{ ProtoModule }{}
|
ctx: gotenberg.NewContext(
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ParsedFlags{
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet,
|
||||||
}
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{},
|
||||||
return gotenberg.NewContext(
|
),
|
||||||
gotenberg.ParsedFlags{
|
expectError: true,
|
||||||
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError),
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "no API from LibreOffice API provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
provider := &struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
api.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
provider.LibreOfficeMock = func() (api.Uno, error) {
|
||||||
return nil, errors.New("foo")
|
return nil, errors.New("foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError),
|
FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod.Descriptor(),
|
provider.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "provision success",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
provider := &struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
api.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
provider.LibreOfficeMock = func() (api.Uno, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return new(api.ApiMock), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError),
|
FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod.Descriptor(),
|
provider.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(UnoconvPDFEngine)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
err := mod.Provision(tc.ctx)
|
engine := new(LibreOfficePdfEngine)
|
||||||
|
err := engine.Provision(tc.ctx)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Merge(t *testing.T) {
|
func TestLibreOfficePdfEngine_Merge(t *testing.T) {
|
||||||
mod := new(UnoconvPDFEngine)
|
engine := new(LibreOfficePdfEngine)
|
||||||
err := mod.Merge(context.TODO(), zap.NewNop(), nil, "")
|
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Convert(t *testing.T) {
|
func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
api unoconv.API
|
scenario string
|
||||||
format string
|
api api.Uno
|
||||||
expectErr bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
format: "",
|
scenario: "convert success",
|
||||||
expectErr: true,
|
api: &api.ApiMock{
|
||||||
},
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||||
{
|
|
||||||
api: func() unoconv.API {
|
|
||||||
unoconvAPI := struct{ ProtoUnoconvAPI }{}
|
|
||||||
unoconvAPI.pdf = func(_ context.Context, _ *zap.Logger, _, __ string, _ unoconv.Options) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
}
|
|
||||||
|
|
||||||
return unoconvAPI
|
|
||||||
}(),
|
|
||||||
format: gotenberg.FormatPDFA1a,
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
api: func() unoconv.API {
|
|
||||||
unoconvAPI := struct{ ProtoUnoconvAPI }{}
|
|
||||||
unoconvAPI.pdf = func(_ context.Context, _ *zap.Logger, _, __ string, _ unoconv.Options) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
},
|
||||||
|
},
|
||||||
return unoconvAPI
|
expectError: false,
|
||||||
}(),
|
},
|
||||||
format: gotenberg.FormatPDFA1a,
|
{
|
||||||
|
scenario: "invalid PDF format",
|
||||||
|
api: &api.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||||
|
return api.ErrInvalidPdfFormat
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "convert fail",
|
||||||
|
api: &api.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(UnoconvPDFEngine)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod.unoconv = tc.api
|
engine := &LibreOfficePdfEngine{unoApi: tc.api}
|
||||||
|
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|
||||||
err := mod.Convert(context.TODO(), zap.NewNop(), tc.format, "", "")
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ unoconv.Provider = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ unoconv.API = (*ProtoUnoconvAPI)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
|
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// convertRoute returns an api.Route which can convert LibreOffice documents
|
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
||||||
// to PDF.
|
// to PDF.
|
||||||
func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/forms/libreoffice/convert",
|
Path: "/forms/libreoffice/convert",
|
||||||
@@ -23,54 +24,53 @@ func convertRoute(uno unoconv.API, 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
|
||||||
landscape bool
|
landscape bool
|
||||||
nativePageRanges string
|
nativePageRanges string
|
||||||
nativePDFA1aFormat bool
|
pdfa string
|
||||||
PDFformat string
|
pdfua bool
|
||||||
merge bool
|
nativePdfFormats bool
|
||||||
|
merge bool
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths(uno.Extensions(), &inputPaths).
|
MandatoryPaths(libreOffice.Extensions(), &inputPaths).
|
||||||
Bool("landscape", &landscape, false).
|
Bool("landscape", &landscape, false).
|
||||||
String("nativePageRanges", &nativePageRanges, "").
|
String("nativePageRanges", &nativePageRanges, "").
|
||||||
Bool("nativePdfA1aFormat", &nativePDFA1aFormat, false).
|
String("pdfa", &pdfa, "").
|
||||||
String("pdfFormat", &PDFformat, "").
|
Bool("pdfua", &pdfua, false).
|
||||||
|
Bool("nativePdfFormats", &nativePdfFormats, true).
|
||||||
Bool("merge", &merge, false).
|
Bool("merge", &merge, false).
|
||||||
Validate()
|
Validate()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if nativePDFA1aFormat && PDFformat != "" {
|
pdfFormats := gotenberg.PdfFormats{
|
||||||
return api.WrapError(
|
PdfA: pdfa,
|
||||||
errors.New("got both 'pdfFormat' and 'nativePdfA1aFormat' form values"),
|
PdfUa: pdfua,
|
||||||
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'pdfFormat' and 'nativePdfA1aFormat' form values are provided"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alright, let's convert each document to PDF.
|
// Alright, let's convert each document to PDF.
|
||||||
|
|
||||||
outputPaths := make([]string, len(inputPaths))
|
outputPaths := make([]string, len(inputPaths))
|
||||||
|
|
||||||
for i, inputPath := range inputPaths {
|
for i, inputPath := range inputPaths {
|
||||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
options := unoconv.Options{
|
options := libreofficeapi.Options{
|
||||||
Landscape: landscape,
|
Landscape: landscape,
|
||||||
PageRanges: nativePageRanges,
|
PageRanges: nativePageRanges,
|
||||||
PDFArchive: nativePDFA1aFormat,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = uno.PDF(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
if nativePdfFormats {
|
||||||
|
options.PdfFormats = pdfFormats
|
||||||
|
}
|
||||||
|
|
||||||
|
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, unoconv.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),
|
||||||
api.NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)),
|
api.NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,26 +90,20 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now, let's check if the client want to convert this result
|
// Now, let's check if the client want to convert this result
|
||||||
// PDF to a specific PDF format.
|
// PDF to specific PDF formats.
|
||||||
|
zeroValued := gotenberg.PdfFormats{}
|
||||||
// Note: nativePdfA1aFormat has not been specified if we reach
|
if !nativePdfFormats && pdfFormats != zeroValued {
|
||||||
// this part of the code. Indeed, the handler returns early on
|
|
||||||
// an error if both nativePdfA1aFormat and pdfFormat are
|
|
||||||
// present.
|
|
||||||
|
|
||||||
if PDFformat != "" {
|
|
||||||
convertInputPath := outputPath
|
convertInputPath := outputPath
|
||||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), PDFformat, convertInputPath, convertOutputPath)
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
fmt.Errorf("convert PDF: %w", err),
|
||||||
api.NewSentinelHTTPError(
|
api.NewSentinelHttpError(
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
|
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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -122,7 +116,7 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 Uno 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 {
|
||||||
@@ -134,27 +128,22 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
|
|
||||||
// Ok, we don't have to merge the PDFs. Let's check if the client
|
// Ok, we don't have to merge the PDFs. Let's check if the client
|
||||||
// want to convert each PDF to a specific PDF format.
|
// want to convert each PDF to a specific PDF format.
|
||||||
|
zeroValued := gotenberg.PdfFormats{}
|
||||||
// Note: nativePdfA1aFormat has not been specified if we reach this
|
if !nativePdfFormats && pdfFormats != zeroValued {
|
||||||
// part of the code. Indeed, the handler returns early on an error
|
|
||||||
// if both nativePdfA1aFormat and pdfFormat are present.
|
|
||||||
|
|
||||||
if PDFformat != "" {
|
|
||||||
convertOutputPaths := make([]string, len(outputPaths))
|
convertOutputPaths := make([]string, len(outputPaths))
|
||||||
|
|
||||||
for i, outputPath := range outputPaths {
|
for i, outputPath := range outputPaths {
|
||||||
convertInputPath := outputPath
|
convertInputPath := outputPath
|
||||||
convertOutputPaths[i] = ctx.GeneratePath(".pdf")
|
convertOutputPaths[i] = ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), PDFformat, convertInputPath, convertOutputPaths[i])
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
fmt.Errorf("convert PDF: %w", err),
|
||||||
api.NewSentinelHTTPError(
|
api.NewSentinelHttpError(
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
|
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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -169,7 +158,7 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
// Package unoconv provides a module which abstracts the CLI tool unoconv.
|
|
||||||
package unoconv
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
package unoconv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
gotenberg.MustRegisterModule(Unoconv{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
|
||||||
// interpreted by LibreOffice.
|
|
||||||
var ErrMalformedPageRanges = errors.New("page ranges are malformed")
|
|
||||||
|
|
||||||
// Unoconv is a module which provides an API to interact with unoconv.
|
|
||||||
type Unoconv struct {
|
|
||||||
binPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options gathers available options when converting a document to PDF.
|
|
||||||
type Options struct {
|
|
||||||
// Landscape allows to change the orientation of the resulting PDF.
|
|
||||||
// Optional.
|
|
||||||
Landscape bool
|
|
||||||
|
|
||||||
// PageRanges allows to select the pages to convert.
|
|
||||||
// TODO: should prefer a method form PDFEngine.
|
|
||||||
// Optional.
|
|
||||||
PageRanges string
|
|
||||||
|
|
||||||
// PDFArchive allows to convert the resulting PDF to PDF/A-1a.
|
|
||||||
// In a module, prefer the Convert method from the gotenberg.PDFEngine
|
|
||||||
// interface.
|
|
||||||
// Optional.
|
|
||||||
PDFArchive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// API is an abstraction on top of unoconv.
|
|
||||||
//
|
|
||||||
// See https://github.com/unoconv/unoconv.
|
|
||||||
type API interface {
|
|
||||||
PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
|
||||||
Extensions() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider is a module interface which exposes a method for creating an API
|
|
||||||
// for other modules.
|
|
||||||
//
|
|
||||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
|
||||||
// provider, _ := ctx.Module(new(unoconv.Provider))
|
|
||||||
// uno, _ := provider.(unoconv.Provider).Unoconv()
|
|
||||||
// }
|
|
||||||
type Provider interface {
|
|
||||||
Unoconv() (API, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Descriptor returns a Unoconv's module descriptor.
|
|
||||||
func (Unoconv) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{
|
|
||||||
ID: "unoconv",
|
|
||||||
New: func() gotenberg.Module { return new(Unoconv) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provision sets the module properties. It returns an error if the environment
|
|
||||||
// variable UNOCONV_BIN_PATH is not set.
|
|
||||||
func (mod *Unoconv) Provision(_ *gotenberg.Context) error {
|
|
||||||
binPath, ok := os.LookupEnv("UNOCONV_BIN_PATH")
|
|
||||||
if !ok {
|
|
||||||
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.binPath = binPath
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validates the module properties.
|
|
||||||
func (mod Unoconv) Validate() error {
|
|
||||||
_, err := os.Stat(mod.binPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("unoconv binary path does not exist: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics returns the metrics.
|
|
||||||
func (mod Unoconv) Metrics() ([]gotenberg.Metric, error) {
|
|
||||||
return []gotenberg.Metric{
|
|
||||||
{
|
|
||||||
Name: "unoconv_active_instances_count",
|
|
||||||
Description: "Current number of active LibreOffice instances.",
|
|
||||||
Read: func() float64 {
|
|
||||||
activeInstancesCountMu.RLock()
|
|
||||||
defer activeInstancesCountMu.RUnlock()
|
|
||||||
|
|
||||||
return activeInstancesCount
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unoconv returns an API for interacting with unoconv.
|
|
||||||
func (mod Unoconv) Unoconv() (API, error) {
|
|
||||||
return mod, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF converts a document to PDF. It creates a dedicated LibreOffice instance
|
|
||||||
// thanks to a custom user profile directory and a free port. Substantial calls
|
|
||||||
// to this method may increase CPU and memory usage drastically. In such a
|
|
||||||
// scenario, the given context may also be done before the end of the
|
|
||||||
// conversion.
|
|
||||||
func (mod Unoconv) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
|
||||||
port, err := func() (int, error) {
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := listener.Close()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("close listener: %s", err.Error()))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
addr := listener.Addr().String()
|
|
||||||
|
|
||||||
_, portStr, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("get free port from host: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strconv.Atoi(portStr)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get free port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userProfileDirPath := gotenberg.NewDirPath()
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"--user-profile",
|
|
||||||
fmt.Sprintf("//%s", userProfileDirPath),
|
|
||||||
"--port",
|
|
||||||
fmt.Sprintf("%d", port),
|
|
||||||
"--format",
|
|
||||||
"pdf",
|
|
||||||
}
|
|
||||||
|
|
||||||
checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
|
|
||||||
if checkedEntry != nil {
|
|
||||||
args = append(args, "-vvv")
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.Landscape {
|
|
||||||
args = append(args, "--printer", "PaperOrientation=landscape")
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.PageRanges != "" {
|
|
||||||
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.PDFArchive {
|
|
||||||
args = append(args, "--export", "SelectPdfVersion=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append(args, "--output", outputPath, inputPath)
|
|
||||||
|
|
||||||
cmd, err := gotenberg.CommandContext(ctx, logger, mod.binPath, args...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create unoconv command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(fmt.Sprintf("print to PDF with: %+v", options))
|
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount += 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
err = cmd.Exec()
|
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount -= 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
// Always remove the user profile directory created by LibreOffice.
|
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/192.
|
|
||||||
go func() {
|
|
||||||
logger.Debug(fmt.Sprintf("remove user profile directory '%s'", userProfileDirPath))
|
|
||||||
|
|
||||||
err := os.RemoveAll(userProfileDirPath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("remove user profile directory: %s", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unoconv/LibreOffice errors are not explicit.
|
|
||||||
// That's why we have to make an educated guess according to the exit code
|
|
||||||
// and given inputs.
|
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "exit status 5") && options.PageRanges != "" {
|
|
||||||
return ErrMalformedPageRanges
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possible errors:
|
|
||||||
// 1. Unoconv/LibreOffice failed for some reason.
|
|
||||||
// 2. Context done.
|
|
||||||
//
|
|
||||||
// On the second scenario, LibreOffice might not had time to remove some of
|
|
||||||
// its temporary files, as it has been killed without warning. The garbage
|
|
||||||
// collector will delete them for us (if the module is loaded).
|
|
||||||
return fmt.Errorf("unoconv PDF: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extensions returns the file extensions available with unoconv.
|
|
||||||
func (mod Unoconv) Extensions() []string {
|
|
||||||
return []string{
|
|
||||||
".bib",
|
|
||||||
".doc",
|
|
||||||
".xml",
|
|
||||||
".docx",
|
|
||||||
".fodt",
|
|
||||||
".html",
|
|
||||||
".ltx",
|
|
||||||
".txt",
|
|
||||||
".odt",
|
|
||||||
".ott",
|
|
||||||
".pdb",
|
|
||||||
".pdf",
|
|
||||||
".psw",
|
|
||||||
".rtf",
|
|
||||||
".sdw",
|
|
||||||
".stw",
|
|
||||||
".sxw",
|
|
||||||
".uot",
|
|
||||||
".vor",
|
|
||||||
".wps",
|
|
||||||
".epub",
|
|
||||||
".png",
|
|
||||||
".bmp",
|
|
||||||
".emf",
|
|
||||||
".eps",
|
|
||||||
".fodg",
|
|
||||||
".gif",
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".met",
|
|
||||||
".odd",
|
|
||||||
".otg",
|
|
||||||
".pbm",
|
|
||||||
".pct",
|
|
||||||
".pgm",
|
|
||||||
".ppm",
|
|
||||||
".ras",
|
|
||||||
".std",
|
|
||||||
".svg",
|
|
||||||
".svm",
|
|
||||||
".swf",
|
|
||||||
".sxd",
|
|
||||||
".sxw",
|
|
||||||
".tif",
|
|
||||||
".tiff",
|
|
||||||
".xhtml",
|
|
||||||
".xpm",
|
|
||||||
".odp",
|
|
||||||
".fodp",
|
|
||||||
".potm",
|
|
||||||
".pot",
|
|
||||||
".pptx",
|
|
||||||
".pps",
|
|
||||||
".ppt",
|
|
||||||
".pwp",
|
|
||||||
".sda",
|
|
||||||
".sdd",
|
|
||||||
".sti",
|
|
||||||
".sxi",
|
|
||||||
".uop",
|
|
||||||
".wmf",
|
|
||||||
".csv",
|
|
||||||
".dbf",
|
|
||||||
".dif",
|
|
||||||
".fods",
|
|
||||||
".ods",
|
|
||||||
".ots",
|
|
||||||
".pxl",
|
|
||||||
".sdc",
|
|
||||||
".slk",
|
|
||||||
".stc",
|
|
||||||
".sxc",
|
|
||||||
".uos",
|
|
||||||
".xls",
|
|
||||||
".xlt",
|
|
||||||
".xlsx",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
activeInstancesCount float64
|
|
||||||
activeInstancesCountMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.Provisioner = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.Validator = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.MetricsProvider = (*Unoconv)(nil)
|
|
||||||
_ API = (*Unoconv)(nil)
|
|
||||||
_ Provider = (*Unoconv)(nil)
|
|
||||||
)
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
package unoconv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnoconv_Descriptor(t *testing.T) {
|
|
||||||
descriptor := Unoconv{}.Descriptor()
|
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
|
||||||
expect := reflect.TypeOf(new(Unoconv))
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected '%'s' but got '%s'", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Provision(t *testing.T) {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
|
||||||
|
|
||||||
err := mod.Provision(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Validate(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
binPath string
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
binPath: "/foo",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
mod.binPath = tc.binPath
|
|
||||||
err := mod.Validate()
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChromium_Metrics(t *testing.T) {
|
|
||||||
metrics, err := new(Unoconv).Metrics()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(metrics) != 1 {
|
|
||||||
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
|
||||||
if actual != 0 {
|
|
||||||
t.Errorf("expected %d unoconv instances, but got %f", 0, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Unoconv(t *testing.T) {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
|
|
||||||
_, err := mod.Unoconv()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_PDF(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger *zap.Logger
|
|
||||||
inputPath string
|
|
||||||
options Options
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
logger: zap.NewExample(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
options: Options{
|
|
||||||
Landscape: true,
|
|
||||||
PageRanges: "1-2",
|
|
||||||
PDFArchive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
options: Options{
|
|
||||||
PageRanges: "foo",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() context.Context {
|
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
|
|
||||||
err := mod.Provision(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputDir, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err := os.RemoveAll(outputDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = mod.PDF(tc.ctx, tc.logger, tc.inputPath, outputDir+"/foo.pdf", tc.options)
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Extensions(t *testing.T) {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
extensions := mod.Extensions()
|
|
||||||
|
|
||||||
actual := len(extensions)
|
|
||||||
expect := 76
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected %d extensions but got %d", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
// Package logging provides a module which creates a zap.Logger for other
|
// Package logging provides a module which creates a zap.Logger instance for
|
||||||
// modules.
|
// other modules.
|
||||||
package logging
|
package logging
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gotenberg.MustRegisterModule(Logging{})
|
gotenberg.MustRegisterModule(new(Logging))
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -30,20 +31,23 @@ const (
|
|||||||
textLoggingFormat = "text"
|
textLoggingFormat = "text"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logging is a module which implements the gotenberg.LoggerProvider interface.
|
// Logging is a module which implements the [gotenberg.LoggerProvider]
|
||||||
|
// interface.
|
||||||
type Logging struct {
|
type Logging struct {
|
||||||
level string
|
level string
|
||||||
format string
|
format string
|
||||||
|
fieldsPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a Logging's module descriptor.
|
// Descriptor returns a [Logging]'s module descriptor.
|
||||||
func (Logging) Descriptor() gotenberg.ModuleDescriptor {
|
func (log *Logging) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "logging",
|
ID: "logging",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("logging", flag.ExitOnError)
|
fs := flag.NewFlagSet("logging", flag.ExitOnError)
|
||||||
fs.String("log-level", infoLoggingLevel, fmt.Sprintf("Set the log level - %s, %s, %s, or %s", errorLoggingLevel, warnLoggingLevel, infoLoggingLevel, debugLoggingLevel))
|
fs.String("log-level", infoLoggingLevel, fmt.Sprintf("Choose the level of logging detail. Options include %s, %s, %s, or %s", errorLoggingLevel, warnLoggingLevel, infoLoggingLevel, debugLoggingLevel))
|
||||||
fs.String("log-format", autoLoggingFormat, fmt.Sprintf("Set log format - %s, %s, or %s", autoLoggingFormat, jsonLoggingFormat, textLoggingFormat))
|
fs.String("log-format", autoLoggingFormat, fmt.Sprintf("Specify the format of logging. Options include %s, %s, or %s", autoLoggingFormat, jsonLoggingFormat, textLoggingFormat))
|
||||||
|
fs.String("log-fields-prefix", "", "Prepend a specified prefix to each field in the logs")
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
@@ -57,12 +61,13 @@ func (log *Logging) Provision(ctx *gotenberg.Context) error {
|
|||||||
|
|
||||||
log.level = flags.MustString("log-level")
|
log.level = flags.MustString("log-level")
|
||||||
log.format = flags.MustString("log-format")
|
log.format = flags.MustString("log-format")
|
||||||
|
log.fieldsPrefix = flags.MustString("log-fields-prefix")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the log level and format.
|
// Validate validates the log level and format.
|
||||||
func (log Logging) Validate() error {
|
func (log *Logging) Validate() error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch log.level {
|
switch log.level {
|
||||||
@@ -88,8 +93,8 @@ func (log Logging) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger returns a zap.Logger.
|
// Logger returns a [zap.Logger].
|
||||||
func (log Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
func (log *Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
lvl, err := newLogLevel(log.level)
|
lvl, err := newLogLevel(log.level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,29 +106,64 @@ func (log Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
|||||||
return nil, fmt.Errorf("get log encoder: %w", err)
|
return nil, fmt.Errorf("get log encoder: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
core := zapcore.NewCore(encoder, os.Stderr, lvl)
|
logger = zap.New(customCore{
|
||||||
logger = zap.New(core)
|
Core: zapcore.NewCore(encoder, os.Stderr, lvl),
|
||||||
|
fieldsPrefix: log.fieldsPrefix,
|
||||||
// nolint
|
})
|
||||||
defer logger.Sync()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return logger.Named(mod.Descriptor().ID), nil
|
return logger.Named(mod.Descriptor().ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLogLevel(level string) (zapcore.Level, error) {
|
// See https://github.com/gotenberg/gotenberg/issues/659.
|
||||||
switch level {
|
type customCore struct {
|
||||||
case errorLoggingLevel:
|
zapcore.Core
|
||||||
return zap.ErrorLevel, nil
|
fieldsPrefix string
|
||||||
case warnLoggingLevel:
|
}
|
||||||
return zap.WarnLevel, nil
|
|
||||||
case infoLoggingLevel:
|
func (c customCore) With(fields []zapcore.Field) zapcore.Core {
|
||||||
return zap.InfoLevel, nil
|
if c.fieldsPrefix != "" {
|
||||||
case debugLoggingLevel:
|
for i := range fields {
|
||||||
return zap.DebugLevel, nil
|
fields[i].Key = c.fieldsPrefix + "_" + fields[i].Key
|
||||||
default:
|
}
|
||||||
return -2, fmt.Errorf("%s is not a recognized log level", level)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return customCore{
|
||||||
|
Core: c.Core.With(fields),
|
||||||
|
fieldsPrefix: c.fieldsPrefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c customCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||||
|
// This is a copy from the zapcore.ioCore implementation. Indeed, by doing
|
||||||
|
// so, we are able to prefix the fields given to the logger methods like
|
||||||
|
// Debug, Info, Warn, Error, etc.
|
||||||
|
if c.Enabled(ent.Level) {
|
||||||
|
return ce.AddCore(ent, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c customCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||||
|
if c.fieldsPrefix != "" {
|
||||||
|
for i := range fields {
|
||||||
|
fields[i].Key = c.fieldsPrefix + "_" + fields[i].Key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Core.Write(entry, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogLevel(level string) (zapcore.Level, error) {
|
||||||
|
lvl := zapcore.InvalidLevel
|
||||||
|
|
||||||
|
err := lvl.UnmarshalText([]byte(level))
|
||||||
|
if err != nil {
|
||||||
|
return lvl, fmt.Errorf("%q is not a recognized log level: %w", level, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lvl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLogEncoder(format string) (zapcore.Encoder, error) {
|
func newLogEncoder(format string) (zapcore.Encoder, error) {
|
||||||
@@ -158,6 +198,7 @@ func newLogEncoder(format string) (zapcore.Encoder, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Singleton so that we instantiate our logger only once.
|
||||||
var logger *zap.Logger = nil
|
var logger *zap.Logger = nil
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
@@ -166,4 +207,5 @@ var (
|
|||||||
_ gotenberg.Provisioner = (*Logging)(nil)
|
_ gotenberg.Provisioner = (*Logging)(nil)
|
||||||
_ gotenberg.Validator = (*Logging)(nil)
|
_ gotenberg.Validator = (*Logging)(nil)
|
||||||
_ gotenberg.LoggerProvider = (*Logging)(nil)
|
_ gotenberg.LoggerProvider = (*Logging)(nil)
|
||||||
|
_ zapcore.Core = (*customCore)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
"go.uber.org/zap/zaptest/observer"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogging_Descriptor(t *testing.T) {
|
func TestLogging_Descriptor(t *testing.T) {
|
||||||
descriptor := Logging{}.Descriptor()
|
descriptor := new(Logging).Descriptor()
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
expect := reflect.TypeOf(new(Logging))
|
expect := reflect.TypeOf(new(Logging))
|
||||||
@@ -28,172 +24,324 @@ func TestLogging_Descriptor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogging_Provision(t *testing.T) {
|
func TestLogging_Provision(t *testing.T) {
|
||||||
logging := new(Logging)
|
for _, tc := range []struct {
|
||||||
fs := logging.Descriptor().FlagSet
|
scenario string
|
||||||
|
level string
|
||||||
|
format string
|
||||||
|
fieldsPrefix string
|
||||||
|
expectLevel string
|
||||||
|
expectFormat string
|
||||||
|
expectFieldsPrefix string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "default values",
|
||||||
|
expectLevel: infoLoggingLevel,
|
||||||
|
expectFormat: autoLoggingFormat,
|
||||||
|
expectFieldsPrefix: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "explicit values",
|
||||||
|
level: "debug",
|
||||||
|
format: "json",
|
||||||
|
fieldsPrefix: "gotenberg",
|
||||||
|
expectLevel: debugLoggingLevel,
|
||||||
|
expectFormat: jsonLoggingFormat,
|
||||||
|
expectFieldsPrefix: "gotenberg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "wrong values", // no validation at this point.
|
||||||
|
level: "foo",
|
||||||
|
format: "foo",
|
||||||
|
expectLevel: "foo",
|
||||||
|
expectFormat: "foo",
|
||||||
|
expectFieldsPrefix: "",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
var flags []string
|
||||||
|
|
||||||
err := fs.Parse([]string{""})
|
if tc.level != "" {
|
||||||
if err != nil {
|
flags = append(flags, "--log-level", tc.level)
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
|
if tc.format != "" {
|
||||||
|
flags = append(flags, "--log-format", tc.format)
|
||||||
|
}
|
||||||
|
|
||||||
err = logging.Provision(ctx)
|
if tc.fieldsPrefix != "" {
|
||||||
if err != nil {
|
flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
|
||||||
t.Errorf("expected no error but got: %v", err)
|
}
|
||||||
|
|
||||||
|
logging := new(Logging)
|
||||||
|
fs := logging.Descriptor().FlagSet
|
||||||
|
|
||||||
|
err := fs.Parse(flags)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while parsing flags but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
|
||||||
|
|
||||||
|
err = logging.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while provisioning but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logging.level != tc.expectLevel {
|
||||||
|
t.Errorf("expected logging level '%s' but got '%s'", tc.expectLevel, logging.level)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logging.format != tc.expectFormat {
|
||||||
|
t.Errorf("expected logging format '%s' but got '%s'", tc.expectFormat, logging.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logging.fieldsPrefix != tc.expectFieldsPrefix {
|
||||||
|
t.Errorf("expected logging fields prefix '%s' but got '%s'", tc.expectFieldsPrefix, logging.fieldsPrefix)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogging_Validate(t *testing.T) {
|
func TestLogging_Validate(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
level, format string
|
scenario string
|
||||||
expectErr bool
|
level string
|
||||||
|
format string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
level: "foo",
|
scenario: "invalid level",
|
||||||
expectErr: true,
|
level: "foo",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: debugLoggingLevel,
|
scenario: "invalid format",
|
||||||
format: "foo",
|
level: debugLoggingLevel,
|
||||||
expectErr: true,
|
format: "foo",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: debugLoggingLevel,
|
scenario: "valid level and format",
|
||||||
format: autoLoggingFormat,
|
level: debugLoggingLevel,
|
||||||
|
format: autoLoggingFormat,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(Logging)
|
logging := new(Logging)
|
||||||
mod.level = tc.level
|
logging.level = tc.level
|
||||||
mod.format = tc.format
|
logging.format = tc.format
|
||||||
|
|
||||||
err := mod.Validate()
|
err := logging.Validate()
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogging_Logger(t *testing.T) {
|
func TestLogging_Logger(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
level, format string
|
scenario string
|
||||||
expectErr bool
|
level string
|
||||||
|
format string
|
||||||
|
fieldsPrefix string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
level: "foo",
|
scenario: "invalid level",
|
||||||
expectErr: true,
|
level: "foo",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: debugLoggingLevel,
|
scenario: "invalid format",
|
||||||
format: "foo",
|
level: debugLoggingLevel,
|
||||||
expectErr: true,
|
format: "foo",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: debugLoggingLevel,
|
scenario: "valid level and format",
|
||||||
format: autoLoggingFormat,
|
level: debugLoggingLevel,
|
||||||
|
format: autoLoggingFormat,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(Logging)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod.level = tc.level
|
logging := new(Logging)
|
||||||
mod.format = tc.format
|
logging.level = tc.level
|
||||||
|
logging.format = tc.format
|
||||||
|
logging.fieldsPrefix = tc.fieldsPrefix
|
||||||
|
|
||||||
_, err := mod.Logger(ProtoModule{
|
_, err := logging.Logger(&gotenberg.ModuleMock{
|
||||||
descriptor: func() gotenberg.ModuleDescriptor {
|
DescriptorMock: func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: nil}
|
return gotenberg.ModuleDescriptor{ID: "mock", New: nil}
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewLogLevel(t *testing.T) {
|
func TestCustomCore(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
level string
|
scenario string
|
||||||
expectZapLevel zapcore.Level
|
level zapcore.Level
|
||||||
expectErr bool
|
fieldsPrefix string
|
||||||
|
expectEntry bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "level enabled",
|
||||||
|
level: zapcore.DebugLevel,
|
||||||
|
fieldsPrefix: "gotenberg",
|
||||||
|
expectEntry: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no fields prefix",
|
||||||
|
level: zapcore.DebugLevel,
|
||||||
|
expectEntry: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "level disabled",
|
||||||
|
level: zapcore.ErrorLevel,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
core, obsvr := observer.New(tc.level)
|
||||||
|
lgr := zap.New(customCore{
|
||||||
|
Core: core,
|
||||||
|
fieldsPrefix: tc.fieldsPrefix,
|
||||||
|
}).With(zap.String("a_field", "a value"))
|
||||||
|
|
||||||
|
lgr.Debug("a debug message", zap.String("another_field", "another value"))
|
||||||
|
|
||||||
|
entries := obsvr.TakeAll()
|
||||||
|
|
||||||
|
if tc.expectEntry && len(entries) == 0 {
|
||||||
|
t.Fatal("expected an entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectEntry && len(entries) != 0 {
|
||||||
|
t.Fatal("expected no entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix string
|
||||||
|
if tc.fieldsPrefix != "" {
|
||||||
|
prefix = tc.fieldsPrefix + "_"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
fields := entry.Context
|
||||||
|
|
||||||
|
if len(fields) != 2 {
|
||||||
|
t.Fatalf("expected 2 fields but got %d", len(fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
if fields[0].Key != fmt.Sprintf("%sa_field", prefix) {
|
||||||
|
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) {
|
||||||
|
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newLogLevel(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
level string
|
||||||
|
expectZapLevel zapcore.Level
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "error level",
|
||||||
level: errorLoggingLevel,
|
level: errorLoggingLevel,
|
||||||
expectZapLevel: zapcore.ErrorLevel,
|
expectZapLevel: zapcore.ErrorLevel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "warning level",
|
||||||
level: warnLoggingLevel,
|
level: warnLoggingLevel,
|
||||||
expectZapLevel: zapcore.WarnLevel,
|
expectZapLevel: zapcore.WarnLevel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "info level",
|
||||||
level: infoLoggingLevel,
|
level: infoLoggingLevel,
|
||||||
expectZapLevel: zapcore.InfoLevel,
|
expectZapLevel: zapcore.InfoLevel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "debug level",
|
||||||
level: debugLoggingLevel,
|
level: debugLoggingLevel,
|
||||||
expectZapLevel: zapcore.DebugLevel,
|
expectZapLevel: zapcore.DebugLevel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "invalid level",
|
||||||
level: "foo",
|
level: "foo",
|
||||||
expectZapLevel: -2,
|
expectZapLevel: zapcore.InvalidLevel,
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
actual, err := newLogLevel(tc.level)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
actual, err := newLogLevel(tc.level)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectZapLevel != actual {
|
if tc.expectZapLevel != actual {
|
||||||
t.Errorf("test %d: expected %d level but got %d", i, tc.expectZapLevel, actual)
|
t.Errorf("expected %d level but got %d", tc.expectZapLevel, actual)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewLogEncoder(t *testing.T) {
|
func Test_newLogEncoder(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
format string
|
scenario string
|
||||||
expectErr bool
|
format string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
format: autoLoggingFormat,
|
scenario: "auto format",
|
||||||
|
format: autoLoggingFormat,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: textLoggingFormat,
|
scenario: "text format",
|
||||||
|
format: textLoggingFormat,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: jsonLoggingFormat,
|
scenario: "json format",
|
||||||
|
format: jsonLoggingFormat,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: "foo",
|
scenario: "invalid format",
|
||||||
expectErr: true,
|
format: "foo",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
_, err := newLogEncoder(tc.format)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
_, err := newLogEncoder(tc.format)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Errorf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// Package pdfcpu provides a module which wraps the
|
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
|
||||||
// https://github.com/pdfcpu/pdfcpu library and implements the
|
// interface using the pdfcpu library. This package allows for the merging of
|
||||||
// gotenberg.PDFEngine interface.
|
// PDF files but does not support conversion to specific PDF formats.
|
||||||
|
//
|
||||||
|
// See: https://github.com/pdfcpu/pdfcpu.
|
||||||
package pdfcpu
|
package pdfcpu
|
||||||
|
|||||||
@@ -4,44 +4,44 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
pdfcpuAPI "github.com/pdfcpu/pdfcpu/pkg/api"
|
pdfcpuAPI "github.com/pdfcpu/pdfcpu/pkg/api"
|
||||||
pdfcpuLog "github.com/pdfcpu/pdfcpu/pkg/log"
|
pdfcpuLog "github.com/pdfcpu/pdfcpu/pkg/log"
|
||||||
pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
|
pdfcpuConfig "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gotenberg.MustRegisterModule(PDFcpu{})
|
gotenberg.MustRegisterModule(new(PdfCpu))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDFcpu is a module which wraps the https://github.com/pdfcpu/pdfcpu library
|
// PdfCpu abstracts the pdfcpu library and implements the [gotenberg.PdfEngine]
|
||||||
// and implements the gotenberg.PDFEngine interface.
|
// interface.
|
||||||
type PDFcpu struct {
|
type PdfCpu struct {
|
||||||
conf *pdfcpuConfig.Configuration
|
conf *pdfcpuConfig.Configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a PDFcpu's module descriptor.
|
// Descriptor returns a [PdfCpu]'s module descriptor.
|
||||||
func (PDFcpu) Descriptor() gotenberg.ModuleDescriptor {
|
func (engine *PdfCpu) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "pdfcpu",
|
ID: "pdfcpu",
|
||||||
New: func() gotenberg.Module { return new(PDFcpu) },
|
New: func() gotenberg.Module { return new(PdfCpu) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets the engine properties.
|
// Provision sets the engine properties.
|
||||||
func (engine *PDFcpu) Provision(_ *gotenberg.Context) error {
|
func (engine *PdfCpu) Provision(ctx *gotenberg.Context) error {
|
||||||
pdfcpuConfig.ConfigPath = "disable"
|
pdfcpuConfig.ConfigPath = "disable"
|
||||||
pdfcpuLog.DisableLoggers()
|
pdfcpuLog.DisableLoggers()
|
||||||
|
|
||||||
engine.conf = pdfcpuConfig.NewDefaultConfiguration()
|
engine.conf = pdfcpuConfig.NewDefaultConfiguration()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge merges the given PDFs into a unique PDF.
|
// Merge combines multiple PDFs into a single PDF.
|
||||||
func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string, outputPath string) error {
|
func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf)
|
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, false, engine.conf)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -49,14 +49,14 @@ func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string
|
|||||||
return fmt.Errorf("merge PDFs with PDFcpu: %w", err)
|
return fmt.Errorf("merge PDFs with PDFcpu: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert is not available for this PDF engine.
|
// Convert is not available in this implementation.
|
||||||
func (engine PDFcpu) Convert(_ context.Context, _ *zap.Logger, format, _, _ 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 '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*PDFcpu)(nil)
|
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||||
_ gotenberg.Provisioner = (*PDFcpu)(nil)
|
_ gotenberg.Provisioner = (*PdfCpu)(nil)
|
||||||
_ gotenberg.PDFEngine = (*PDFcpu)(nil)
|
_ gotenberg.PdfEngine = (*PdfCpu)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,92 +7,98 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPDFcpu_Descriptor(t *testing.T) {
|
func TestPdfCpu_Descriptor(t *testing.T) {
|
||||||
descriptor := PDFcpu{}.Descriptor()
|
descriptor := new(PdfCpu).Descriptor()
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
expect := reflect.TypeOf(new(PDFcpu))
|
expect := reflect.TypeOf(new(PdfCpu))
|
||||||
|
|
||||||
if actual != expect {
|
if actual != expect {
|
||||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFcpu_Provision(t *testing.T) {
|
func TestPdfCpu_Provision(t *testing.T) {
|
||||||
mod := new(PDFcpu)
|
engine := new(PdfCpu)
|
||||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||||
|
|
||||||
err := mod.Provision(ctx)
|
err := engine.Provision(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error but got: %v", err)
|
t.Errorf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFcpu_Merge(t *testing.T) {
|
func TestPdfCpu_Merge(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
inputPaths []string
|
scenario string
|
||||||
expectErr bool
|
inputPaths []string
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "invalid input path",
|
||||||
|
inputPaths: []string{
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "single file success",
|
||||||
inputPaths: []string{
|
inputPaths: []string{
|
||||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
},
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "many files success",
|
||||||
inputPaths: []string{
|
inputPaths: []string{
|
||||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
inputPaths: []string{
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
func() {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod := new(PDFcpu)
|
engine := new(PdfCpu)
|
||||||
|
err := engine.Provision(nil)
|
||||||
err := mod.Provision(nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputDir, err := gotenberg.MkdirAll()
|
fs := gotenberg.NewFileSystem()
|
||||||
|
outputDir, err := fs.MkdirAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := os.RemoveAll(outputDir)
|
err = os.RemoveAll(fs.WorkingDirPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = mod.Merge(nil, nil, tc.inputPaths, outputDir+"/foo.pdf")
|
err = engine.Merge(nil, nil, tc.inputPaths, outputDir+"/foo.pdf")
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFcpu_Convert(t *testing.T) {
|
func TestPdfCpu_Convert(t *testing.T) {
|
||||||
mod := new(PDFcpu)
|
mod := new(PdfCpu)
|
||||||
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
|
err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|
||||||
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// Package pdfengines provides a module which gathers modules that implements
|
// Package pdfengines a way to gather and manage multiple modules that
|
||||||
// the gotenberg.PDFEngine interface.
|
// implement the gotenberg.PdfEngine interface.
|
||||||
package pdfengines
|
package pdfengines
|
||||||
|
|||||||
@@ -4,35 +4,30 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/multierr"
|
"go.uber.org/multierr"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one
|
type multiPdfEngines struct {
|
||||||
// or more gotenberg.PDFEngine. It provides a sort of fallback mechanism: if an
|
engines []gotenberg.PdfEngine
|
||||||
// engine's method returns an error, it calls the same method from another
|
|
||||||
// engine.
|
|
||||||
type multiPDFEngines struct {
|
|
||||||
engines []gotenberg.PDFEngine
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMultiPDFEngines returns a multiPDFEngines. Arguments' order determines the
|
func newMultiPdfEngines(engines ...gotenberg.PdfEngine) *multiPdfEngines {
|
||||||
// order of the engines called.
|
return &multiPdfEngines{
|
||||||
func newMultiPDFEngines(engines ...gotenberg.PDFEngine) *multiPDFEngines {
|
|
||||||
return &multiPDFEngines{
|
|
||||||
engines: engines,
|
engines: engines,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge tries to merge the given PDFs into a unique PDF thanks to its
|
// Merge tries to merge the given PDFs into a unique PDF thanks to its
|
||||||
// children. If the context is done, it stops and returns an error.
|
// children. If the context is done, it stops and returns an error.
|
||||||
func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
var err error
|
var err error
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
for _, engine := range multi.engines {
|
for _, engine := range multi.engines {
|
||||||
go func(engine gotenberg.PDFEngine) {
|
go func(engine gotenberg.PdfEngine) {
|
||||||
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
|
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
|
||||||
}(engine)
|
}(engine)
|
||||||
|
|
||||||
@@ -52,13 +47,13 @@ func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inpu
|
|||||||
|
|
||||||
// Convert converts the given PDF to a specific PDF format. thanks to its
|
// Convert converts the given PDF to a specific PDF format. thanks to its
|
||||||
// children. If the context is done, it stops and returns an error.
|
// children. If the context is done, it stops and returns an error.
|
||||||
func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
var err error
|
var err error
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
for _, engine := range multi.engines {
|
for _, engine := range multi.engines {
|
||||||
go func(engine gotenberg.PDFEngine) {
|
go func(engine gotenberg.PdfEngine) {
|
||||||
errChan <- engine.Convert(ctx, logger, format, inputPath, outputPath)
|
errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
|
||||||
}(engine)
|
}(engine)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -72,10 +67,10 @@ func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, fo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF to '%s' with multi PDF engines: %w", format, err)
|
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.PDFEngine = (*multiPDFEngines)(nil)
|
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,213 +3,177 @@ package pdfengines
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewMultiPDFEngines(t *testing.T) {
|
func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||||
engine1 := &ProtoPDFEngine{
|
for _, tc := range []struct {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
scenario string
|
||||||
return nil
|
engine *multiPdfEngines
|
||||||
},
|
ctx context.Context
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
expectError bool
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
engine2 := &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
multi := newMultiPDFEngines(engine1, engine2)
|
|
||||||
|
|
||||||
if len(multi.engines) != 2 {
|
|
||||||
t.Fatalf("expected %d engines but got %d", 2, len(multi.engines))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(engine1, multi.engines[0]) {
|
|
||||||
t.Errorf("expected %v, but got: %v", engine1, multi.engines[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(engine2, multi.engines[1]) {
|
|
||||||
t.Errorf("expected %v, but got: %v", engine2, multi.engines[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiPDFEngines_Merge(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
ctx context.Context
|
|
||||||
engines []gotenberg.PDFEngine
|
|
||||||
expectErr bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
scenario: "nominal behavior",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPdfEngines(
|
||||||
return []gotenberg.PDFEngine{
|
&gotenberg.PdfEngineMock{
|
||||||
ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return nil
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
scenario: "at least one engine does not return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPdfEngines(
|
||||||
return []gotenberg.PDFEngine{
|
&gotenberg.PdfEngineMock{
|
||||||
ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
&gotenberg.PdfEngineMock{
|
||||||
return nil
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
},
|
return nil
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
scenario: "all engines return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPdfEngines(
|
||||||
return []gotenberg.PDFEngine{
|
&gotenberg.PdfEngineMock{
|
||||||
ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
&gotenberg.PdfEngineMock{
|
||||||
return errors.New("bar")
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
},
|
return errors.New("foo")
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
expectErr: true,
|
ctx: context.Background(),
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "context expired",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
ctx: func() context.Context {
|
ctx: func() context.Context {
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
cancel()
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engines: func() []gotenberg.PDFEngine {
|
expectError: true,
|
||||||
return []gotenberg.PDFEngine{
|
|
||||||
ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
multi := newMultiPDFEngines(tc.engines...)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
err := multi.Merge(tc.ctx, nil, nil, "")
|
err := tc.engine.Merge(tc.ctx, zap.NewNop(), nil, "")
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiPDFEngines_Convert(t *testing.T) {
|
func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
ctx context.Context
|
scenario string
|
||||||
engines []gotenberg.PDFEngine
|
engine *multiPdfEngines
|
||||||
expectErr bool
|
ctx context.Context
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
scenario: "nominal behavior",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPdfEngines(
|
||||||
return []gotenberg.PDFEngine{
|
&gotenberg.PdfEngineMock{
|
||||||
ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return nil
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
scenario: "at least one engine does not return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPdfEngines(
|
||||||
return []gotenberg.PDFEngine{
|
&gotenberg.PdfEngineMock{
|
||||||
ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
&gotenberg.PdfEngineMock{
|
||||||
return nil
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
},
|
return nil
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
scenario: "all engines return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPdfEngines(
|
||||||
return []gotenberg.PDFEngine{
|
&gotenberg.PdfEngineMock{
|
||||||
ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
&gotenberg.PdfEngineMock{
|
||||||
return errors.New("bar")
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
},
|
return errors.New("foo")
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
expectErr: true,
|
ctx: context.Background(),
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "context expired",
|
||||||
|
engine: newMultiPdfEngines(
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
ctx: func() context.Context {
|
ctx: func() context.Context {
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
cancel()
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engines: func() []gotenberg.PDFEngine {
|
expectError: true,
|
||||||
return []gotenberg.PDFEngine{
|
|
||||||
ProtoPDFEngine{
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
multi := newMultiPDFEngines(tc.engines...)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
err := multi.Convert(tc.ctx, nil, "", "", "")
|
err := tc.engine.Convert(tc.ctx, zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,70 +3,80 @@ package pdfengines
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gotenberg.MustRegisterModule(PDFEngines{})
|
gotenberg.MustRegisterModule(new(PdfEngines))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDFEngines is a module which gathers available gotenberg.PDFEngine modules.
|
// PdfEngines acts as an aggregator and manager for multiple PDF engine
|
||||||
// The available gotenberg.PDFEngine modules can be either all
|
// modules. It enables the selection and ordering of PDF engines based on user
|
||||||
// gotenberg.PDFEngine modules or the modules selected by the user thanks to
|
// preferences passed via command-line flags. The [PdfEngines] module also
|
||||||
// the "engines" flag.
|
// implements the [gotenberg.PdfEngine] interface, providing a unified approach
|
||||||
|
// to PDF processing across the various engines it manages.
|
||||||
//
|
//
|
||||||
// PDFEngines wraps the gotenberg.PDFEngine modules in an internal struct which
|
// When processing PDFs, [PdfEngines] will attempt to use the engines in the
|
||||||
// also implements gotenberg.PDFEngine. This struct provides a sort of fallback
|
// order they were defined. If the primary engine encounters an error,
|
||||||
// mechanism: if an engine's method returns an error, it calls the same method
|
// [PdfEngines] can fall back to the next available engine. It also implements
|
||||||
// from another engine.
|
// the [api.Router] interface to expose relevant PDF processing routes if
|
||||||
//
|
// enabled.
|
||||||
// This module implements the gotenberg.PDFEngineProvider interface.
|
type PdfEngines struct {
|
||||||
type PDFEngines struct {
|
|
||||||
names []string
|
names []string
|
||||||
engines []gotenberg.PDFEngine
|
engines []gotenberg.PdfEngine
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a PDFEngines' module descriptor.
|
// Descriptor returns a PdfEngines' module descriptor.
|
||||||
func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
|
func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "pdfengines",
|
ID: "pdfengines",
|
||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
||||||
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the PDF engines - all by default")
|
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the PDF engines and their order - all by default")
|
||||||
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
||||||
|
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
New: func() gotenberg.Module { return new(PDFEngines) },
|
New: func() gotenberg.Module { return new(PdfEngines) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision gets either all gotenberg.PDFEngine modules or the modules
|
// Provision gets either all [gotenberg.PdfEngine] modules or the modules
|
||||||
// selected by the user thanks to the "engines" flag.
|
// selected by the user thanks to the "engines" flag.
|
||||||
func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
names := flags.MustStringSlice("pdfengines-engines")
|
names := flags.MustStringSlice("pdfengines-engines")
|
||||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
mod.engines = make([]gotenberg.PDFEngine, len(engines))
|
mod.engines = make([]gotenberg.PdfEngine, len(engines))
|
||||||
|
|
||||||
for i, engine := range engines {
|
for i, engine := range engines {
|
||||||
mod.engines[i] = engine.(gotenberg.PDFEngine)
|
mod.engines[i] = engine.(gotenberg.PdfEngine)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(names) > 0 {
|
if len(names) > 0 {
|
||||||
// Selection from user.
|
// Selection from user.
|
||||||
mod.names = names
|
mod.names = names
|
||||||
|
|
||||||
|
// Example in case of deprecated module name.
|
||||||
|
//for i, name := range names {
|
||||||
|
// if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
||||||
|
// logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
||||||
|
// mod.names[i] = "libreoffice-pdfengine"
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +90,10 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates there is at least one gotenberg.PDFEngine module
|
// Validate validates there is at least one [gotenberg.PdfEngine] module
|
||||||
// available. It also validates that selected gotenberg.PDFEngine modules
|
// available. It also validates that selected [gotenberg.PdfEngine] modules
|
||||||
// actually exist.
|
// actually exist.
|
||||||
func (mod PDFEngines) Validate() error {
|
func (mod *PdfEngines) Validate() error {
|
||||||
if len(mod.engines) == 0 {
|
if len(mod.engines) == 0 {
|
||||||
return errors.New("no PDF engine")
|
return errors.New("no PDF engine")
|
||||||
}
|
}
|
||||||
@@ -118,30 +128,41 @@ func (mod PDFEngines) Validate() error {
|
|||||||
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
|
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDFEngine returns a gotenberg.PDFEngine.
|
// SystemMessages returns one message with the selected [gotenberg.PdfEngine]
|
||||||
func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
|
// modules.
|
||||||
engines := make([]gotenberg.PDFEngine, len(mod.engines))
|
func (mod *PdfEngines) SystemMessages() []string {
|
||||||
|
return []string{
|
||||||
|
strings.Join(mod.names[:], " "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
i := 0
|
// PdfEngine returns a [gotenberg.PdfEngine].
|
||||||
for _, engine := range mod.engines {
|
func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||||
engines[i] = engine
|
engines := make([]gotenberg.PdfEngine, len(mod.names))
|
||||||
i++
|
|
||||||
|
for i, name := range mod.names {
|
||||||
|
for _, engine := range mod.engines {
|
||||||
|
if name == engine.(gotenberg.Module).Descriptor().ID {
|
||||||
|
engines[i] = engine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newMultiPDFEngines(engines...), nil
|
return newMultiPdfEngines(engines...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routes returns the HTTP routes.
|
// Routes returns the HTTP routes.
|
||||||
func (mod PDFEngines) Routes() ([]api.Route, error) {
|
func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||||
if mod.disableRoutes {
|
if mod.disableRoutes {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
engine, err := mod.PDFEngine()
|
engine, err := mod.PdfEngine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Should not happen, unless our provider implementation
|
// Should not happen, unless our provider implementation
|
||||||
// changes in the future.
|
// changes in the future.
|
||||||
return nil, fmt.Errorf("get pdf engine: %w", err)
|
return nil, fmt.Errorf("get pdf mod: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []api.Route{
|
return []api.Route{
|
||||||
@@ -152,9 +173,10 @@ func (mod PDFEngines) Routes() ([]api.Route, error) {
|
|||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*PDFEngines)(nil)
|
_ gotenberg.Module = (*PdfEngines)(nil)
|
||||||
_ gotenberg.Provisioner = (*PDFEngines)(nil)
|
_ gotenberg.Provisioner = (*PdfEngines)(nil)
|
||||||
_ gotenberg.Validator = (*PDFEngines)(nil)
|
_ gotenberg.Validator = (*PdfEngines)(nil)
|
||||||
_ gotenberg.PDFEngineProvider = (*PDFEngines)(nil)
|
_ gotenberg.SystemLogger = (*PdfEngines)(nil)
|
||||||
_ api.Router = (*PDFEngines)(nil)
|
_ gotenberg.PdfEngineProvider = (*PdfEngines)(nil)
|
||||||
|
_ api.Router = (*PdfEngines)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,120 +1,106 @@
|
|||||||
package pdfengines
|
package pdfengines
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
func TestPdfEngines_Descriptor(t *testing.T) {
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
descriptor := new(PdfEngines).Descriptor()
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoValidator struct {
|
|
||||||
ProtoModule
|
|
||||||
validate func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoValidator) Validate() error {
|
|
||||||
return mod.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngine struct {
|
|
||||||
ProtoValidator
|
|
||||||
merge func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
|
||||||
convert func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return mod.merge(ctx, logger, inputPaths, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
|
||||||
return mod.convert(ctx, logger, format, inputPath, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPDFEngine_Descriptor(t *testing.T) {
|
|
||||||
descriptor := PDFEngines{}.Descriptor()
|
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
expect := reflect.TypeOf(new(PDFEngines))
|
expect := reflect.TypeOf(new(PdfEngines))
|
||||||
|
|
||||||
if actual != expect {
|
if actual != expect {
|
||||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_Provision(t *testing.T) {
|
func TestPdfEngines_Provision(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
ctx *gotenberg.Context
|
ctx *gotenberg.Context
|
||||||
expectNames []string
|
expectedPdfEngines []string
|
||||||
expectEnginesCount int
|
expectError bool
|
||||||
expectErr bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
scenario: "no selection from user",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
engine := struct{ ProtoPDFEngine }{}
|
provider := &struct {
|
||||||
engine.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.ValidatorMock
|
||||||
|
gotenberg.PdfEngineMock
|
||||||
|
}{}
|
||||||
|
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||||
|
}
|
||||||
|
engine.ValidateMock = func() error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
engine.validate = func() error { return errors.New("foo") }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
engine.Descriptor(),
|
engine.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectedPdfEngines: []string{"bar"},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "selection from user",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
engine := struct{ ProtoPDFEngine }{}
|
provider := &struct {
|
||||||
engine.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
engine.validate = func() error { return nil }
|
engine1 := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
return gotenberg.NewContext(
|
gotenberg.ValidatorMock
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.PdfEngineMock
|
||||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
}{}
|
||||||
},
|
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
engine.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectNames: []string{"foo"},
|
|
||||||
expectEnginesCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
||||||
}
|
}
|
||||||
engine1.validate = func() error { return nil }
|
engine1.ValidateMock = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
engine2 := &struct {
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.ValidatorMock
|
||||||
|
gotenberg.PdfEngineMock
|
||||||
|
}{}
|
||||||
|
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
||||||
}
|
}
|
||||||
engine2.validate = func() error { return nil }
|
engine2.ValidateMock = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fs := new(PDFEngines).Descriptor().FlagSet
|
fs := new(PdfEngines).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
|
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -124,181 +110,229 @@ func TestPDFEngine_Provision(t *testing.T) {
|
|||||||
FlagSet: fs,
|
FlagSet: fs,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
engine1.Descriptor(),
|
engine1.Descriptor(),
|
||||||
engine2.Descriptor(),
|
engine2.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectNames: []string{"b", "a"},
|
expectedPdfEngines: []string{"b", "a"},
|
||||||
expectEnginesCount: 2,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "no valid PDF engine",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
provider := &struct {
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
engine1.validate = func() error { return nil }
|
engine := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
gotenberg.ValidatorMock
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.PdfEngineMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
}{}
|
||||||
|
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||||
}
|
}
|
||||||
engine2.validate = func() error { return nil }
|
engine.ValidateMock = func() error {
|
||||||
|
return errors.New("foo")
|
||||||
fs := new(PDFEngines).Descriptor().FlagSet
|
|
||||||
err := fs.Parse([]string{"--pdfengines-engines=b"})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected error but got: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: fs,
|
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
engine1.Descriptor(),
|
provider.Descriptor(),
|
||||||
engine2.Descriptor(),
|
engine.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectNames: []string{"b"},
|
expectError: true,
|
||||||
expectEnginesCount: 2,
|
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(PDFEngines)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
err := mod.Provision(tc.ctx)
|
mod := new(PdfEngines)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tc.expectNames) != len(mod.names) {
|
|
||||||
t.Errorf("test %d: expected %d names but got %d", i, len(tc.expectNames), len(mod.names))
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectEnginesCount != len(mod.engines) {
|
|
||||||
t.Errorf("test %d: expected %d engines but got %d", i, tc.expectEnginesCount, len(mod.engines))
|
|
||||||
}
|
|
||||||
|
|
||||||
for index, name := range mod.names {
|
|
||||||
if name != tc.expectNames[index] {
|
|
||||||
t.Errorf("test %d: expected name at index %d to be %s, but got: %s", i, index, name, tc.expectNames[index])
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tc.expectedPdfEngines) != len(mod.names) {
|
||||||
|
t.Fatalf("expected %d names but got %d", len(tc.expectedPdfEngines), len(mod.names))
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, name := range mod.names {
|
||||||
|
if name != tc.expectedPdfEngines[index] {
|
||||||
|
t.Fatalf("expected scenario at index %d to be %s, but got: %s", index, name, tc.expectedPdfEngines[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_Validate(t *testing.T) {
|
func TestPdfEngines_Validate(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
names []string
|
scenario string
|
||||||
engines []gotenberg.PDFEngine
|
names []string
|
||||||
expectErr bool
|
engines []gotenberg.PdfEngine
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectErr: true,
|
scenario: "existing PDF engine",
|
||||||
},
|
names: []string{"foo"},
|
||||||
{
|
engines: func() []gotenberg.PdfEngine {
|
||||||
names: []string{"foo"},
|
engine := &struct {
|
||||||
engines: func() []gotenberg.PDFEngine {
|
gotenberg.ModuleMock
|
||||||
engine := struct{ ProtoPDFEngine }{}
|
gotenberg.PdfEngineMock
|
||||||
engine.descriptor = func() gotenberg.ModuleDescriptor {
|
}{}
|
||||||
|
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []gotenberg.PDFEngine{
|
return []gotenberg.PdfEngine{
|
||||||
engine,
|
engine,
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: []string{"foo", "bar", "baz"},
|
scenario: "non-existing bar PDF engine",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
names: []string{"foo", "bar", "baz"},
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
engines: func() []gotenberg.PdfEngine {
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
engine1 := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PdfEngineMock
|
||||||
|
}{}
|
||||||
|
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||||
}
|
}
|
||||||
|
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
engine2 := &struct {
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PdfEngineMock
|
||||||
|
}{}
|
||||||
|
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
|
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []gotenberg.PDFEngine{
|
return []gotenberg.PdfEngine{
|
||||||
engine1,
|
engine1,
|
||||||
engine2,
|
engine2,
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "no PDF engine",
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(PDFEngines)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod.names = tc.names
|
mod := PdfEngines{
|
||||||
mod.engines = tc.engines
|
names: tc.names,
|
||||||
err := mod.Validate()
|
engines: tc.engines,
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
err := mod.Validate()
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_PDFEngine(t *testing.T) {
|
func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||||
mod := new(PDFEngines)
|
mod := new(PdfEngines)
|
||||||
mod.engines = []gotenberg.PDFEngine{
|
mod.names = []string{"foo", "bar"}
|
||||||
struct{ ProtoPDFEngine }{},
|
|
||||||
|
messages := mod.SystemMessages()
|
||||||
|
if len(messages) != 1 {
|
||||||
|
t.Errorf("expected one and only one message, but got %d", len(messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := mod.PDFEngine()
|
expect := strings.Join(mod.names[:], " ")
|
||||||
|
if messages[0] != expect {
|
||||||
|
t.Errorf("expected message '%s', but got '%s'", expect, messages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPdfEngines_PdfEngine(t *testing.T) {
|
||||||
|
mod := PdfEngines{
|
||||||
|
names: []string{"foo", "bar"},
|
||||||
|
engines: func() []gotenberg.PdfEngine {
|
||||||
|
engine1 := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PdfEngineMock
|
||||||
|
}{}
|
||||||
|
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine2 := &struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PdfEngineMock
|
||||||
|
}{}
|
||||||
|
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []gotenberg.PdfEngine{
|
||||||
|
engine1,
|
||||||
|
engine2,
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mod.PdfEngine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error but got: %v", err)
|
t.Errorf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_Routes(t *testing.T) {
|
func TestPdfEngines_Routes(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
expectRoutes int
|
expectRoutes int
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectRoutes: 2,
|
scenario: "routes not disabled",
|
||||||
|
expectRoutes: 2,
|
||||||
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
scenario: "routes disabled",
|
||||||
|
expectRoutes: 0,
|
||||||
disableRoutes: true,
|
disableRoutes: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mod := new(PDFEngines)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
mod.engines = []gotenberg.PDFEngine{
|
mod := new(PdfEngines)
|
||||||
struct{ ProtoPDFEngine }{},
|
mod.disableRoutes = tc.disableRoutes
|
||||||
}
|
|
||||||
mod.disableRoutes = tc.disableRoutes
|
|
||||||
|
|
||||||
routes, err := mod.Routes()
|
routes, err := mod.Routes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectRoutes != len(routes) {
|
if tc.expectRoutes != len(routes) {
|
||||||
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes))
|
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoValidator)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoValidator)(nil)
|
|
||||||
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoPDFEngine)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoPDFEngine)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mergeRoute returns an api.Route which can merge PDFs.
|
// mergeRoute returns an [api.Route] which can merge PDFs.
|
||||||
func mergeRoute(engine gotenberg.PDFEngine) api.Route {
|
func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/forms/pdfengines/merge",
|
Path: "/forms/pdfengines/merge",
|
||||||
@@ -22,18 +23,24 @@ 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
|
||||||
|
pdfua bool
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
String("pdfFormat", &PDFformat, "").
|
String("pdfa", &pdfa, "").
|
||||||
|
Bool("pdfua", &pdfua, false).
|
||||||
Validate()
|
Validate()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pdfFormats := gotenberg.PdfFormats{
|
||||||
|
PdfA: pdfa,
|
||||||
|
PdfUa: pdfua,
|
||||||
|
}
|
||||||
|
|
||||||
// Alright, let's merge the PDFs.
|
// Alright, let's merge the PDFs.
|
||||||
|
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
@@ -45,21 +52,21 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
|
|||||||
|
|
||||||
// So far so good, the PDFs are merged into one unique PDF.
|
// So far so good, the PDFs are merged into one unique PDF.
|
||||||
// Now, let's check if the client want to convert this result PDF
|
// Now, let's check if the client want to convert this result PDF
|
||||||
// to a specific PDF format.
|
// to specific PDF formats.
|
||||||
|
zeroValued := gotenberg.PdfFormats{}
|
||||||
if PDFformat != "" {
|
if pdfFormats != zeroValued {
|
||||||
convertInputPath := outputPath
|
convertInputPath := outputPath
|
||||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err = engine.Convert(ctx, ctx.Log(), PDFformat, convertInputPath, convertOutputPath)
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
fmt.Errorf("convert PDF: %w", err),
|
||||||
api.NewSentinelHTTPError(
|
api.NewSentinelHttpError(
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
|
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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -84,9 +91,9 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertRoute returns an api.Route which can convert a PDF to a specific PDF
|
// 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,
|
||||||
Path: "/forms/pdfengines/convert",
|
Path: "/forms/pdfengines/convert",
|
||||||
@@ -97,34 +104,50 @@ 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
|
||||||
|
pdfua bool
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
MandatoryString("pdfFormat", &PDFformat).
|
String("pdfa", &pdfa, "").
|
||||||
|
Bool("pdfua", &pdfua, false).
|
||||||
Validate()
|
Validate()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alright, let's merge the PDFs.
|
pdfFormats := gotenberg.PdfFormats{
|
||||||
|
PdfA: pdfa,
|
||||||
|
PdfUa: pdfua,
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroValued := gotenberg.PdfFormats{}
|
||||||
|
if pdfFormats == zeroValued {
|
||||||
|
return api.WrapError(
|
||||||
|
errors.New("no PDF formats"),
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"Invalid form data: either 'pdfa' or 'pdfua' form fields must be provided",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(), PDFformat, inputPath, outputPaths[i])
|
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert PDF: %w", err),
|
fmt.Errorf("convert PDF: %w", err),
|
||||||
api.NewSentinelHTTPError(
|
api.NewSentinelHttpError(
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
|
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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,394 +6,408 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
|
||||||
"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/v8/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMergeHandler(t *testing.T) {
|
func TestMergeHandler(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
ctx *api.MockContext
|
scenario string
|
||||||
engine gotenberg.PDFEngine
|
ctx *api.ContextMock
|
||||||
expectErr bool
|
engine gotenberg.PdfEngine
|
||||||
expectHTTPErr bool
|
expectError bool
|
||||||
expectHTTPStatus int
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
expectOutputPathsCount int
|
expectOutputPathsCount int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: &api.MockContext{Context: &api.Context{}},
|
scenario: "missing at least one mandatory file",
|
||||||
expectErr: true,
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
expectHTTPErr: true,
|
expectError: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "error from PDF engine",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
},
|
||||||
},
|
},
|
||||||
}
|
expectError: true,
|
||||||
}(),
|
expectHttpError: false,
|
||||||
expectErr: true,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "cannot add output paths",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return gotenberg.ErrPDFFormatNotAvailable
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
expectHTTPErr: true,
|
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetCancelled(true)
|
ctx.SetCancelled(true)
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
},
|
},
|
||||||
}
|
expectError: true,
|
||||||
}(),
|
expectHttpError: false,
|
||||||
expectErr: true,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "success",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
},
|
},
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
expectError: false,
|
||||||
return nil
|
expectHttpError: false,
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "ErrPdfFormatNotSupported",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
|
"file2.pdf": "/file2.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, 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
|
||||||
|
},
|
||||||
|
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 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{
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
c := echo.New().NewContext(nil, nil)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
c.Set("context", tc.ctx.Context)
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
c := echo.New().NewContext(nil, nil)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
err := mergeRoute(tc.engine).Handler(c)
|
err := mergeRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none", err)
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpErr api.HTTPError
|
|
||||||
isHTTPErr := errors.As(err, &httpErr)
|
|
||||||
|
|
||||||
if tc.expectHTTPErr && !isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected HTTP error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectHTTPErr && isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
|
||||||
status, _ := httpErr.HTTPError()
|
|
||||||
if status != tc.expectHTTPStatus {
|
|
||||||
t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
t.Fatalf("expected no error 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()))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertHandler(t *testing.T) {
|
func TestConvertHandler(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
ctx *api.MockContext
|
scenario string
|
||||||
engine gotenberg.PDFEngine
|
ctx *api.ContextMock
|
||||||
expectErr bool
|
engine gotenberg.PdfEngine
|
||||||
expectHTTPErr bool
|
expectError bool
|
||||||
expectHTTPStatus int
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
expectOutputPathsCount int
|
expectOutputPathsCount int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: &api.MockContext{Context: &api.Context{}},
|
scenario: "missing at least one mandatory file",
|
||||||
expectErr: true,
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
expectHTTPErr: true,
|
expectError: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "no PDF formats",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
ctx.SetValues(map[string][]string{
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
expectHTTPErr: true,
|
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
expectError: true,
|
||||||
return &ProtoPDFEngine{
|
expectHttpError: true,
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
return gotenberg.ErrPDFFormatNotAvailable
|
expectOutputPathsCount: 0,
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
expectHTTPErr: true,
|
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "ErrPdfFormatNotSupported",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
"pdfa": {
|
||||||
"foo",
|
gotenberg.PdfA1b,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return gotenberg.ErrPdfFormatNotSupported
|
||||||
return errors.New("foo")
|
},
|
||||||
},
|
},
|
||||||
}
|
expectError: true,
|
||||||
}(),
|
expectHttpError: true,
|
||||||
expectErr: true,
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "error from PDF engine",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
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 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{
|
||||||
|
"pdfa": {
|
||||||
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
})
|
||||||
ctx.SetCancelled(true)
|
ctx.SetCancelled(true)
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
},
|
},
|
||||||
}
|
expectError: true,
|
||||||
}(),
|
expectHttpError: false,
|
||||||
expectErr: true,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "success with PDF/A & PDF/UA form fields (single file)",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
"pdfa": {
|
||||||
"foo",
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
},
|
},
|
||||||
}
|
expectError: false,
|
||||||
}(),
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *api.MockContext {
|
scenario: "success with PDF/A & PDF/UA form fields (many files)",
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"file.pdf": "/file.pdf",
|
||||||
"bar.pdf": "/foo/bar.pdf",
|
"file2.pdf": "/file2.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
"pdfa": {
|
||||||
"foo",
|
gotenberg.PdfA1b,
|
||||||
|
},
|
||||||
|
"pdfua": {
|
||||||
|
"true",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: &gotenberg.PdfEngineMock{
|
||||||
return &ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
},
|
},
|
||||||
}
|
expectError: false,
|
||||||
}(),
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 2,
|
expectOutputPathsCount: 2,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
c := echo.New().NewContext(nil, nil)
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
c.Set("context", tc.ctx.Context)
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
c := echo.New().NewContext(nil, nil)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
err := convertRoute(tc.engine).Handler(c)
|
err := convertRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected error but got none", err)
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpErr api.HTTPError
|
|
||||||
isHTTPErr := errors.As(err, &httpErr)
|
|
||||||
|
|
||||||
if tc.expectHTTPErr && !isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected HTTP error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectHTTPErr && isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
|
||||||
status, _ := httpErr.HTTPError()
|
|
||||||
if status != tc.expectHTTPStatus {
|
|
||||||
t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
if !tc.expectError && err != nil {
|
||||||
t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
t.Fatalf("expected no error 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()))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
// Package pdftk provides a module which abstracts the CLI tool PDFtk and
|
// Package pdftk provides an implementation of the gotenberg.PdfEngine
|
||||||
// implements the gotenberg.PDFEngine interface.
|
// interface using the PDFtk command-line tool. This package allows for the
|
||||||
|
// merging of PDF files but does not support conversion to specific PDF
|
||||||
|
// formats. The path to the PDFtk binary must be specified using the
|
||||||
|
// PDFTK_BIN_PATH environment variable.
|
||||||
|
//
|
||||||
|
// See: https://gitlab.com/pdftk-java/pdftk.
|
||||||
package pdftk
|
package pdftk
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user