Compare commits

..

58 Commits

Author SHA1 Message Date
dependabot[bot]
ffeb4703fd chore(deps): bump golang.org/x/net from 0.40.0 to 0.41.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 10:08:55 +02:00
dependabot[bot]
fcf41187fa chore(deps): bump golang.org/x/text from 0.25.0 to 0.26.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 10:06:42 +02:00
dependabot[bot]
f0fc704efc chore(deps): bump golang.org/x/sync from 0.14.0 to 0.15.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/sync/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 10:06:30 +02:00
Julien Neuhart
aa58615650 fix(api): outpout filename as path - fixes #1227 2025-06-06 15:14:06 +00:00
Julien Neuhart
b0a84bcbf7 chore(integration): prettify 2025-06-02 11:45:44 +02:00
Julien Neuhart
cf2d413a28 test(integration): comment scenario that is not happening anymore on amd architectures 2025-06-02 11:27:25 +02:00
dependabot[bot]
9226b78b0c chore(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.1.1+incompatible to 28.2.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.1.1...v28.2.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.2.2+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 07:09:19 +02:00
dependabot[bot]
a635fd8a7b chore(deps): bump github.com/shirou/gopsutil/v4 from 4.25.4 to 4.25.5
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.25.4 to 4.25.5.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.25.4...v4.25.5)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.25.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 07:06:58 +02:00
Julien Neuhart
6f714277f7 docs(README): remove Zolsec from sponsors 2025-05-28 15:25:04 +02:00
dependabot[bot]
4aff4982b0 chore(deps): bump github.com/labstack/echo/v4 from 4.13.3 to 4.13.4
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.13.3 to 4.13.4.
- [Release notes](https://github.com/labstack/echo/releases)
- [Changelog](https://github.com/labstack/echo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo/compare/v4.13.3...v4.13.4)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v4
  dependency-version: 4.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 07:11:51 +02:00
Julien Neuhart
8634989497 fix(Dockerfile): typo 2025-05-22 09:41:41 +02:00
Julien Neuhart
a50e60b2a0 fix(Dockerfile): remove fonts-dejavu-extra as the package is already packaged in fonts-dejavu 2025-05-22 09:41:07 +02:00
Julien Neuhart
8875f4dfc4 ci: fix needs entries in merge_clean_snapshot_tags step 2025-05-21 00:34:56 +02:00
Julien Neuhart
61c8217ed5 feat(Dockerfile): support for arbitrary user ids - closes #1049 2025-05-21 00:34:56 +02:00
Julien Neuhart
f75912033e feat(chromium): split filenames can be controlled with the Gotenberg-Output-Filename header - closes #1130 2025-05-20 14:14:05 +02:00
Julien Neuhart
1d8630810b fix(godoc): typo 2025-05-20 11:50:18 +02:00
Julien Neuhart
ce155fb43e feat(chromium): add generateTaggedPdf to form fields - closes #1210 2025-05-20 11:49:10 +02:00
dependabot[bot]
67ebe17fcb chore(deps): bump github.com/mholt/archives from 0.1.1 to 0.1.2
Bumps [github.com/mholt/archives](https://github.com/mholt/archives) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/mholt/archives/releases)
- [Commits](https://github.com/mholt/archives/compare/v0.1.1...v0.1.2)

---
updated-dependencies:
- dependency-name: github.com/mholt/archives
  dependency-version: 0.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 09:09:25 +02:00
Julien Neuhart
f13a6c0a3b Revert "fix(libreoffice): correct values for CreateDate, MetadataDate, and ModifyDate metadata entries - fixes #1193"
This reverts commit e662f5ce0b.
2025-05-18 14:15:22 +02:00
Julien Neuhart
e1870429b9 Revert "test(integration): fix rules failed toleration"
This reverts commit de1b887d29.
2025-05-18 14:15:12 +02:00
Hector Zarate
5aa1cd7f40 feat(Dockerfile): pin versions of dependencies (#1190)
* Pin versions of dependencies

* no need to override default values in github build action

* unpin curl,gnupg,tiny,python3,default-jre-headless

* Revert "unpin curl,gnupg,tiny,python3,default-jre-headless"

This reverts commit 95b0250700.

* remove pin from Libreoffice

* Update build/Dockerfile

Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>

---------

Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>
2025-05-18 14:08:11 +02:00
Julien Neuhart
de1b887d29 test(integration): fix rules failed toleration 2025-05-15 14:23:31 +02:00
Julien Neuhart
e662f5ce0b fix(libreoffice): correct values for CreateDate, MetadataDate, and ModifyDate metadata entries - fixes #1193 2025-05-15 10:38:22 +02:00
Julien Neuhart
790b69a32e test(integration): fix typo 2025-05-15 10:16:57 +02:00
Julien Neuhart
9aaab97d6a fix(pdfengines): read metadata better webhook error - fixes #1148 2025-05-15 09:22:52 +02:00
Julien Neuhart
66317197b6 fix: graceful shutdown with asynchronous processes - fixes #1022 2025-05-14 15:27:14 +02:00
Julien Neuhart
91757335ac ci: make it work for forks too 2025-05-14 09:37:42 +02:00
Julien Neuhart
be3145aae9 chore(libreoffice): switch to env var for handling non-basic latin characters in filenames 2025-05-13 16:10:14 +02:00
Julien Neuhart
79425af848 feat(Dockerfile): use jlink to switch to a lightweight JRE 2025-05-13 16:06:59 +02:00
Julien Neuhart
45b47a1071 feat(Dockerfile): use upx-ucl to reduce Go binaries size 2025-05-13 16:06:59 +02:00
Julien Neuhart
4b30567c39 feat(chromium): close page after conversion 2025-05-12 09:35:10 +02:00
dependabot[bot]
1ed33a7400 chore(deps): bump golang.org/x/net from 0.39.0 to 0.40.0 (#1204)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/net/compare/v0.39.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 08:56:09 +02:00
dependabot[bot]
ca1d11006c chore(deps-dev): bump prettier-plugin-sh from 0.17.2 to 0.17.4 (#1205)
Bumps [prettier-plugin-sh](https://github.com/un-ts/prettier) from 0.17.2 to 0.17.4.
- [Release notes](https://github.com/un-ts/prettier/releases)
- [Changelog](https://github.com/un-ts/prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un-ts/prettier/compare/prettier-plugin-sh@0.17.2...prettier-plugin-sh@0.17.4)

---
updated-dependencies:
- dependency-name: prettier-plugin-sh
  dependency-version: 0.17.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 08:56:00 +02:00
teng
71f1f3dd22 feat(chromium): change sort of wait delay action and wait expression action (#1186)
eg: scroll screen to load all lazy image.
2025-05-06 10:26:46 +02:00
dependabot[bot]
64add9a542 chore(deps-dev): bump prettier-plugin-sh from 0.15.0 to 0.17.2
Bumps [prettier-plugin-sh](https://github.com/un-ts/prettier) from 0.15.0 to 0.17.2.
- [Release notes](https://github.com/un-ts/prettier/releases)
- [Changelog](https://github.com/un-ts/prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un-ts/prettier/compare/prettier-plugin-sh@0.15.0...prettier-plugin-sh@0.17.2)

---
updated-dependencies:
- dependency-name: prettier-plugin-sh
  dependency-version: 0.17.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 19:40:13 +02:00
dependabot[bot]
a7c02438e4 chore(deps-dev): bump prettier from 3.5.2 to 3.5.3
Bumps [prettier](https://github.com/prettier/prettier) from 3.5.2 to 3.5.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.2...3.5.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 19:39:58 +02:00
dependabot[bot]
910af7a44c chore(deps): bump golang.org/x/sync from 0.13.0 to 0.14.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.13.0 to 0.14.0.
- [Commits](https://github.com/golang/sync/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 19:21:55 +02:00
dependabot[bot]
05f9ac3745 chore(deps-dev): bump prettier-plugin-gherkin from 3.1.1 to 3.1.2
Bumps [prettier-plugin-gherkin](https://github.com/mapado/prettier-plugin-gherkin) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/mapado/prettier-plugin-gherkin/releases)
- [Changelog](https://github.com/mapado/prettier-plugin-gherkin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mapado/prettier-plugin-gherkin/compare/v3.1.1...v3.1.2)

---
updated-dependencies:
- dependency-name: prettier-plugin-gherkin
  dependency-version: 3.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 19:21:35 +02:00
dependabot[bot]
5c43eefe46 chore(deps): bump golangci/golangci-lint-action from 7 to 8
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7 to 8.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 15:52:15 +02:00
Julien Neuhart
b63f4c53d2 ci(lint): update golangci-lint version 2025-05-05 15:49:24 +02:00
Julien Neuhart
3d652bc829 ci(dependabot): add npm 2025-05-05 15:48:52 +02:00
dependabot[bot]
c78ef8771d chore(deps): bump github.com/shirou/gopsutil/v4 from 4.25.3 to 4.25.4
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.25.3 to 4.25.4.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.25.3...v4.25.4)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 08:40:41 +02:00
dependabot[bot]
9a38f16219 chore(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.0.4+incompatible to 28.1.1+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.0.4...v28.1.1)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.1.1+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-26 13:49:14 +02:00
dependabot[bot]
e5363205a7 chore(deps): bump github.com/testcontainers/testcontainers-go
Bumps [github.com/testcontainers/testcontainers-go](https://github.com/testcontainers/testcontainers-go) from 0.36.0 to 0.37.0.
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: github.com/testcontainers/testcontainers-go
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-26 13:48:41 +02:00
Hector Zarate
088b3274f2 feat(dep): add dependabot for mod and docker files (#1179)
* add dependabot for mod and docker files

* lint dependabot file
2025-04-26 13:07:51 +02:00
Julien Neuhart
fabe323e34 fix(chromium): add --no-zygote and --disable-dev-shm-usage flags - fixes #1177 2025-04-17 11:28:17 +02:00
Julien Neuhart
a731bf034e chore(deps): update Go dependencies 2025-04-17 11:27:47 +02:00
Julien Neuhart
3cbf772acf docs(godoc): fix a bunch of typos 2025-04-17 11:15:52 +02:00
Julien Neuhart
9701f88ae3 fix(pdfcpu): use custom sort to retrieve the splitted PDFs 2025-04-12 19:40:16 +02:00
Julien Neuhart
7ca1d118a1 chore(deps): update Go dependencies 2025-04-12 17:37:15 +02:00
Julien Neuhart
a7dfa21cbf fix(pdfengines): correct merge order - fixes #1168 2025-04-12 17:25:39 +02:00
Julien Neuhart
ab81444773 fix(Makefile): add double-quote to new flag 2025-04-12 17:24:23 +02:00
Julien Neuhart
02c4343dfd fix(test): missing env var for debug 2025-04-10 16:28:15 +02:00
Hector Zarate
1bfe86738f feat(debug): only build debug data when flag is true (#1167)
* only build debug info when log level is debug

* introduce new flag 'gotenberg-build-debug-data'

* add gotenberg-build-debug-data flag to Makefile

* add new integration test for gotenberg-build-debug-data flag

* update GET /debug (Enabled) test with gotenberg-build-debug-data flag

* reorder GOTENBERG_BUILD_DEBUG_DATA in Makefile

* reorder Build Debug Data Disabled test scenario

---------

Co-authored-by: Hector Zarate <hector@Hectors-MacBook-Pro.local>
2025-04-10 13:30:24 +02:00
Julien Neuhart
7fe0e05050 test(webhook): add integration testing for the webhook feature - testing the custom Content-Disposition header from #1165 2025-04-08 18:51:51 +02:00
stephentgrammer
10588efe25 fix(webhook): content-disposition header (#1164)
* fix(webhook): pass content-disposition header through and fix spelling of attachment on default

* code review

Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>

* formatting

---------

Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>
2025-04-04 03:04:14 +02:00
Julien Neuhart
bf4607d368 chore(.gitignore): remove coverage entries [skip ci] 2025-04-02 15:04:52 +02:00
Julien Neuhart
45f04ae8d4 fix(chromium): printBackground now works as expected and PDF are not tagged anymore - fixes #1154 and fixes #1058 2025-04-02 14:07:15 +02:00
67 changed files with 1088 additions and 487 deletions

9
.env
View File

@@ -1,13 +1,6 @@
GOLANG_VERSION=1.24
GOTENBERG_VERSION=snapshot
DOCKER_REGISTRY=gotenberg
DOCKER_REPOSITORY=gotenberg
GOTENBERG_VERSION=snapshot
GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001
NOTO_COLOR_EMOJI_VERSION=v2.047 # See https://github.com/googlefonts/noto-emoji/releases.
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
PDFCPU_VERSION=v0.8.1 # See https://github.com/pdfcpu/pdfcpu/releases.
GOTENBERG_VERSION=snapshot
DOCKERFILE=build/Dockerfile
DOCKERFILE_CLOUDRUN=build/Dockerfile.cloudrun
DOCKER_BUILD_CONTEXT='.'

View File

@@ -48,6 +48,7 @@ runs:
uses: actions/checkout@v4
- name: Log in to Docker Hub
if: inputs.docker_hub_username != ''
uses: docker/login-action@v3
with:
username: ${{ inputs.docker_hub_username }}
@@ -64,7 +65,7 @@ runs:
--dry-run "${{ inputs.dry_run }}"
- name: Run integration tests
if: ${{ inputs.skip_integrations_tests != 'true' }}
if: inputs.skip_integrations_tests != 'true'
shell: bash
run: |
.github/actions/build-test-push/test.sh \
@@ -74,6 +75,7 @@ runs:
--dry-run "${{ inputs.dry_run }}"
- name: Push
if: inputs.docker_hub_username != ''
shell: bash
run: |
.github/actions/build-test-push/push.sh \

View File

@@ -129,13 +129,7 @@ join() {
no_arch_tag="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version"
cmd="docker buildx build \
--build-arg GOLANG_VERSION=$GOLANG_VERSION \
--build-arg GOTENBERG_VERSION=$version \
--build-arg GOTENBERG_USER_GID=$GOTENBERG_USER_GID \
--build-arg GOTENBERG_USER_UID=$GOTENBERG_USER_UID \
--build-arg NOTO_COLOR_EMOJI_VERSION=$NOTO_COLOR_EMOJI_VERSION \
--build-arg PDFTK_VERSION=$PDFTK_VERSION \
--build-arg PDFCPU_VERSION=$PDFCPU_VERSION \
--platform $platform \
--load \
${tags_flags[*]} \

View File

@@ -1,7 +1,18 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/build"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -29,9 +29,9 @@ jobs:
go-version-file: go.mod
- name: Run linters
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v8
with:
version: v2.0.2
version: v2.1.6
lint-prettier:
name: Lint non-Golang codebase
@@ -161,12 +161,34 @@ jobs:
platform: linux/arm/v7
alternate_repository: snapshot
merge_clean_snapshot_tags:
merge_clean_snapshot_guard:
needs:
- snapshot_amd64
- snapshot_386
- snapshot_arm64
- snapshot_arm_v7
name: Secrets access check
runs-on: ubuntu-latest
outputs:
continue: ${{ steps.check.outputs.continue }}
steps:
- name: Check
id: check
run: |
if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]; then
echo "continue=true" >> "$GITHUB_OUTPUT"
else
echo "continue=false" >> "$GITHUB_OUTPUT"
fi
merge_clean_snapshot_tags:
if: needs.merge_clean_snapshot_guard.outputs.continue == 'true'
needs:
- merge_clean_snapshot_guard
- snapshot_amd64
- snapshot_386
- snapshot_arm64
- snapshot_arm_v7
name: Merge and clean snapshot tags
runs-on: ubuntu-latest
steps:

View File

@@ -11,6 +11,7 @@ jobs:
cleanup:
name: Cleanup Docker images
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Check out code
uses: actions/checkout@v4

2
.gitignore vendored
View File

@@ -1,5 +1,3 @@
/coverage.html
/coverage.txt
.idea
.vscode
.DS_Store

View File

@@ -7,17 +7,11 @@ help: ## Show the help
.PHONY: build
build: ## Build the Gotenberg's Docker image
docker build \
--build-arg GOLANG_VERSION=$(GOLANG_VERSION) \
--build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) \
--build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) \
--build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) \
--build-arg NOTO_COLOR_EMOJI_VERSION=$(NOTO_COLOR_EMOJI_VERSION) \
--build-arg PDFTK_VERSION=$(PDFTK_VERSION) \
--build-arg PDFCPU_VERSION=$(PDFCPU_VERSION) \
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
-f $(DOCKERFILE) $(DOCKER_BUILD_CONTEXT)
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
GOTENBERG_BUILD_DEBUG_DATA=true
API_PORT=3000
API_PORT_FROM_ENV=
API_BIND_IP=
@@ -91,6 +85,7 @@ run: ## Start a Gotenberg container
$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
gotenberg \
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
--gotenberg-build-debug-data="$(GOTENBERG_BUILD_DEBUG_DATA)" \
--api-port=$(API_PORT) \
--api-port-from-env=$(API_PORT_FROM_ENV) \
--api-bind-ip=$(API_BIND_IP) \

View File

@@ -43,12 +43,9 @@ Head to the [documentation](https://gotenberg.dev/docs/getting-started/introduct
<a href="https://thecodingmachine.com">
<img src="https://user-images.githubusercontent.com/8983173/130324668-9d6e7b35-53a3-49c7-a574-38190d2bd6b0.png" alt="TheCodingMachine Logo" width="333" height="163" />
</a>
<a href="https://zolsec.com?utm_source=gotenberg_github&utm_medium=website" target="_blank">
<img src="https://github.com/gotenberg/gotenberg/assets/8983173/707ccc97-a79b-4dcb-8fc8-6827366e5be3" alt="Zolsec Logo" width="333" height="163" />
</a>
<a href="https://pdfme.com?utm_source=gotenberg_github&utm_medium=website" target="_blank">
<img src="https://github.com/user-attachments/assets/2a75dd40-ca18-4d34-acd5-5dd474595168" alt="pdfme Logo" width="333" height="163" />
</a>
</p>
Sponsorships help maintaining and improving Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️
Sponsorships help maintain and improve Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️

View File

@@ -2,7 +2,7 @@
## Supported Versions
Please ensure to keep your environment up-to-date and use only the latest version of Gotenberg.
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

View File

@@ -1,17 +1,17 @@
# ARG instructions do not create additional layers. Instead, next layers will
# concatenate them. Also, we have to repeat ARG instructions in each build
# stage that uses them.
ARG GOLANG_VERSION
ARG GOLANG_VERSION=1.24
# ----------------------------------------------
# pdfcpu binary build stage
# ----------------------------------------------
# Note: this stage is required as pdfcpu does not release an armhf variant by
# default.
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
ARG PDFCPU_VERSION
# See https://github.com/pdfcpu/pdfcpu/releases.
ARG PDFCPU_VERSION=v0.8.1
ENV CGO_ENABLED=0
# Define the working directory outside of $GOPATH (we're using go modules).
@@ -33,7 +33,7 @@ RUN go build -o pdfcpu -ldflags "-s -w -X 'main.version=$PDFCPU_VERSION' -X 'git
# ----------------------------------------------
FROM golang:$GOLANG_VERSION AS gotenberg-binary-stage
ARG GOTENBERG_VERSION
ARG GOTENBERG_VERSION=snapshot
ENV CGO_ENABLED=0
# Define the working directory outside of $GOPATH (we're using go modules).
@@ -49,25 +49,77 @@ RUN go mod download &&\
COPY cmd ./cmd
COPY pkg ./pkg
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
RUN go build -o gotenberg -ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
# ----------------------------------------------
# Compress Golang binaries stage
# ----------------------------------------------
FROM debian:12-slim AS compress-go-binaries-stage
RUN \
echo "deb http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list &&\
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bookworm-backports upx-ucl
COPY --from=pdfcpu-binary-stage /home/pdfcpu /home/pdfcpu
COPY --from=gotenberg-binary-stage /home/gotenberg /home/gotenberg
RUN \
upx-ucl --best /home/pdfcpu &&\
upx-ucl --best /home/gotenberg
# ----------------------------------------------
# Custom JRE stage
# Credits: https://github.com/jodconverter/docker-image-jodconverter-runtime
# ----------------------------------------------
FROM debian:12-slim AS custom-jre-stage
RUN \
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends default-jdk-headless binutils
# Note: jdeps helps finding which modules a JAR requires.
# Currently only for PDFtk, as we don't rely on LibreOffice UNO Java SDK.
ENV JAVA_MODULES=java.base,java.desktop,java.naming,java.sql
RUN jlink \
--add-modules $JAVA_MODULES \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /custom-jre
# ----------------------------------------------
# Base image stage
# ----------------------------------------------
FROM debian:12-slim AS base-image-stage
COPY --from=custom-jre-stage /custom-jre /opt/java
ENV PATH="/opt/java/bin:${PATH}"
# ----------------------------------------------
# Final stage
# ----------------------------------------------
FROM debian:12-slim
FROM base-image-stage
ARG GOTENBERG_VERSION
ARG GOTENBERG_USER_GID
ARG GOTENBERG_USER_UID
ARG NOTO_COLOR_EMOJI_VERSION
ARG PDFTK_VERSION
ARG GOTENBERG_VERSION=snapshot
ARG GOTENBERG_USER_GID=1001
ARG GOTENBERG_USER_UID=1001
# See https://github.com/googlefonts/noto-emoji/releases.
ARG NOTO_COLOR_EMOJI_VERSION=v2.047
# See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
ARG PDFTK_VERSION=v3.3.3
LABEL org.opencontainers.image.title="Gotenberg" \
org.opencontainers.image.description="A containerized API for seamless PDF conversion." \
org.opencontainers.image.version="$GOTENBERG_VERSION" \
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"
org.opencontainers.image.description="A containerized API for seamless PDF conversion." \
org.opencontainers.image.version="$GOTENBERG_VERSION" \
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 \
# Create a non-root user.
@@ -82,7 +134,7 @@ RUN \
# Note: tini is a helper for reaping zombie processes.
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg tini python3 default-jre-headless &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg=2.2.40-1.1 tini=0.19.0-1 python3=3.11.2-1+b1 &&\
# 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.
@@ -99,40 +151,39 @@ RUN \
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
./ttf-mscorefonts-installer_3.8.1_all.deb \
culmus \
fonts-beng \
fonts-hosny-amiri \
fonts-lklug-sinhala \
fonts-lohit-guru \
fonts-lohit-knda \
fonts-samyak-gujr \
fonts-samyak-mlym \
fonts-samyak-taml \
fonts-sarai \
fonts-sil-abyssinica \
fonts-sil-padauk \
fonts-telu \
fonts-thai-tlwg \
culmus=0.133-1 \
fonts-beng=2:1.3 \
fonts-hosny-amiri=0.113-1 \
fonts-lklug-sinhala=0.6-4 \
fonts-lohit-guru=2.91.2-3 \
fonts-lohit-knda=2.5.4-3 \
fonts-samyak-gujr=1.2.2-6 \
fonts-samyak-mlym=1.2.2-6 \
fonts-samyak-taml=1.2.2-6 \
fonts-sarai=1.0-3 \
fonts-sil-abyssinica=2.100-3 \
fonts-sil-padauk=5.000-3 \
fonts-telu=2:1.3 \
fonts-thai-tlwg=1:0.7.3-1 \
ttf-wqy-zenhei \
fonts-arphic-ukai \
fonts-arphic-uming \
fonts-ipafont-mincho \
fonts-ipafont-gothic \
fonts-unfonts-core \
fonts-arphic-ukai=0.2.20080216.2-5 \
fonts-arphic-uming=0.2.20080216.2-11 \
fonts-ipafont-mincho=00303-23 \
fonts-ipafont-gothic=00303-23 \
fonts-unfonts-core=1:1.0.2-080608-18 \
# LibreOffice recommends.
fonts-crosextra-caladea \
fonts-crosextra-carlito \
fonts-dejavu \
fonts-dejavu-extra \
fonts-liberation \
fonts-liberation2 \
fonts-linuxlibertine \
fonts-noto-cjk \
fonts-noto-core \
fonts-noto-mono \
fonts-noto-ui-core \
fonts-sil-gentium \
fonts-sil-gentium-basic &&\
fonts-crosextra-caladea=20200211-1 \
fonts-crosextra-carlito=20220224-1 \
fonts-dejavu=2.37-6 \
fonts-liberation=1:1.07.4-11 \
fonts-liberation2=2.1.5-1 \
fonts-linuxlibertine=5.3.0-6 \
fonts-noto-cjk=1:20220127+repack1-1 \
fonts-noto-core=20201225-1 \
fonts-noto-mono=20201225-1 \
fonts-noto-ui-core=20201225-1 \
fonts-sil-gentium=20081126:1.03-4 \
fonts-sil-gentium-basic=1.102-1.1 &&\
rm -f ./ttf-mscorefonts-installer_3.8.1_all.deb &&\
# Add Color and Black-and-White Noto emoji font.
# Credits:
@@ -149,22 +200,29 @@ RUN \
/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 &&\
apt-get upgrade -yqq &&\
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; \
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 &&\
apt-get upgrade -yqq &&\
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 \
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium; \
apt-get update -qq &&\
apt-get upgrade -yqq &&\
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/*
# Set default characterset encoding to UTF-8.
# See:
# https://github.com/gotenberg/gotenberg/issues/104
# https://github.com/gotenberg/gotenberg/issues/730
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
RUN \
# Install LibreOffice & unoconverter.
echo "deb http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list &&\
@@ -186,11 +244,11 @@ RUN \
# 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 && \
printf '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
chmod +x /usr/bin/pdftk &&\
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf exiftool &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf=11.3.0-1+deb12u1 exiftool &&\
# See https://github.com/nextcloud/docker/issues/380.
mkdir -p /usr/share/man/man1 &&\
# Verify installations.
@@ -200,17 +258,24 @@ RUN \
# Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Support for arbitrary user IDs (OpenShift).
# See:
# https://github.com/gotenberg/gotenberg/issues/1049.
# https://docs.redhat.com/en/documentation/openshift_container_platform/4.15/html/images/creating-images#use-uid_create-images.
RUN \
usermod -aG root gotenberg &&\
chgrp -R 0 /home/gotenberg &&\
chmod -R g=u /home/gotenberg
# 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 pdfcpu binary from the pdfcpu-binary-stage.
COPY --from=pdfcpu-binary-stage /home/pdfcpu /usr/bin/
# Copy the Gotenberg binary from the gotenberg-binary-stage.
COPY --from=gotenberg-binary-stage /home/gotenberg /usr/bin/
# Copy the Golang binaries.
COPY --from=compress-go-binaries-stage /home/pdfcpu /usr/bin/
COPY --from=compress-go-binaries-stage /home/gotenberg /usr/bin/
# Environment variables required by modules or else.
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium

View File

@@ -2,6 +2,7 @@ package gotenbergcmd
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
@@ -41,6 +42,7 @@ func Run() {
// Create the root FlagSet and adds the modules flags to it.
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
fs.Bool("gotenberg-build-debug-data", true, "Set if build data is needed")
descriptors := gotenberg.GetModuleDescriptors()
var modsInfo string
@@ -137,8 +139,10 @@ func Run() {
}(l.(gotenberg.SystemLogger))
}
// Build the debug data.
gotenberg.BuildDebug(ctx)
if parsedFlags.MustBool("gotenberg-build-debug-data") {
// Build the debug data.
gotenberg.BuildDebug(ctx)
}
quit := make(chan os.Signal, 1)
@@ -170,7 +174,9 @@ func Run() {
id := app.(gotenberg.Module).Descriptor().ID
err = app.Stop(gracefulShutdownCtx)
if err != nil {
if errors.Is(err, gotenberg.ErrCancelGracefulShutdownContext) {
cancel()
} else if err != nil {
return fmt.Errorf("stopping %s: %w", id, err)
}

46
go.mod
View File

@@ -4,49 +4,49 @@ go 1.24.0
require (
github.com/alexliesenfeld/health v0.8.0
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
github.com/barasher/go-exiftool v1.10.0
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8
github.com/chromedp/chromedp v0.13.3
github.com/chromedp/cdproto v0.0.0-20250416210000-d7e4d624041a
github.com/chromedp/chromedp v0.13.6
github.com/google/uuid v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/labstack/echo/v4 v4.13.3
github.com/labstack/echo/v4 v4.13.4
github.com/labstack/gommon v0.4.2
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microcosm-cc/bluemonday v1.0.27
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/prometheus/client_golang v1.21.1
github.com/prometheus/client_golang v1.22.0
github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/pflag v1.0.6
github.com/ulikunitz/xz v0.5.12 // indirect
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0
golang.org/x/text v0.23.0
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0
golang.org/x/text v0.26.0
)
require (
github.com/cucumber/godog v0.15.0
github.com/dlclark/regexp2 v1.11.5
github.com/docker/docker v28.0.4+incompatible
github.com/docker/docker v28.2.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/mholt/archives v0.1.0
github.com/shirou/gopsutil/v4 v4.25.2
github.com/testcontainers/testcontainers-go v0.36.0
github.com/mholt/archives v0.1.2
github.com/shirou/gopsutil/v4 v4.25.5
github.com/testcontainers/testcontainers-go v0.37.0
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/STARRY-S/zip v0.2.2 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
@@ -55,6 +55,8 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
@@ -64,7 +66,7 @@ require (
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect
github.com/go-logr/logr v1.4.2 // indirect
@@ -83,12 +85,15 @@ require (
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/minio/minlz v1.0.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
@@ -99,7 +104,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
@@ -121,7 +126,6 @@ require (
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

107
go.sum
View File

@@ -17,20 +17,20 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/STARRY-S/zip v0.2.2 h1:8QeCbIi1Z9U5MgoDARJR1ClbBo9RD46SmVy+dl0woCk=
github.com/STARRY-S/zip v0.2.2/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
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/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
@@ -48,16 +48,20 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0=
github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
github.com/chromedp/cdproto v0.0.0-20250416210000-d7e4d624041a h1:TVjyMfX22UgV/jP0A4UwrrCmI9zfvp6ZS5UBZMy3HDw=
github.com/chromedp/cdproto v0.0.0-20250416210000-d7e4d624041a/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk=
github.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
@@ -81,8 +85,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -90,8 +94,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -205,32 +209,38 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q=
github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I=
github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A=
github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=
github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -255,11 +265,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
@@ -270,8 +280,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
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/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sorairolake/lzip-go v0.3.7 h1:vP2uiD/NoklLyzYMdgOWkZME0ulkSfVTTE4MNRKCwNs=
@@ -294,8 +304,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00=
github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0=
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
@@ -354,8 +364,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
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=
@@ -401,8 +411,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
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=
@@ -416,8 +426,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -442,13 +452,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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=
@@ -456,8 +466,8 @@ 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -519,10 +529,11 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
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-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
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=
@@ -530,8 +541,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
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.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -543,8 +554,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
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=

55
package-lock.json generated
View File

@@ -5,9 +5,9 @@
"packages": {
"": {
"devDependencies": {
"prettier": "3.5.2",
"prettier-plugin-gherkin": "^3.1.1",
"prettier-plugin-sh": "^0.15.0"
"prettier": "3.5.3",
"prettier-plugin-gherkin": "^3.1.2",
"prettier-plugin-sh": "^0.17.4"
}
},
"node_modules/@cucumber/gherkin": {
@@ -63,6 +63,16 @@
"uuid": "9.0.1"
}
},
"node_modules/@reteps/dockerfmt": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt/-/dockerfmt-0.3.6.tgz",
"integrity": "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^v12.20.0 || ^14.13.0 || >=16.0.0"
}
},
"node_modules/@types/uuid": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.6.tgz",
@@ -77,17 +87,10 @@
"dev": true,
"license": "MIT"
},
"node_modules/mvdan-sh": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
"integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/prettier": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -101,9 +104,9 @@
}
},
"node_modules/prettier-plugin-gherkin": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-gherkin/-/prettier-plugin-gherkin-3.1.1.tgz",
"integrity": "sha512-cCfjqKMdR2a8jOf8yKg32iUfRq0Dfmx+K2qTMOD/ixJJq0Rp7QS8BaoABWC7CDGXbvOkVeWOvzhxWvYcEbjglw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/prettier-plugin-gherkin/-/prettier-plugin-gherkin-3.1.2.tgz",
"integrity": "sha512-LgehRNKCAqZsoE/yblmqXYfMpEK2mhVWQKNj/TFzeLR+0rhFGHqq3RlYcYhPgzbr0n6w2Bv+0Gm1yxNdzkdacw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -113,14 +116,14 @@
}
},
"node_modules/prettier-plugin-sh": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.15.0.tgz",
"integrity": "sha512-U0PikJr/yr2bzzARl43qI0mApBj0C1xdAfA04AZa6LnvIKawXHhuy2fFo6LNA7weRzGlAiNbaEFfKMFo0nZr/A==",
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.17.4.tgz",
"integrity": "sha512-aAVKXZ7GTEMZdZsIPSwMwddwPvt2ibMbRGd4OJAP0G7QoeYZV+mPNg2Oln3R53sZ4PVjeAA7Xzi/PuI0QlHHfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mvdan-sh": "^0.10.1",
"sh-syntax": "^0.4.2"
"@reteps/dockerfmt": "^0.3.5",
"sh-syntax": "^0.5.6"
},
"engines": {
"node": ">=16.0.0"
@@ -140,19 +143,19 @@
"license": "Apache-2.0"
},
"node_modules/sh-syntax": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
"integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.5.7.tgz",
"integrity": "sha512-74m9dt91konrF5+m0kASugzi37VxKsnTJQ6yvdDZu3IijG5/vIZpImP6FadsJLWNt2X2YD0VaTwW5W7Ox7mFVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.6.2"
"tslib": "^2.8.1"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
"url": "https://opencollective.com/sh-syntax"
}
},
"node_modules/tslib": {

View File

@@ -1,7 +1,7 @@
{
"devDependencies": {
"prettier": "3.5.2",
"prettier-plugin-gherkin": "^3.1.1",
"prettier-plugin-sh": "^0.15.0"
"prettier": "3.5.3",
"prettier-plugin-gherkin": "^3.1.2",
"prettier-plugin-sh": "^0.17.4"
}
}

View File

@@ -74,7 +74,7 @@ func (cmd *Cmd) Start() error {
}
// Wait waits for the command to complete. It should be called when using the
// Start method, so that the command does not leak zombies.
// Start method so that the command does not leak zombies.
func (cmd *Cmd) Wait() error {
err := cmd.process.Wait()
if err != nil {
@@ -84,7 +84,7 @@ func (cmd *Cmd) Wait() error {
return nil
}
// Exec executes the command and wait for its completion or until the context
// Exec executes the command and waits for its completion or until the context
// is done. In any case, it kills the unix process and all its children.
func (cmd *Cmd) Exec() (int, error) {
if cmd.ctx == nil {

View File

@@ -5,7 +5,7 @@ import (
"reflect"
)
// Context is a struct which helps to initialize modules. When provisioning, a
// Context is a struct that helps to initialize modules. When provisioning, a
// module may use the context to get other modules that it needs internally.
type Context struct {
flags ParsedFlags
@@ -58,7 +58,7 @@ func (ctx *Context) Module(kind interface{}) (interface{}, error) {
return mods[0], nil
}
// Modules returns the list of modules which satisfies the requested interface.
// Modules return the list of modules which satisfies the requested interface.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// mods, _ := ctx.Modules(new(ModuleInterface))

View File

@@ -23,7 +23,7 @@ func (o *OsMkdirAll) MkdirAll(path string, perm os.FileMode) error { return os.M
// PathRename defines the method signature for renaming files. Implement this
// interface if you don't want to rely on [os.Rename], notably for testing
// purpose.
// purposes.
type PathRename interface {
// Rename uses the same signature as [os.Rename].
Rename(oldpath, newpath string) error

View File

@@ -11,7 +11,7 @@ import (
)
// GarbageCollect scans the root path and deletes files or directories with
// names containing specific substrings and before a given experiation time.
// names containing specific substrings and before a given expiration time.
func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr []string, expirationTime time.Time) error {
logger = logger.Named("gc")

View File

@@ -28,29 +28,29 @@ func TestGarbageCollect(t *testing.T) {
{
scenario: "remove include substrings",
rootPath: func() string {
path := fmt.Sprintf("%s/a_directory", os.TempDir())
p := fmt.Sprintf("%s/a_directory", os.TempDir())
err := os.MkdirAll(path, 0o755)
err := os.MkdirAll(p, 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file", path), []byte{1}, 0o755)
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file", p), []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)
err = os.WriteFile(fmt.Sprintf("%s/a_bar_file", p), []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)
err = os.WriteFile(fmt.Sprintf("%s/a_baz_file", p), []byte{1}, 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return path
return p
}(),
includeSubstr: []string{"foo", path.Join(os.TempDir(), "/a_directory/a_bar_file")},
expectError: false,
@@ -81,18 +81,18 @@ func TestGarbageCollect(t *testing.T) {
}
for _, name := range tc.expectNotExists {
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
_, err = os.Stat(path)
p := fmt.Sprintf("%s/%s", tc.rootPath, name)
_, err = os.Stat(p)
if !os.IsNotExist(err) {
t.Errorf("expected '%s' not to exist but it does: %v", path, err)
t.Errorf("expected '%s' not to exist but it does: %v", p, err)
}
}
for _, name := range tc.expectExists {
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
_, err = os.Stat(path)
p := fmt.Sprintf("%s/%s", tc.rootPath, name)
_, err = os.Stat(p)
if os.IsNotExist(err) {
t.Errorf("expected '%s' to exist but it does not: %v", path, err)
t.Errorf("expected '%s' to exist but it does not: %v", p, err)
}
}
})

View File

@@ -18,7 +18,7 @@ type LoggerProvider interface {
Logger(mod Module) (*zap.Logger, error)
}
// LeveledLogger is wrapper around a [zap.Logger] so that it may be used by a
// LeveledLogger is a wrapper around a [zap.Logger] so that it may be used by a
// [retryablehttp.Client].
type LeveledLogger struct {
logger *zap.Logger
@@ -31,22 +31,22 @@ func NewLeveledLogger(logger *zap.Logger) *LeveledLogger {
}
}
// Error logs a message at error level using the wrapped zap.Logger.
// Error logs a message at the error level using the wrapped zap.Logger.
func (leveled LeveledLogger) Error(msg string, keysAndValues ...interface{}) {
leveled.logger.Error(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}
// Warn logs a message at warning level using the wrapped zap.Logger.
// Warn logs a message at the warning level using the wrapped zap.Logger.
func (leveled LeveledLogger) Warn(msg string, keysAndValues ...interface{}) {
leveled.logger.Warn(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}
// Info logs a message at info level using the wrapped zap.Logger.
// Info logs a message at the info level using the wrapped zap.Logger.
func (leveled LeveledLogger) Info(msg string, keysAndValues ...interface{}) {
leveled.logger.Info(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}
// Debug logs a message at debug level using the wrapped zap.Logger.
// Debug logs a message at the debug level using the wrapped zap.Logger.
func (leveled LeveledLogger) Debug(msg string, keysAndValues ...interface{}) {
leveled.logger.Debug(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}

View File

@@ -13,7 +13,7 @@ var (
ErrPdfEngineMethodNotSupported = errors.New("method not supported")
// ErrPdfSplitModeNotSupported is returned when the Split method of the
// PdfEngine interface does not sumport a requested PDF split mode.
// PdfEngine interface does not support a requested PDF split mode.
ErrPdfSplitModeNotSupported = errors.New("split mode not supported")
// ErrPdfFormatNotSupported is returned when the Convert method of the
@@ -86,7 +86,7 @@ type PdfFormats struct {
}
// PdfEngine provides an interface for operations on PDFs. Implementations
// can utilize various tools like PDFtk, or implement functionality directly in
// can use various tools like PDFtk, or implement functionality directly in
// Go.
//
//nolint:dupl

View File

@@ -0,0 +1,8 @@
package gotenberg
import "errors"
// ErrCancelGracefulShutdownContext tells that a module wants to abort a
// graceful shutdown and stops Gotenberg right away as there are no more
// ongoing processes.
var ErrCancelGracefulShutdownContext = errors.New("cancel graceful shutdown's context")

View File

@@ -1,6 +1,7 @@
package gotenberg
import (
"path/filepath"
"regexp"
"sort"
"strconv"
@@ -54,6 +55,9 @@ func (s AlphanumericSort) Less(i, j int) bool {
// If that fails, it then attempts a trailing numeric pattern.
// If no number is found, it returns -1 and the original string.
func extractNumber(str string) (int, string) {
// See https://github.com/gotenberg/gotenberg/issues/1168.
str = filepath.Base(str)
// Check for a numeric prefix.
if matches := prefixRegexp.FindStringSubmatch(str); len(matches) > 2 {
if num, err := strconv.Atoi(matches[1]); err == nil {

View File

@@ -26,7 +26,7 @@ func init() {
gotenberg.MustRegisterModule(new(Api))
}
// Api is a module which provides an HTTP server. Other modules may add routes,
// Api is a module that provides an HTTP server. Other modules may add routes,
// middlewares or health checks.
type Api struct {
port int
@@ -48,6 +48,7 @@ type Api struct {
externalMiddlewares []Middleware
healthChecks []health.CheckerOption
readyFn []func() error
asyncCounters []AsynchronousCounter
fs *gotenberg.FileSystem
logger *zap.Logger
srv *echo.Echo
@@ -60,7 +61,7 @@ type downloadFromConfig struct {
disable bool
}
// Router is a module interface which adds routes to the [Api].
// Router is a module interface that adds routes to the [Api].
type Router interface {
Routes() ([]Route, error)
}
@@ -83,17 +84,17 @@ type Route struct {
// Optional.
DisableLogging bool
// Handler is the function which handles the request.
// Handler is the function that handles the request.
// Required.
Handler echo.HandlerFunc
}
// MiddlewareProvider is a module interface which adds middlewares to the [Api].
// MiddlewareProvider is a module interface that adds middlewares to the [Api].
type MiddlewareProvider interface {
Middlewares() ([]Middleware, error)
}
// MiddlewareStack is a type which helps to determine in which stack the
// MiddlewareStack is a type that helps to determine in which stack the
// middlewares provided by the [MiddlewareProvider] modules should be located.
type MiddlewareStack uint32
@@ -103,7 +104,7 @@ const (
MultipartStack
)
// MiddlewarePriority is a type which helps to determine the execution order of
// MiddlewarePriority is a type that helps to determine the execution order of
// middlewares provided by the [MiddlewareProvider] modules in a stack.
type MiddlewarePriority uint32
@@ -115,7 +116,7 @@ const (
VeryHighPriority
)
// Middleware is a middleware which can be added to the [Api]'s middlewares
// Middleware is a middleware that can be added to the [Api]'s middlewares
// chain.
//
// middleware := Middleware{
@@ -157,7 +158,7 @@ type Middleware struct {
Handler echo.MiddlewareFunc
}
// HealthChecker is a module interface which allows adding health checks to the
// HealthChecker is a module interface that allows adding health checks to the
// API.
//
// See https://github.com/alexliesenfeld/health for more details.
@@ -166,6 +167,14 @@ type HealthChecker interface {
Ready() error
}
// AsynchronousCounter is a module interface that returns the number of active
// asynchronous requests.
//
// See https://github.com/gotenberg/gotenberg/issues/1022.
type AsynchronousCounter interface {
AsyncCount() int64
}
// Descriptor returns an [Api]'s module descriptor.
func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
@@ -307,6 +316,17 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
a.readyFn = append(a.readyFn, healthChecker.Ready)
}
// Get asynchronous counters.
mods, err = ctx.Modules(new(AsynchronousCounter))
if err != nil {
return fmt.Errorf("get asynchronous counters: %w", err)
}
a.asyncCounters = make([]AsynchronousCounter, len(mods))
for i, asyncCounter := range mods {
a.asyncCounters[i] = asyncCounter.(AsynchronousCounter)
}
// Logger.
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
if err != nil {
@@ -431,7 +451,7 @@ func (a *Api) Start() error {
}
}
// Check if the user wish to add logging entries related to the health
// Check if the user wishes to add logging entries related to the health
// check route.
if a.disableHealthCheckLogging {
disableLoggingForPaths = append(disableLoggingForPaths, "health")
@@ -442,6 +462,7 @@ func (a *Api) Start() error {
latencyMiddleware(),
rootPathMiddleware(a.rootPath),
traceMiddleware(a.traceHeader),
outputFilenameMiddleware(),
loggerMiddleware(a.logger, disableLoggingForPaths),
)
@@ -597,7 +618,28 @@ func (a *Api) StartupMessage() string {
// Stop stops the HTTP server.
func (a *Api) Stop(ctx context.Context) error {
return a.srv.Shutdown(ctx)
for {
count := int64(0)
for _, asyncCounter := range a.asyncCounters {
count += asyncCounter.AsyncCount()
}
select {
case <-ctx.Done():
return a.srv.Shutdown(ctx)
default:
a.logger.Debug(fmt.Sprintf("%d asynchronous requests", count))
if count > 0 {
time.Sleep(1 * time.Second)
continue
}
a.logger.Debug("no more asynchronous requests, continue with shutdown")
err := a.srv.Shutdown(ctx)
if err != nil {
return fmt.Errorf("shutdown: %w", err)
}
return gotenberg.ErrCancelGracefulShutdownContext
}
}
}
// Interface guards.

View File

@@ -409,6 +409,13 @@ func (ctx *Context) GeneratePath(extension string) string {
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New().String(), extension)
}
// GeneratePathFromFilename generates a path within the context's working
// directory, using the given filename (with extension). It does not create
// a file.
func (ctx *Context) GeneratePathFromFilename(filename string) string {
return fmt.Sprintf("%s/%s", ctx.dirPath, filename)
}
// CreateSubDirectory creates a subdirectory within the context's working
// directory.
func (ctx *Context) CreateSubDirectory(dirName string) (string, error) {
@@ -506,7 +513,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
// OutputFilename returns the filename based on the given output path or the
// "Gotenberg-Output-Filename" header's value.
func (ctx *Context) OutputFilename(outputPath string) string {
filename := ctx.echoCtx.Request().Header.Get("Gotenberg-Output-Filename")
filename := ctx.echoCtx.Get("outputFilename").(string)
if filename == "" {
return filepath.Base(outputPath)

View File

@@ -1,3 +1,3 @@
// Package api provides a module which is an HTTP server. Other modules may
// Package api provides a module, which is an HTTP server. Other modules may
// add multipart/form-data routes, middlewares, and health checks.
package api

View File

@@ -27,7 +27,7 @@ type FormData struct {
}
// 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 a message) wrapped
// inside.
//
// var foo string
@@ -96,7 +96,7 @@ func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
}
// 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, or is empty, or the "key" does not exist.
//
// var foo int
//
@@ -116,7 +116,7 @@ func (form *FormData) Float64(key string, target *float64, defaultValue float64)
}
// 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 value is not float64, is empty, or the "key" does not exist.
//
// var foo float64
//
@@ -136,8 +136,8 @@ func (form *FormData) Duration(key string, target *time.Duration, defaultValue t
}
// 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"
// does not exist.
// populates an error if the value is not time.Duration, or is empty, or the
// "key" does not exist.
//
// var foo time.Duration
//
@@ -146,7 +146,7 @@ func (form *FormData) MandatoryDuration(key string, target *time.Duration) *Form
return form.mustMandatoryField(key, target)
}
// Inches binds a form field to a float64 variable. It populates an error
// Inches bind a form field to a float64 variable. It populates an error
// if the value cannot be computed back to inches.
//
// var foo float64
@@ -303,7 +303,7 @@ func (form *FormData) Path(filename string, target *string) *FormData {
return form.path(filename, target)
}
// MandatoryPath binds the absolute path ofa form data file to a string
// MandatoryPath binds the absolute path of a form data file to a string
// variable. It populates an error if the file does not exist.
//
// var path string
@@ -348,7 +348,7 @@ func (form *FormData) MandatoryContent(filename string, target *string) *FormDat
return form.readFile(path, filename, target)
}
// Paths binds the absolute paths of form data files, according to a list of
// Paths bind the absolute paths of form data files, according to a list of
// file extensions, to a string slice variable.
//
// var paths []string
@@ -379,7 +379,7 @@ func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *For
return form
}
// paths binds the absolute paths of form data files, according to a list of
// paths bind the absolute paths of form data files, according to a list of
// file extensions, to a string slice variable.
func (form *FormData) paths(extensions []string, target *[]string) *FormData {
for filename, path := range form.files {

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
@@ -86,7 +87,7 @@ func httpErrorHandler() echo.HTTPErrorHandler {
}
// latencyMiddleware sets the start time in the [echo.Context] under
// "startTime". Its value will be used later to calculate a request latency.
// "startTime". Its value will be used later to calculate request latency.
//
// startTime := c.Get("startTime").(time.Time)
func latencyMiddleware() echo.MiddlewareFunc {
@@ -109,7 +110,7 @@ func latencyMiddleware() echo.MiddlewareFunc {
// rootPath := c.Get("rootPath").(string)
// healthURI := fmt.Sprintf("%s/health", rootPath)
//
// // Skip the middleware if health check URI.
// // Skip the middleware if it's the health check URI.
// if c.Request().RequestURI == healthURI {
// // Call the next middleware in the chain.
// return next(c)
@@ -150,6 +151,25 @@ func traceMiddleware(header string) echo.MiddlewareFunc {
}
}
// outputFilenameMiddleware sets the output filename in the [echo.Context]
// under "outputFilename".
//
// outputFilename := c.Get("outputFilename").(string)
func outputFilenameMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
filename := c.Request().Header.Get("Gotenberg-Output-Filename")
// See https://github.com/gotenberg/gotenberg/issues/1227.
if filename != "" {
filename = filepath.Base(filename)
}
c.Set("outputFilename", filename)
// 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.
//
@@ -241,7 +261,7 @@ func basicAuthMiddleware(username, password string) echo.MiddlewareFunc {
})
}
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
// contextMiddleware, middleware for "multipart/form-data" requests, sets the
// [Context] and related context.CancelFunc in the [echo.Context] under
// "context" and "cancel". If the process is synchronous, it also handles the
// result of a "multipart/form-data" request.
@@ -256,7 +276,7 @@ func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration, bodyLimi
trace := c.Get("trace").(string)
// 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 correctly handle a timeout scenario.
ctx, cancel, err := newContext(c, logger, fs, timeout, bodyLimit, downloadFromCfg, traceHeader, trace)
if err != nil {
cancel()

View File

@@ -54,7 +54,7 @@ func (ctx *ContextMock) SetFiles(files map[string]string) {
ctx.files = files
}
// SetCancelled sets if the context is cancelled or not.
// SetCancelled sets if the context is canceled or not.
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.SetCancelled(true)
@@ -120,7 +120,7 @@ func (provider *MiddlewareProviderMock) Middlewares() ([]Middleware, error) {
return provider.MiddlewaresMock()
}
// HealthCheckerMock is mock for the [HealthChecker] interface.
// HealthCheckerMock is a mock for the [HealthChecker] interface.
type HealthCheckerMock struct {
ChecksMock func() ([]health.CheckerOption, error)
ReadyMock func() error

View File

@@ -12,6 +12,7 @@ import (
"github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/dlclark/regexp2"
@@ -93,6 +94,9 @@ func (b *chromiumBrowser) Start(logger *zap.Logger) error {
chromedp.UserDataDir(b.userProfileDirPath),
// See https://github.com/gotenberg/gotenberg/issues/831.
chromedp.Flag("disable-pdf-tagging", true),
// See https://github.com/gotenberg/gotenberg/issues/1177.
chromedp.Flag("no-zygote", true),
chromedp.Flag("disable-dev-shm-usage", true),
)
if b.arguments.incognito {
@@ -211,7 +215,7 @@ func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
}
// Also remove Chromium specific files in the temporary directory.
// Also, remove Chromium-specific files in the temporary directory.
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{".org.chromium.Chromium", ".com.google.Chrome"}, expirationTime)
if err != nil {
logger.Error(err.Error())
@@ -271,12 +275,14 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
userAgentOverride(logger, options.UserAgent),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
forceExactColorsActionFunc(),
forceExactColorsActionFunc(logger, options.PrintBackground),
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
// PDF specific.
printToPdfActionFunc(logger, outputPath, options),
// Teardown.
page.Close(),
})
}
@@ -294,13 +300,15 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
userAgentOverride(logger, options.UserAgent),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
forceExactColorsActionFunc(),
forceExactColorsActionFunc(logger, true),
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
// Screenshot specific.
setDeviceMetricsOverride(logger, options.Width, options.Height),
captureScreenshotActionFunc(logger, outputPath, options),
// Teardown.
page.Close(),
})
}
@@ -314,7 +322,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
return errors.New("context has no deadline")
}
// We validate the "main" URL against our allow / deny lists.
// We validate the "main" URL against our allowed / deny lists.
err := gotenberg.FilterDeadline(b.arguments.allowList, b.arguments.denyList, url, deadline)
if err != nil {
return fmt.Errorf("filter URL: %w", err)
@@ -329,7 +337,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx)
defer taskCancel()
// We validate all others requests against our allow / deny lists.
// We validate all other requests against our allowed / deny lists.
// If a request does not pass the validation, we make it fail. It also set
// the extra HTTP headers, if any.
// See https://github.com/gotenberg/gotenberg/issues/1011.

View File

@@ -26,7 +26,7 @@ func init() {
var (
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
// "screen" nor "print". Empty value are allowed though.
// "screen" nor "print". Empty value is allowed, though.
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
// ErrInvalidEvaluationExpression happens if an evaluation expression
@@ -38,11 +38,11 @@ var (
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
// ErrInvalidHttpStatusCode happens when the status code from the main page
// matches with one of the entry in [Options.FailOnHttpStatusCodes].
// matches with one of the entries in [Options.FailOnHttpStatusCodes].
ErrInvalidHttpStatusCode = errors.New("invalid HTTP status code")
// ErrInvalidResourceHttpStatusCode happens when the status code from one
// or more resources matches with one of the entry in
// or more resources matches with one of the entries in
// [Options.FailOnResourceHttpStatusCodes].
ErrInvalidResourceHttpStatusCode = errors.New("invalid resource HTTP status code")
@@ -68,12 +68,12 @@ var (
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
// ErrPageRangesSyntaxError happens if the PdfOptions have an invalid page
// ranges.
// range.
ErrPageRangesSyntaxError = errors.New("page ranges syntax error")
)
// Chromium is a module which provides both an [Api] and routes for converting
// HTML document to PDF.
// Chromium is a module that provides both an [Api] and routes for converting
// an HTML document to PDF.
type Chromium struct {
autoStart bool
disableRoutes bool
@@ -128,15 +128,15 @@ type Options struct {
UserAgent string
// ExtraHttpHeaders are extra HTTP headers to send by Chromium while
// loading he HTML document.
// loading the HTML document.
ExtraHttpHeaders []ExtraHttpHeader
// EmulatedMediaType is the media type to emulate, either "screen" or
// "print".
EmulatedMediaType string
// OmitBackground hides default white background and allows generating PDFs
// with transparency.
// OmitBackground hides the default white background and allows generating
// PDFs with transparency.
OmitBackground bool
}
@@ -197,9 +197,9 @@ type PdfOptions struct {
// Page ranges to print, e.g., '1-5, 8, 11-13'. Empty means all pages.
PageRanges string
// HeaderTemplate is the HTML template of the header. It should be valid
// HTML markup with following classes used to inject printing values into
// them:
// HeaderTemplate is the HTML template of the header. It should be a valid
// HTML markup with the following classes used to inject printing values
// into them:
// - date: formatted print date
// - title: document title
// - url: document location
@@ -220,6 +220,10 @@ type PdfOptions struct {
// GenerateDocumentOutline defines whether the document outline should be
// embedded into the PDF.
GenerateDocumentOutline bool
// GenerateTaggedPdf defines whether to generate tagged (accessible)
// PDF.
GenerateTaggedPdf bool
}
// DefaultPdfOptions returns the default values for PdfOptions.
@@ -241,6 +245,7 @@ func DefaultPdfOptions() PdfOptions {
FooterTemplate: "<html><head></head><body></body></html>",
PreferCssPageSize: false,
GenerateDocumentOutline: false,
GenerateTaggedPdf: false,
}
}
@@ -338,7 +343,7 @@ type Api interface {
Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
}
// Provider is a module interface which exposes a method for creating an [Api]
// Provider is a module interface that exposes a method for creating an [Api]
// for other modules.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
@@ -473,8 +478,8 @@ func (mod *Chromium) StartupMessage() string {
// Stop stops the current browser instance.
func (mod *Chromium) Stop(ctx context.Context) error {
// Block until the context is done so that other module may gracefully stop
// before we do a shutdown.
// Block until the context is done so that another module may gracefully
// stop before we do a shutdown.
mod.logger.Debug("wait for the end of grace duration")
<-ctx.Done()

View File

@@ -72,10 +72,10 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
var extraHttpHeadersToSet []ExtraHttpHeader
if len(options.extraHttpHeaders) > 0 {
// The user want to set extra HTTP headers.
// The user wants to set extra HTTP headers.
// First, we have to check if at least one header has to be
// set for current request.
// set for the current request.
for _, header := range options.extraHttpHeaders {
if header.Scope == nil {
// Non-scoped header.
@@ -147,8 +147,8 @@ type eventResponseReceivedOptions struct {
invalidResourceHttpStatusCodeMu *sync.RWMutex
}
// listenForEventResponseReceived listens for an invalid HTTP status code that
// is returned by the main page or by one or more resources.
// listenForEventResponseReceived listens for an invalid HTTP status code
// returned by the main page or by one or more resources.
// See:
// https://github.com/gotenberg/gotenberg/issues/613.
// https://github.com/gotenberg/gotenberg/issues/1021.

View File

@@ -25,7 +25,7 @@ import (
)
// FormDataChromiumOptions creates [Options] from the form data. Fallback to
// default value if the considered key is not present.
// the default value if the considered key is not present.
func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
defaultOptions := DefaultOptions()
@@ -194,7 +194,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
}
// FormDataChromiumPdfOptions creates [PdfOptions] from the form data. Fallback to
// default value if the considered key is not present.
// the default value if the considered key is not present.
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
form, options := FormDataChromiumOptions(ctx)
defaultPdfOptions := DefaultPdfOptions()
@@ -207,6 +207,7 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
headerTemplate, footerTemplate string
preferCssPageSize bool
generateDocumentOutline bool
generateTaggedPdf bool
)
form.
@@ -224,7 +225,8 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate).
Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).
Bool("preferCssPageSize", &preferCssPageSize, defaultPdfOptions.PreferCssPageSize).
Bool("generateDocumentOutline", &generateDocumentOutline, defaultPdfOptions.GenerateDocumentOutline)
Bool("generateDocumentOutline", &generateDocumentOutline, defaultPdfOptions.GenerateDocumentOutline).
Bool("generateTaggedPdf", &generateTaggedPdf, defaultPdfOptions.GenerateTaggedPdf)
pdfOptions := PdfOptions{
Options: options,
@@ -243,13 +245,14 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
FooterTemplate: footerTemplate,
PreferCssPageSize: preferCssPageSize,
GenerateDocumentOutline: generateDocumentOutline,
GenerateTaggedPdf: generateTaggedPdf,
}
return form, pdfOptions
}
// FormDataChromiumScreenshotOptions creates [ScreenshotOptions] from the form
// data. Fallback to default value if the considered key is not present.
// data. Fallback to the default value if the considered key is not present.
func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, ScreenshotOptions) {
form, options := FormDataChromiumOptions(ctx)
defaultScreenshotOptions := DefaultScreenshotOptions()
@@ -483,7 +486,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
}
// screenshotMarkdownRoute returns an [api.Route] which can take a screenshot
// from markdown files.
// from Markdown files.
func screenshotMarkdownRoute(chromium Api) api.Route {
return api.Route{
Method: http.MethodPost,
@@ -522,7 +525,7 @@ func screenshotMarkdownRoute(chromium Api) api.Route {
}
func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string) (string, error) {
// We have to convert each markdown file referenced in the HTML
// We have to convert each Markdown file referenced in the HTML
// file to... HTML. Thanks to the "html/template" package, we are
// able to provide the "toHTML" function which the user may call
// directly inside the HTML file.
@@ -598,6 +601,9 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}) error {
outputPath := ctx.GeneratePath(".pdf")
// See https://github.com/gotenberg/gotenberg/issues/1130.
filename := ctx.OutputFilename(outputPath)
outputPath = ctx.GeneratePathFromFilename(filename)
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
err = handleChromiumError(err, options.Options)

View File

@@ -24,7 +24,7 @@ type streamReader struct {
// Read a chunk of the stream.
func (reader *streamReader) Read(p []byte) (n int, err error) {
if reader.r != nil {
// Continue reading from buffer.
// Continue reading from the buffer.
return reader.read(p)
}

View File

@@ -49,9 +49,8 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOpti
WithPageRanges(pageRanges).
WithPreferCSSPageSize(options.PreferCssPageSize).
WithGenerateDocumentOutline(options.GenerateDocumentOutline).
// Does not seem to work.
// See https://github.com/gotenberg/gotenberg/issues/831.
WithGenerateTaggedPDF(false)
// See https://github.com/gotenberg/gotenberg/issues/1210.
WithGenerateTaggedPDF(options.GenerateTaggedPdf)
hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
options.FooterTemplate != DefaultPdfOptions().FooterTemplate
@@ -392,26 +391,30 @@ func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackground, pr
}
}
func forceExactColorsActionFunc() chromedp.ActionFunc {
func forceExactColorsActionFunc(logger *zap.Logger, printBackground bool) 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; }';
css := "html { -webkit-print-color-adjust: exact !important; }"
if !printBackground {
// The -webkit-print-color-adjust: exact CSS property forces the
// print of the background, whatever the printToPDF args.
// See https://github.com/gotenberg/gotenberg/issues/1154.
additionalCss := "html, body { background: none !important; }"
logger.Debug(fmt.Sprintf("inject %s as printBackground is %t", additionalCss, printBackground))
css += additionalCss
}
script := fmt.Sprintf(`
(() => {
const css = '%s';
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
})();
`
`, css)
evaluate := chromedp.Evaluate(script, nil)
err := evaluate.Do(ctx)
if err == nil {
return nil
}

View File

@@ -142,15 +142,15 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
fileMetadata[0].SetStrings(key, val)
case []interface{}:
// See https://github.com/gotenberg/gotenberg/issues/1048.
strings := make([]string, len(val))
strs := make([]string, len(val))
for i, entry := range val {
if str, ok := entry.(string); ok {
strings[i] = str
strs[i] = str
continue
}
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
}
fileMetadata[0].SetStrings(key, strings)
fileMetadata[0].SetStrings(key, strs)
case bool:
fileMetadata[0].SetString(key, fmt.Sprintf("%t", val))
case int:

View File

@@ -24,23 +24,23 @@ func init() {
}
var (
// ErrInvalidPdfFormats happens if the PDF formats option cannot be handled
// by LibreOffice.
// ErrInvalidPdfFormats happens if LibreOffice cannot handle the PDF
// formats option.
ErrInvalidPdfFormats = errors.New("invalid PDF formats")
// ErrUnoException happens when unoconverter returns an exit code 5.
// ErrUnoException happens when unoconverter returns exit code 5.
ErrUnoException = errors.New("uno exception")
// ErrRuntimeException happens when unoconverter returns an exit code 6.
// ErrRuntimeException happens when unoconverter returns exit code 6.
ErrRuntimeException = errors.New("uno exception")
// ErrCoreDumped happens randomly; sometime a conversion will work as
// ErrCoreDumped happens randomly; sometimes a conversion will work as
// expected, and some other time the same conversion will fail.
// See https://github.com/gotenberg/gotenberg/issues/639.
ErrCoreDumped = errors.New("core dumped")
)
// Api is a module which provides a [Uno] to interact with LibreOffice.
// Api is a module that provides a [Uno] to interact with LibreOffice.
type Api struct {
autoStart bool
args libreOfficeArguments
@@ -56,10 +56,10 @@ type Options struct {
// Password specifies the password for opening the source file.
Password string
// Landscape allows to change the orientation of the resulting PDF.
// Landscape allows changing the orientation of the resulting PDF.
Landscape bool
// PageRanges allows to select the pages to convert.
// PageRanges allows selecting the pages to convert.
PageRanges string
// UpdateIndexes specifies whether to update the indexes before conversion,
@@ -83,7 +83,7 @@ type Options struct {
// Named Destination.
ExportBookmarksToPdfDestination bool
// ExportPlaceholders exports the placeholders fields visual markings only.
// ExportPlaceholders exports the placeholder fields visual markings only.
// The exported placeholder is ineffective.
ExportPlaceholders bool
@@ -94,15 +94,16 @@ type Options struct {
// Notes pages are available in Impress documents only.
ExportNotesPages bool
// ExportOnlyNotesPages specifies, if the property ExportNotesPages is set
// to true, if only notes pages are exported to PDF.
// ExportOnlyNotesPages specifies if the property ExportNotesPages is set
// to true if only notes pages are exported to PDF.
ExportOnlyNotesPages bool
// ExportNotesInMargin specifies if notes in margin are exported to PDF.
// ExportNotesInMargin specifies if notes in the margin are exported to
// PDF.
ExportNotesInMargin bool
// ConvertOooTargetToPdfTarget specifies that the target documents with
// .od[tpgs] extension, will have that extension changed to .pdf when the
// .od[tpgs] extension will have that extension changed to .pdf when the
// link is exported to PDF. The source document remains untouched.
ConvertOooTargetToPdfTarget bool
@@ -190,7 +191,7 @@ type Uno interface {
Extensions() []string
}
// Provider is a module interface which exposes a method for creating a
// Provider is a module interface that exposes a method for creating a
// [Uno] for other modules.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
@@ -300,8 +301,8 @@ func (a *Api) StartupMessage() string {
// 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.
// Block until the context is done so that another module may gracefully
// stop before we do a shutdown.
a.logger.Debug("wait for the end of grace duration")
<-ctx.Done()

View File

@@ -4,16 +4,13 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -200,7 +197,7 @@ func (p *libreOfficeProcess) Stop(logger *zap.Logger) error {
logger.Debug(fmt.Sprintf("'%s' LibreOffice's user profile directory removed", userProfileDirPath))
}
// Also remove LibreOffice specific files in the temporary directory.
// Also, remove LibreOffice specific files in the temporary directory.
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{"OSL_PIPE", ".tmp"}, expirationTime)
if err != nil {
logger.Error(err.Error())
@@ -333,11 +330,6 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
)
}
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...)
@@ -353,10 +345,10 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
}
// LibreOffice's errors are not explicit.
// For instance, an exit code 5 may be explained by a malformed page
// ranges, but also by a not required password.
// For instance, exit code 5 may be explained by a malformed page range
// but also by a not required password.
// We may want to retry in case of a core dumped event.
// We may want to retry in case of a core-dumped event.
// See https://github.com/gotenberg/gotenberg/issues/639.
if strings.Contains(err.Error(), "core dumped") {
return ErrCoreDumped
@@ -374,65 +366,6 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
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)

View File

@@ -14,7 +14,7 @@ func init() {
gotenberg.MustRegisterModule(new(LibreOffice))
}
// LibreOffice is a module which provides a route for converting documents to
// LibreOffice is a module that provides a route for converting documents to
// PDF with LibreOffice.
type LibreOffice struct {
api libeofficeapi.Uno

View File

@@ -58,7 +58,7 @@ func (engine *LibreOfficePdfEngine) Split(ctx context.Context, logger *zap.Logge
// Flatten is not available in this implementation.
func (engine *LibreOfficePdfEngine) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
return fmt.Errorf("Flatten PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
return fmt.Errorf("flatten PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Convert converts the given PDF to a specific PDF format. Currently, only the

View File

@@ -164,7 +164,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
}
if nativePdfFormats && splitMode == zeroValuedSplitMode {
// Only apply natively given PDF formats if we're not
// Only natively apply given PDF formats if we're not
// splitting the PDF later.
options.PdfFormats = pdfFormats
}

View File

@@ -31,7 +31,7 @@ const (
textLoggingFormat = "text"
)
// Logging is a module which implements the [gotenberg.LoggerProvider]
// Logging is a module that implements the [gotenberg.LoggerProvider]
// interface.
type Logging struct {
level string

View File

@@ -146,7 +146,7 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenb
return nil, fmt.Errorf("walk directory to find resulting PDFs from split with pdfcpu: %w", err)
}
sort.Sort(gotenberg.AlphanumericSort(outputPaths))
sort.Sort(digitSuffixSort(outputPaths))
return outputPaths, nil
}

View File

@@ -0,0 +1,68 @@
package pdfcpu
import (
"path/filepath"
"regexp"
"sort"
"strconv"
)
type digitSuffixSort []string
func (s digitSuffixSort) Len() int {
return len(s)
}
func (s digitSuffixSort) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s digitSuffixSort) Less(i, j int) bool {
numI, restI := extractNumber(s[i])
numJ, restJ := extractNumber(s[j])
// If both strings contain a number, compare them numerically.
if numI != -1 && numJ != -1 {
if numI != numJ {
return numI < numJ
}
// If the numbers are equal, compare the "rest" strings.
return restI < restJ
}
// If one contains a number and the other doesn't, the one with the number
// comes first.
if numI != -1 {
return true
}
if numJ != -1 {
return false
}
// Neither has a number; fall back to lexicographical order.
return s[i] < s[j]
}
func extractNumber(str string) (int, string) {
str = filepath.Base(str)
// Check for a number immediately before an extension.
if matches := extensionSuffixRegexp.FindStringSubmatch(str); len(matches) > 3 {
if num, err := strconv.Atoi(matches[2]); err == nil {
// Remove the numeric block but keep the extension.
return num, matches[1] + matches[3]
}
}
// No numeric portion found.
return -1, str
}
// Regular expressions used by extractNumber.
var (
// Matches a numeric block immediately before a file extension.
extensionSuffixRegexp = regexp.MustCompile(`^(.*?)(\d+)(\.[^.]+)$`)
)
// Interface guard.
var _ sort.Interface = (*digitSuffixSort)(nil)

View File

@@ -0,0 +1,29 @@
package pdfcpu
import (
"reflect"
"sort"
"testing"
)
func TestDigitSuffixSort(t *testing.T) {
for _, tc := range []struct {
scenario string
values []string
expectedSort []string
}{
{
scenario: "UUIDs with digit suffixes",
values: []string{"2521a33d-1fb4-4279-80fe-8a945285b8f4_12.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_1.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_10.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_3.pdf"},
expectedSort: []string{"2521a33d-1fb4-4279-80fe-8a945285b8f4_1.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_3.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_10.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_12.pdf"},
},
} {
t.Run(tc.scenario, func(t *testing.T) {
sort.Sort(digitSuffixSort(tc.values))
if !reflect.DeepEqual(tc.values, tc.expectedSort) {
t.Fatalf("expected %+v but got: %+v", tc.expectedSort, tc.values)
}
})
}
}

View File

@@ -124,7 +124,7 @@ func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, i
return fmt.Errorf("flatten PDF with multi PDF engines: %w", err)
}
// 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.
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
var err error

View File

@@ -92,7 +92,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
defaultNames[i] = engine.(gotenberg.Module).Descriptor().ID
}
// Example in case of deprecated module name.
// Example in the case of deprecated module name.
//for i, name := range defaultNames {
// if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
// logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))

View File

@@ -500,6 +500,11 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
err = c.JSON(http.StatusOK, res)
if err != nil {
if strings.Contains(err.Error(), "request method or response status code does not allow body") {
// High probability that the user is using the webhook
// feature. It does not make sense for this route.
return api.ErrNoOutputFile
}
return fmt.Errorf("return JSON response: %w", err)
}

View File

@@ -33,7 +33,7 @@ func (engine *PdfTk) Descriptor() gotenberg.ModuleDescriptor {
}
}
// Provision sets the modules properties.
// Provision sets the module properties.
func (engine *PdfTk) Provision(ctx *gotenberg.Context) error {
binPath, ok := os.LookupEnv("PDFTK_BIN_PATH")
if !ok {

View File

@@ -20,7 +20,7 @@ func init() {
gotenberg.MustRegisterModule(new(Prometheus))
}
// Prometheus is a module which collects metrics and exposes them via an HTTP
// Prometheus is a module that collects metrics and exposes them via an HTTP
// route.
type Prometheus struct {
namespace string
@@ -49,7 +49,7 @@ func (mod *Prometheus) Descriptor() gotenberg.ModuleDescriptor {
}
}
// Provision sets the modules properties.
// Provision sets the module properties.
func (mod *Prometheus) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags()
mod.namespace = flags.MustString("prometheus-namespace")

View File

@@ -34,7 +34,7 @@ func (engine *QPdf) Descriptor() gotenberg.ModuleDescriptor {
}
}
// Provision sets the modules properties.
// Provision sets the module properties.
func (engine *QPdf) Provision(ctx *gotenberg.Context) error {
binPath, ok := os.LookupEnv("QPDF_BIN_PATH")
if !ok {

View File

@@ -26,14 +26,14 @@ type client struct {
}
// send call the webhook either to send the success response or the error response.
func (c client) send(body io.Reader, headers map[string]string, erroed bool) error {
func (c client) send(body io.Reader, headers map[string]string, errored bool) error {
url := c.url
if erroed {
if errored {
url = c.errorUrl
}
method := c.method
if erroed {
if errored {
method = c.errorMethod
}
@@ -54,9 +54,9 @@ func (c client) send(body io.Reader, headers map[string]string, erroed bool) err
contentLength, ok := headers[echo.HeaderContentLength]
if ok {
// Golang "http" package should automatically calculate the size of the
// body. But, when using a buffered file reader, it does not work.
// Worse, the "Content-Length" header is also removed. Therefore, in
// order to keep this valuable information, we have to trust the caller
// body. But when using a buffered file reader, it does not work.
// Worse, the "Content-Length" header is also removed. Therefore,
// to keep this valuable information, we have to trust the caller
// by reading the value of the "Content-Length" entry and set it as the
// content length of the request. It's kinda suboptimal, but hey, at
// least it works.
@@ -100,7 +100,7 @@ func (c client) send(body io.Reader, headers map[string]string, erroed bool) err
fields[3] = zap.String("latency_human", finishTime.Sub(c.startTime).String())
fields[4] = zap.Int64("bytes_out", req.ContentLength)
if erroed {
if errored {
c.logger.Warn("request to webhook with error details handled", fields...)
return nil

View File

@@ -113,7 +113,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
}
}
// Retrieve values from echo.Context before it get recycled.
// Retrieve values from echo.Context before it gets recycled.
// See https://github.com/gotenberg/gotenberg/issues/1000.
startTime := c.Get("startTime").(time.Time)
traceHeader := c.Get("traceHeader").(string)
@@ -173,28 +173,40 @@ func webhookMiddleware(w *Webhook) api.Middleware {
}
}
w.asyncCount.Add(1)
// As a webhook URL has been given, we handle the request in a
// goroutine and return immediately.
go func() {
defer cancel()
defer w.asyncCount.Add(-1)
// Call the next middleware in the chain.
err := next(c)
if err != nil {
if errors.Is(err, api.ErrNoOutputFile) {
errNoOutputFile := fmt.Errorf("%w - the webhook middleware cannot handle the result of this route", err)
handleAsyncError(api.WrapError(
errNoOutputFile,
api.NewSentinelHttpError(
http.StatusBadRequest,
"The webhook middleware can only work with multipart/form-data routes that results in output files",
),
))
return
}
// The process failed for whatever reason. Let's send the
// details to the webhook.
ctx.Log().Error(err.Error())
handleAsyncError(err)
return
}
// No error, let's get build the output file.
// No error, let's get to build the output file.
outputPath, err := ctx.BuildOutputFile()
if err != nil {
ctx.Log().Error(fmt.Sprintf("build output file: %s", err))
handleAsyncError(err)
return
}
@@ -202,7 +214,6 @@ func webhookMiddleware(w *Webhook) api.Middleware {
if err != nil {
ctx.Log().Error(fmt.Sprintf("open output file: %s", err))
handleAsyncError(err)
return
}
@@ -218,7 +229,6 @@ func webhookMiddleware(w *Webhook) api.Middleware {
if err != nil {
ctx.Log().Error(fmt.Sprintf("read header of output file: %s", err))
handleAsyncError(err)
return
}
@@ -226,7 +236,6 @@ func webhookMiddleware(w *Webhook) api.Middleware {
if err != nil {
ctx.Log().Error(fmt.Sprintf("get stat from output file: %s", err))
handleAsyncError(err)
return
}
@@ -234,15 +243,20 @@ func webhookMiddleware(w *Webhook) api.Middleware {
if err != nil {
ctx.Log().Error(fmt.Sprintf("reset output file reader: %s", err))
handleAsyncError(err)
return
}
headers := map[string]string{
echo.HeaderContentDisposition: fmt.Sprintf("attachement; filename=%q", ctx.OutputFilename(outputPath)),
echo.HeaderContentType: http.DetectContentType(fileHeader),
echo.HeaderContentLength: strconv.FormatInt(fileStat.Size(), 10),
traceHeader: trace,
echo.HeaderContentType: http.DetectContentType(fileHeader),
echo.HeaderContentLength: strconv.FormatInt(fileStat.Size(), 10),
traceHeader: trace,
}
// Allow for custom Content-Disposition header.
// See https://github.com/gotenberg/gotenberg/issues/1165.
_, ok := extraHttpHeaders[echo.HeaderContentDisposition]
if !ok {
headers[echo.HeaderContentDisposition] = fmt.Sprintf("attachment; filename=%q", ctx.OutputFilename(outputPath))
}
// Send the output file to the webhook.

View File

@@ -1,6 +1,7 @@
package webhook
import (
"sync/atomic"
"time"
"github.com/dlclark/regexp2"
@@ -14,7 +15,7 @@ func init() {
gotenberg.MustRegisterModule(new(Webhook))
}
// Webhook is a module which provides a middleware for uploading output files
// Webhook is a module that provides a middleware for uploading output files
// to any destinations in an asynchronous fashion.
type Webhook struct {
allowList *regexp2.Regexp
@@ -25,6 +26,7 @@ type Webhook struct {
retryMinWait time.Duration
retryMaxWait time.Duration
clientTimeout time.Duration
asyncCount atomic.Int64
disable bool
}
@@ -62,6 +64,7 @@ func (w *Webhook) Provision(ctx *gotenberg.Context) error {
w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")
w.clientTimeout = flags.MustDuration("webhook-client-timeout")
w.disable = flags.MustBool("webhook-disable")
w.asyncCount.Store(0)
return nil
}
@@ -77,9 +80,15 @@ func (w *Webhook) Middlewares() ([]api.Middleware, error) {
}, nil
}
// AsyncCount returns the number of asynchronous requests.
func (w *Webhook) AsyncCount() int64 {
return w.asyncCount.Load()
}
// Interface guards.
var (
_ gotenberg.Module = (*Webhook)(nil)
_ gotenberg.Provisioner = (*Webhook)(nil)
_ api.MiddlewareProvider = (*Webhook)(nil)
_ gotenberg.Module = (*Webhook)(nil)
_ gotenberg.Provisioner = (*Webhook)(nil)
_ api.MiddlewareProvider = (*Webhook)(nil)
_ api.AsynchronousCounter = (*Webhook)(nil)
)

View File

@@ -393,6 +393,7 @@ Feature: /forms/chromium/convert/html
| marginRight | foo | field |
| preferCssPageSize | foo | field |
| generateDocumentOutline | foo | field |
| generateTaggedPdf | foo | field |
| printBackground | foo | field |
| omitBackground | foo | field |
| landscape | foo | field |
@@ -408,7 +409,7 @@ Feature: /forms/chromium/convert/html
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string:
"""
Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form file 'index.html' is required
Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateTaggedPdf' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form file 'index.html' is required
"""
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/page-1-html/index.html | file |
@@ -419,20 +420,22 @@ Feature: /forms/chromium/convert/html
"""
omitBackground requires printBackground set to true
"""
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/page-1-html/index.html | file |
| paperWidth | 0 | field |
| paperHeight | 0 | field |
| marginTop | 1000000 | field |
| marginBottom | 1000000 | field |
| marginLeft | 1000000 | field |
| marginRight | 1000000 | field |
Then the response status code should be 400
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string:
"""
Chromium does not handle the provided settings; please check for aberrant form values
"""
# Does not seems to happen on amd architectures anymore since Chromium 137.
# See: https://github.com/gotenberg/gotenberg/actions/runs/15384321883/job/43280184372.
# When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
# | files | testdata/page-1-html/index.html | file |
# | paperWidth | 0 | field |
# | paperHeight | 0 | field |
# | marginTop | 1000000 | field |
# | marginBottom | 1000000 | field |
# | marginLeft | 1000000 | field |
# | marginRight | 1000000 | field |
# Then the response status code should be 400
# Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
# Then the response body should match string:
# """
# Chromium does not handle the provided settings; please check for aberrant form values
# """
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/page-1-html/index.html | file |
| nativePageRanges | foo | field |
@@ -581,6 +584,36 @@ Feature: /forms/chromium/convert/html
Page 3
"""
# See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/html (Split Output Filename)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/pages-3-html/index.html | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip"
Then there should be 2 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.zip |
| foo_0.pdf |
| foo_1.pdf |
Then the "foo_0.pdf" PDF should have 2 page(s)
Then the "foo_1.pdf" PDF should have 1 page(s)
Then the "foo_0.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "foo_0.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Then the "foo_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Scenario: POST /forms/chromium/convert/html (Split Pages)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -703,6 +736,40 @@ Feature: /forms/chromium/convert/html
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
# See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/html (Split & PDF/A-1b & PDF/UA-1 & Output Filename)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
| files | testdata/pages-3-html/index.html | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| pdfa | PDF/A-1b | field |
| pdfua | true | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip"
Then there should be 2 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.zip |
| foo_0.pdf |
| foo_1.pdf |
Then the "foo_0.pdf" PDF should have 2 page(s)
Then the "foo_1.pdf" PDF should have 1 page(s)
Then the "foo_0.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "foo_0.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Then the "foo_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Scenario: POST /forms/chromium/convert/html (Metadata)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):

View File

@@ -469,6 +469,7 @@ Feature: /forms/chromium/convert/markdown
| marginRight | foo | field |
| preferCssPageSize | foo | field |
| generateDocumentOutline | foo | field |
| generateTaggedPdf | foo | field |
| printBackground | foo | field |
| omitBackground | foo | field |
| landscape | foo | field |
@@ -484,7 +485,7 @@ Feature: /forms/chromium/convert/markdown
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string:
"""
Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form file 'index.html' is required; no form file found for extensions: [.md]
Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateTaggedPdf' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form file 'index.html' is required; no form file found for extensions: [.md]
"""
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
| files | testdata/page-1-markdown/index.html | file |
@@ -496,21 +497,23 @@ Feature: /forms/chromium/convert/markdown
"""
omitBackground requires printBackground set to true
"""
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
| files | testdata/page-1-markdown/index.html | file |
| files | testdata/page-1-markdown/page_1.md | file |
| paperWidth | 0 | field |
| paperHeight | 0 | field |
| marginTop | 1000000 | field |
| marginBottom | 1000000 | field |
| marginLeft | 1000000 | field |
| marginRight | 1000000 | field |
Then the response status code should be 400
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string:
"""
Chromium does not handle the provided settings; please check for aberrant form values
"""
# Does not seems to happen on amd architectures anymore since Chromium 137.
# See: https://github.com/gotenberg/gotenberg/actions/runs/15384321883/job/43280184372.
# When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
# | files | testdata/page-1-markdown/index.html | file |
# | files | testdata/page-1-markdown/page_1.md | file |
# | paperWidth | 0 | field |
# | paperHeight | 0 | field |
# | marginTop | 1000000 | field |
# | marginBottom | 1000000 | field |
# | marginLeft | 1000000 | field |
# | marginRight | 1000000 | field |
# Then the response status code should be 400
# Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
# Then the response body should match string:
# """
# Chromium does not handle the provided settings; please check for aberrant form values
# """
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
| files | testdata/page-1-markdown/index.html | file |
| files | testdata/page-1-markdown/page_1.md | file |
@@ -675,6 +678,39 @@ Feature: /forms/chromium/convert/markdown
Page 3
"""
# See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/markdown (Split Output Filename)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
| files | testdata/pages-3-markdown/index.html | file |
| files | testdata/pages-3-markdown/page_1.md | file |
| files | testdata/pages-3-markdown/page_2.md | file |
| files | testdata/pages-3-markdown/page_3.md | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip"
Then there should be 2 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.zip |
| foo_0.pdf |
| foo_1.pdf |
Then the "foo_0.pdf" PDF should have 2 page(s)
Then the "foo_1.pdf" PDF should have 1 page(s)
Then the "foo_0.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "foo_0.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Then the "foo_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Scenario: POST /forms/chromium/convert/markdown (Split Pages)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -819,6 +855,43 @@ Feature: /forms/chromium/convert/markdown
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
# See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/markdown (Split & PDF/A-1b & PDF/UA-1 & Output Filename)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
| files | testdata/pages-3-markdown/index.html | file |
| files | testdata/pages-3-markdown/page_1.md | file |
| files | testdata/pages-3-markdown/page_2.md | file |
| files | testdata/pages-3-markdown/page_3.md | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| pdfa | PDF/A-1b | field |
| pdfua | true | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip"
Then there should be 2 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.zip |
| foo_0.pdf |
| foo_1.pdf |
Then the "foo_0.pdf" PDF should have 2 page(s)
Then the "foo_1.pdf" PDF should have 1 page(s)
Then the "foo_0.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "foo_0.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Then the "foo_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Scenario: POST /forms/chromium/convert/markdown (Metadata)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):

View File

@@ -451,6 +451,7 @@ Feature: /forms/chromium/convert/url
| marginRight | foo | field |
| preferCssPageSize | foo | field |
| generateDocumentOutline | foo | field |
| generateTaggedPdf | foo | field |
| printBackground | foo | field |
| omitBackground | foo | field |
| landscape | foo | field |
@@ -466,7 +467,7 @@ Feature: /forms/chromium/convert/url
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string:
"""
Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'url' is required
Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateTaggedPdf' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'url' is required
"""
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
@@ -478,21 +479,23 @@ Feature: /forms/chromium/convert/url
"""
omitBackground requires printBackground set to true
"""
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
| paperWidth | 0 | field |
| paperHeight | 0 | field |
| marginTop | 1000000 | field |
| marginBottom | 1000000 | field |
| marginLeft | 1000000 | field |
| marginRight | 1000000 | field |
Then the response status code should be 400
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string:
"""
Chromium does not handle the provided settings; please check for aberrant form values
"""
# Does not seems to happen on amd architectures anymore since Chromium 137.
# See: https://github.com/gotenberg/gotenberg/actions/runs/15384321883/job/43280184372.
# Given I have a static server
# When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
# | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
# | paperWidth | 0 | field |
# | paperHeight | 0 | field |
# | marginTop | 1000000 | field |
# | marginBottom | 1000000 | field |
# | marginLeft | 1000000 | field |
# | marginRight | 1000000 | field |
# Then the response status code should be 400
# Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
# Then the response body should match string:
# """
# Chromium does not handle the provided settings; please check for aberrant form values
# """
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
@@ -655,6 +658,37 @@ Feature: /forms/chromium/convert/url
Page 3
"""
# See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/url (Split Output Filename)
Given I have a default Gotenberg container
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/pages-3-html/index.html | field |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip"
Then there should be 2 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.zip |
| foo_0.pdf |
| foo_1.pdf |
Then the "foo_0.pdf" PDF should have 2 page(s)
Then the "foo_1.pdf" PDF should have 1 page(s)
Then the "foo_0.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "foo_0.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Then the "foo_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Scenario: POST /forms/chromium/convert/url (Split Pages)
Given I have a default Gotenberg container
Given I have a static server
@@ -782,6 +816,41 @@ Feature: /forms/chromium/convert/url
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
# See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/url (Split & PDF/A-1b & PDF/UA-1 & Output Filename)
Given I have a default Gotenberg container
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
| url | http://host.docker.internal:%d/html/testdata/pages-3-html/index.html | field |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| pdfa | PDF/A-1b | field |
| pdfua | true | field |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip"
Then there should be 2 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.zip |
| foo_0.pdf |
| foo_1.pdf |
Then the "foo_0.pdf" PDF should have 2 page(s)
Then the "foo_1.pdf" PDF should have 1 page(s)
Then the "foo_0.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "foo_0.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Then the "foo_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Scenario: POST /forms/chromium/convert/url (Metadata)
Given I have a default Gotenberg container
Given I have a static server

View File

@@ -86,6 +86,7 @@ Feature: /debug
"chromium-proxy-server": "",
"chromium-restart-after": "10",
"chromium-start-timeout": "20s",
"gotenberg-build-debug-data": "true",
"gotenberg-graceful-shutdown-duration": "30s",
"libreoffice-auto-start": "false",
"libreoffice-disable-routes": "false",
@@ -120,6 +121,24 @@ Feature: /debug
}
"""
Scenario: GET /debug (No Debug Data)
Given I have a Gotenberg container with the following environment variable(s):
| GOTENBERG_BUILD_DEBUG_DATA | false |
| API_ENABLE_DEBUG_ROUTE | true |
When I make a "GET" request to Gotenberg at the "/debug" endpoint
Then the response status code should be 200
Then the response header "Content-Type" should be "application/json"
Then the response body should match JSON:
"""
{
"version": "",
"architecture": "",
"modules": null,
"modules_additional_data": null,
"flags": null
}
"""
Scenario: GET /debug (Gotenberg Trace)
Given I have a Gotenberg container with the following environment variable(s):
| API_ENABLE_DEBUG_ROUTE | true |

View File

@@ -40,6 +40,25 @@ Feature: /forms/libreoffice/convert
Page 2
"""
# See:
# https://github.com/gotenberg/gotenberg/issues/104
# https://github.com/gotenberg/gotenberg/issues/730
Scenario: POST /forms/libreoffice/convert (Non-basic Latin Characters)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
| files | testdata/Special_Chars_ß.docx | file |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response:
| foo.pdf |
Then the "foo.pdf" PDF should have 1 page(s)
Then the "foo.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Scenario: POST /forms/libreoffice/convert (Protected)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):

View File

@@ -0,0 +1,33 @@
Feature: Output Filename
Scenario: Default (Single Output File)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then there should be the following file(s) in the response:
| foo.pdf |
Scenario: Default (Many Output Files)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| files | testdata/page_2.pdf | file |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip"
Then there should be the following file(s) in the response:
| foo.zip |
# See https://github.com/gotenberg/gotenberg/issues/1227.
Scenario: Path As Filename
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| Gotenberg-Output-Filename | /tmp/foo | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then there should be the following file(s) in the response:
| foo.pdf |

View File

@@ -233,6 +233,24 @@ Feature: /forms/pdfengines/{write|read}
}
"""
Scenario: POST /forms/pdfengines/metadata/read (Webhook)
Given I have a default Gotenberg container
Given I have a webhook server
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header |
| Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header |
Then the response status code should be 204
When I wait for the asynchronous request to the webhook
Then the webhook request header "Content-Type" should be "application/json"
Then the webhook request body should match JSON:
"""
{
"status": 400,
"message": "The webhook middleware can only work with multipart/form-data routes that results in output files"
}
"""
Scenario: POST /forms/pdfengines/metadata/write (Basic Auth)
Given I have a Gotenberg container with the following environment variable(s):
| API_ENABLE_BASIC_AUTH | true |

View File

@@ -0,0 +1,33 @@
# TODO:
# 1. Other HTTP Methods
# 2. Errors
Feature: Webhook
Scenario: Default
Given I have a default Gotenberg container
Given I have a webhook server
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header |
| Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header |
Then the response status code should be 204
When I wait for the asynchronous request to the webhook
Then the webhook request header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the webhook request
Scenario: Extra HTTP Headers
Given I have a default Gotenberg container
Given I have a webhook server
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header |
| Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header |
| Gotenberg-Webhook-Extra-Http-Headers | {"X-Foo":"bar","Content-Disposition":"inline"} | header |
Then the response status code should be 204
When I wait for the asynchronous request to the webhook
Then the webhook request header "Content-Type" should be "application/pdf"
Then the webhook request header "X-Foo" should be "bar"
# https://github.com/gotenberg/gotenberg/issues/1165
Then the webhook request header "Content-Disposition" should be "inline"
Then there should be 1 PDF(s) in the webhook request

View File

@@ -407,11 +407,7 @@ func (s *scenario) theBodyShouldMatchString(kind string, expectedDoc *godog.DocS
} else if s.server.req == nil {
return errors.New("no webhook request found")
} else {
body, err := io.ReadAll(s.server.req.Body)
if err != nil {
return fmt.Errorf("read request body: %w", err)
}
actual = string(body)
actual = string(s.server.bodyCopy)
}
expected := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion)
@@ -431,11 +427,7 @@ func (s *scenario) theBodyShouldContainString(kind string, expectedDoc *godog.Do
} else if s.server.req == nil {
return errors.New("no webhook request found")
} else {
body, err := io.ReadAll(s.server.req.Body)
if err != nil {
return fmt.Errorf("read request body: %w", err)
}
actual = string(body)
actual = string(s.server.bodyCopy)
}
expected := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion)
@@ -455,11 +447,7 @@ func (s *scenario) theBodyShouldMatchJSON(kind string, expectedDoc *godog.DocStr
} else if s.server.req == nil {
return errors.New("no webhook request found")
} else {
b, err := io.ReadAll(s.server.req.Body)
if err != nil {
return fmt.Errorf("read request body: %w", err)
}
body = b
body = s.server.bodyCopy
}
var expected, actual interface{}

View File

@@ -13,14 +13,16 @@ import (
"strings"
"github.com/cucumber/godog"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/mholt/archives"
)
type server struct {
srv *echo.Echo
req *http.Request
errChan chan error
srv *echo.Echo
req *http.Request
bodyCopy []byte
errChan chan error
}
func newServer(ctx context.Context, workdir string) (*server, error) {
@@ -50,6 +52,8 @@ func newServer(ctx context.Context, workdir string) (*server, error) {
return webhookErr(fmt.Errorf("read request body: %w", err))
}
s.bodyCopy = body
cd := s.req.Header.Get("Content-Disposition")
if cd == "" {
return webhookErr(fmt.Errorf("no Content-Disposition header"))
@@ -62,7 +66,16 @@ func newServer(ctx context.Context, workdir string) (*server, error) {
filename, ok := params["filename"]
if !ok {
return webhookErr(errors.New("no filename in Content-Disposition header"))
filename = uuid.NewString()
contentType := s.req.Header.Get("Content-Type")
switch contentType {
case "application/zip":
filename = fmt.Sprintf("%s.zip", filename)
case "application/pdf":
filename = fmt.Sprintf("%s.pdf", filename)
default:
return webhookErr(errors.New("no filename in Content-Disposition header"))
}
}
dirPath := fmt.Sprintf("%s/%s", workdir, s.req.Header.Get("Gotenberg-Trace"))
@@ -115,6 +128,11 @@ func newServer(ctx context.Context, workdir string) (*server, error) {
}
webhookErrorHandler := func(c echo.Context) error {
s.req = c.Request()
body, err := io.ReadAll(s.req.Body)
if err != nil {
return webhookErr(fmt.Errorf("read request body: %w", err))
}
s.bodyCopy = body
return webhookErr(c.String(http.StatusOK, http.StatusText(http.StatusOK)))
}

Binary file not shown.