Compare commits

...

78 Commits

Author SHA1 Message Date
Julien Neuhart
45318497d2 chore(deps): update Go dependencies 2026-01-27 11:20:29 +00:00
dependabot[bot]
b762143d75 chore(deps): bump golang.org/x/net from 0.48.0 to 0.49.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 09:23:09 +01:00
dependabot[bot]
ba1a730d3a chore(deps-dev): bump prettier from 3.7.4 to 3.8.1
Bumps [prettier](https://github.com/prettier/prettier) from 3.7.4 to 3.8.1.
- [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.7.4...3.8.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 09:23:01 +01:00
Adam Romanek
463bb2f91c fix(webhook): detach context from request lifecycle to prevent cancellation in async mode 2026-01-26 09:11:48 +01:00
Adam Romanek
e14cab0c8b fix(api): propagate request context to stop processing on client disconnect 2026-01-26 09:11:48 +01:00
Daniel Moran
3220ca4140 feat(chromium): add waitForSelector option to Chromium conversions (#1446)
* Add `waitForSelector` option to Chromium conversions

Closes #960

As an alternative to waiting on an expression, this allows users to wait
for a specific node matching a selector to become visible in the HTML /
at the remote URL before converting to PDF.

* Fix style / prettify
2026-01-17 14:57:10 +01:00
David
92de0cf6fe feat(prometheus): add new flag --prometheus-metrics-path 2026-01-14 10:04:57 +01:00
dependabot[bot]
d111591c8b chore(deps): bump golang.org/x/term from 0.38.0 to 0.39.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/term/compare/v0.38.0...v0.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 09:22:10 +01:00
dependabot[bot]
935686b9e3 chore(deps): bump golang.org/x/text from 0.32.0 to 0.33.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.32.0...v0.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 09:18:20 +01:00
John W. Mendes
b1ad21c7d3 chore(go): update Go version to 1.25.5 (#1440)
* Update Go to 1.25.5

* Update go version in go.mod
2026-01-05 15:42:47 +01:00
dependabot[bot]
4f800c8a76 chore(deps): bump github.com/shirou/gopsutil/v4 from 4.25.11 to 4.25.12 (#1438)
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.25.11 to 4.25.12.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.25.11...v4.25.12)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.25.12
  dependency-type: direct:production
  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>
2026-01-05 13:19:58 +01:00
dependabot[bot]
5c498219ed chore(deps): bump github.com/labstack/echo/v4 from 4.14.0 to 4.15.0 (#1439)
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.14.0 to 4.15.0.
- [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.14.0...v4.15.0)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v4
  dependency-version: 4.15.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>
2026-01-05 13:19:43 +01:00
markfrost
0b211c39eb feat(chromium): add IgnoreResourceHttpStatusDomains option to filter out resources based on their hostnames (#1434)
* Introduced `IgnoreResourceHttpStatusDomains` option to filter out resources based on their hostnames

- Introduced `IgnoreResourceHttpStatusDomains` option to filter out resources based on their hostnames during HTTP status code checks.
- Updated relevant functions to handle domain normalization and matching.
- Enhanced the form data handling to include the new option.
- Added integration test scenario to verify the functionality of ignoring specified domains.

* Updated the `normalizeDomains` function to initialize the `normalized` slice with a predefined capacity based on the input `domains` slice length, improving memory allocation efficiency.
2025-12-25 15:36:45 +01:00
dependabot[bot]
77b9776d9c chore(deps): bump github.com/labstack/echo/v4 from 4.13.4 to 4.14.0
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.13.4 to 4.14.0.
- [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.4...v4.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 09:28:49 +01:00
dependabot[bot]
04e596b616 chore(deps): bump golang.org/x/text from 0.31.0 to 0.32.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.31.0 to 0.32.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.31.0...v0.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 09:25:25 +01:00
dependabot[bot]
9fba76fe5c chore(deps-dev): bump prettier from 3.7.3 to 3.7.4
Bumps [prettier](https://github.com/prettier/prettier) from 3.7.3 to 3.7.4.
- [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.7.3...3.7.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 10:12:14 +01:00
Julien Neuhart
8b9858aa66 fix(chromium): resource intensive health check 2025-12-03 22:34:39 +01:00
dependabot[bot]
2f7218b09c chore(deps): bump github.com/shirou/gopsutil/v4 from 4.25.10 to 4.25.11
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.25.10 to 4.25.11.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.25.10...v4.25.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 10:11:22 +01:00
dependabot[bot]
0a05091241 chore(deps-dev): bump prettier from 3.6.2 to 3.7.3
Bumps [prettier](https://github.com/prettier/prettier) from 3.6.2 to 3.7.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.6.2...3.7.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.7.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 10:11:09 +01:00
dependabot[bot]
507d0cb2e2 chore(deps): bump go.uber.org/zap from 1.27.0 to 1.27.1
Bumps [go.uber.org/zap](https://github.com/uber-go/zap) from 1.27.0 to 1.27.1.
- [Release notes](https://github.com/uber-go/zap/releases)
- [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/zap/compare/v1.27.0...v1.27.1)

---
updated-dependencies:
- dependency-name: go.uber.org/zap
  dependency-version: 1.27.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 09:02:55 +01:00
dependabot[bot]
fb61948244 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 09:02:11 +01:00
Alex Weston
15fdc99ec0 fix(chromium): add net::ERR_HTTP2_PROTOCOL_ERROR in the EventLoadingFailed list 2025-11-21 21:30:03 +01:00
dependabot[bot]
dd07c73c28 chore(deps): bump golang.org/x/crypto from 0.44.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 08:52:48 +01:00
Julien Neuhart
43354cafb1 chore(deps): update Go dependencies 2025-11-15 15:15:50 +01:00
Julien Neuhart
b2c3187690 fix(chromium): better native errors handling 2025-11-15 15:10:09 +01:00
Julien Neuhart
4a788ce9d8 fix(lint): non-used method in tests 2025-11-13 22:12:02 +01:00
Julien Neuhart
f9ad8bd2d5 test(integration): add more tags, complete embed feature tests suite, fix order with metadata 2025-11-13 22:08:28 +01:00
Julien Neuhart
4429aea8a4 chore(tests): rename embed feature file 2025-11-13 20:33:10 +01:00
Julien Neuhart
7a60e82d03 chore(deps): use node 24.11 for dev tools 2025-11-10 13:14:50 +00:00
dependabot[bot]
3544e9c355 chore(deps): bump golangci/golangci-lint-action from 8 to 9
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 8 to 9.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 13:32:15 +01:00
dependabot[bot]
ca4f0141c6 chore(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.1+incompatible to 28.5.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.1...v28.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 09:20:23 +01:00
dependabot[bot]
fcaa26a84d chore(deps): bump golang.org/x/sync from 0.17.0 to 0.18.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/sync/compare/v0.17.0...v0.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 08:57:38 +01:00
dependabot[bot]
ea103d3070 chore(deps): bump github.com/testcontainers/testcontainers-go
Bumps [github.com/testcontainers/testcontainers-go](https://github.com/testcontainers/testcontainers-go) from 0.39.0 to 0.40.0.
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 08:57:25 +01:00
Tom Quist
286efbe548 chore(deps): bump Go from 1.25.0 to 1.25.4 2025-11-06 16:42:20 +01:00
Hubert Lenoir
a0ee800002 feat(pdfengines): add embed feature 2025-11-05 13:57:23 +01:00
hubert.lenoir
f4d3fba306 feat: add tests tags 2025-11-04 17:44:37 +01:00
dependabot[bot]
d5a58cfcb5 chore(deps): bump github.com/shirou/gopsutil/v4 from 4.25.9 to 4.25.10
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.25.9 to 4.25.10.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.25.9...v4.25.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 06:36:00 +01:00
Julien Neuhart
0e6bb27ecf fix(chromium): add missing wrapped error 2025-10-24 14:49:54 +00:00
Julien Neuhart
83fdc01df1 fix(chromium): accept case-insensitive values for the cookies' sameSite attribute - closes #1331 2025-10-24 14:46:14 +00:00
Julien Neuhart
c7ccabc6cb fix(chromium): mark the flag --chromium-incognito as deprecated and ignore its value 2025-10-24 14:03:17 +00:00
pebosi
3eefe33e64 Update pdfcpu version 2025-10-22 09:26:02 +02:00
dependabot[bot]
c44dc7f0ab chore(deps): bump github.com/mholt/archives from 0.1.4 to 0.1.5
Bumps [github.com/mholt/archives](https://github.com/mholt/archives) from 0.1.4 to 0.1.5.
- [Release notes](https://github.com/mholt/archives/releases)
- [Commits](https://github.com/mholt/archives/compare/v0.1.4...v0.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 14:02:16 +02:00
dependabot[bot]
1639d82348 chore(deps): bump actions/setup-node from 5 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 14:02:06 +02:00
dependabot[bot]
b31e520731 chore(deps-dev): bump prettier-plugin-gherkin from 3.1.2 to 3.1.3
Bumps [prettier-plugin-gherkin](https://github.com/mapado/prettier-plugin-gherkin) from 3.1.2 to 3.1.3.
- [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.2...v3.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 14:01:50 +02:00
dependabot[bot]
e85d4143c0 chore(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.0+incompatible to 28.5.1+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 10:03:53 +02:00
dependabot[bot]
da20f04e25 chore(deps): bump github.com/chromedp/chromedp from 0.14.1 to 0.14.2
Bumps [github.com/chromedp/chromedp](https://github.com/chromedp/chromedp) from 0.14.1 to 0.14.2.
- [Release notes](https://github.com/chromedp/chromedp/releases)
- [Commits](https://github.com/chromedp/chromedp/compare/v0.14.1...v0.14.2)

---
updated-dependencies:
- dependency-name: github.com/chromedp/chromedp
  dependency-version: 0.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 10:03:44 +02:00
dependabot[bot]
9a49ea426e chore(deps): bump golang.org/x/net from 0.44.0 to 0.46.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.44.0 to 0.46.0.
- [Commits](https://github.com/golang/net/compare/v0.44.0...v0.46.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 10:03:23 +02:00
dependabot[bot]
c0c4c21682 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 10:03:15 +02:00
Hugo Tacla
c7424dfcb3 feat(Dockerfile): add basic LibreOffice Hyphenation support 2025-10-09 12:45:52 +02:00
Hugo Tacla
e33ca6af12 fix(deps): revert indirect nwaples/rardecode/v2 bump 2025-10-09 07:44:43 +02:00
Julien Neuhart
9dc6e308b6 ci(deps): update golangci-lint to v2.5.0 2025-10-07 09:52:59 +00:00
Julien Neuhart
4fe2ff5553 chore(deps): update No Emoji to v2.051 2025-10-07 09:44:38 +00:00
Julien Neuhart
e5ccb82db8 chore(deps): update Go dependencies 2025-10-07 09:43:41 +00:00
Julien Neuhart
63ae987010 feat(build): add ppc64le architecture (#1344)
* Support ppc64le architecture

# Conflicts:
#	.github/workflows/continuous-delivery.yml
#	.github/workflows/continuous-integration.yml

* Adding descriptive missing tags

* empty commit for triggering CI

* ci(ppc64le): wrong runner

* fix(ci): iincrease timeout for integration tests to 40 minutes

---------

Co-authored-by: Andrea Esposito <andrea.esposito@horsa.it>
2025-10-07 09:46:24 +02:00
dependabot[bot]
efa0d3fd75 chore(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.4.0+incompatible to 28.5.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.4.0...v28.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 07:42:18 +02:00
dependabot[bot]
fd5930bc56 chore(deps): bump github.com/shirou/gopsutil/v4 from 4.25.8 to 4.25.9
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.25.8 to 4.25.9.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.25.8...v4.25.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 07:41:53 +02:00
Julien Neuhart
a057c6253b chore(deps): update Go dependencies 2025-09-26 13:15:44 +00:00
Julien Neuhart
ed5d1fa891 fix(Dockerfile): remove upx stage 2025-09-20 00:57:17 +02:00
Julien Neuhart
76d36d18be chore(deps): update Go dependencies 2025-09-17 07:02:45 +00:00
dependabot[bot]
a78c433a60 chore(deps): bump golang.org/x/net from 0.43.0 to 0.44.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.43.0 to 0.44.0.
- [Commits](https://github.com/golang/net/compare/v0.43.0...v0.44.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-16 13:53:51 +02:00
Julien Neuhart
aff7530cb0 feat(build): use trixie-backports for LibreOffice 25.8 - closes #1323 2025-09-09 19:58:14 +02:00
Julien Neuhart
e7e7267fc2 docs(README): add JetBrains logo 2025-09-09 19:37:27 +02:00
dependabot[bot]
9aaab2de3b chore(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.3.3+incompatible to 28.4.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.3...v28.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 09:21:34 +02:00
dependabot[bot]
8f0121a92d chore(deps): bump golang.org/x/sync from 0.16.0 to 0.17.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/sync/compare/v0.16.0...v0.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 09:21:19 +02:00
dependabot[bot]
beb1e386c2 chore(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.23.0 to 1.23.2.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.23.0...v1.23.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 09:21:01 +02:00
dependabot[bot]
5b84a5e0e9 chore(deps): bump actions/setup-node from 4 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 09:20:44 +02:00
dependabot[bot]
a9613651a5 chore(deps): bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 09:20:22 +02:00
Julien Neuhart
d6ebc7f230 chore(pdfengines): better custom error message if invalid args 2025-09-04 14:49:04 +02:00
Julien Neuhart
84ca118524 chore(deps): update Go dependencies 2025-09-04 14:49:04 +02:00
Julien Neuhart
0ad6623e24 fix(pdgengines): encrypt behavior accross different engines 2025-09-04 14:49:04 +02:00
Julien Neuhart
ba944f9dc2 chore: slight improvement of the encrypt feature to be more in line with others features 2025-09-04 14:49:04 +02:00
Stevenson Michel
99307befdd feat: add PDF encryption feature (#1217)
* Adding PDF encryption option

* LibreOffice
* QPDF
* PDFtk
* pdfcpu

* Update pkg/modules/pdfengines/pdfengines.go

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

* npx prettier

* go fmt

* PR comments

* Update test/integration/scenario/scenario.go

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

* renamed ProtectWithPassword to encrypt

* more clean up

* Use the same input path as requested

* This commit completes the encryption implementation

* Clean up. Added webhook, disable route and download from tests

---------

Co-authored-by: Julien Neuhart <neuhart.julien@gmail.com>
2025-09-04 14:49:04 +02:00
Julien Neuhart
638c4e6b23 chore(deps): update Go dependencies 2025-09-01 17:29:00 +02:00
Julien Neuhart
18bb7ef00d ci(actions): switch to actions/checkout@v5 2025-08-31 18:19:21 +02:00
Julien Neuhart
3dd9ce1fcd feat(chromium): add MathJaX support 2025-08-29 21:06:20 +02:00
Julien Neuhart
fd4814194c chore(deps): update Go dependencies 2025-08-29 20:20:48 +02:00
Julien Neuhart
4d526fe419 fix(build): use buffered invoke mode and do not auto-start LibreOffice and Chromium 2025-08-29 20:20:18 +02:00
Julien Neuhart
50d1b187bb fix(build): switch from /home/gotenberg to /opt/gotenberg for hype´ data 2025-08-20 09:38:05 +00:00
64 changed files with 2575 additions and 640 deletions

View File

@@ -14,7 +14,7 @@ inputs:
description: The Docker Hub password description: The Docker Hub password
required: true required: true
platform: platform:
description: linux/amd64, linux/386, linux/arm64, linux/arm/v7 description: linux/amd64, linux/ppc64le, linux/386, linux/arm64, linux/arm/v7
required: true required: true
version: version:
description: Gotenberg version description: Gotenberg version
@@ -48,7 +48,7 @@ runs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Log in to Docker Hub - name: Log in to Docker Hub
if: inputs.docker_hub_username != '' if: inputs.docker_hub_username != ''

View File

@@ -31,7 +31,7 @@ runs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3

View File

@@ -56,6 +56,7 @@ declare -A merge_map
for tag in "${tags_to_merge[@]}"; do for tag in "${tags_to_merge[@]}"; do
target_tag="${tag//-amd64/}" target_tag="${tag//-amd64/}"
target_tag="${target_tag//-ppc64le/}"
target_tag="${target_tag//-arm64/}" target_tag="${target_tag//-arm64/}"
target_tag="${target_tag//-arm/}" target_tag="${target_tag//-arm/}"
target_tag="${target_tag//-386/}" target_tag="${target_tag//-386/}"

View File

@@ -17,7 +17,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build and push - name: Build and push
id: build_push id: build_push
@@ -34,9 +34,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
tags: ${{ steps.build_push.outputs.tags }} tags: ${{ steps.build_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build and push - name: Build and push
id: build_push id: build_push
@@ -48,6 +50,27 @@ jobs:
platform: linux/386 platform: linux/386
skip_integrations_tests: true skip_integrations_tests: true
release_ppc64le:
name: Release linux/ppc64le
runs-on: ubuntu-24.04-ppc64le
outputs:
tags: ${{ steps.build_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build and push
id: build_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: ${{ github.event.release.tag_name }}
platform: linux/ppc64le
skip_integrations_tests: true
release_arm64: release_arm64:
name: Release linux/arm64 name: Release linux/arm64
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
@@ -57,7 +80,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build and push - name: Build and push
id: build_push id: build_push
@@ -78,7 +101,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build and push - name: Build and push
id: build_push id: build_push
@@ -94,20 +117,21 @@ jobs:
needs: needs:
- release_amd64 - release_amd64
- release_386 - release_386
- release_ppc64le
- release_arm64 - release_arm64
- release_arm_v7 - release_arm_v7
name: Merge and clean release tags name: Merge and clean release tags
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Merge - name: Merge
uses: ./.github/actions/merge uses: ./.github/actions/merge
with: with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}" tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_ppc64le.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}"
alternate_registry: thecodingmachine alternate_registry: thecodingmachine
- name: Merge AWS Lambda - name: Merge AWS Lambda
@@ -123,4 +147,4 @@ jobs:
with: with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }},${{ needs.release_amd64.outputs.tags_aws_lambda }},${{ needs.release_arm64.outputs.tags_aws_lambda }}" tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_ppc64le.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }},${{ needs.release_amd64.outputs.tags_aws_lambda }},${{ needs.release_arm64.outputs.tags_aws_lambda }}"

View File

@@ -21,27 +21,27 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Run linters - name: Run linters
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v9
with: with:
version: v2.4.0 version: v2.5.0
lint-prettier: lint-prettier:
name: Lint non-Golang codebase name: Lint non-Golang codebase
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version-file: .node-version node-version-file: .node-version
@@ -59,10 +59,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
@@ -81,7 +81,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -93,6 +93,30 @@ jobs:
platform: linux/amd64 platform: linux/amd64
alternate_repository: snapshot alternate_repository: snapshot
snapshot_ppc64le:
if: github.event_name == 'pull_request'
needs:
- test-unit
name: Snapshot linux/ppc64le
runs-on: ubuntu-24.04-ppc64le
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: pr-${{ github.event.pull_request.number }}
platform: linux/ppc64le
alternate_repository: snapshot
snapshot_386: snapshot_386:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
needs: needs:
@@ -105,7 +129,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -129,7 +153,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -153,7 +177,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -169,6 +193,7 @@ jobs:
needs: needs:
- snapshot_amd64 - snapshot_amd64
- snapshot_386 - snapshot_386
- snapshot_ppc64le
- snapshot_arm64 - snapshot_arm64
- snapshot_arm_v7 - snapshot_arm_v7
name: Secrets access check name: Secrets access check
@@ -191,20 +216,21 @@ jobs:
- merge_clean_snapshot_guard - merge_clean_snapshot_guard
- snapshot_amd64 - snapshot_amd64
- snapshot_386 - snapshot_386
- snapshot_ppc64le
- snapshot_arm64 - snapshot_arm64
- snapshot_arm_v7 - snapshot_arm_v7
name: Merge and clean snapshot tags name: Merge and clean snapshot tags
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Merge - name: Merge
uses: ./.github/actions/merge uses: ./.github/actions/merge
with: with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}" tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_ppc64le.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}"
- name: Merge AWS Lambda - name: Merge AWS Lambda
uses: ./.github/actions/merge uses: ./.github/actions/merge
@@ -218,7 +244,7 @@ jobs:
with: with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }},${{ needs.snapshot_amd64.outputs.tags_aws_lambda }},${{ needs.snapshot_arm64.outputs.tags_aws_lambda }}" tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_ppc64le.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }},${{ needs.snapshot_amd64.outputs.tags_aws_lambda }},${{ needs.snapshot_arm64.outputs.tags_aws_lambda }}"
edge_amd64: edge_amd64:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
@@ -232,7 +258,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -255,7 +281,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -266,6 +292,29 @@ jobs:
version: edge version: edge
platform: linux/386 platform: linux/386
edge_ppc64le:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-unit
name: Edge linux/ppc64le
runs-on: ubuntu-24.04-ppc64le
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: edge
platform: linux/ppc64le
edge_arm64: edge_arm64:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: needs:
@@ -278,7 +327,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -301,7 +350,7 @@ jobs:
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }} tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Build, test and push - name: Build, test and push
id: build_test_push id: build_test_push
@@ -316,20 +365,21 @@ jobs:
needs: needs:
- edge_amd64 - edge_amd64
- edge_386 - edge_386
- edge_ppc64le
- edge_arm64 - edge_arm64
- edge_arm_v7 - edge_arm_v7
name: Merge and clean edge tags name: Merge and clean edge tags
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Merge - name: Merge
uses: ./.github/actions/merge uses: ./.github/actions/merge
with: with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}" tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_ppc64le.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}"
alternate_registry: thecodingmachine alternate_registry: thecodingmachine
- name: Merge AWS Lambda - name: Merge AWS Lambda
@@ -345,4 +395,4 @@ jobs:
with: with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }},${{ needs.edge_amd64.outputs.tags_aws_lambda }},${{ needs.edge_arm64.outputs.tags_aws_lambda }}" tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_ppc64le.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }},${{ needs.edge_amd64.outputs.tags_aws_lambda }},${{ needs.edge_arm64.outputs.tags_aws_lambda }}"

View File

@@ -14,7 +14,7 @@ jobs:
continue-on-error: true continue-on-error: true
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Cleanup - name: Cleanup
uses: ./.github/actions/clean uses: ./.github/actions/clean

View File

@@ -1 +1 @@
23.9.0 24.11.0

View File

@@ -34,7 +34,6 @@ CHROMIUM_RESTART_AFTER=10
CHROMIUM_MAX_QUEUE_SIZE=0 CHROMIUM_MAX_QUEUE_SIZE=0
CHROMIUM_AUTO_START=false CHROMIUM_AUTO_START=false
CHROMIUM_START_TIMEOUT=20s CHROMIUM_START_TIMEOUT=20s
CHROMIUM_INCOGNITO=false
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
CHROMIUM_DISABLE_WEB_SECURITY=false CHROMIUM_DISABLE_WEB_SECURITY=false
@@ -62,11 +61,14 @@ PDFENGINES_FLATTEN_ENGINES=qpdf
PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine
PDFENGINES_READ_METADATA_ENGINES=exiftool PDFENGINES_READ_METADATA_ENGINES=exiftool
PDFENGINES_WRITE_METADATA_ENGINES=exiftool PDFENGINES_WRITE_METADATA_ENGINES=exiftool
PDFENGINES_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk
PDFENGINES_DISABLE_ROUTES=false PDFENGINES_DISABLE_ROUTES=false
PDFENGINES_EMBED_ENGINES=pdfcpu
PROMETHEUS_NAMESPACE=gotenberg PROMETHEUS_NAMESPACE=gotenberg
PROMETHEUS_COLLECT_INTERVAL=1s PROMETHEUS_COLLECT_INTERVAL=1s
PROMETHEUS_DISABLE_ROUTE_LOGGING=false PROMETHEUS_DISABLE_ROUTE_LOGGING=false
PROMETHEUS_DISABLE_COLLECT=false PROMETHEUS_DISABLE_COLLECT=false
PROMETHEUS_METRICS_PATH=/prometheus/metrics
WEBHOOK_ENABLE_SYNC_MODE=false WEBHOOK_ENABLE_SYNC_MODE=false
WEBHOOK_ALLOW_LIST= WEBHOOK_ALLOW_LIST=
WEBHOOK_DENY_LIST= WEBHOOK_DENY_LIST=
@@ -108,7 +110,6 @@ run: ## Start a Gotenberg container
--chromium-auto-start=$(CHROMIUM_AUTO_START) \ --chromium-auto-start=$(CHROMIUM_AUTO_START) \
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \ --chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \ --chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
--chromium-incognito=$(CHROMIUM_INCOGNITO) \
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \ --chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \ --chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
--chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \ --chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \
@@ -136,11 +137,14 @@ run: ## Start a Gotenberg container
--pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \ --pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \
--pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \ --pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \
--pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \ --pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \
--pdfengines-encrypt-engines=$(PDFENGINES_ENCRYPT_ENGINES) \
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \ --pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
--pdfengines-embed-engines=$(PDFENGINES_EMBED_ENGINES) \
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \ --prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \ --prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \ --prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \ --prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
--prometheus-metrics-path=$(PROMETHEUS_METRICS_PATH) \
--webhook-enable-sync-mode="$(WEBHOOK_ENABLE_SYNC_MODE)" \ --webhook-enable-sync-mode="$(WEBHOOK_ENABLE_SYNC_MODE)" \
--webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \ --webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \
--webhook-deny-list="$(WEBHOOK_DENY_LIST)" \ --webhook-deny-list="$(WEBHOOK_DENY_LIST)" \
@@ -158,14 +162,45 @@ test-unit: ## Run unit tests
PLATFORM= PLATFORM=
NO_CONCURRENCY=false NO_CONCURRENCY=false
# Available tags:
# chromium
# chromium-convert-html
# chromium-convert-markdown
# chromium-convert-url
# debug
# health
# libreoffice
# libreoffice-convert
# output-filename
# pdfengines
# pdfengines-convert
# pdfengines-embed
# embed
# pdfengines-encrypt
# encrypt
# pdfengines-flatten
# flatten
# pdfengines-merge
# merge
# pdfengines-metadata
# metadata
# pdfengines-split
# split
# prometheus-metrics
# root
# version
# webhook
# download-from
TAGS=
.PHONY: test-integration .PHONY: test-integration
test-integration: ## Run integration tests test-integration: ## Run integration tests
go test -timeout 20m -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \ go test -timeout 40m -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \
--gotenberg-docker-repository=$(DOCKER_REPOSITORY) \ --gotenberg-docker-repository=$(DOCKER_REPOSITORY) \
--gotenberg-version=$(GOTENBERG_VERSION) \ --gotenberg-version=$(GOTENBERG_VERSION) \
--gotenberg-container-platform=$(PLATFORM) \ --gotenberg-container-platform=$(PLATFORM) \
--no-concurrency=$(NO_CONCURRENCY) --no-concurrency=$(NO_CONCURRENCY) \
--tags="$(TAGS)"
.PHONY: lint .PHONY: lint
lint: ## Lint Golang codebase lint: ## Lint Golang codebase

View File

@@ -49,3 +49,15 @@ Head to the [documentation](https://gotenberg.dev/docs/getting-started/introduct
</p> </p>
Sponsorships help maintain and improve Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️ Sponsorships help maintain and improve Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️
---
<p align="center">
<strong>Powered by</strong>
</p>
<p align="center">
<a href="https://jb.gg/OpenSource">
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains logo" width="200"/>
</a>
</p>

View File

@@ -1,7 +1,7 @@
# ARG instructions do not create additional layers. Instead, next layers will # ARG instructions do not create additional layers. Instead, next layers will
# concatenate them. Also, we have to repeat ARG instructions in each build # concatenate them. Also, we have to repeat ARG instructions in each build
# stage that uses them. # stage that uses them.
ARG GOLANG_VERSION=1.25.0 ARG GOLANG_VERSION=1.25.5
# ---------------------------------------------- # ----------------------------------------------
# pdfcpu binary build stage # pdfcpu binary build stage
@@ -11,7 +11,7 @@ ARG GOLANG_VERSION=1.25.0
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
# See https://github.com/pdfcpu/pdfcpu/releases. # See https://github.com/pdfcpu/pdfcpu/releases.
ARG PDFCPU_VERSION=v0.11.0 ARG PDFCPU_VERSION=v0.11.1
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
# Define the working directory outside of $GOPATH (we're using go modules). # Define the working directory outside of $GOPATH (we're using go modules).
@@ -51,24 +51,6 @@ COPY pkg ./pkg
RUN go build -o gotenberg -ldflags "-s -w -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:13-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 # Custom JRE stage
# Credits: https://github.com/jodconverter/docker-image-jodconverter-runtime # Credits: https://github.com/jodconverter/docker-image-jodconverter-runtime
@@ -110,7 +92,7 @@ ARG GOTENBERG_VERSION=snapshot
ARG GOTENBERG_USER_GID=1001 ARG GOTENBERG_USER_GID=1001
ARG GOTENBERG_USER_UID=1001 ARG GOTENBERG_USER_UID=1001
# See https://github.com/googlefonts/noto-emoji/releases. # See https://github.com/googlefonts/noto-emoji/releases.
ARG NOTO_COLOR_EMOJI_VERSION=v2.048 ARG NOTO_COLOR_EMOJI_VERSION=v2.051
# See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package. # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
ARG PDFTK_VERSION=v3.3.3 ARG PDFTK_VERSION=v3.3.3
@@ -193,6 +175,20 @@ RUN \
# Cleanup. # Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN \
# Install Hyphenation for LibreOffice.
# Credits: https://wiki.archlinux.org/title/LibreOffice.
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
hyphen-af hyphen-as hyphen-be hyphen-bg hyphen-bn hyphen-ca hyphen-cs hyphen-da hyphen-de hyphen-el \
hyphen-en-gb hyphen-en-us hyphen-eo hyphen-es hyphen-fr hyphen-gl hyphen-gu hyphen-hi hyphen-hr hyphen-hu \
hyphen-id hyphen-is hyphen-it hyphen-kn hyphen-lt hyphen-lv hyphen-ml hyphen-mn hyphen-mr hyphen-nl \
hyphen-no hyphen-or hyphen-pa hyphen-pl hyphen-pt-br hyphen-pt-pt hyphen-ro hyphen-ru hyphen-sk hyphen-sl \
hyphen-sr hyphen-sv hyphen-ta hyphen-te hyphen-th hyphen-uk hyphen-zu &&\
# Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN \ RUN \
# Install Chromium. # Install Chromium.
apt-get update -qq &&\ apt-get update -qq &&\
@@ -211,10 +207,11 @@ ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8
RUN \ RUN \
# Install LibreOffice & unoconverter. # Install LibreOffice & unoconverter. \
echo "deb http://deb.debian.org/debian trixie-backports main" >> /etc/apt/sources.list &&\
apt-get update -qq &&\ apt-get update -qq &&\
apt-get upgrade -yqq &&\ apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends libreoffice &&\ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t trixie-backports libreoffice &&\
curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.1.1/unoconv -o /usr/bin/unoconverter &&\ curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.1.1/unoconv -o /usr/bin/unoconverter &&\
chmod +x /usr/bin/unoconverter &&\ chmod +x /usr/bin/unoconverter &&\
# unoconverter will look for the Python binary, which has to be at version 3. # unoconverter will look for the Python binary, which has to be at version 3.
@@ -261,15 +258,15 @@ COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
# Copy dictionnaries so that hypens work on Chromium. # Copy dictionnaries so that hypens work on Chromium.
# See https://github.com/gotenberg/gotenberg/issues/1293. # See https://github.com/gotenberg/gotenberg/issues/1293.
COPY --chown=gotenberg:gotenberg build/chromium-hyphen-data /home/gotenberg/chromium-hyphen-data COPY --chown=gotenberg:gotenberg build/chromium-hyphen-data /opt/gotenberg/chromium-hyphen-data
# Copy the Golang binaries. # Copy the Golang binaries.
COPY --from=compress-go-binaries-stage /home/pdfcpu /usr/bin/ COPY --from=pdfcpu-binary-stage /home/pdfcpu /usr/bin/
COPY --from=compress-go-binaries-stage /home/gotenberg /usr/bin/ COPY --from=gotenberg-binary-stage /home/gotenberg /usr/bin/
# Environment variables required by modules or else. # Environment variables required by modules or else.
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium ENV CHROMIUM_BIN_PATH=/usr/bin/chromium
ENV CHROMIUM_HYPHEN_DATA_DIR_PATH=/home/gotenberg/chromium-hyphen-data ENV CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/chromium-hyphen-data
ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
ENV PDFTK_BIN_PATH=/usr/bin/pdftk ENV PDFTK_BIN_PATH=/usr/bin/pdftk

View File

@@ -11,12 +11,10 @@ COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt
# AWS. # AWS.
ENV AWS_LWA_PORT=3000 ENV AWS_LWA_PORT=3000
ENV AWS_LWA_READINESS_CHECK_PATH=/health ENV AWS_LWA_READINESS_CHECK_PATH=/health
ENV AWS_LWA_INVOKE_MODE=response_stream ENV AWS_LWA_INVOKE_MODE=buffered
# Gotenberg. # Gotenberg.
ENV API_PORT_FROM_ENV=AWS_LWA_PORT ENV API_PORT_FROM_ENV=AWS_LWA_PORT
ENV CHROMIUM_AUTO_START=true
ENV LIBREOFFICE_AUTO_START=true
ENV WEBHOOK_ENABLE_SYNC_MODE=true ENV WEBHOOK_ENABLE_SYNC_MODE=true
ENV GOTENBERG_BUILD_DEBUG_DATA=false ENV GOTENBERG_BUILD_DEBUG_DATA=false

88
go.mod
View File

@@ -1,33 +1,33 @@
module github.com/gotenberg/gotenberg/v8 module github.com/gotenberg/gotenberg/v8
go 1.25.0 go 1.25.5
require ( require (
github.com/alexliesenfeld/health v0.8.1 github.com/alexliesenfeld/health v0.8.1
github.com/barasher/go-exiftool v1.10.0 github.com/barasher/go-exiftool v1.10.0
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1 github.com/chromedp/chromedp v0.14.2
github.com/cucumber/godog v0.15.1 github.com/cucumber/godog v0.15.1
github.com/dlclark/regexp2 v1.11.5 github.com/dlclark/regexp2 v1.11.5
github.com/docker/docker v28.3.3+incompatible github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.6.0 github.com/docker/go-connections v0.6.0
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-retryablehttp v0.7.8
github.com/labstack/echo/v4 v4.13.4 github.com/labstack/echo/v4 v4.15.0
github.com/labstack/gommon v0.4.2 github.com/labstack/gommon v0.4.2
github.com/mholt/archives v0.1.3 github.com/mholt/archives v0.1.5
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/prometheus/client_golang v1.23.0 github.com/prometheus/client_golang v1.23.2
github.com/russross/blackfriday/v2 v2.1.0 github.com/shirou/gopsutil/v4 v4.25.12
github.com/shirou/gopsutil/v4 v4.25.7 github.com/spf13/pflag v1.0.10
github.com/spf13/pflag v1.0.7 github.com/testcontainers/testcontainers-go v0.40.0
github.com/testcontainers/testcontainers-go v0.38.0
go.uber.org/multierr v1.11.0 go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.1
golang.org/x/net v0.43.0 golang.org/x/net v0.49.0
golang.org/x/sync v0.16.0 golang.org/x/sync v0.19.0
golang.org/x/term v0.34.0 golang.org/x/term v0.39.0
golang.org/x/text v0.28.0 golang.org/x/text v0.33.0
) )
require ( require (
@@ -55,9 +55,9 @@ require (
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b // indirect github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
@@ -65,63 +65,61 @@ require (
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.5 // indirect github.com/hashicorp/go-memdb v1.3.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/magiconair/properties v1.8.10 // indirect github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nwaples/rardecode/v2 v2.1.1 // indirect github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sorairolake/lzip-go v0.3.7 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.14.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.11.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.41.0 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/sys v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect google.golang.org/protobuf v1.36.11 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

436
go.sum
View File

@@ -1,28 +1,9 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= 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/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 h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
@@ -45,19 +26,14 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU= github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.14.1 h1:0uAbnxewy/Q+Bg7oafVePE/6EXEho9hnaC38f+TTENg= github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
github.com/chromedp/chromedp v0.14.1/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= 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 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 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 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -85,8 +61,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/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 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -94,18 +70,14 @@ 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 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= 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/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.9.1/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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b h1:6Q4zRHXS/YLOl9Ng1b1OOOBWMidAQZR3Gel0UKPC/KU=
github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -124,38 +96,13 @@ github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
@@ -176,25 +123,18 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -204,22 +144,22 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 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/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 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/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU= github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 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/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
@@ -228,8 +168,8 @@ github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/minio/minlz v1.0.1/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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 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.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 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/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 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@@ -242,321 +182,143 @@ 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/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew= github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
github.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 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/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM= github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.7 h1:vP2uiD/NoklLyzYMdgOWkZME0ulkSfVTTE4MNRKCwNs= github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/sorairolake/lzip-go v0.3.7/go.mod h1:THOHr0FlNVCw2eOIEE9shFJAG1QxQg/pf2XUPAmNIqg= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.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.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 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=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

92
package-lock.json generated
View File

@@ -5,62 +5,32 @@
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"prettier": "3.6.2", "prettier": "3.8.1",
"prettier-plugin-gherkin": "^3.1.2", "prettier-plugin-gherkin": "^3.1.3",
"prettier-plugin-sh": "^0.18.0" "prettier-plugin-sh": "^0.18.0"
} }
}, },
"node_modules/@cucumber/gherkin": { "node_modules/@cucumber/gherkin": {
"version": "27.0.0", "version": "32.2.0",
"resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-27.0.0.tgz", "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-32.2.0.tgz",
"integrity": "sha512-j5rCsjqzRiC3iVTier3sa0kzyNbkcAmF7xr7jKnyO7qDeK3Z8Ye1P3KSVpeQRMY+KCDJ3WbTDdyxH0FwfA/fIw==", "integrity": "sha512-X8xuVhSIqlUjxSRifRJ7t0TycVWyX58fygJH3wDNmHINLg9sYEkvQT0SO2G5YlRZnYc11TIFr4YPenscvdlBIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@cucumber/messages": ">=19.1.4 <=22" "@cucumber/messages": ">=19.1.4 <28"
}
},
"node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-22.0.0.tgz",
"integrity": "sha512-EuaUtYte9ilkxcKmfqGF9pJsHRUU0jwie5ukuZ/1NPTuHS1LxHPsGEODK17RPRbZHOFhqybNzG2rHAwThxEymg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/uuid": "9.0.1",
"class-transformer": "0.5.1",
"reflect-metadata": "0.1.13",
"uuid": "9.0.0"
}
},
"node_modules/@cucumber/gherkin/node_modules/@types/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
"dev": true,
"license": "MIT"
},
"node_modules/@cucumber/gherkin/node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
} }
}, },
"node_modules/@cucumber/messages": { "node_modules/@cucumber/messages": {
"version": "23.0.0", "version": "27.2.0",
"resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-23.0.0.tgz", "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.2.0.tgz",
"integrity": "sha512-2ZWKNnikNMBnle3P3cgy+fgzhABrY4oBbiWp9wcI8ANdT4LlYRN6jQ6j/9CQGTDGRfkyRtr/5VkYq7q24XCsuA==", "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/uuid": "9.0.6", "@types/uuid": "10.0.0",
"class-transformer": "0.5.1", "class-transformer": "0.5.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.2.2",
"uuid": "9.0.1" "uuid": "11.0.5"
} }
}, },
"node_modules/@reteps/dockerfmt": { "node_modules/@reteps/dockerfmt": {
@@ -74,9 +44,9 @@
} }
}, },
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "9.0.6", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew==", "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -88,9 +58,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.6.2", "version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -104,15 +74,15 @@
} }
}, },
"node_modules/prettier-plugin-gherkin": { "node_modules/prettier-plugin-gherkin": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/prettier-plugin-gherkin/-/prettier-plugin-gherkin-3.1.2.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-gherkin/-/prettier-plugin-gherkin-3.1.3.tgz",
"integrity": "sha512-LgehRNKCAqZsoE/yblmqXYfMpEK2mhVWQKNj/TFzeLR+0rhFGHqq3RlYcYhPgzbr0n6w2Bv+0Gm1yxNdzkdacw==", "integrity": "sha512-w9uB413NlSi8ZQwpexyu+ttriJJ88eZLV0x88ZTkzkLZyHYEX5wrNtaCx/yFYviIu/tuwsBqDPM47VODnIV/hw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@cucumber/gherkin": "^27.0.0", "@cucumber/gherkin": "^32.0.0",
"@cucumber/messages": "^23.0.0", "@cucumber/messages": "^27.2.0",
"prettier": "^3.0.0" "prettier": "^3.5.3"
} }
}, },
"node_modules/prettier-plugin-sh": { "node_modules/prettier-plugin-sh": {
@@ -136,9 +106,9 @@
} }
}, },
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.1.13", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
@@ -166,9 +136,9 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "9.0.1", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"dev": true, "dev": true,
"funding": [ "funding": [
"https://github.com/sponsors/broofa", "https://github.com/sponsors/broofa",
@@ -176,7 +146,7 @@
], ],
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"devDependencies": { "devDependencies": {
"prettier": "3.6.2", "prettier": "3.8.1",
"prettier-plugin-gherkin": "^3.1.2", "prettier-plugin-gherkin": "^3.1.3",
"prettier-plugin-sh": "^0.18.0" "prettier-plugin-sh": "^0.18.0"
} }
} }

View File

@@ -52,6 +52,8 @@ type PdfEngineMock struct {
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
EncryptMock func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error
EmbedFilesMock func(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error
} }
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
@@ -78,6 +80,14 @@ func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logg
return engine.WriteMetadataMock(ctx, logger, metadata, inputPath) return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
} }
func (engine *PdfEngineMock) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return engine.EncryptMock(ctx, logger, inputPath, userPassword, ownerPassword)
}
func (engine *PdfEngineMock) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
return engine.EmbedFilesMock(ctx, logger, filePaths, inputPath)
}
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface. // PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
type PdfEngineProviderMock struct { type PdfEngineProviderMock struct {
PdfEngineMock func() (PdfEngine, error) PdfEngineMock func() (PdfEngine, error)

View File

@@ -3,6 +3,7 @@ package gotenberg
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -23,8 +24,31 @@ var (
// ErrPdfEngineMetadataValueNotSupported is returned when a metadata value // ErrPdfEngineMetadataValueNotSupported is returned when a metadata value
// is not supported. // is not supported.
ErrPdfEngineMetadataValueNotSupported = errors.New("metadata value not supported") ErrPdfEngineMetadataValueNotSupported = errors.New("metadata value not supported")
// ErrPdfEncryptionNotSupported is returned when encryption
// is not supported by the PDF engine.
ErrPdfEncryptionNotSupported = errors.New("encryption not supported")
) )
// PdfEngineInvalidArgsError represents an error returned by a PDF engine when
// invalid arguments are provided. It includes the name of the engine and a
// detailed message describing the issue.
type PdfEngineInvalidArgsError struct {
engine string
msg string
}
// Error implements the error interface.
func (e *PdfEngineInvalidArgsError) Error() string {
return fmt.Sprintf("%s: %s", e.engine, e.msg)
}
// NewPdfEngineInvalidArgs creates a new PdfEngineInvalidArgsError with the
// given engine name and message.
func NewPdfEngineInvalidArgs(engine, msg string) error {
return &PdfEngineInvalidArgsError{engine, msg}
}
const ( const (
// SplitModeIntervals represents a mode where a PDF is split at specific // SplitModeIntervals represents a mode where a PDF is split at specific
// intervals. // intervals.
@@ -100,7 +124,7 @@ type PdfEngine interface {
// Flatten merges existing annotation appearances with page content, // Flatten merges existing annotation appearances with page content,
// effectively deleting the original annotations. This process can flatten // effectively deleting the original annotations. This process can flatten
// forms as well, as forms share a relationship with annotations. Note that // forms as well as forms share a relationship with annotations. Note that
// this operation is irreversible. // this operation is irreversible.
Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error
@@ -113,6 +137,16 @@ type PdfEngine interface {
// WriteMetadata writes the metadata into a given PDF file. // WriteMetadata writes the metadata into a given PDF file.
WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
// Encrypt adds password protection to a PDF file.
// The userPassword is required to open the document.
// The ownerPassword provides full access to the document.
// If the ownerPassword is empty, it defaults to the userPassword.
Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error
// EmbedFiles embeds files into a PDF. All files are embedded as file attachments
// without modifying the main PDF content.
EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error
} }
// PdfEngineProvider offers an interface to instantiate a [PdfEngine]. // PdfEngineProvider offers an interface to instantiate a [PdfEngine].

View File

@@ -38,11 +38,12 @@ var (
// Context is the request context for a "multipart/form-data" requests. // Context is the request context for a "multipart/form-data" requests.
type Context struct { type Context struct {
dirPath string dirPath string
values map[string][]string values map[string][]string
files map[string]string files map[string]string
outputPaths []string filesByField map[string][]string
cancelled bool outputPaths []string
cancelled bool
logger *zap.Logger logger *zap.Logger
echoCtx echo.Context echoCtx echo.Context
@@ -79,11 +80,14 @@ type downloadFrom struct {
// ExtraHttpHeaders are the HTTP headers to send alongside. // ExtraHttpHeaders are the HTTP headers to send alongside.
ExtraHttpHeaders map[string]string `json:"extraHttpHeaders"` ExtraHttpHeaders map[string]string `json:"extraHttpHeaders"`
// Download as embed file
Embedded bool `json:"embedded"`
} }
// newContext returns a [Context] by parsing a "multipart/form-data" request. // newContext returns a [Context] by parsing a "multipart/form-data" request.
func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSystem, timeout time.Duration, bodyLimit int64, downloadFromCfg downloadFromConfig, traceHeader, trace string) (*Context, context.CancelFunc, error) { func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSystem, timeout time.Duration, bodyLimit int64, downloadFromCfg downloadFromConfig, traceHeader, trace string) (*Context, context.CancelFunc, error) {
processCtx, processCancel := context.WithTimeout(context.Background(), timeout) processCtx, processCancel := context.WithTimeout(echoCtx.Request().Context(), timeout)
// We want to make sure the multipart/form-data does not exceed a given // We want to make sure the multipart/form-data does not exceed a given
// limit. We consider: form fields (keys, values, files) and files // limit. We consider: form fields (keys, values, files) and files
@@ -184,6 +188,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
ctx.dirPath = dirPath ctx.dirPath = dirPath
ctx.values = form.Value ctx.values = form.Value
ctx.files = make(map[string]string) ctx.files = make(map[string]string)
ctx.filesByField = make(map[string][]string)
// First, try to download files listed in the "downloadFrom" form field, if // First, try to download files listed in the "downloadFrom" form field, if
// any. // any.
@@ -318,6 +323,9 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
} }
ctx.files[filename] = path ctx.files[filename] = path
if dl.Embedded {
ctx.filesByField[EmbedsFormField] = append(ctx.filesByField[EmbedsFormField], path)
}
return nil return nil
}) })
@@ -373,17 +381,22 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
} }
// Then, copy the form files, if any. // Then, copy the form files, if any.
for _, files := range form.File { for fieldName, files := range form.File {
for _, fh := range files { for _, fh := range files {
err = copyToDisk(fh) err = copyToDisk(fh)
if err != nil { if err != nil {
return ctx, cancel, fmt.Errorf("copy to disk: %w", err) return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
} }
// Track files by field name
filename := norm.NFC.String(filepath.Base(fh.Filename))
filePath := ctx.files[filename]
ctx.filesByField[fieldName] = append(ctx.filesByField[fieldName], filePath)
} }
} }
ctx.Log().Debug(fmt.Sprintf("form fields: %+v", ctx.values)) ctx.Log().Debug(fmt.Sprintf("form fields: %+v", ctx.values))
ctx.Log().Debug(fmt.Sprintf("form files: %+v", ctx.files)) ctx.Log().Debug(fmt.Sprintf("form files: %+v", ctx.files))
ctx.Log().Debug(fmt.Sprintf("form files by field: %+v", ctx.filesByField))
ctx.Log().Debug(fmt.Sprintf("total bytes: %d", totalBytesRead.Load())) ctx.Log().Debug(fmt.Sprintf("total bytes: %d", totalBytesRead.Load()))
return ctx, cancel, err return ctx, cancel, err
@@ -397,9 +410,10 @@ func (ctx *Context) Request() *http.Request {
// FormData return a [FormData]. // FormData return a [FormData].
func (ctx *Context) FormData() *FormData { func (ctx *Context) FormData() *FormData {
return &FormData{ return &FormData{
values: ctx.values, values: ctx.values,
files: ctx.files, files: ctx.files,
errors: nil, filesByField: ctx.filesByField,
errors: nil,
} }
} }

View File

@@ -0,0 +1,71 @@
package api
import (
"bytes"
"context"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
// Propagate HTTP request context cancellation to processing modules to save resources
// https://github.com/gotenberg/gotenberg/issues/1455
func TestNewContext_Cancellation(t *testing.T) {
e := echo.New()
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
err := writer.Close()
if err != nil {
t.Fatalf("failed to close multipart writer: %v", err)
}
// Create a request with a cancellable context.
reqCtx, cancelReq := context.WithCancel(context.Background())
req := httptest.NewRequest(http.MethodPost, "/", body).WithContext(reqCtx)
req.Header.Set("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
logger := zap.NewNop()
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
timeout := time.Duration(10) * time.Second
downloadFromCfg := downloadFromConfig{
disable: true,
}
ctx, cancel, err := newContext(c, logger, fs, timeout, 0, downloadFromCfg, "trace", "trace")
if err != nil {
t.Fatalf("expected no error from newContext, got: %v", err)
}
defer cancel()
// Verify initial state: context SHOULD NOT be done yet.
select {
case <-ctx.Done():
t.Fatal("context should not be done immediately")
default:
}
// Simulate Client Disconnect
cancelReq()
// Verify Propagation
select {
case <-ctx.Done():
// Success! The context was cancelled.
if ctx.Err() != context.Canceled {
t.Errorf("expected context error to be 'context.Canceled', got: %v", ctx.Err())
}
case <-time.After(100 * time.Millisecond):
t.Fatal("expected context to be cancelled after request context cancellation, but it timed out")
}
}

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -16,14 +17,20 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
// EmbedsFormField represents the form field name for embedding files.
const (
EmbedsFormField string = "embeds"
)
// FormData is a helper for validating and hydrating values from a // FormData is a helper for validating and hydrating values from a
// "multipart/form-data" request. // "multipart/form-data" request.
// //
// form := ctx.FormData() // form := ctx.FormData()
type FormData struct { type FormData struct {
values map[string][]string values map[string][]string
files map[string]string files map[string]string
errors error filesByField map[string][]string
errors error
} }
// Validate returns nil or an error related to the [FormData] values, with a // Validate returns nil or an error related to the [FormData] values, with a
@@ -358,6 +365,26 @@ func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
return form.paths(extensions, target) return form.paths(extensions, target)
} }
// Embeds binds the absolute paths of form data files that should be
// embedded in the PDF. Only files uploaded with the "embeds" field name
// will be included.
//
// var embeds []string
//
// ctx.FormData().Embeds(&embeds)
func (form *FormData) Embeds(target *[]string) *FormData {
if form.errors != nil {
return form
}
// Get files from the "embeds" field
if paths, ok := form.filesByField[EmbedsFormField]; ok {
*target = append(*target, paths...)
}
return form
}
// MandatoryPaths binds the absolute paths of form data files, according to a // MandatoryPaths binds the absolute paths of form data files, according to a
// list of file extensions, to a string slice variable. It populates an error // list of file extensions, to a string slice variable. It populates an error
// if there is no file for given file extensions. // if there is no file for given file extensions.
@@ -381,8 +408,15 @@ func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *For
// paths bind 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. // file extensions, to a string slice variable.
// embeds are excluded.
func (form *FormData) paths(extensions []string, target *[]string) *FormData { func (form *FormData) paths(extensions []string, target *[]string) *FormData {
embeds, ok := form.filesByField[EmbedsFormField]
for filename, path := range form.files { for filename, path := range form.files {
if ok && slices.Contains(embeds, path) {
continue
}
for _, ext := range extensions { for _, ext := range extensions {
// See https://github.com/gotenberg/gotenberg/issues/228. // See https://github.com/gotenberg/gotenberg/issues/228.
if strings.ToLower(filepath.Ext(filename)) == ext { if strings.ToLower(filepath.Ext(filename)) == ext {

View File

@@ -1612,6 +1612,24 @@ func TestFormData_Paths(t *testing.T) {
}, },
expectCount: 2, expectCount: 2,
}, },
{
scenario: "files except embeds",
form: &FormData{
files: map[string]string{
"foo.pdf": "/foo.pdf",
"embed_1.pdf": "/embed_1.pdf",
"embed_2.xml": "/embed_2.xml",
},
filesByField: map[string][]string{
"embeds": {"/embed_1.pdf", "/embed_2.xml"},
},
},
extensions: []string{".pdf"},
expect: []string{
"/foo.pdf",
},
expectCount: 1,
},
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
var actual []string var actual []string
@@ -1740,3 +1758,28 @@ func TestFormData_mustAssign(t *testing.T) {
var target []string var target []string
form.mustAssign("foo", "foo", &target) form.mustAssign("foo", "foo", &target)
} }
func TestFormData_Embeds(t *testing.T) {
expected := []string{"/bar.xml", "/baz.xml"}
var actual []string
form := &FormData{
files: map[string]string{
"foo.pdf": "/foo.pdf",
"bar.xml": "/bar.xml",
"baz.xml": "/baz.xml",
},
filesByField: map[string][]string{
"embeds": {"/bar.xml", "/baz.xml"},
},
}
form.Embeds(&actual)
if len(actual) != len(expected) {
t.Errorf("expected %d embeds but got %d", len(expected), len(actual))
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("expected %v but got %v", expected, actual)
}
}

View File

@@ -61,6 +61,11 @@ func ParseError(err error) (int, string) {
return http.StatusBadRequest, "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues" return http.StatusBadRequest, "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues"
} }
var invalidArgsError *gotenberg.PdfEngineInvalidArgsError
if errors.As(err, &invalidArgsError) {
return http.StatusBadRequest, invalidArgsError.Error()
}
var httpErr HttpError var httpErr HttpError
if errors.As(err, &httpErr) { if errors.As(err, &httpErr) {
return httpErr.HttpError() return httpErr.HttpError()

View File

@@ -10,6 +10,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
cdprotobrowser "github.com/chromedp/cdproto/browser"
"github.com/chromedp/cdproto/fetch" "github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/page"
@@ -31,7 +32,6 @@ type browser interface {
type browserArguments struct { type browserArguments struct {
// Executor args. // Executor args.
binPath string binPath string
incognito bool
allowInsecureLocalhost bool allowInsecureLocalhost bool
ignoreCertificateErrors bool ignoreCertificateErrors bool
disableWebSecurity bool disableWebSecurity bool
@@ -108,10 +108,6 @@ func (b *chromiumBrowser) Start(logger *zap.Logger) error {
chromedp.Flag("disable-component-update", false), chromedp.Flag("disable-component-update", false),
) )
if b.arguments.incognito {
opts = append(opts, chromedp.Flag("incognito", b.arguments.incognito))
}
if b.arguments.allowInsecureLocalhost { if b.arguments.allowInsecureLocalhost {
// See https://github.com/gotenberg/gotenberg/issues/488. // See https://github.com/gotenberg/gotenberg/issues/488.
opts = append(opts, chromedp.Flag("allow-insecure-localhost", true)) opts = append(opts, chromedp.Flag("allow-insecure-localhost", true))
@@ -255,13 +251,20 @@ func (b *chromiumBrowser) Healthy(logger *zap.Logger) bool {
b.ctxMu.RLock() b.ctxMu.RLock()
defer b.ctxMu.RUnlock() defer b.ctxMu.RUnlock()
timeoutCtx, timeoutCancel := context.WithTimeout(b.ctx, time.Duration(10)*time.Second) // Create a timeout based on the existing browser context (b.ctx).
defer timeoutCancel() // IMPORTANT: We do NOT call chromedp.NewContext here.
// We want to execute this against the main browser connection,
// avoiding the creation of a new target (tab).
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
defer cancel()
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx) // Check if the browser is responsive by asking for its version.
defer taskCancel() // This involves a simple JSON payload roundtrip over the websocket.
// See https://github.com/gotenberg/gotenberg/issues/1169.
err := chromedp.Run(taskCtx, chromedp.Navigate("about:blank")) err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
_, _, _, _, _, err := cdprotobrowser.GetVersion().Do(ctx)
return err
}))
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("browser health check failed: %s", err)) logger.Error(fmt.Sprintf("browser health check failed: %s", err))
return false return false
@@ -287,6 +290,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
forceExactColorsActionFunc(logger, options.PrintBackground), forceExactColorsActionFunc(logger, options.PrintBackground),
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType), emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression), waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay), waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
// PDF specific. // PDF specific.
printToPdfActionFunc(logger, outputPath, options), printToPdfActionFunc(logger, outputPath, options),
@@ -312,6 +316,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
forceExactColorsActionFunc(logger, true), forceExactColorsActionFunc(logger, true),
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType), emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression), waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay), waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
// Screenshot specific. // Screenshot specific.
setDeviceMetricsOverride(logger, options.Width, options.Height), setDeviceMetricsOverride(logger, options.Width, options.Height),
@@ -373,6 +378,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
invalidHttpStatusCode: &invalidHttpStatusCode, invalidHttpStatusCode: &invalidHttpStatusCode,
invalidHttpStatusCodeMu: &invalidHttpStatusCodeMu, invalidHttpStatusCodeMu: &invalidHttpStatusCodeMu,
failOnResourceOnHttpStatusCode: options.FailOnResourceHttpStatusCodes, failOnResourceOnHttpStatusCode: options.FailOnResourceHttpStatusCodes,
ignoreResourceHttpStatusDomains: options.IgnoreResourceHttpStatusDomains,
invalidResourceHttpStatusCode: &invalidResourceHttpStatusCode, invalidResourceHttpStatusCode: &invalidResourceHttpStatusCode,
invalidResourceHttpStatusCodeMu: &invalidResourceHttpStatusCodeMu, invalidResourceHttpStatusCodeMu: &invalidResourceHttpStatusCodeMu,
}) })
@@ -410,6 +416,10 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
if err != nil { if err != nil {
errMessage := err.Error() errMessage := err.Error()
if strings.Contains(errMessage, "Printing failed (-32000)") {
return ErrPrintingFailed
}
if strings.Contains(errMessage, "Show invalid printer settings error (-32000)") || strings.Contains(errMessage, "content area is empty (-32602)") { if strings.Contains(errMessage, "Show invalid printer settings error (-32000)") || strings.Contains(errMessage, "content area is empty (-32602)") {
return ErrInvalidPrinterSettings return ErrInvalidPrinterSettings
} }
@@ -418,6 +428,10 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
return ErrPageRangesSyntaxError return ErrPageRangesSyntaxError
} }
if strings.Contains(errMessage, "Page range exceeds page count (-32000)") {
return ErrPageRangesExceedsPageCount
}
if strings.Contains(errMessage, "rpcc: message too large") { if strings.Contains(errMessage, "rpcc: message too large") {
return ErrRpccMessageTooLarge return ErrRpccMessageTooLarge
} }

View File

@@ -33,6 +33,10 @@ var (
// returns an exception or undefined. // returns an exception or undefined.
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression") ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
// ErrInvalidSelectorQuery happens if a selector query returns an exception
// or undefined.
ErrInvalidSelectorQuery = errors.New("invalid selector query")
// ErrRpccMessageTooLarge happens when the messages received by // ErrRpccMessageTooLarge happens when the messages received by
// ChromeDevTools are larger than 100 MB. // ChromeDevTools are larger than 100 MB.
ErrRpccMessageTooLarge = errors.New("rpcc message too large") ErrRpccMessageTooLarge = errors.New("rpcc message too large")
@@ -63,13 +67,20 @@ var (
// PdfOptions.OmitBackground is set to true but not PdfOptions.PrintBackground. // PdfOptions.OmitBackground is set to true but not PdfOptions.PrintBackground.
ErrOmitBackgroundWithoutPrintBackground = errors.New("omit background without print background") ErrOmitBackgroundWithoutPrintBackground = errors.New("omit background without print background")
// ErrPrintingFailed happens if the printing failed for an unknown reason.
ErrPrintingFailed = errors.New("printing failed")
// ErrInvalidPrinterSettings happens if the PdfOptions have one or more // ErrInvalidPrinterSettings happens if the PdfOptions have one or more
// aberrant values. // aberrant values.
ErrInvalidPrinterSettings = errors.New("invalid printer settings") ErrInvalidPrinterSettings = errors.New("invalid printer settings")
// ErrPageRangesSyntaxError happens if the PdfOptions have an invalid page // ErrPageRangesSyntaxError happens if the PdfOptions page
// range. // range syntax is invalid.
ErrPageRangesSyntaxError = errors.New("page ranges syntax error") ErrPageRangesSyntaxError = errors.New("page ranges syntax error")
// ErrPageRangesExceedsPageCount happens if the PdfOptions have an invalid
// page range.
ErrPageRangesExceedsPageCount = errors.New("page ranges exceeds page count")
) )
// Chromium is a module that provides both an [Api] and routes for converting // Chromium is a module that provides both an [Api] and routes for converting
@@ -101,6 +112,20 @@ type Options struct {
// status code from at least one resource matches with one if its entries. // status code from at least one resource matches with one if its entries.
FailOnResourceHttpStatusCodes []int64 FailOnResourceHttpStatusCodes []int64
// IgnoreResourceHttpStatusDomains excludes resources whose hostname matches
// one of these domains from the application of
// [Options.FailOnResourceHttpStatusCodes].
//
// A match happens if the hostname equals the domain or is a subdomain of it
// (e.g., "browser.sentry-cdn.com" matches "sentry-cdn.com").
//
// Values are normalized (trimmed, lowercased) and may be provided as:
// - "example.com"
// - "*.example.com" or ".example.com"
// - "example.com:443" (port is ignored)
// - "https://example.com/path" (scheme/path are ignored)
IgnoreResourceHttpStatusDomains []string
// FailOnResourceLoadingFailed sets if the conversion should fail like the // FailOnResourceLoadingFailed sets if the conversion should fail like the
// main page if Chromium fails to load at least one resource. // main page if Chromium fails to load at least one resource.
FailOnResourceLoadingFailed bool FailOnResourceLoadingFailed bool
@@ -121,6 +146,10 @@ type Options struct {
// converting an HTML document until it returns true // converting an HTML document until it returns true
WaitForExpression string WaitForExpression string
// WaitForSelector is the element query to wait until visible before
// converting an HTML document.
WaitForSelector string
// Cookies are the cookies to put in the Chromium cookies' jar. // Cookies are the cookies to put in the Chromium cookies' jar.
Cookies []Cookie Cookies []Cookie
@@ -143,19 +172,21 @@ type Options struct {
// DefaultOptions returns the default values for Options. // DefaultOptions returns the default values for Options.
func DefaultOptions() Options { func DefaultOptions() Options {
return Options{ return Options{
SkipNetworkIdleEvent: true, SkipNetworkIdleEvent: true,
FailOnHttpStatusCodes: []int64{499, 599}, FailOnHttpStatusCodes: []int64{499, 599},
FailOnResourceHttpStatusCodes: nil, FailOnResourceHttpStatusCodes: nil,
FailOnResourceLoadingFailed: false, IgnoreResourceHttpStatusDomains: nil,
FailOnConsoleExceptions: false, FailOnResourceLoadingFailed: false,
WaitDelay: 0, FailOnConsoleExceptions: false,
WaitWindowStatus: "", WaitDelay: 0,
WaitForExpression: "", WaitWindowStatus: "",
Cookies: nil, WaitForExpression: "",
UserAgent: "", WaitForSelector: "",
ExtraHttpHeaders: nil, Cookies: nil,
EmulatedMediaType: "", UserAgent: "",
OmitBackground: false, ExtraHttpHeaders: nil,
EmulatedMediaType: "",
OmitBackground: false,
} }
} }
@@ -364,7 +395,6 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for Chromium. Set to 0 to disable this feature") fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for Chromium. Set to 0 to disable this feature")
fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion") fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion")
fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart") fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart")
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
fs.Bool("chromium-allow-insecure-localhost", false, "Ignore TLS/SSL errors on localhost") fs.Bool("chromium-allow-insecure-localhost", false, "Ignore TLS/SSL errors on localhost")
fs.Bool("chromium-ignore-certificate-errors", false, "Ignore the certificate errors") fs.Bool("chromium-ignore-certificate-errors", false, "Ignore the certificate errors")
fs.Bool("chromium-disable-web-security", false, "Don't enforce the same-origin policy") fs.Bool("chromium-disable-web-security", false, "Don't enforce the same-origin policy")
@@ -378,6 +408,13 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript") fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
fs.Bool("chromium-disable-routes", false, "Disable the routes") fs.Bool("chromium-disable-routes", false, "Disable the routes")
// Deprecated flags.
fs.Bool("chromium-incognito", false, "Start Chromium with incognito mode")
err := fs.MarkDeprecated("chromium-incognito", "this flag is ignored as it provides no benefits")
if err != nil {
panic(err)
}
return fs return fs
}(), }(),
New: func() gotenberg.Module { return new(Chromium) }, New: func() gotenberg.Module { return new(Chromium) },
@@ -402,7 +439,6 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
mod.args = browserArguments{ mod.args = browserArguments{
binPath: binPath, binPath: binPath,
incognito: flags.MustBool("chromium-incognito"),
allowInsecureLocalhost: flags.MustBool("chromium-allow-insecure-localhost"), allowInsecureLocalhost: flags.MustBool("chromium-allow-insecure-localhost"),
ignoreCertificateErrors: flags.MustBool("chromium-ignore-certificate-errors"), ignoreCertificateErrors: flags.MustBool("chromium-ignore-certificate-errors"),
disableWebSecurity: flags.MustBool("chromium-disable-web-security"), disableWebSecurity: flags.MustBool("chromium-disable-web-security"),

View File

@@ -4,7 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"slices" "slices"
"strings"
"sync" "sync"
"github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/cdp"
@@ -143,6 +145,7 @@ type eventResponseReceivedOptions struct {
invalidHttpStatusCode *error invalidHttpStatusCode *error
invalidHttpStatusCodeMu *sync.RWMutex invalidHttpStatusCodeMu *sync.RWMutex
failOnResourceOnHttpStatusCode []int64 failOnResourceOnHttpStatusCode []int64
ignoreResourceHttpStatusDomains []string
invalidResourceHttpStatusCode *error invalidResourceHttpStatusCode *error
invalidResourceHttpStatusCodeMu *sync.RWMutex invalidResourceHttpStatusCodeMu *sync.RWMutex
} }
@@ -157,6 +160,8 @@ func listenForEventResponseReceived(
logger *zap.Logger, logger *zap.Logger,
options eventResponseReceivedOptions, options eventResponseReceivedOptions,
) { ) {
normalizedIgnoreDomains := normalizeDomains(options.ignoreResourceHttpStatusDomains)
for _, code := range []int64{199, 299, 399, 499, 599} { for _, code := range []int64{199, 299, 399, 499, 599} {
if slices.Contains(options.failOnHttpStatusCodes, code) { if slices.Contains(options.failOnHttpStatusCodes, code) {
for i := code - 99; i <= code; i++ { for i := code - 99; i <= code; i++ {
@@ -190,6 +195,11 @@ func listenForEventResponseReceived(
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for a resource: %+v", ev.Response)) logger.Debug(fmt.Sprintf("event EventResponseReceived fired for a resource: %+v", ev.Response))
if slices.Contains(options.failOnResourceOnHttpStatusCode, ev.Response.Status) { if slices.Contains(options.failOnResourceOnHttpStatusCode, ev.Response.Status) {
if !shouldCheckResourceHttpStatusCode(ev.Response.URL, normalizedIgnoreDomains) {
logger.Debug(fmt.Sprintf("skip resource HTTP status code check for '%s' due to domain filtering", ev.Response.URL))
return
}
options.invalidResourceHttpStatusCodeMu.Lock() options.invalidResourceHttpStatusCodeMu.Lock()
defer options.invalidResourceHttpStatusCodeMu.Unlock() defer options.invalidResourceHttpStatusCodeMu.Unlock()
@@ -202,6 +212,79 @@ func listenForEventResponseReceived(
}) })
} }
func shouldCheckResourceHttpStatusCode(rawURL string, ignoreDomains []string) bool {
host := hostnameFromURL(rawURL)
if len(ignoreDomains) > 0 && matchesAnyDomain(host, ignoreDomains) {
return false
}
return true
}
func hostnameFromURL(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil {
return ""
}
return strings.ToLower(u.Hostname())
}
func normalizeDomains(domains []string) []string {
normalized := make([]string, 0, len(domains))
for _, domain := range domains {
d := normalizeDomain(domain)
if d == "" {
continue
}
normalized = append(normalized, d)
}
return normalized
}
func normalizeDomain(domain string) string {
d := strings.ToLower(strings.TrimSpace(domain))
if d == "" {
return ""
}
// Accept "example.com", "*.example.com", ".example.com", "https://example.com/path",
// or "example.com:443".
if strings.Contains(d, "://") || strings.HasPrefix(d, "//") {
u, err := url.Parse(d)
if err == nil && u.Hostname() != "" {
d = strings.ToLower(u.Hostname())
}
} else {
// Make it parseable as a URL to extract the hostname and drop any port/path.
u, err := url.Parse("https://" + d)
if err == nil && u.Hostname() != "" {
d = strings.ToLower(u.Hostname())
}
}
d = strings.TrimPrefix(d, "*.")
d = strings.TrimPrefix(d, ".")
return d
}
func matchesAnyDomain(host string, domains []string) bool {
if host == "" || len(domains) == 0 {
return false
}
for _, domain := range domains {
if host == domain || strings.HasSuffix(host, "."+domain) {
return true
}
}
return false
}
type eventLoadingFailedOptions struct { type eventLoadingFailedOptions struct {
loadingFailed *error loadingFailed *error
loadingFailedMu *sync.RWMutex loadingFailedMu *sync.RWMutex
@@ -235,6 +318,7 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, option
"net::ERR_BLOCKED_BY_CLIENT", "net::ERR_BLOCKED_BY_CLIENT",
"net::ERR_BLOCKED_BY_RESPONSE", "net::ERR_BLOCKED_BY_RESPONSE",
"net::ERR_FILE_NOT_FOUND", "net::ERR_FILE_NOT_FOUND",
"net::ERR_HTTP2_PROTOCOL_ERROR",
} }
if !slices.Contains(errors, ev.ErrorText) { if !slices.Contains(errors, ev.ErrorText) {
logger.Debug(fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors)) logger.Debug(fmt.Sprintf("skip EventLoadingFailed: '%s' is not part of %+v", ev.ErrorText, errors))

View File

@@ -0,0 +1,63 @@
package chromium
import "testing"
func TestNormalizeDomain(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "empty", in: "", want: ""},
{name: "spaces", in: " ", want: ""},
{name: "simple", in: "example.com", want: "example.com"},
{name: "mixed case", in: "ExAmPlE.Com", want: "example.com"},
{name: "leading wildcard", in: "*.example.com", want: "example.com"},
{name: "leading dot", in: ".example.com", want: "example.com"},
{name: "with scheme and path", in: "https://example.com/foo/bar", want: "example.com"},
{name: "with port", in: "example.com:443", want: "example.com"},
{name: "with scheme and port", in: "https://example.com:443/foo", want: "example.com"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeDomain(tt.in); got != tt.want {
t.Fatalf("normalizeDomain(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
func TestMatchesAnyDomain(t *testing.T) {
host := "browser.sentry-cdn.com"
if !matchesAnyDomain(host, []string{"sentry-cdn.com"}) {
t.Fatalf("expected %q to match %q", host, "sentry-cdn.com")
}
if matchesAnyDomain("not-sentry-cdn.com", []string{"sentry-cdn.com"}) {
t.Fatalf("expected %q to not match %q", "not-sentry-cdn.com", "sentry-cdn.com")
}
}
func TestShouldCheckResourceHttpStatusCode_IgnoreDomains(t *testing.T) {
ignore := normalizeDomains([]string{"sentry.io"})
if shouldCheckResourceHttpStatusCode("https://sentry.io/api/123", ignore) {
t.Fatalf("expected ignored domain to be skipped")
}
if shouldCheckResourceHttpStatusCode("https://sub.sentry.io/api/123", ignore) {
t.Fatalf("expected ignored subdomain to be skipped")
}
if !shouldCheckResourceHttpStatusCode("https://other.com/api/123", ignore) {
t.Fatalf("expected non-ignored domain to be checked")
}
}
func TestShouldCheckResourceHttpStatusCode_NonHTTPURL(t *testing.T) {
if !shouldCheckResourceHttpStatusCode("data:text/plain,hello", nil) {
t.Fatalf("expected data: URL to be checked (no host filtering possible)")
}
}

View File

@@ -14,9 +14,9 @@ import (
"time" "time"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"github.com/gomarkdown/markdown"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
@@ -24,25 +24,43 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines" "github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
) )
// FormDataChromiumOptions creates [Options] from the form data. Fallback to var sameSiteRegexp = regexp2.MustCompile(
// the default value if the considered key is not present. `("sameSite"\s*:\s*")(?i:(lax|strict|none))(")`,
regexp2.None,
)
// FormDataChromiumOptions creates [Options] from the form data.
//
// It falls back to the default value if the considered key is not present.
//
// JSON-encoded fields:
// - failOnHttpStatusCodes: []int
// - failOnResourceHttpStatusCodes: []int
// - ignoreResourceHttpStatusDomains: []string
// - cookies: []Cookie
// - extraHttpHeaders: map[string]string
//
// Domain filtering only applies to resource checks triggered by
// "failOnResourceHttpStatusCodes".
func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) { func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
defaultOptions := DefaultOptions() defaultOptions := DefaultOptions()
var ( var (
skipNetworkIdleEvent bool skipNetworkIdleEvent bool
failOnHttpStatusCodes []int64 failOnHttpStatusCodes []int64
failOnResourceHttpStatusCodes []int64 failOnResourceHttpStatusCodes []int64
failOnResourceLoadingFailed bool ignoreResourceHttpStatusDomains []string
failOnConsoleExceptions bool failOnResourceLoadingFailed bool
waitDelay time.Duration failOnConsoleExceptions bool
waitWindowStatus string waitDelay time.Duration
waitForExpression string waitWindowStatus string
cookies []Cookie waitForExpression string
userAgent string waitForSelector string
extraHttpHeaders []ExtraHttpHeader cookies []Cookie
emulatedMediaType string userAgent string
omitBackground bool extraHttpHeaders []ExtraHttpHeader
emulatedMediaType string
omitBackground bool
) )
form := ctx.FormData(). form := ctx.FormData().
@@ -73,18 +91,55 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
return nil return nil
}). }).
Custom("ignoreResourceHttpStatusDomains", func(value string) error {
if value == "" {
ignoreResourceHttpStatusDomains = defaultOptions.IgnoreResourceHttpStatusDomains
return nil
}
err := json.Unmarshal([]byte(value), &ignoreResourceHttpStatusDomains)
if err != nil {
return fmt.Errorf("unmarshal ignoreResourceHttpStatusDomains: %w", err)
}
return nil
}).
Bool("failOnResourceLoadingFailed", &failOnResourceLoadingFailed, defaultOptions.FailOnResourceLoadingFailed). Bool("failOnResourceLoadingFailed", &failOnResourceLoadingFailed, defaultOptions.FailOnResourceLoadingFailed).
Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions). Bool("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay). Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus). String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression). String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
String("waitForSelector", &waitForSelector, defaultOptions.WaitForSelector).
Custom("cookies", func(value string) error { Custom("cookies", func(value string) error {
if value == "" { if value == "" {
cookies = defaultOptions.Cookies cookies = defaultOptions.Cookies
return nil return nil
} }
err := json.Unmarshal([]byte(value), &cookies) // sameSite attribute from cookies must accept case-insensitive
// values.
// See https://github.com/gotenberg/gotenberg/issues/1331.
normalized, err := sameSiteRegexp.ReplaceFunc(value, func(m regexp2.Match) string {
groups := m.Groups()
provided := groups[2].String()
var canon string
switch strings.ToLower(provided) {
case "lax":
canon = "Lax"
case "strict":
canon = "Strict"
case "none":
canon = "None"
default:
canon = provided
}
return groups[1].String() + canon + groups[3].String()
}, -1, -1)
if err != nil {
return fmt.Errorf("normalize sameSite from cookies: %w", err)
}
err = json.Unmarshal([]byte(normalized), &cookies)
if err != nil { if err != nil {
return fmt.Errorf("unmarshal cookies: %w", err) return fmt.Errorf("unmarshal cookies: %w", err)
} }
@@ -141,7 +196,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
var scopeRegexp *regexp2.Regexp var scopeRegexp *regexp2.Regexp
if len(scope) > 0 { if len(scope) > 0 {
p, errCompile := regexp2.Compile(scope, 0) p, errCompile := regexp2.Compile(scope, regexp2.None)
if errCompile != nil { if errCompile != nil {
err = multierr.Append(err, fmt.Errorf("invalid scope regex pattern for header '%s': %w", k, errCompile)) err = multierr.Append(err, fmt.Errorf("invalid scope regex pattern for header '%s': %w", k, errCompile))
continue continue
@@ -175,19 +230,21 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground) Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground)
options := Options{ options := Options{
SkipNetworkIdleEvent: skipNetworkIdleEvent, SkipNetworkIdleEvent: skipNetworkIdleEvent,
FailOnHttpStatusCodes: failOnHttpStatusCodes, FailOnHttpStatusCodes: failOnHttpStatusCodes,
FailOnResourceHttpStatusCodes: failOnResourceHttpStatusCodes, FailOnResourceHttpStatusCodes: failOnResourceHttpStatusCodes,
FailOnResourceLoadingFailed: failOnResourceLoadingFailed, IgnoreResourceHttpStatusDomains: ignoreResourceHttpStatusDomains,
FailOnConsoleExceptions: failOnConsoleExceptions, FailOnResourceLoadingFailed: failOnResourceLoadingFailed,
WaitDelay: waitDelay, FailOnConsoleExceptions: failOnConsoleExceptions,
WaitWindowStatus: waitWindowStatus, WaitDelay: waitDelay,
WaitForExpression: waitForExpression, WaitWindowStatus: waitWindowStatus,
Cookies: cookies, WaitForExpression: waitForExpression,
UserAgent: userAgent, WaitForSelector: waitForSelector,
ExtraHttpHeaders: extraHttpHeaders, Cookies: cookies,
EmulatedMediaType: emulatedMediaType, UserAgent: userAgent,
OmitBackground: omitBackground, ExtraHttpHeaders: extraHttpHeaders,
EmulatedMediaType: emulatedMediaType,
OmitBackground: omitBackground,
} }
return form, options return form, options
@@ -332,6 +389,8 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
mode := pdfengines.FormDataPdfSplitMode(form, false) mode := pdfengines.FormDataPdfSplitMode(form, false)
pdfFormats := pdfengines.FormDataPdfFormats(form) pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form)
var url string var url string
err := form. err := form.
@@ -341,7 +400,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata) err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths)
if err != nil { if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err) return fmt.Errorf("convert URL to PDF: %w", err)
} }
@@ -393,6 +452,8 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
mode := pdfengines.FormDataPdfSplitMode(form, false) mode := pdfengines.FormDataPdfSplitMode(form, false)
pdfFormats := pdfengines.FormDataPdfFormats(form) pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form)
var inputPath string var inputPath string
err := form. err := form.
@@ -403,7 +464,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
} }
url := fmt.Sprintf("file://%s", inputPath) url := fmt.Sprintf("file://%s", inputPath)
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata) err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths)
if err != nil { if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err) return fmt.Errorf("convert HTML to PDF: %w", err)
} }
@@ -456,6 +517,8 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
mode := pdfengines.FormDataPdfSplitMode(form, false) mode := pdfengines.FormDataPdfSplitMode(form, false)
pdfFormats := pdfengines.FormDataPdfFormats(form) pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form)
var ( var (
inputPath string inputPath string
@@ -475,7 +538,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("transform markdown file(s) to HTML: %w", err) return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
} }
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata) err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths)
if err != nil { if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err) return fmt.Errorf("convert markdown to PDF: %w", err)
} }
@@ -561,7 +624,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
return "", fmt.Errorf("read markdown file '%s': %w", filename, err) return "", fmt.Errorf("read markdown file '%s': %w", filename, err)
} }
unsafe := blackfriday.Run(b) unsafe := markdown.ToHTML(b, nil, nil)
sanitized := bluemonday.UGCPolicy().SanitizeBytes(unsafe) sanitized := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
// #nosec // #nosec
@@ -599,7 +662,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
return fmt.Sprintf("file://%s", inputPath), nil return fmt.Sprintf("file://%s", inputPath), nil
} }
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 { func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}, userPassword, ownerPassword string, embedPaths []string) error {
outputPath := ctx.GeneratePath(".pdf") outputPath := ctx.GeneratePath(".pdf")
// See https://github.com/gotenberg/gotenberg/issues/1130. // See https://github.com/gotenberg/gotenberg/issues/1130.
filename := ctx.OutputFilename(outputPath) filename := ctx.OutputFilename(outputPath)
@@ -618,6 +681,16 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
) )
} }
if errors.Is(err, ErrPrintingFailed) {
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHttpError(
http.StatusBadRequest,
"Chromium failed to print the PDF; this usually happens when the page is too large",
),
)
}
if errors.Is(err, ErrInvalidPrinterSettings) { if errors.Is(err, ErrInvalidPrinterSettings) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
@@ -628,12 +701,22 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
) )
} }
if errors.Is(err, ErrPageRangesExceedsPageCount) {
return api.WrapError(
fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHttpError(
http.StatusBadRequest,
fmt.Sprintf("The page ranges '%s' (nativePageRanges) exceeds the page count", options.PageRanges),
),
)
}
if errors.Is(err, ErrPageRangesSyntaxError) { if errors.Is(err, ErrPageRangesSyntaxError) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert to PDF: %w", err), fmt.Errorf("convert to PDF: %w", err),
api.NewSentinelHttpError( api.NewSentinelHttpError(
http.StatusBadRequest, http.StatusBadRequest,
fmt.Sprintf("Chromium does not handle the page ranges '%s' (nativePageRanges)", options.PageRanges), fmt.Sprintf("Chromium does not handle the page ranges '%s' (nativePageRanges) syntax", options.PageRanges),
), ),
) )
} }
@@ -651,11 +734,21 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
return fmt.Errorf("convert PDF(s): %w", err) return fmt.Errorf("convert PDF(s): %w", err)
} }
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err)
}
err = pdfengines.WriteMetadataStub(ctx, engine, metadata, convertOutputPaths) err = pdfengines.WriteMetadataStub(ctx, engine, metadata, convertOutputPaths)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
} }
err = pdfengines.EncryptPdfStub(ctx, engine, userPassword, ownerPassword, convertOutputPaths)
if err != nil {
return fmt.Errorf("encrypt PDFs: %w", err)
}
zeroValuedSplitMode := gotenberg.SplitMode{} zeroValuedSplitMode := gotenberg.SplitMode{}
zeroValuedPdfFormats := gotenberg.PdfFormats{} zeroValuedPdfFormats := gotenberg.PdfFormats{}
if mode != zeroValuedSplitMode && pdfFormats != zeroValuedPdfFormats { if mode != zeroValuedSplitMode && pdfFormats != zeroValuedPdfFormats {
@@ -719,6 +812,22 @@ func handleChromiumError(err error, options Options) error {
) )
} }
if errors.Is(err, ErrInvalidSelectorQuery) {
if options.WaitForSelector == "" {
// We only expect to see this error if the user specified a selector.
// If they didn't and we still generated the error, return a 500.
return err
}
return api.WrapError(
err,
api.NewSentinelHttpError(
http.StatusBadRequest,
fmt.Sprintf("The selector '%s' (waitForSelector) returned an exception or undefined", options.WaitForSelector),
),
)
}
if errors.Is(err, ErrInvalidHttpStatusCode) { if errors.Is(err, ErrInvalidHttpStatusCode) {
return api.WrapError( return api.WrapError(
err, err,

View File

@@ -512,3 +512,19 @@ func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScrip
} }
} }
} }
func waitForSelectorVisibleBeforePrintActionFunc(logger *zap.Logger, selector string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if selector == "" {
logger.Debug("no wait selector")
return nil
}
logger.Debug(fmt.Sprintf("wait until '%s' is visible before print", selector))
err := chromedp.WaitVisible(selector, chromedp.ByQuery, chromedp.RetryInterval(time.Duration(100)*time.Millisecond)).Do(ctx)
if err != nil {
return fmt.Errorf("wait visible: %v: %w", err, ErrInvalidSelectorQuery)
}
return nil
}
}

View File

@@ -176,6 +176,16 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
return nil return nil
} }
// Encrypt is not available in this implementation.
func (engine *ExifTool) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return fmt.Errorf("encrypt PDF using ExifTool: %w", gotenberg.ErrPdfEncryptionNotSupported)
}
// EmbedFiles is not available in this implementation.
func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
return fmt.Errorf("embed files with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*ExifTool)(nil) _ gotenberg.Module = (*ExifTool)(nil)

View File

@@ -91,6 +91,16 @@ func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *z
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Encrypt is not available in this implementation.
func (engine *LibreOfficePdfEngine) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return fmt.Errorf("encrypt PDF using LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// EmbedFiles is not available in this implementation.
func (engine *LibreOfficePdfEngine) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
return fmt.Errorf("embed files with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil) _ gotenberg.Module = (*LibreOfficePdfEngine)(nil)

View File

@@ -30,6 +30,8 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
splitMode := pdfengines.FormDataPdfSplitMode(form, false) splitMode := pdfengines.FormDataPdfSplitMode(form, false)
pdfFormats := pdfengines.FormDataPdfFormats(form) pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form)
zeroValuedSplitMode := gotenberg.SplitMode{} zeroValuedSplitMode := gotenberg.SplitMode{}
@@ -251,6 +253,11 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
} }
} }
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err)
}
err = pdfengines.WriteMetadataStub(ctx, engine, metadata, outputPaths) err = pdfengines.WriteMetadataStub(ctx, engine, metadata, outputPaths)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
@@ -263,6 +270,11 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
} }
} }
err = pdfengines.EncryptPdfStub(ctx, engine, userPassword, ownerPassword, outputPaths)
if err != nil {
return fmt.Errorf("encrypt PDFs: %w", err)
}
if len(outputPaths) > 1 && splitMode == zeroValuedSplitMode { if len(outputPaths) > 1 && splitMode == zeroValuedSplitMode {
// If .zip archive, document.docx -> document.docx.pdf. // If .zip archive, document.docx -> document.docx.pdf.
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {

View File

@@ -171,6 +171,65 @@ func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, met
return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// EmbedFiles embeds files into a PDF. All files are embedded as file attachments
// without modifying the main PDF content.
func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
if len(filePaths) == 0 {
return nil
}
logger.Debug(fmt.Sprintf("embedding %d file(s) to %s: %v", len(filePaths), inputPath, filePaths))
args := []string{
"attachments", "add",
inputPath,
}
args = append(args, filePaths...)
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil {
return fmt.Errorf("create command for attaching files: %w", err)
}
_, err = cmd.Exec()
if err != nil {
return fmt.Errorf("attach files with pdfcpu: %w", err)
}
return nil
}
// Encrypt adds password protection to a PDF file using pdfcpu.
func (engine *PdfCpu) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
if userPassword == "" {
return errors.New("user password cannot be empty")
}
if ownerPassword == "" {
ownerPassword = userPassword
}
var args []string
args = append(args, "encrypt")
args = append(args, "-mode", "aes")
args = append(args, "-upw", userPassword)
args = append(args, "-opw", ownerPassword)
args = append(args, "-perm", "all")
args = append(args, inputPath, inputPath)
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil {
return fmt.Errorf("create command: %w", err)
}
_, err = cmd.Exec()
if err != nil {
return fmt.Errorf("encrypt PDF with pdfcpu: %w", err)
}
return nil
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*PdfCpu)(nil) _ gotenberg.Module = (*PdfCpu)(nil)

View File

@@ -18,6 +18,8 @@ type multiPdfEngines struct {
convertEngines []gotenberg.PdfEngine convertEngines []gotenberg.PdfEngine
readMetadataEngines []gotenberg.PdfEngine readMetadataEngines []gotenberg.PdfEngine
writeMetadataEngines []gotenberg.PdfEngine writeMetadataEngines []gotenberg.PdfEngine
passwordEngines []gotenberg.PdfEngine
embedEngines []gotenberg.PdfEngine
} }
func newMultiPdfEngines( func newMultiPdfEngines(
@@ -26,7 +28,9 @@ func newMultiPdfEngines(
flattenEngines, flattenEngines,
convertEngines, convertEngines,
readMetadataEngines, readMetadataEngines,
writeMetadataEngines []gotenberg.PdfEngine, writeMetadataEngines,
passwordEngines,
embedEngines []gotenberg.PdfEngine,
) *multiPdfEngines { ) *multiPdfEngines {
return &multiPdfEngines{ return &multiPdfEngines{
mergeEngines: mergeEngines, mergeEngines: mergeEngines,
@@ -35,11 +39,13 @@ func newMultiPdfEngines(
convertEngines: convertEngines, convertEngines: convertEngines,
readMetadataEngines: readMetadataEngines, readMetadataEngines: readMetadataEngines,
writeMetadataEngines: writeMetadataEngines, writeMetadataEngines: writeMetadataEngines,
passwordEngines: passwordEngines,
embedEngines: embedEngines,
} }
} }
// Merge tries to merge the given PDFs into a unique PDF thanks to its // Merge combines multiple PDF files into a single document using the first
// children. If the context is done, it stops and returns an error. // available engine that supports PDF merging.
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
var err error var err error
errChan := make(chan error, 1) errChan := make(chan error, 1)
@@ -68,8 +74,8 @@ type splitResult struct {
err error err error
} }
// Split tries to split at intervals a given PDF thanks to its children. If the // Split divides the PDF into separate pages using the first available engine
// context is done, it stops and returns an error. // that supports PDF splitting.
func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
var err error var err error
var mu sync.Mutex // to safely append errors. var mu sync.Mutex // to safely append errors.
@@ -99,8 +105,8 @@ func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mod
return nil, fmt.Errorf("split PDF with multi PDF engines: %w", err) return nil, fmt.Errorf("split PDF with multi PDF engines: %w", err)
} }
// Flatten merges existing annotation appearances with page content, thanks to // Flatten merges existing annotation appearances with page content using the
// its children. If the context is done, it stops and returns an error // first available engine that supports flattening.
func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error { func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
var err error var err error
errChan := make(chan error, 1) errChan := make(chan error, 1)
@@ -124,8 +130,8 @@ func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, i
return fmt.Errorf("flatten PDF with multi PDF engines: %w", err) 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 transforms the given PDF to a specific PDF format using the first
// children. If the context is done, it stops and returns an error. // available engine that supports PDF conversion.
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
var err error var err error
errChan := make(chan error, 1) errChan := make(chan error, 1)
@@ -154,6 +160,8 @@ type readMetadataResult struct {
err error err error
} }
// ReadMetadata extracts metadata from a PDF file using the first available
// engine that supports metadata reading.
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) { func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
var err error var err error
var mu sync.Mutex // to safely append errors. var mu sync.Mutex // to safely append errors.
@@ -183,6 +191,8 @@ func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logg
return nil, fmt.Errorf("read PDF metadata with multi PDF engines: %w", err) return nil, fmt.Errorf("read PDF metadata with multi PDF engines: %w", err)
} }
// WriteMetadata embeds metadata into a PDF file using the first available
// engine that supports metadata writing.
func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
var err error var err error
errChan := make(chan error, 1) errChan := make(chan error, 1)
@@ -206,6 +216,56 @@ func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Log
return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err) return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
} }
// Encrypt adds password protection to a PDF file using the first available
// engine that supports password protection.
func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
var err error
errChan := make(chan error, 1)
for _, engine := range multi.passwordEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Encrypt(ctx, logger, inputPath, userPassword, ownerPassword)
}(engine)
select {
case protectErr := <-errChan:
errored := multierr.AppendInto(&err, protectErr)
if !errored {
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("encrypt PDF using multi PDF engines: %w", err)
}
// EmbedFiles embeds files into a PDF using the first available
// engine that supports file embedding.
func (multi *multiPdfEngines) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
var err error
errChan := make(chan error, 1)
for _, engine := range multi.embedEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.EmbedFiles(ctx, logger, filePaths, inputPath)
}(engine)
select {
case embedErr := <-errChan:
errored := multierr.AppendInto(&err, embedErr)
if !errored {
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("embed files into PDF using multi PDF engines: %w", err)
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil) _ gotenberg.PdfEngine = (*multiPdfEngines)(nil)

View File

@@ -103,6 +103,97 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
} }
} }
func TestMultiPdfEngines_Encrypt(t *testing.T) {
for _, tc := range []struct {
scenario string
engine *multiPdfEngines
ctx context.Context
expectError bool
}{
{
scenario: "nominal behavior",
engine: &multiPdfEngines{
passwordEngines: []gotenberg.PdfEngine{
&gotenberg.PdfEngineMock{
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return nil
},
},
},
},
ctx: context.Background(),
},
{
scenario: "at least one engine does not return an error",
engine: &multiPdfEngines{
passwordEngines: []gotenberg.PdfEngine{
&gotenberg.PdfEngineMock{
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return errors.New("foo")
},
},
&gotenberg.PdfEngineMock{
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return nil
},
},
},
},
ctx: context.Background(),
},
{
scenario: "all engines return an error",
engine: &multiPdfEngines{
passwordEngines: []gotenberg.PdfEngine{
&gotenberg.PdfEngineMock{
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return errors.New("foo")
},
},
&gotenberg.PdfEngineMock{
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return errors.New("foo")
},
},
},
},
ctx: context.Background(),
expectError: true,
},
{
scenario: "context expired",
engine: &multiPdfEngines{
passwordEngines: []gotenberg.PdfEngine{
&gotenberg.PdfEngineMock{
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
return nil
},
},
},
},
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
}(),
expectError: true,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
err := tc.engine.Encrypt(tc.ctx, zap.NewNop(), "", "", "")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestMultiPdfEngines_Split(t *testing.T) { func TestMultiPdfEngines_Split(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string

View File

@@ -33,6 +33,8 @@ type PdfEngines struct {
convertNames []string convertNames []string
readMetadataNames []string readMetadataNames []string
writeMetadataNames []string writeMetadataNames []string
encryptNames []string
embedNames []string
engines []gotenberg.PdfEngine engines []gotenberg.PdfEngine
disableRoutes bool disableRoutes bool
} }
@@ -49,6 +51,8 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all") fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all")
fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all") fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all")
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all") fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
fs.StringSlice("pdfengines-encrypt-engines", []string{"qpdf", "pdftk", "pdfcpu"}, "Set the PDF engines and their order for the password protection feature - empty means all")
fs.StringSlice("pdfengines-embed-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the file embedding feature - empty means all")
fs.Bool("pdfengines-disable-routes", false, "Disable the routes") fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
// Deprecated flags. // Deprecated flags.
@@ -74,6 +78,8 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
convertNames := flags.MustStringSlice("pdfengines-convert-engines") convertNames := flags.MustStringSlice("pdfengines-convert-engines")
readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines") readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines")
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines") writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
encryptNames := flags.MustStringSlice("pdfengines-encrypt-engines")
embedNames := flags.MustStringSlice("pdfengines-embed-engines")
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes") mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
engines, err := ctx.Modules(new(gotenberg.PdfEngine)) engines, err := ctx.Modules(new(gotenberg.PdfEngine))
@@ -130,6 +136,16 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
mod.writeMetadataNames = writeMetadataNames mod.writeMetadataNames = writeMetadataNames
} }
mod.encryptNames = defaultNames
if len(encryptNames) > 0 {
mod.encryptNames = encryptNames
}
mod.embedNames = defaultNames
if len(embedNames) > 0 {
mod.embedNames = embedNames
}
return nil return nil
} }
@@ -183,6 +199,8 @@ func (mod *PdfEngines) Validate() error {
findNonExistingEngines(mod.convertNames) findNonExistingEngines(mod.convertNames)
findNonExistingEngines(mod.readMetadataNames) findNonExistingEngines(mod.readMetadataNames)
findNonExistingEngines(mod.writeMetadataNames) findNonExistingEngines(mod.writeMetadataNames)
findNonExistingEngines(mod.encryptNames)
findNonExistingEngines(mod.embedNames)
if len(nonExistingEngines) == 0 { if len(nonExistingEngines) == 0 {
return nil return nil
@@ -201,6 +219,7 @@ func (mod *PdfEngines) SystemMessages() []string {
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")), fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")), fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")), fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
fmt.Sprintf("encrypt engines - %s", strings.Join(mod.encryptNames[:], " ")),
} }
} }
@@ -227,6 +246,8 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
engines(mod.convertNames), engines(mod.convertNames),
engines(mod.readMetadataNames), engines(mod.readMetadataNames),
engines(mod.writeMetadataNames), engines(mod.writeMetadataNames),
engines(mod.encryptNames),
engines(mod.embedNames),
), nil ), nil
} }
@@ -250,6 +271,8 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
convertRoute(engine), convertRoute(engine),
readMetadataRoute(engine), readMetadataRoute(engine),
writeMetadataRoute(engine), writeMetadataRoute(engine),
encryptRoute(engine),
embedRoute(engine),
}, nil }, nil
} }

View File

@@ -254,6 +254,53 @@ func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata ma
return nil return nil
} }
// FormDataPdfEmbeds extracts embedded file paths from form data.
// Only files uploaded with the "embeds" field name are included.
func FormDataPdfEmbeds(form *api.FormData) []string {
var embedPaths []string
form.Embeds(&embedPaths)
return embedPaths
}
// FormDataPdfEncrypt extracts encryption parameters from form data.
func FormDataPdfEncrypt(form *api.FormData) (userPassword, ownerPassword string) {
form.String("userPassword", &userPassword, "")
form.String("ownerPassword", &ownerPassword, "")
return userPassword, ownerPassword
}
// EncryptPdfStub adds password protection to PDF files.
func EncryptPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, userPassword, ownerPassword string, inputPaths []string) error {
if userPassword == "" {
return nil
}
for _, inputPath := range inputPaths {
err := engine.Encrypt(ctx, ctx.Log(), inputPath, userPassword, ownerPassword)
if err != nil {
return fmt.Errorf("encrypt PDF '%s': %w", inputPath, err)
}
}
return nil
}
// EmbedFilesStub embeds files into PDF files.
func EmbedFilesStub(ctx *api.Context, engine gotenberg.PdfEngine, embedPaths []string, inputPaths []string) error {
if len(embedPaths) == 0 {
return nil
}
for _, inputPath := range inputPaths {
err := engine.EmbedFiles(ctx, ctx.Log(), embedPaths, inputPath)
if err != nil {
return fmt.Errorf("embed files into PDF '%s': %w", inputPath, err)
}
}
return nil
}
// mergeRoute returns an [api.Route] which can merge PDFs. // mergeRoute returns an [api.Route] which can merge PDFs.
func mergeRoute(engine gotenberg.PdfEngine) api.Route { func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
@@ -266,6 +313,8 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
form := ctx.FormData() form := ctx.FormData()
pdfFormats := FormDataPdfFormats(form) pdfFormats := FormDataPdfFormats(form)
metadata := FormDataPdfMetadata(form, false) metadata := FormDataPdfMetadata(form, false)
userPassword, ownerPassword := FormDataPdfEncrypt(form)
embedPaths := FormDataPdfEmbeds(form)
var inputPaths []string var inputPaths []string
var flatten bool var flatten bool
@@ -288,6 +337,11 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("convert PDF: %w", err) return fmt.Errorf("convert PDF: %w", err)
} }
err = EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err)
}
err = WriteMetadataStub(ctx, engine, metadata, outputPaths) err = WriteMetadataStub(ctx, engine, metadata, outputPaths)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
@@ -300,6 +354,11 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
} }
} }
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, outputPaths)
if err != nil {
return fmt.Errorf("encrypt PDFs: %w", err)
}
err = ctx.AddOutputPaths(outputPaths...) err = ctx.AddOutputPaths(outputPaths...)
if err != nil { if err != nil {
return fmt.Errorf("add output paths: %w", err) return fmt.Errorf("add output paths: %w", err)
@@ -323,6 +382,8 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
mode := FormDataPdfSplitMode(form, true) mode := FormDataPdfSplitMode(form, true)
pdfFormats := FormDataPdfFormats(form) pdfFormats := FormDataPdfFormats(form)
metadata := FormDataPdfMetadata(form, false) metadata := FormDataPdfMetadata(form, false)
userPassword, ownerPassword := FormDataPdfEncrypt(form)
embedPaths := FormDataPdfEmbeds(form)
var inputPaths []string var inputPaths []string
var flatten bool var flatten bool
@@ -344,6 +405,11 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("convert PDFs: %w", err) return fmt.Errorf("convert PDFs: %w", err)
} }
err = EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err)
}
err = WriteMetadataStub(ctx, engine, metadata, convertOutputPaths) err = WriteMetadataStub(ctx, engine, metadata, convertOutputPaths)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
@@ -356,6 +422,11 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
} }
} }
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, convertOutputPaths)
if err != nil {
return fmt.Errorf("encrypt PDFs: %w", err)
}
zeroValuedSplitMode := gotenberg.SplitMode{} zeroValuedSplitMode := gotenberg.SplitMode{}
zeroValuedPdfFormats := gotenberg.PdfFormats{} zeroValuedPdfFormats := gotenberg.PdfFormats{}
if mode != zeroValuedSplitMode && pdfFormats != zeroValuedPdfFormats { if mode != zeroValuedSplitMode && pdfFormats != zeroValuedPdfFormats {
@@ -456,7 +527,6 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
if err != nil { if err != nil {
return fmt.Errorf("rename output path: %w", err) return fmt.Errorf("rename output path: %w", err)
} }
outputPaths[i] = inputPath outputPaths[i] = inputPath
} }
} }
@@ -548,3 +618,76 @@ func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
}, },
} }
} }
// encryptRoute returns an [api.Route] which can add password protection to PDFs.
func encryptRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/pdfengines/encrypt",
IsMultipart: true,
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form := ctx.FormData()
var inputPaths []string
var userPassword string
var ownerPassword string
err := form.
MandatoryPaths([]string{".pdf"}, &inputPaths).
MandatoryString("userPassword", &userPassword).
String("ownerPassword", &ownerPassword, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, inputPaths)
if err != nil {
return fmt.Errorf("encrypt PDFs: %w", err)
}
err = ctx.AddOutputPaths(inputPaths...)
if err != nil {
return fmt.Errorf("add output paths: %w", err)
}
return nil
},
}
}
// embedRoute returns an [api.Route] which can add embedded files to PDFs.
func embedRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/pdfengines/embed",
IsMultipart: true,
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form := ctx.FormData()
embedPaths := FormDataPdfEmbeds(form)
var inputPaths []string
err := form.
MandatoryPaths([]string{".pdf"}, &inputPaths).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
err = EmbedFilesStub(ctx, engine, embedPaths, inputPaths)
if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err)
}
err = ctx.AddOutputPaths(inputPaths...)
if err != nil {
return fmt.Errorf("add output paths: %w", err)
}
return nil
},
}
}

View File

@@ -145,6 +145,49 @@ func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, meta
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Encrypt adds password protection to a PDF file using PDFtk.
func (engine *PdfTk) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
if userPassword == "" {
return errors.New("user password cannot be empty")
}
if ownerPassword == userPassword || ownerPassword == "" {
return gotenberg.NewPdfEngineInvalidArgs("pdftk", "both 'userPassword' and 'ownerPassword' must be provided and different. Consider switching to another PDF engine if this behavior does not work with your workflow")
}
// Create a temp output file in the same directory.
tmpPath := inputPath + ".tmp"
var args []string
args = append(args, inputPath)
args = append(args, "output", tmpPath)
args = append(args, "encrypt_128bit")
args = append(args, "user_pw", userPassword)
args = append(args, "owner_pw", ownerPassword)
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil {
return fmt.Errorf("create command: %w", err)
}
_, err = cmd.Exec()
if err != nil {
return fmt.Errorf("encrypt PDF with PDFtk: %w", err)
}
err = os.Rename(tmpPath, inputPath)
if err != nil {
return fmt.Errorf("rename temporary output file with input file: %w", err)
}
return nil
}
// EmbedFiles is not available in this implementation.
func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
return fmt.Errorf("embed files with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*PdfTk)(nil) _ gotenberg.Module = (*PdfTk)(nil)

View File

@@ -27,6 +27,7 @@ type Prometheus struct {
interval time.Duration interval time.Duration
disableRouteLogging bool disableRouteLogging bool
disableCollect bool disableCollect bool
metricsPath string
metrics []gotenberg.Metric metrics []gotenberg.Metric
registry *prometheus.Registry registry *prometheus.Registry
@@ -42,6 +43,7 @@ func (mod *Prometheus) Descriptor() gotenberg.ModuleDescriptor {
fs.Duration("prometheus-collect-interval", time.Duration(1)*time.Second, "Set the interval for collecting modules' metrics") fs.Duration("prometheus-collect-interval", time.Duration(1)*time.Second, "Set the interval for collecting modules' metrics")
fs.Bool("prometheus-disable-route-logging", false, "Disable the route logging") fs.Bool("prometheus-disable-route-logging", false, "Disable the route logging")
fs.Bool("prometheus-disable-collect", false, "Disable the collect of metrics") fs.Bool("prometheus-disable-collect", false, "Disable the collect of metrics")
fs.String("prometheus-metrics-path", "/prometheus/metrics", "Path for Prometheus metrics endpoint")
return fs return fs
}(), }(),
@@ -56,6 +58,7 @@ func (mod *Prometheus) Provision(ctx *gotenberg.Context) error {
mod.interval = flags.MustDuration("prometheus-collect-interval") mod.interval = flags.MustDuration("prometheus-collect-interval")
mod.disableRouteLogging = flags.MustBool("prometheus-disable-route-logging") mod.disableRouteLogging = flags.MustBool("prometheus-disable-route-logging")
mod.disableCollect = flags.MustBool("prometheus-disable-collect") mod.disableCollect = flags.MustBool("prometheus-disable-collect")
mod.metricsPath = flags.MustString("prometheus-metrics-path")
if mod.disableCollect { if mod.disableCollect {
// Exit early. // Exit early.
@@ -98,6 +101,10 @@ func (mod *Prometheus) Validate() error {
return errors.New("namespace must not be empty") return errors.New("namespace must not be empty")
} }
if mod.metricsPath == "" {
return errors.New("metrics path cannot be empty")
}
metricsMap := make(map[string]string, len(mod.metrics)) metricsMap := make(map[string]string, len(mod.metrics))
for _, metric := range mod.metrics { for _, metric := range mod.metrics {
@@ -171,7 +178,7 @@ func (mod *Prometheus) Routes() ([]api.Route, error) {
return []api.Route{ return []api.Route{
{ {
Method: http.MethodGet, Method: http.MethodGet,
Path: "/prometheus/metrics", Path: mod.metricsPath,
DisableLogging: mod.disableRouteLogging, DisableLogging: mod.disableRouteLogging,
Handler: echo.WrapHandler( Handler: echo.WrapHandler(
promhttp.HandlerFor(mod.registry, promhttp.HandlerOpts{}), promhttp.HandlerFor(mod.registry, promhttp.HandlerOpts{}),

View File

@@ -172,6 +172,40 @@ func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metad
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Encrypt adds password protection to a PDF file using QPDF.
func (engine *QPdf) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
if userPassword == "" {
return errors.New("user password cannot be empty")
}
if ownerPassword == "" {
ownerPassword = userPassword
}
var args []string
args = append(args, inputPath)
args = append(args, engine.globalArgs...)
args = append(args, "--replace-input")
args = append(args, "--encrypt", userPassword, ownerPassword, "256", "--")
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
if err != nil {
return fmt.Errorf("create command: %w", err)
}
_, err = cmd.Exec()
if err != nil {
return fmt.Errorf("encrypt PDF with QPDF: %w", err)
}
return nil
}
// EmbedFiles is not available in this implementation.
func (engine *QPdf) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
return fmt.Errorf("embed files with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
var ( var (
_ gotenberg.Module = (*QPdf)(nil) _ gotenberg.Module = (*QPdf)(nil)
_ gotenberg.Provisioner = (*QPdf)(nil) _ gotenberg.Provisioner = (*QPdf)(nil)

View File

@@ -272,6 +272,29 @@ func webhookMiddleware(w *Webhook) api.Middleware {
}) })
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)
} }
if deadline, ok := ctx.Deadline(); ok {
// Create a new context derived from Background (detached from Request)
// but with the same deadline as the original context.
detachedCtx, detachedCancel := context.WithDeadline(context.Background(), deadline)
// Replace the embedded context in the api.Context struct.
// The modules downstream will now use this detached context.
ctx.Context = detachedCtx
// We must wrap the cancel function.
// 1. detachedCancel() cleans up our new detached context.
// 2. originalCancel() (captured from c.Get("cancel")) cleans up the working directory.
originalCancel := cancel
cancel = func() {
detachedCancel()
originalCancel()
}
} else {
// Fallback if no deadline was set (rare, as newContext enforces it).
ctx.Context = context.Background()
}
// As a webhook URL has been given, we handle the request in a // As a webhook URL has been given, we handle the request in a
// goroutine and return immediately. // goroutine and return immediately.
w.asyncCount.Add(1) w.asyncCount.Add(1)

View File

@@ -1,6 +1,8 @@
# TODO: # TODO:
# 1. JavaScript disabled on some feature. # 1. JavaScript disabled on some feature.
@chromium
@chromium-convert-html
Feature: /forms/chromium/convert/html Feature: /forms/chromium/convert/html
Scenario: POST /forms/chromium/convert/html (Default) Scenario: POST /forms/chromium/convert/html (Default)
@@ -193,6 +195,36 @@ Feature: /forms/chromium/convert/html
Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. Wait delay > 2 seconds or expression window globalVar === 'ready' returns true.
""" """
Scenario: POST /forms/chromium/convert/html (Wait For Selector)
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/feature-rich-html/index.html | 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 NOT have the following content at page 1:
"""
Wait on selector returns 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/feature-rich-html/index.html | file |
| waitForSelector | #wait-selector | field |
| 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:
"""
Wait on selector returns true.
"""
Scenario: POST /forms/chromium/convert/html (Emulated Media Type) Scenario: POST /forms/chromium/convert/html (Emulated Media Type)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -341,6 +373,16 @@ Feature: /forms/chromium/convert/html
https://gethttpstatus.com/400 - 400: Bad Request https://gethttpstatus.com/400 - 400: Bad Request
""" """
Scenario: POST /forms/chromium/convert/html (Fail On Resource HTTP Status Codes - Ignore Domains)
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/feature-rich-html/index.html | file |
| failOnResourceHttpStatusCodes | [499,599] | field |
| ignoreResourceHttpStatusDomains | ["gethttpstatus.com"] | field |
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
Scenario: POST /forms/chromium/convert/html (Fail On Resource Loading Failed) Scenario: POST /forms/chromium/convert/html (Fail On Resource Loading Failed)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -443,7 +485,16 @@ Feature: /forms/chromium/convert/html
Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string: Then the response body should match string:
""" """
Chromium does not handle the page ranges 'foo' (nativePageRanges) Chromium does not handle the page ranges 'foo' (nativePageRanges) syntax
"""
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 | 2-3 | 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:
"""
The page ranges '2-3' (nativePageRanges) exceeds the page count
""" """
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): 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 | | files | testdata/page-1-html/index.html | file |
@@ -557,6 +608,7 @@ Feature: /forms/chromium/convert/html
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
""" """
@split
Scenario: POST /forms/chromium/convert/html (Split Intervals) Scenario: POST /forms/chromium/convert/html (Split Intervals)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -585,6 +637,8 @@ Feature: /forms/chromium/convert/html
""" """
# See https://github.com/gotenberg/gotenberg/issues/1130. # See https://github.com/gotenberg/gotenberg/issues/1130.
@split
@output-filename
Scenario: POST /forms/chromium/convert/html (Split Output Filename) Scenario: POST /forms/chromium/convert/html (Split Output Filename)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -614,6 +668,7 @@ Feature: /forms/chromium/convert/html
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/html (Split Pages) Scenario: POST /forms/chromium/convert/html (Split Pages)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -637,6 +692,7 @@ Feature: /forms/chromium/convert/html
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/html (Split Pages & Unify) Scenario: POST /forms/chromium/convert/html (Split Pages & Unify)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -660,6 +716,7 @@ Feature: /forms/chromium/convert/html
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/html (Split Many PDFs - Lot of Pages) Scenario: POST /forms/chromium/convert/html (Split Many PDFs - Lot of Pages)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -693,6 +750,7 @@ Feature: /forms/chromium/convert/html
Page 12 Page 12
""" """
@convert
Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1) Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -703,8 +761,10 @@ Feature: /forms/chromium/convert/html
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@convert
@split
Scenario: POST /forms/chromium/convert/html (Split & PDF/A-1b & PDF/UA-1) Scenario: POST /forms/chromium/convert/html (Split & PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -734,9 +794,12 @@ Feature: /forms/chromium/convert/html
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
# See https://github.com/gotenberg/gotenberg/issues/1130. # See https://github.com/gotenberg/gotenberg/issues/1130.
@convert
@split
@output-filename
Scenario: POST /forms/chromium/convert/html (Split & PDF/A-1b & PDF/UA-1 & Output Filename) Scenario: POST /forms/chromium/convert/html (Split & PDF/A-1b & PDF/UA-1 & Output Filename)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -768,8 +831,9 @@ Feature: /forms/chromium/convert/html
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@metadata
Scenario: POST /forms/chromium/convert/html (Metadata) Scenario: POST /forms/chromium/convert/html (Metadata)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -805,6 +869,7 @@ Feature: /forms/chromium/convert/html
} }
""" """
@flatten
Scenario: POST /forms/chromium/convert/html (Flatten) Scenario: POST /forms/chromium/convert/html (Flatten)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
@@ -815,7 +880,51 @@ Feature: /forms/chromium/convert/html
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) @encrypt
Scenario: POST /forms/chromium/convert/html (Encrypt - user password only)
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/page-1-html/index.html | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
@encrypt
Scenario: POST /forms/chromium/convert/html (Encrypt - both user and owner passwords)
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/page-1-html/index.html | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
@embed
Scenario: POST /forms/chromium/convert/html (Embeds)
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/page-1-html/index.html | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | 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 response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert
@metadata
@flatten
@embed
Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
Given I have a default Gotenberg container 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): 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 | | files | testdata/page-1-html/index.html | file |
@@ -823,15 +932,19 @@ Feature: /forms/chromium/convert/html
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| flatten | true | field | | flatten | true | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
| Gotenberg-Output-Filename | foo | header | | Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 9 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
| files | teststore/foo.pdf | file | | files | teststore/foo.pdf | file |
Then the response status code should be 200 Then the response status code should be 200
@@ -874,6 +987,7 @@ Feature: /forms/chromium/convert/html
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_chromium_convert_html" | | "trace":"forms_chromium_convert_html" |
@download-from
Scenario: POST /forms/chromium/convert/html (Download From) Scenario: POST /forms/chromium/convert/html (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -883,6 +997,7 @@ Feature: /forms/chromium/convert/html
Then the file request header "X-Foo" should be "bar" Then the file request header "X-Foo" should be "bar"
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
@webhook
Scenario: POST /forms/chromium/convert/html (Webhook) Scenario: POST /forms/chromium/convert/html (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -1,6 +1,8 @@
# TODO: # TODO:
# 1. JavaScript disabled on some feature. # 1. JavaScript disabled on some feature.
@chromium
@chromium-convert-markdown
Feature: /forms/chromium/convert/markdown Feature: /forms/chromium/convert/markdown
Scenario: POST /forms/chromium/convert/markdown (Default) Scenario: POST /forms/chromium/convert/markdown (Default)
@@ -522,7 +524,17 @@ Feature: /forms/chromium/convert/markdown
Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string: Then the response body should match string:
""" """
Chromium does not handle the page ranges 'foo' (nativePageRanges) Chromium does not handle the page ranges 'foo' (nativePageRanges) syntax
"""
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 |
| nativePageRanges | 2-3 | 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:
"""
The page ranges '2-3' (nativePageRanges) exceeds the page count
""" """
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): 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/index.html | file |
@@ -648,6 +660,7 @@ Feature: /forms/chromium/convert/markdown
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
""" """
@split
Scenario: POST /forms/chromium/convert/markdown (Split Intervals) Scenario: POST /forms/chromium/convert/markdown (Split Intervals)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -679,6 +692,8 @@ Feature: /forms/chromium/convert/markdown
""" """
# See https://github.com/gotenberg/gotenberg/issues/1130. # See https://github.com/gotenberg/gotenberg/issues/1130.
@split
@output-filename
Scenario: POST /forms/chromium/convert/markdown (Split Output Filename) Scenario: POST /forms/chromium/convert/markdown (Split Output Filename)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -711,6 +726,7 @@ Feature: /forms/chromium/convert/markdown
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/markdown (Split Pages) Scenario: POST /forms/chromium/convert/markdown (Split Pages)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -737,6 +753,7 @@ Feature: /forms/chromium/convert/markdown
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/markdown (Split Pages & Unify) Scenario: POST /forms/chromium/convert/markdown (Split Pages & Unify)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -763,6 +780,7 @@ Feature: /forms/chromium/convert/markdown
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/markdown (Split Many PDFs - Lot of Pages) Scenario: POST /forms/chromium/convert/markdown (Split Many PDFs - Lot of Pages)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -808,6 +826,7 @@ Feature: /forms/chromium/convert/markdown
Page 12 Page 12
""" """
@convert
Scenario: POST /forms/chromium/convert/markdown (PDF/A-1b & PDF/UA-1) Scenario: POST /forms/chromium/convert/markdown (PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -819,8 +838,10 @@ Feature: /forms/chromium/convert/markdown
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@convert
@split
Scenario: POST /forms/chromium/convert/markdown (Split & PDF/A-1b & PDF/UA-1) Scenario: POST /forms/chromium/convert/markdown (Split & PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -853,9 +874,12 @@ Feature: /forms/chromium/convert/markdown
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
# See https://github.com/gotenberg/gotenberg/issues/1130. # See https://github.com/gotenberg/gotenberg/issues/1130.
@convert
@split
@output-filename
Scenario: POST /forms/chromium/convert/markdown (Split & PDF/A-1b & PDF/UA-1 & Output Filename) Scenario: POST /forms/chromium/convert/markdown (Split & PDF/A-1b & PDF/UA-1 & Output Filename)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -890,8 +914,9 @@ Feature: /forms/chromium/convert/markdown
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@metadata
Scenario: POST /forms/chromium/convert/markdown (Metadata) Scenario: POST /forms/chromium/convert/markdown (Metadata)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -928,6 +953,7 @@ Feature: /forms/chromium/convert/markdown
} }
""" """
@flatten
Scenario: POST /forms/chromium/convert/markdown (Flatten) Scenario: POST /forms/chromium/convert/markdown (Flatten)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
@@ -939,7 +965,54 @@ Feature: /forms/chromium/convert/markdown
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Scenario: POST /forms/chromium/convert/markdown (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) @encrypt
Scenario: POST /forms/chromium/convert/markdown (Encrypt - user password only)
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/page-1-markdown/index.html | file |
| files | testdata/page-1-markdown/page_1.md | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
@encrypt
Scenario: POST /forms/chromium/convert/markdown (Encrypt - both user and owner passwords)
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/page-1-markdown/index.html | file |
| files | testdata/page-1-markdown/page_1.md | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
@embed
Scenario: POST /forms/chromium/convert/markdown (Embeds)
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/page-1-markdown/index.html | file |
| files | testdata/page-1-markdown/page_1.md | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | 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 response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert
@metadata
@flatten
@embed
Scenario: POST /forms/chromium/convert/markdown (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
Given I have a default Gotenberg container 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): 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/index.html | file |
@@ -948,15 +1021,19 @@ Feature: /forms/chromium/convert/markdown
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| flatten | true | field | | flatten | true | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
| Gotenberg-Output-Filename | foo | header | | Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 9 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
| files | teststore/foo.pdf | file | | files | teststore/foo.pdf | file |
Then the response status code should be 200 Then the response status code should be 200
@@ -1001,6 +1078,7 @@ Feature: /forms/chromium/convert/markdown
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_chromium_convert_html" | | "trace":"forms_chromium_convert_html" |
@download-from
Scenario: POST /forms/chromium/convert/markdown (Download From) Scenario: POST /forms/chromium/convert/markdown (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -1011,6 +1089,7 @@ Feature: /forms/chromium/convert/markdown
Then the file request header "X-Foo" should be "bar" Then the file request header "X-Foo" should be "bar"
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
@webhook
Scenario: POST /forms/chromium/convert/markdown (Webhook) Scenario: POST /forms/chromium/convert/markdown (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -1,6 +1,8 @@
# TODO: # TODO:
# 1. JavaScript disabled on some feature. # 1. JavaScript disabled on some feature.
@chromium
@chromium-convert-url
Feature: /forms/chromium/convert/url Feature: /forms/chromium/convert/url
Scenario: POST /forms/chromium/convert/url (Default) Scenario: POST /forms/chromium/convert/url (Default)
@@ -173,6 +175,22 @@ Feature: /forms/chromium/convert/url
Then the server request cookie "cookie_1" should be "foo" Then the server request cookie "cookie_1" should be "foo"
Then the server request cookie "cookie_2" should be "" Then the server request cookie "cookie_2" should be ""
# See https://github.com/gotenberg/gotenberg/issues/1130.
Scenario: POST /forms/chromium/convert/url (case-insensitive sameSite)
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/page-1-html/index.html | field |
| cookies | [{"name":"cookie_1","value":"foo","domain":"host.docker.internal:%d"},{"name":"cookie_2","value":"bar","domain":"domain.com","sameSite":"lax"}] | field |
| 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 server request cookie "cookie_1" should be "foo"
Then the server request cookie "cookie_2" should be ""
Scenario: POST /forms/chromium/convert/url (Wait Delay) Scenario: POST /forms/chromium/convert/url (Wait Delay)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -237,6 +255,38 @@ Feature: /forms/chromium/convert/url
Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. Wait delay > 2 seconds or expression window globalVar === 'ready' returns true.
""" """
Scenario: POST /forms/chromium/convert/url (Wait For Selector)
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/feature-rich-html-remote/index.html | field |
| 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 NOT have the following content at page 1:
"""
Wait on selector returns 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/feature-rich-html-remote/index.html | field |
| waitForSelector | #wait-selector | field |
| 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:
"""
Wait on selector returns true.
"""
Scenario: POST /forms/chromium/convert/url (Emulated Media Type) Scenario: POST /forms/chromium/convert/url (Emulated Media Type)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -504,7 +554,17 @@ Feature: /forms/chromium/convert/url
Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
Then the response body should match string: Then the response body should match string:
""" """
Chromium does not handle the page ranges 'foo' (nativePageRanges) Chromium does not handle the page ranges 'foo' (nativePageRanges) syntax
"""
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 |
| nativePageRanges | 2-3 | 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:
"""
The page ranges '2-3' (nativePageRanges) exceeds the page count
""" """
Given I have a static server 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): When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
@@ -630,6 +690,7 @@ Feature: /forms/chromium/convert/url
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
""" """
@split
Scenario: POST /forms/chromium/convert/url (Split Intervals) Scenario: POST /forms/chromium/convert/url (Split Intervals)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -659,6 +720,8 @@ Feature: /forms/chromium/convert/url
""" """
# See https://github.com/gotenberg/gotenberg/issues/1130. # See https://github.com/gotenberg/gotenberg/issues/1130.
@split
@output-filename
Scenario: POST /forms/chromium/convert/url (Split Output Filename) Scenario: POST /forms/chromium/convert/url (Split Output Filename)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -689,6 +752,7 @@ Feature: /forms/chromium/convert/url
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/url (Split Pages) Scenario: POST /forms/chromium/convert/url (Split Pages)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -713,6 +777,7 @@ Feature: /forms/chromium/convert/url
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/url (Split Pages & Unify) Scenario: POST /forms/chromium/convert/url (Split Pages & Unify)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -737,6 +802,7 @@ Feature: /forms/chromium/convert/url
Page 3 Page 3
""" """
@split
Scenario: POST /forms/chromium/convert/url (Split Many PDFs - Lot of Pages) Scenario: POST /forms/chromium/convert/url (Split Many PDFs - Lot of Pages)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -771,6 +837,7 @@ Feature: /forms/chromium/convert/url
Page 12 Page 12
""" """
@convert
Scenario: POST /forms/chromium/convert/url (PDF/A-1b & PDF/UA-1) Scenario: POST /forms/chromium/convert/url (PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -782,7 +849,7 @@ Feature: /forms/chromium/convert/url
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
Scenario: POST /forms/chromium/convert/url (Split & PDF/A-1b & PDF/UA-1) Scenario: POST /forms/chromium/convert/url (Split & PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container Given I have a default Gotenberg container
@@ -814,9 +881,12 @@ Feature: /forms/chromium/convert/url
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
# See https://github.com/gotenberg/gotenberg/issues/1130. # See https://github.com/gotenberg/gotenberg/issues/1130.
@convert
@split
@output-filename
Scenario: POST /forms/chromium/convert/url (Split & PDF/A-1b & PDF/UA-1 & Output Filename) 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 default Gotenberg container
Given I have a static server Given I have a static server
@@ -849,8 +919,9 @@ Feature: /forms/chromium/convert/url
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@metadata
Scenario: POST /forms/chromium/convert/url (Metadata) Scenario: POST /forms/chromium/convert/url (Metadata)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -887,6 +958,7 @@ Feature: /forms/chromium/convert/url
} }
""" """
@flatten
Scenario: POST /forms/chromium/convert/url (Flatten) Scenario: POST /forms/chromium/convert/url (Flatten)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -898,6 +970,53 @@ Feature: /forms/chromium/convert/url
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
@encrypt
Scenario: POST /forms/chromium/convert/url (Encrypt - user password only)
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/page-1-html/index.html | field |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
@encrypt
Scenario: POST /forms/chromium/convert/url (Encrypt - both user and owner passwords)
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/page-1-html/index.html | field |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
@embed
Scenario: POST /foo/forms/chromium/convert/url (Embeds)
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/page-1-html/index.html | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | 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 webhook request:
| foo.pdf |
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert
@metadata
@flatten
@embed
Scenario: POST /forms/chromium/convert/url (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) Scenario: POST /forms/chromium/convert/url (PDF/A-1b & PDF/UA-1 & Metadata & Flatten)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -907,15 +1026,19 @@ Feature: /forms/chromium/convert/url
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| flatten | true | field | | flatten | true | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
| Gotenberg-Output-Filename | foo | header | | Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 9 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
| files | teststore/foo.pdf | file | | files | teststore/foo.pdf | file |
Then the response status code should be 200 Then the response status code should be 200
@@ -960,6 +1083,7 @@ Feature: /forms/chromium/convert/url
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_chromium_convert_url" | | "trace":"forms_chromium_convert_url" |
@webhook
Scenario: POST /forms/chromium/convert/url (Webhook) Scenario: POST /forms/chromium/convert/url (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -1,3 +1,4 @@
@debug
Feature: /debug Feature: /debug
Scenario: GET /debug (Disabled) Scenario: GET /debug (Disabled)
@@ -108,6 +109,7 @@ Feature: /debug
"prometheus-disable-collect": "false", "prometheus-disable-collect": "false",
"prometheus-disable-route-logging": "false", "prometheus-disable-route-logging": "false",
"prometheus-namespace": "gotenberg", "prometheus-namespace": "gotenberg",
"prometheus-metrics-path": "/prometheus/metrics",
"webhook-allow-list": "", "webhook-allow-list": "",
"webhook-client-timeout": "30s", "webhook-client-timeout": "30s",
"webhook-deny-list": "", "webhook-deny-list": "",

View File

@@ -2,6 +2,7 @@
# 1. Check if down for each module. # 1. Check if down for each module.
# 2. Restarting modules do not make health check fail. # 2. Restarting modules do not make health check fail.
@health
Feature: /health Feature: /health
Scenario: GET /health Scenario: GET /health

View File

@@ -1,3 +1,5 @@
@libreoffice
@libreoffice-convert
Feature: /forms/libreoffice/convert Feature: /forms/libreoffice/convert
Scenario: POST /forms/libreoffice/convert (Single Document) Scenario: POST /forms/libreoffice/convert (Single Document)
@@ -272,6 +274,7 @@ Feature: /forms/libreoffice/convert
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
""" """
@merge
Scenario: POST /forms/libreoffice/convert (Merge) Scenario: POST /forms/libreoffice/convert (Merge)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -294,6 +297,8 @@ Feature: /forms/libreoffice/convert
Page 2 Page 2
""" """
@merge
@split
Scenario: POST /forms/libreoffice/convert (Merge & Split) Scenario: POST /forms/libreoffice/convert (Merge & Split)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -319,6 +324,7 @@ Feature: /forms/libreoffice/convert
Page 2 Page 2
""" """
@split
Scenario: POST /forms/libreoffice/convert (Split Intervals) Scenario: POST /forms/libreoffice/convert (Split Intervals)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -346,6 +352,7 @@ Feature: /forms/libreoffice/convert
Page 3 Page 3
""" """
@split
Scenario: POST /forms/libreoffice/convert (Split Pages) Scenario: POST /forms/libreoffice/convert (Split Pages)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -369,6 +376,7 @@ Feature: /forms/libreoffice/convert
Page 3 Page 3
""" """
@split
Scenario: POST /forms/libreoffice/convert (Split Pages & Unify) Scenario: POST /forms/libreoffice/convert (Split Pages & Unify)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -391,6 +399,7 @@ Feature: /forms/libreoffice/convert
Page 3 Page 3
""" """
@split
Scenario: POST /forms/libreoffice/convert (Split Many PDFs - Lot of Pages) Scenario: POST /forms/libreoffice/convert (Split Many PDFs - Lot of Pages)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -438,6 +447,7 @@ Feature: /forms/libreoffice/convert
Page 12 Page 12
""" """
@convert
Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1) Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -448,8 +458,10 @@ Feature: /forms/libreoffice/convert
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@split
@convert
Scenario: POST /forms/libreoffice/convert (Split & PDF/A-1b & PDF/UA-1) Scenario: POST /forms/libreoffice/convert (Split & PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -479,8 +491,9 @@ Feature: /forms/libreoffice/convert
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@metadata
Scenario: POST /forms/libreoffice/convert (Metadata) Scenario: POST /forms/libreoffice/convert (Metadata)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -516,6 +529,7 @@ Feature: /forms/libreoffice/convert
} }
""" """
@flatten
Scenario: POST /forms/libreoffice/convert (Flatten) Scenario: POST /forms/libreoffice/convert (Flatten)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
@@ -526,7 +540,51 @@ Feature: /forms/libreoffice/convert
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) @encrypt
Scenario: POST /forms/libreoffice/convert (Encrypt - user password only)
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/page_1.docx | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
@encrypt
Scenario: POST /forms/libreoffice/convert (Encrypt - both user and owner passwords)
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/page_1.docx | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
@embed
Scenario: POST /forms/libreoffice/convert (Embeds)
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/page_1.docx | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | 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 response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert
@metadata
@flatten
@embed
Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
| files | testdata/page_1.docx | file | | files | testdata/page_1.docx | file |
@@ -534,15 +592,19 @@ Feature: /forms/libreoffice/convert
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| flatten | true | field | | flatten | true | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
| Gotenberg-Output-Filename | foo | header | | Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 10 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
| files | teststore/foo.pdf | file | | files | teststore/foo.pdf | file |
Then the response status code should be 200 Then the response status code should be 200
@@ -585,6 +647,7 @@ Feature: /forms/libreoffice/convert
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_libreoffice_convert" | | "trace":"forms_libreoffice_convert" |
@download-from
Scenario: POST /forms/libreoffice/convert (Download From) Scenario: POST /forms/libreoffice/convert (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -594,6 +657,7 @@ Feature: /forms/libreoffice/convert
Then the file request header "X-Foo" should be "bar" Then the file request header "X-Foo" should be "bar"
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
@webhook
Scenario: POST /forms/libreoffice/convert (Webhook) Scenario: POST /forms/libreoffice/convert (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -1,3 +1,4 @@
@output-filename
Feature: Output Filename Feature: Output Filename
Scenario: Default (Single Output File) Scenario: Default (Single Output File)

View File

@@ -1,6 +1,8 @@
# TODO: # TODO:
# 1. PDF/UA-2. # 1. PDF/UA-2.
@pdfengines
@pdfengines-convert
Feature: /forms/pdfengines/convert Feature: /forms/pdfengines/convert
Scenario: POST /forms/pdfengines/convert (Single PDF/A-1b) Scenario: POST /forms/pdfengines/convert (Single PDF/A-1b)
@@ -53,7 +55,7 @@ Feature: /forms/pdfengines/convert
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
Scenario: POST /forms/pdfengines/convert (Many PDFs) Scenario: POST /forms/pdfengines/convert (Many PDFs)
Given I have a default Gotenberg container Given I have a default Gotenberg container
@@ -115,6 +117,7 @@ Feature: /forms/pdfengines/convert
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_convert" | | "trace":"forms_pdfengines_convert" |
@output-filename
Scenario: POST /forms/pdfengines/convert (Output Filename - Single PDF) Scenario: POST /forms/pdfengines/convert (Output Filename - Single PDF)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s):
@@ -126,6 +129,7 @@ Feature: /forms/pdfengines/convert
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
@output-filename
Scenario: POST /forms/pdfengines/convert (Output Filename - Many PDFs) Scenario: POST /forms/pdfengines/convert (Output Filename - Many PDFs)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s):
@@ -140,6 +144,7 @@ Feature: /forms/pdfengines/convert
| page_1.pdf | | page_1.pdf |
| page_2.pdf | | page_2.pdf |
@download-from
Scenario: POST /forms/pdfengines/convert (Download From) Scenario: POST /forms/pdfengines/convert (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -150,6 +155,7 @@ Feature: /forms/pdfengines/convert
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
@webhook
Scenario: POST /forms/pdfengines/convert (Webhook) Scenario: POST /forms/pdfengines/convert (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -0,0 +1,72 @@
@pdfengines
@pdfengines-embed
@embed
Feature: /forms/pdfengines/embed
Scenario: POST /forms/pdfengines/embed
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/embed" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
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:
| page_1.pdf |
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
@download-from
Scenario: POST /forms/pdfengines/embed with (Download From)
Given I have a default Gotenberg container
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/embed" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/embed_1.xml","embedded": true},{"url":"http://host.docker.internal:%d/static/testdata/embed_2.xml","embedded": false}] | field |
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:
| page_1.pdf |
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should NOT have the "embed_2.xml" file embedded
@webhook
Scenario: POST /forms/pdfengines/embed (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/embed" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | 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
Then the webhook request PDF(s) should have the "embed_1.xml" file embedded
Then the webhook request PDF(s) should have the "embed_2.xml" file embedded
Scenario: POST /forms/pdfengines/embed (Basic Auth)
Given I have a Gotenberg container with the following environment variable(s):
| API_ENABLE_BASIC_AUTH | true |
| GOTENBERG_API_BASIC_AUTH_USERNAME | foo |
| GOTENBERG_API_BASIC_AUTH_PASSWORD | bar |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/embed" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
Then the response status code should be 401
Scenario: POST /foo/forms/pdfengines/embed (Root Path)
Given I have a Gotenberg container with the following environment variable(s):
| API_ENABLE_DEBUG_ROUTE | true |
| API_ROOT_PATH | /foo/ |
When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/embed" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"

View File

@@ -0,0 +1,182 @@
@pdfengines
@pdfengines-encrypt
@encrypt
Feature: /forms/pdfengines/encrypt
Scenario: POST /forms/pdfengines/encrypt (default - user password only)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (default - both user and owner passwords)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (QPDF - user password only)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_ENCRYPT_ENGINES | qpdf |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (QPDF - both user and owner passwords)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_ENCRYPT_ENGINES | qpdf |
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (PDFtk - user password only)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_ENCRYPT_ENGINES | pdftk |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
Then the response status code should be 400
Then the response body should match string:
"""
pdftk: both 'userPassword' and 'ownerPassword' must be provided and different. Consider switching to another PDF engine if this behavior does not work with your workflow
"""
Scenario: POST /forms/pdfengines/encrypt (PDFtk - both user and owner passwords)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_ENCRYPT_ENGINES | pdftk |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (pdfcpu - user password only)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_ENCRYPT_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (pdfcpu - both user and owner passwords)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_ENCRYPT_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (Many PDFs)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| files | testdata/page_2.pdf | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (Bad Request)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
Then the response status code should be 400
Then the response body should match string:
"""
Invalid form data: form field 'userPassword' is required
"""
Scenario: POST /forms/pdfengines/encrypt (Routes Disabled)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_DISABLE_ROUTES | true |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
Then the response status code should be 404
Scenario: POST /forms/pdfengines/encrypt (Gotenberg Trace)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
| Gotenberg-Trace | forms_pdfengines_encrypt | header |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
Then the response header "Gotenberg-Trace" should be "forms_pdfengines_encrypt"
Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_encrypt" |
@download-from
Scenario: POST /forms/pdfengines/encrypt (Download From)
Given I have a default Gotenberg container
Given I have a static server
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
@webhook
Scenario: POST /forms/pdfengines/encrypt (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/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
| 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
Then the response PDF(s) should be encrypted
Scenario: POST /forms/pdfengines/encrypt (Basic Auth)
Given I have a Gotenberg container with the following environment variable(s):
| API_ENABLE_BASIC_AUTH | true |
| GOTENBERG_API_BASIC_AUTH_USERNAME | foo |
| GOTENBERG_API_BASIC_AUTH_PASSWORD | bar |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
Then the response status code should be 401
Scenario: POST /foo/forms/pdfengines/encrypt (Root Path)
Given I have a Gotenberg container with the following environment variable(s):
| API_ENABLE_DEBUG_ROUTE | true |
| API_ROOT_PATH | /foo/ |
When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/encrypt" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| userPassword | foo | field |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"

View File

@@ -1,3 +1,6 @@
@pdfengines
@pdfengines-flatten
@flatten
Feature: /forms/pdfengines/flatten Feature: /forms/pdfengines/flatten
Scenario: POST /forms/pdfengines/flatten (Single PDF) Scenario: POST /forms/pdfengines/flatten (Single PDF)
@@ -48,6 +51,7 @@ Feature: /forms/pdfengines/flatten
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_flatten" | | "trace":"forms_pdfengines_flatten" |
@output-filename
Scenario: POST /forms/pdfengines/flatten (Output Filename - Single PDF) Scenario: POST /forms/pdfengines/flatten (Output Filename - Single PDF)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s):
@@ -58,6 +62,7 @@ Feature: /forms/pdfengines/flatten
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
@output-filename
Scenario: POST /forms/pdfengines/flatten (Output Filename - Many PDFs) Scenario: POST /forms/pdfengines/flatten (Output Filename - Many PDFs)
Given I have a default Gotenberg container 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): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s):
@@ -71,6 +76,7 @@ Feature: /forms/pdfengines/flatten
| page_1.pdf | | page_1.pdf |
| page_2.pdf | | page_2.pdf |
@download-from
Scenario: POST /forms/pdfengines/flatten (Download From) Scenario: POST /forms/pdfengines/flatten (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -79,7 +85,9 @@ Feature: /forms/pdfengines/flatten
Then the file request header "X-Foo" should be "bar" Then the file request header "X-Foo" should be "bar"
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
Then the response PDF(s) should be flatten
@webhook
Scenario: POST /forms/pdfengines/flatten (Webhook) Scenario: POST /forms/pdfengines/flatten (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -1,6 +1,9 @@
@pdfengines
@pdfengines-encrypt
@merge
Feature: /forms/pdfengines/merge Feature: /forms/pdfengines/merge
Scenario: POST /forms/pdfengines/merge (default - QPDF) Scenario: POST /forms/pdfengines/merge (default)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file | | files | testdata/page_1.pdf | file |
@@ -21,6 +24,28 @@ Feature: /forms/pdfengines/merge
Page 2 Page 2
""" """
Scenario: POST /forms/pdfengines/merge (QPDF)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_MERGE_ENGINES | qpdf |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" 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/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 2 page(s)
Then the "foo.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "foo.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Scenario: POST /forms/pdfengines/merge (pdfcpu) Scenario: POST /forms/pdfengines/merge (pdfcpu)
Given I have a Gotenberg container with the following environment variable(s): Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_MERGE_ENGINES | pdfcpu | | PDFENGINES_MERGE_ENGINES | pdfcpu |
@@ -106,6 +131,7 @@ Feature: /forms/pdfengines/merge
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
""" """
@convert
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1) Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
@@ -129,8 +155,9 @@ Feature: /forms/pdfengines/merge
Page 2 Page 2
""" """
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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@metadata
Scenario: POST /forms/pdfengines/merge (Metadata) Scenario: POST /forms/pdfengines/merge (Metadata)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
@@ -176,6 +203,7 @@ Feature: /forms/pdfengines/merge
} }
""" """
@flatten
Scenario: POST /forms/pdfengines/merge (Flatten) Scenario: POST /forms/pdfengines/merge (Flatten)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
@@ -199,7 +227,50 @@ Feature: /forms/pdfengines/merge
""" """
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) @encrypt
Scenario: POST /forms/pdfengines/merge (Encrypt - user password only)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| files | testdata/page_2.pdf | file |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
@encrypt
Scenario: POST /forms/pdfengines/merge (Encrypt - both user and owner passwords)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| files | testdata/page_2.pdf | file |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
@embed
Scenario: POST /foo/forms/pdfengines/merge (Embeds)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| files | testdata/page_2.pdf | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
| Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert
@metadata
@flatten
@embed
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file | | files | testdata/page_1.pdf | file |
@@ -208,6 +279,8 @@ Feature: /forms/pdfengines/merge
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| flatten | true | field | | flatten | true | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
| Gotenberg-Output-Filename | foo | header | | Gotenberg-Output-Filename | foo | header |
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
@@ -223,9 +296,11 @@ Feature: /forms/pdfengines/merge
""" """
Page 2 Page 2
""" """
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 10 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
| files | teststore/foo.pdf | file | | files | teststore/foo.pdf | file |
Then the response status code should be 200 Then the response status code should be 200
@@ -270,6 +345,7 @@ Feature: /forms/pdfengines/merge
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_merge" | | "trace":"forms_pdfengines_merge" |
@download-from
Scenario: POST /forms/pdfengines/merge (Download From) Scenario: POST /forms/pdfengines/merge (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -278,6 +354,7 @@ Feature: /forms/pdfengines/merge
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
@webhook
Scenario: POST /forms/pdfengines/merge (Webhook) Scenario: POST /forms/pdfengines/merge (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -1,3 +1,6 @@
@pdfengines
@pdfengines-metadata
@metadata
Feature: /forms/pdfengines/{write|read} Feature: /forms/pdfengines/{write|read}
Scenario: POST /forms/pdfengines/metadata/{write|read} (Single PDF) Scenario: POST /forms/pdfengines/metadata/{write|read} (Single PDF)
@@ -151,6 +154,7 @@ Feature: /forms/pdfengines/{write|read}
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_metadata_read" | | "trace":"forms_pdfengines_metadata_read" |
@output-filename
Scenario: POST /forms/pdfengines/metadata/write (Output Filename - Single PDF) Scenario: POST /forms/pdfengines/metadata/write (Output Filename - Single PDF)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s):
@@ -162,6 +166,7 @@ Feature: /forms/pdfengines/{write|read}
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
@output-filename
Scenario: POST /forms/pdfengines/metadata/write (Output Filename - Many PDFs) Scenario: POST /forms/pdfengines/metadata/write (Output Filename - Many PDFs)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s):
@@ -176,6 +181,7 @@ Feature: /forms/pdfengines/{write|read}
| page_1.pdf | | page_1.pdf |
| page_2.pdf | | page_2.pdf |
@download-from
Scenario: POST /forms/pdfengines/metadata/write (Download From) Scenario: POST /forms/pdfengines/metadata/write (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -187,6 +193,7 @@ Feature: /forms/pdfengines/{write|read}
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf" Then the response header "Content-Type" should be "application/pdf"
@download-from
Scenario: POST /forms/pdfengines/metadata/read (Download From) Scenario: POST /forms/pdfengines/metadata/read (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -196,6 +203,7 @@ Feature: /forms/pdfengines/{write|read}
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/json" Then the response header "Content-Type" should be "application/json"
@webhook
Scenario: POST /forms/pdfengines/metadata/write (Webhook) Scenario: POST /forms/pdfengines/metadata/write (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server
@@ -233,6 +241,7 @@ Feature: /forms/pdfengines/{write|read}
} }
""" """
@webhook
Scenario: POST /forms/pdfengines/metadata/read (Webhook) Scenario: POST /forms/pdfengines/metadata/read (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -1,6 +1,9 @@
@pdfengines
@pdfengines-split
@split
Feature: /forms/pdfengines/split Feature: /forms/pdfengines/split
Scenario: POST /forms/pdfengines/split (Intervals - Default - pdfcpu) Scenario: POST /forms/pdfengines/split (Intervals - Default)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file | | files | testdata/pages_3.pdf | file |
@@ -27,7 +30,7 @@ Feature: /forms/pdfengines/split
Page 3 Page 3
""" """
Scenario: POST /forms/pdfengines/split (Pages - Default - pdfcpu) Scenario: POST /forms/pdfengines/split (Pages - Default)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file | | files | testdata/pages_3.pdf | file |
@@ -50,7 +53,7 @@ Feature: /forms/pdfengines/split
Page 3 Page 3
""" """
Scenario: POST /forms/pdfengines/split (Pages & Unify - Default - pdfcpu) Scenario: POST /forms/pdfengines/split (Pages & Unify - Default)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file | | files | testdata/pages_3.pdf | file |
@@ -72,6 +75,81 @@ Feature: /forms/pdfengines/split
Page 3 Page 3
""" """
Scenario: POST /forms/pdfengines/split (Intervals - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_SPLIT_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
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:
| pages_3_0.pdf |
| pages_3_1.pdf |
Then the "pages_3_0.pdf" PDF should have 2 page(s)
Then the "pages_3_1.pdf" PDF should have 1 page(s)
Then the "pages_3_0.pdf" PDF should have the following content at page 1:
"""
Page 1
"""
Then the "pages_3_0.pdf" PDF should have the following content at page 2:
"""
Page 2
"""
Then the "pages_3_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Scenario: POST /forms/pdfengines/split (Pages - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_SPLIT_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| splitMode | pages | field |
| splitSpan | 2- | field |
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:
| pages_3_0.pdf |
| pages_3_1.pdf |
Then the "pages_3_0.pdf" PDF should have 1 page(s)
Then the "pages_3_1.pdf" PDF should have 1 page(s)
Then the "pages_3_0.pdf" PDF should have the following content at page 1:
"""
Page 2
"""
Then the "pages_3_1.pdf" PDF should have the following content at page 1:
"""
Page 3
"""
Scenario: POST /forms/pdfengines/split (Pages & Unify - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_SPLIT_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| splitMode | pages | field |
| splitSpan | 2- | field |
| splitUnify | true | field |
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:
| pages_3.pdf |
Then the "pages_3.pdf" PDF should have 2 page(s)
Then the "pages_3.pdf" PDF should have the following content at page 1:
"""
Page 2
"""
Then the "pages_3.pdf" PDF should have the following content at page 2:
"""
Page 3
"""
Scenario: POST /forms/pdfengines/split (Pages & Unify - QPDF) Scenario: POST /forms/pdfengines/split (Pages & Unify - QPDF)
Given I have a Gotenberg container with the following environment variable(s): Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_SPLIT_ENGINES | qpdf | | PDFENGINES_SPLIT_ENGINES | qpdf |
@@ -239,6 +317,7 @@ Feature: /forms/pdfengines/split
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
""" """
@convert
Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1) Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
@@ -268,8 +347,9 @@ Feature: /forms/pdfengines/split
Page 3 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/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) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
@metadata
Scenario: POST /forms/pdfengines/split (Metadata) Scenario: POST /forms/pdfengines/split (Metadata)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
@@ -336,6 +416,7 @@ Feature: /forms/pdfengines/split
} }
""" """
@flatten
Scenario: POST /forms/pdfengines/split (Flatten) Scenario: POST /forms/pdfengines/split (Flatten)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
@@ -365,7 +446,57 @@ Feature: /forms/pdfengines/split
""" """
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) @encrypt
Scenario: POST /forms/pdfengines/split (Encrypt - user password only)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| userPassword | foo | field |
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 the response PDF(s) should be encrypted
@encrypt
Scenario: POST /forms/pdfengines/split (Encrypt - both user and owner passwords)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
| userPassword | foo | field |
| ownerPassword | bar | field |
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 the response PDF(s) should be encrypted
@embed
Scenario: POST /foo/forms/pdfengines/split (Embeds)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
| splitMode | intervals | field |
| splitSpan | 2 | field |
Then the response status code should be 200
And the response header "Content-Type" should be "application/zip"
And there should be 2 PDF(s) in the response
And there should be the following file(s) in the response:
| pages_3_0.pdf |
| pages_3_1.pdf |
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
# FIXME: once decrypt is done, add encrypt and check after the content of the PDFs.
@convert
@metadata
@flatten
@embed
Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file | | files | testdata/pages_3.pdf | file |
@@ -375,6 +506,8 @@ Feature: /forms/pdfengines/split
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| flatten | true | field | | flatten | true | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
Then the response status code should be 200 Then the response status code should be 200
Then the response header "Content-Type" should be "application/zip" Then the response header "Content-Type" should be "application/zip"
Then there should be 2 PDF(s) in the response Then there should be 2 PDF(s) in the response
@@ -395,9 +528,11 @@ Feature: /forms/pdfengines/split
""" """
Page 3 Page 3
""" """
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 10 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
| files | teststore/pages_3_0.pdf | file | | files | teststore/pages_3_0.pdf | file |
| files | teststore/pages_3_1.pdf | file | | files | teststore/pages_3_1.pdf | file |
@@ -459,6 +594,7 @@ Feature: /forms/pdfengines/split
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_split" | | "trace":"forms_pdfengines_split" |
@output-filename
Scenario: POST /forms/pdfengines/split (Output Filename - Single PDF) Scenario: POST /forms/pdfengines/split (Output Filename - Single PDF)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
@@ -472,6 +608,7 @@ Feature: /forms/pdfengines/split
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
@output-filename
Scenario: POST /forms/pdfengines/split (Output Filename - Many PDFs) Scenario: POST /forms/pdfengines/split (Output Filename - Many PDFs)
Given I have a default Gotenberg container Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
@@ -486,6 +623,7 @@ Feature: /forms/pdfengines/split
| pages_3_0.pdf | | pages_3_0.pdf |
| pages_3_1.pdf | | pages_3_1.pdf |
@download-from
Scenario: POST /forms/pdfengines/split (Download From) Scenario: POST /forms/pdfengines/split (Download From)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a static server Given I have a static server
@@ -497,6 +635,7 @@ Feature: /forms/pdfengines/split
Then the file request header "X-Foo" should be "bar" Then the file request header "X-Foo" should be "bar"
Then the response header "Content-Type" should be "application/zip" Then the response header "Content-Type" should be "application/zip"
@webhook
Scenario: POST /forms/pdfengines/split (Webhook) Scenario: POST /forms/pdfengines/split (Webhook)
Given I have a default Gotenberg container Given I have a default Gotenberg container
Given I have a webhook server Given I have a webhook server

View File

@@ -2,6 +2,7 @@
# 1. Count restarts. # 1. Count restarts.
# 2. Count queue size. # 2. Count queue size.
@prometheus-metrics
Feature: /prometheus/metrics Feature: /prometheus/metrics
Scenario: GET /prometheus/metrics (Enabled) Scenario: GET /prometheus/metrics (Enabled)
@@ -28,6 +29,31 @@ Feature: /prometheus/metrics
Then the Gotenberg container should log the following entries: Then the Gotenberg container should log the following entries:
| "path":"/prometheus/metrics" | | "path":"/prometheus/metrics" |
Scenario: GET /custom/metrics (Custom Metrics Path)
Given I have a Gotenberg container with the following environment variable(s):
| PROMETHEUS_METRICS_PATH | /custom/metrics |
When I make a "GET" request to Gotenberg at the "/custom/metrics" endpoint
Then the response status code should be 200
Then the response header "Content-Type" should be "text/plain; version=0.0.4; charset=utf-8; escaping=underscores"
Then the response body should match string:
"""
# HELP gotenberg_chromium_requests_queue_size Current number of Chromium conversion requests waiting to be treated.
# TYPE gotenberg_chromium_requests_queue_size gauge
gotenberg_chromium_requests_queue_size 0
# HELP gotenberg_chromium_restarts_count Current number of Chromium restarts.
# TYPE gotenberg_chromium_restarts_count gauge
gotenberg_chromium_restarts_count 0
# HELP gotenberg_libreoffice_requests_queue_size Current number of LibreOffice conversion requests waiting to be treated.
# TYPE gotenberg_libreoffice_requests_queue_size gauge
gotenberg_libreoffice_requests_queue_size 0
# HELP gotenberg_libreoffice_restarts_count Current number of LibreOffice restarts.
# TYPE gotenberg_libreoffice_restarts_count gauge
gotenberg_libreoffice_restarts_count 0
"""
Then the Gotenberg container should log the following entries:
| "path":"/custom/metrics" |
Scenario: GET /prometheus/metrics (Custom Namespace) Scenario: GET /prometheus/metrics (Custom Namespace)
Given I have a Gotenberg container with the following environment variable(s): Given I have a Gotenberg container with the following environment variable(s):
| PROMETHEUS_NAMESPACE | foo | | PROMETHEUS_NAMESPACE | foo |

View File

@@ -1,3 +1,4 @@
@root
Feature: / Feature: /
Scenario: GET / Scenario: GET /

View File

@@ -1,3 +1,4 @@
@version
Feature: /version Feature: /version
Scenario: GET /version Scenario: GET /version

View File

@@ -2,6 +2,7 @@
# 1. Other HTTP Methods # 1. Other HTTP Methods
# 2. Errors # 2. Errors
@webhook
Feature: Webhook Feature: Webhook
Scenario: Default Scenario: Default

View File

@@ -19,6 +19,7 @@ func TestMain(m *testing.M) {
version := flag.String("gotenberg-version", "", "") version := flag.String("gotenberg-version", "", "")
platform := flag.String("gotenberg-container-platform", "", "") platform := flag.String("gotenberg-container-platform", "", "")
noConcurrency := flag.Bool("no-concurrency", false, "") noConcurrency := flag.Bool("no-concurrency", false, "")
tags := flag.String("tags", "", "")
flag.Parse() flag.Parse()
if *platform == "" { if *platform == "" {
@@ -47,6 +48,7 @@ func TestMain(m *testing.M) {
Paths: []string{"features"}, Paths: []string{"features"},
Output: colors.Colored(os.Stdout), Output: colors.Colored(os.Stdout),
Concurrency: concurrency, Concurrency: concurrency,
Tags: *tags,
}, },
}.Run() }.Run()

View File

@@ -580,6 +580,7 @@ func (s *scenario) thePdfsShouldBeValidWithAToleranceOf(ctx context.Context, kin
return fmt.Errorf("unknown %q", validate) return fmt.Errorf("unknown %q", validate)
} }
re := regexp.MustCompile(`failedRules="(\d+)"`)
for _, path := range paths { for _, path := range paths {
cmd := []string{ cmd := []string{
"verapdf", "verapdf",
@@ -593,9 +594,7 @@ func (s *scenario) thePdfsShouldBeValidWithAToleranceOf(ctx context.Context, kin
return fmt.Errorf("exec %q: %w", cmd, err) return fmt.Errorf("exec %q: %w", cmd, err)
} }
re := regexp.MustCompile(`failedRules="(\d+)"`)
matches := re.FindStringSubmatch(output) matches := re.FindStringSubmatch(output)
if len(matches) < 2 { if len(matches) < 2 {
return errors.New("expected failed rules") return errors.New("expected failed rules")
} }
@@ -812,6 +811,8 @@ func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should stri
return fmt.Errorf("walk %q: %w", s.workdir, err) return fmt.Errorf("walk %q: %w", s.workdir, err)
} }
invert := should == "should NOT"
for _, path := range paths { for _, path := range paths {
cmd := []string{ cmd := []string{
"verapdf", "verapdf",
@@ -826,7 +827,6 @@ func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should stri
return fmt.Errorf("exec %q: %w", cmd, err) return fmt.Errorf("exec %q: %w", cmd, err)
} }
invert := should == "should NOT"
if invert && strings.Contains(output, "<featuresReport></featuresReport>") { if invert && strings.Contains(output, "<featuresReport></featuresReport>") {
return fmt.Errorf("PDF %q is flatten", path) return fmt.Errorf("PDF %q is flatten", path)
} }
@@ -839,6 +839,112 @@ func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should stri
return nil return nil
} }
func (s *scenario) thePdfsShouldBeEncrypted(ctx context.Context, kind string, should string) error {
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
_, err := os.Stat(dirPath)
if os.IsNotExist(err) {
return fmt.Errorf("directory %q does not exist", dirPath)
}
var paths []string
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, pathErr error) error {
if pathErr != nil {
return pathErr
}
if strings.EqualFold(filepath.Ext(info.Name()), ".pdf") {
paths = append(paths, path)
}
return nil
})
if err != nil {
return fmt.Errorf("walk %q: %w", dirPath, err)
}
invert := should == "should NOT"
re := regexp.MustCompile(`CommandLineError:Incorrectpassword`)
for _, path := range paths {
cmd := []string{
"pdfinfo",
filepath.Base(path),
}
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
if err != nil {
return fmt.Errorf("exec %q: %w", cmd, err)
}
output = strings.ReplaceAll(output, " ", "")
output = strings.ReplaceAll(output, "\n", "")
matches := re.FindStringSubmatch(output)
isEncrypted := len(matches) >= 1 && matches[0] == "CommandLineError:Incorrectpassword"
if invert && isEncrypted {
return fmt.Errorf("PDF %q is encrypted", path)
}
if !invert && !isEncrypted {
return fmt.Errorf("PDF %q is not encrypted: %q", path, output)
}
}
return nil
}
func (s *scenario) thePdfsShouldHaveEmbeddedFile(ctx context.Context, kind, should, embed string) error {
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
_, err := os.Stat(dirPath)
if os.IsNotExist(err) {
return fmt.Errorf("directory %q does not exist", dirPath)
}
var paths []string
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, pathErr error) error {
if pathErr != nil {
return pathErr
}
if strings.EqualFold(filepath.Ext(info.Name()), ".pdf") {
paths = append(paths, path)
}
return nil
})
if err != nil {
return fmt.Errorf("walk %q: %w", dirPath, err)
}
invert := should == "should NOT"
for _, path := range paths {
cmd := []string{
"verapdf",
"--off",
"--loglevel",
"0",
"--extract",
"embeddedFile",
filepath.Base(path),
}
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
if err != nil {
return fmt.Errorf("exec %q: %w", cmd, err)
}
found := strings.Contains(output, fmt.Sprintf("<fileName>%s</fileName>", embed))
if invert && found {
return fmt.Errorf("embed %q found", embed)
}
if !invert && !found {
return fmt.Errorf("embed %q not found", embed)
}
}
return nil
}
func InitializeScenario(ctx *godog.ScenarioContext) { func InitializeScenario(ctx *godog.ScenarioContext) {
s := &scenario{} s := &scenario{}
ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) { ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
@@ -871,6 +977,8 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Then(`^there should be the following file\(s\) in the (response|webhook request):$`, s.thereShouldBeTheFollowingFiles) ctx.Then(`^there should be the following file\(s\) in the (response|webhook request):$`, s.thereShouldBeTheFollowingFiles)
ctx.Then(`^the (response|webhook request) PDF\(s\) should be valid "([^"]*)" with a tolerance of (\d+) failed rule\(s\)$`, s.thePdfsShouldBeValidWithAToleranceOf) ctx.Then(`^the (response|webhook request) PDF\(s\) should be valid "([^"]*)" with a tolerance of (\d+) failed rule\(s\)$`, s.thePdfsShouldBeValidWithAToleranceOf)
ctx.Then(`^the (response|webhook request) PDF\(s\) (should|should NOT) be flatten$`, s.thePdfsShouldBeFlatten) ctx.Then(`^the (response|webhook request) PDF\(s\) (should|should NOT) be flatten$`, s.thePdfsShouldBeFlatten)
ctx.Then(`^the (response|webhook request) PDF\(s\) (should|should NOT) be encrypted`, s.thePdfsShouldBeEncrypted)
ctx.Then(`^the (response|webhook request) PDF\(s\) (should|should NOT) have the "([^"]*)" file embedded$`, s.thePdfsShouldHaveEmbeddedFile)
ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages) ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)

5
test/integration/testdata/embed_1.xml vendored Normal file
View File

@@ -0,0 +1,5 @@
<xml>
<test>test 1.1</test>
<test>test 1.2</test>
<test>test 1.3</test>
</xml>

5
test/integration/testdata/embed_2.xml vendored Normal file
View File

@@ -0,0 +1,5 @@
<xml>
<test>test 2.1</test>
<test>test 2.2</test>
<test>test 2.3</test>
</xml>

View File

@@ -37,8 +37,14 @@
const delay = (ms) => new Promise((res) => setTimeout(res, ms)); const delay = (ms) => new Promise((res) => setTimeout(res, ms));
delay(2000).then(() => { delay(2000).then(() => {
document.getElementById("wait").style.display = "contents"; const waitText = document.getElementById("wait");
waitText.style.display = "contents";
window.globalVar = "ready"; window.globalVar = "ready";
const newText = document.createElement("p");
newText.id = "wait-selector";
newText.textContent = "Wait on selector returns true.";
waitText.parentNode.insertBefore(newText, waitText);
}); });
</script> </script>

View File

@@ -46,8 +46,14 @@
const delay = (ms) => new Promise((res) => setTimeout(res, ms)); const delay = (ms) => new Promise((res) => setTimeout(res, ms));
delay(2000).then(() => { delay(2000).then(() => {
document.getElementById("wait").style.display = "contents"; const waitText = document.getElementById("wait");
waitText.style.display = "contents";
window.globalVar = "ready"; window.globalVar = "ready";
const newText = document.createElement("p");
newText.id = "wait-selector";
newText.textContent = "Wait on selector returns true.";
waitText.parentNode.insertBefore(newText, waitText);
}); });
</script> </script>