mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3113e034f6 | ||
|
|
85291fdec3 | ||
|
|
57e1b7efda | ||
|
|
be78a71bb7 | ||
|
|
2baa59cb3a | ||
|
|
aea7c5952a | ||
|
|
cbde321d3b | ||
|
|
aa5de988cf | ||
|
|
fd485a0d3e | ||
|
|
aea06a98b7 | ||
|
|
82401bdfdd | ||
|
|
12c25a2d21 | ||
|
|
241d5077c9 | ||
|
|
37757315d0 | ||
|
|
1195c37508 | ||
|
|
3d9b2deb59 | ||
|
|
32d948554f | ||
|
|
b7a6b7aba2 | ||
|
|
05e15a1d06 | ||
|
|
f57ecb6ef2 | ||
|
|
45318497d2 | ||
|
|
b762143d75 | ||
|
|
ba1a730d3a | ||
|
|
463bb2f91c | ||
|
|
e14cab0c8b | ||
|
|
3220ca4140 | ||
|
|
92de0cf6fe | ||
|
|
d111591c8b | ||
|
|
935686b9e3 | ||
|
|
b1ad21c7d3 | ||
|
|
4f800c8a76 | ||
|
|
5c498219ed | ||
|
|
0b211c39eb | ||
|
|
77b9776d9c | ||
|
|
04e596b616 | ||
|
|
9fba76fe5c |
2
.github/workflows/continuous-integration.yml
vendored
2
.github/workflows/continuous-integration.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.5.0
|
||||
version: v2.10.1
|
||||
|
||||
lint-prettier:
|
||||
name: Lint non-Golang codebase
|
||||
|
||||
66
AGENTS.md
Normal file
66
AGENTS.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Operational Guidelines for Gotenberg
|
||||
|
||||
As an AI agent working on the Gotenberg repository, you are expected to act with the diligence and architectural foresight of a Senior Go Engineer. Gotenberg is a widely used production dependency; stability and backward compatibility are paramount.
|
||||
|
||||
## 1. Core Philosophy & Stability
|
||||
|
||||
- **Backward Compatibility is Law:** This project creates a public API. Never modify existing flags, configuration environment variables, or API form fields unless explicitly instructed to perform a breaking change. If a change is breaking, it must be flagged immediately in the plan.
|
||||
- **Defensive Programming:** Assume input data is malformed. Handle errors explicitly. Do not panic.
|
||||
- **Atomic Commits:** Isolate refactoring from feature additions. A Pull Request should do one thing well.
|
||||
|
||||
## 2. Development Workflow & Tooling
|
||||
|
||||
You must rely strictly on the project's Makefile for build and verification tasks. Do not run `go` commands directly unless debugging a specific package requires it.
|
||||
|
||||
- **Formatting:** Run `make fmt` to format Go code before committing.
|
||||
- **Linting:**
|
||||
- Run `make lint` to ensure Go code strictly adheres to the `.golangci.yml` configuration.
|
||||
- Run `make lint-prettier` to verify formatting for non-Go files (Markdown, YAML, etc.).
|
||||
- Zero linting errors are permitted.
|
||||
- **Building:** Run `make build` to verify compilation and Docker image construction.
|
||||
|
||||
## 3. Architecture & Code Structure
|
||||
|
||||
- **Idiomatic Go:** Follow "Effective Go" principles.
|
||||
- **Directory Separation:**
|
||||
- `cmd/`: Application entry points only. Contains wiring and startup logic. **No business logic is permitted here.**
|
||||
- `pkg/`: Core library code and modules. All business logic resides here.
|
||||
- **Module System:** Gotenberg is modular (e.g., Chromium, LibreOffice). When adding features, determine if they belong to an existing module or require a new strict isolation.
|
||||
|
||||
## 4. Testing Standards
|
||||
|
||||
Gotenberg utilizes a split testing strategy. **Integration tests are the primary and preferred method for verifying features.**
|
||||
|
||||
- **Integration Tests (`make test-integration`):**
|
||||
- **First Priority:** Always start here when adding features or routes.
|
||||
- Gotenberg uses **Gherkin (Godog)** for end-to-end verification.
|
||||
- You **must** create or update the corresponding `.feature` file in `test/integration`.
|
||||
- These tests run within the Docker context; ensure environment consistency.
|
||||
- **Unit Tests (`make test-unit`):**
|
||||
- Use table-driven tests for pure logic within `pkg/`.
|
||||
- Mock external dependencies (filesystem, network) where appropriate.
|
||||
|
||||
## 5. Documentation Requirements
|
||||
|
||||
- **No README Updates:** Do not modify the root `README.md` unless explicitly asked.
|
||||
- **GoDoc is Mandatory:**
|
||||
- **New Packages:** If creating a new package, you must include a `doc.go` file containing the package-level documentation.
|
||||
- **Exported Symbols:** Every exported function, type, constant, and variable must have a proper GoDoc comment starting with its name.
|
||||
- **Quality:** Comments must be complete sentences explaining _what_ the symbol does and _how_ to use it.
|
||||
- **Example:**
|
||||
```go
|
||||
// Convert transforms the input document to PDF using the Chromium engine.
|
||||
// It returns an error if the connection to the browser instance fails.
|
||||
func Convert(...) error
|
||||
```
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
A task is considered complete only when:
|
||||
|
||||
1. The code compiles via `make build`.
|
||||
2. The code is formatted via `make fmt`.
|
||||
3. All linters pass via `make lint` and `make lint-prettier`.
|
||||
4. Integration scenarios pass via `make test-integration`.
|
||||
5. Unit tests pass via `make test-unit`.
|
||||
6. All exported symbols and new packages have compliant GoDoc.
|
||||
10
Makefile
10
Makefile
@@ -10,6 +10,7 @@ build: ## Build the Gotenberg's Docker image
|
||||
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||
-f $(DOCKERFILE) $(DOCKER_BUILD_CONTEXT)
|
||||
|
||||
TZ=UTC
|
||||
GOTENBERG_HIDE_BANNER=false
|
||||
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
||||
GOTENBERG_BUILD_DEBUG_DATA=true
|
||||
@@ -30,8 +31,9 @@ API-DOWNLOAD-FROM-FROM-MAX-RETRY=4
|
||||
API-DISABLE-DOWNLOAD-FROM=false
|
||||
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||
API_ENABLE_DEBUG_ROUTE=false
|
||||
CHROMIUM_RESTART_AFTER=10
|
||||
CHROMIUM_RESTART_AFTER=100
|
||||
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||
CHROMIUM_MAX_CONCURRENCY=6
|
||||
CHROMIUM_AUTO_START=false
|
||||
CHROMIUM_START_TIMEOUT=20s
|
||||
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
|
||||
@@ -68,6 +70,7 @@ PROMETHEUS_NAMESPACE=gotenberg
|
||||
PROMETHEUS_COLLECT_INTERVAL=1s
|
||||
PROMETHEUS_DISABLE_ROUTE_LOGGING=false
|
||||
PROMETHEUS_DISABLE_COLLECT=false
|
||||
PROMETHEUS_METRICS_PATH=/prometheus/metrics
|
||||
WEBHOOK_ENABLE_SYNC_MODE=false
|
||||
WEBHOOK_ALLOW_LIST=
|
||||
WEBHOOK_DENY_LIST=
|
||||
@@ -85,6 +88,7 @@ run: ## Start a Gotenberg container
|
||||
-p $(API_PORT):$(API_PORT) \
|
||||
-e GOTENBERG_API_BASIC_AUTH_USERNAME=$(GOTENBERG_API_BASIC_AUTH_USERNAME) \
|
||||
-e GOTENBERG_API_BASIC_AUTH_PASSWORD=$(GOTENBERG_API_BASIC_AUTH_PASSWORD) \
|
||||
-e TZ=$(TZ) \
|
||||
$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||
gotenberg \
|
||||
--gotenberg-hide-banner=$(GOTENBERG_HIDE_BANNER) \
|
||||
@@ -108,6 +112,7 @@ run: ## Start a Gotenberg container
|
||||
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
|
||||
--chromium-max-concurrency=$(CHROMIUM_MAX_CONCURRENCY) \
|
||||
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
|
||||
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
|
||||
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
|
||||
@@ -143,6 +148,7 @@ run: ## Start a Gotenberg container
|
||||
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
||||
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
|
||||
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
|
||||
--prometheus-metrics-path=$(PROMETHEUS_METRICS_PATH) \
|
||||
--webhook-enable-sync-mode="$(WEBHOOK_ENABLE_SYNC_MODE)" \
|
||||
--webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \
|
||||
--webhook-deny-list="$(WEBHOOK_DENY_LIST)" \
|
||||
@@ -162,6 +168,7 @@ PLATFORM=
|
||||
NO_CONCURRENCY=false
|
||||
# Available tags:
|
||||
# chromium
|
||||
# chromium-concurrent
|
||||
# chromium-convert-html
|
||||
# chromium-convert-markdown
|
||||
# chromium-convert-url
|
||||
@@ -214,6 +221,7 @@ lint-todo: ## Find TODOs in Golang codebase
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## Format Golang codebase and "optimize" the dependencies
|
||||
go fix ./...
|
||||
golangci-lint fmt
|
||||
go mod tidy
|
||||
|
||||
|
||||
50
README.md
50
README.md
@@ -4,60 +4,46 @@
|
||||
<p align="center">A containerized API for seamless PDF conversion</p>
|
||||
<p align="center">
|
||||
<a href="https://hub.docker.com/r/gotenberg/gotenberg"><img alt="Total downloads (gotenberg/gotenberg)" src="https://img.shields.io/docker/pulls/gotenberg/gotenberg"></a>
|
||||
<a href="https://hub.docker.com/r/thecodingmachine/gotenberg"><img alt="Total downloads (thecodingmachine/gotenberg)" src="https://img.shields.io/docker/pulls/thecodingmachine/gotenberg"></a>
|
||||
<a href="https://github.com/gotenberg/gotenberg/actions/workflows/continuous-integration.yml"><img alt="Continuous Integration" src="https://github.com/gotenberg/gotenberg/actions/workflows/continuous-integration.yml/badge.svg"></a>
|
||||
<a href="https://pkg.go.dev/github.com/gotenberg/gotenberg/v8"><img alt="Go Reference" src="https://pkg.go.dev/badge/github.com/gotenberg/gotenberg.svg"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/2996"><img src="https://trendshift.io/api/badge/repositories/2996" alt="gotenberg%2Fgotenberg | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
<p align="center"><a href="https://gotenberg.dev/docs/getting-started/introduction">Documentation</a> · <a href="https://gotenberg.dev/docs/getting-started/installation#live-demo-">Live Demo</a> 🔥</p>
|
||||
<p align="center"><a href="https://gotenberg.dev/docs/getting-started/introduction">Read the Documentation</a> · <a href="https://gotenberg.dev/docs/getting-started/installation#live-demo-">Try the Live Demo</a> 🔥</p>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
**Gotenberg** provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice for converting
|
||||
numerous document formats (HTML, Markdown, Word, Excel, etc.) into PDF files, and more!
|
||||
**Gotenberg** is a containerized API that abstracts the complexity of PDF conversion.
|
||||
|
||||
It provides a `multipart/form-data` interface for interacting with powerful engines like Chromium and LibreOffice.
|
||||
Instead of managing heavy dependencies, browser versions, or fonts in your own backend, simply send your files to
|
||||
Gotenberg and get a PDF in return.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Open a terminal and run the following command:
|
||||
|
||||
```
|
||||
```bash
|
||||
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
|
||||
```
|
||||
|
||||
Alternatively, using the historic Docker repository from our sponsor [TheCodingMachine](https://www.thecodingmachine.com):
|
||||
|
||||
```
|
||||
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:8
|
||||
```
|
||||
|
||||
The API is now available on your host at http://localhost:3000.
|
||||
|
||||
Head to the [documentation](https://gotenberg.dev/docs/getting-started/introduction) to learn how to interact with it 🚀
|
||||
With the API running at `http://localhost:3000`, you are now ready to head
|
||||
to the **[Full Documentation](https://gotenberg.dev/docs/getting-started/introduction)** to discover how to convert URLs,
|
||||
local files, inject custom CSS, merge PDFs, and more.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a href="https://thecodingmachine.com">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/130324668-9d6e7b35-53a3-49c7-a574-38190d2bd6b0.png" alt="TheCodingMachine Logo" width="333" height="163" />
|
||||
</a>
|
||||
<a href="https://pdfme.com?utm_source=gotenberg_github&utm_medium=website" target="_blank">
|
||||
<img src="https://github.com/user-attachments/assets/2a75dd40-ca18-4d34-acd5-5dd474595168" alt="pdfme Logo" width="333" height="163" />
|
||||
</a>
|
||||
</p>
|
||||
Open-source development takes a significant amount of time, energy, and dedication. If Gotenberg helps streamline your
|
||||
workflow or powers your business, please consider supporting its continuous improvement by [**becoming a sponsor**](https://github.com/sponsors/gulien)! ❤️
|
||||
|
||||
Sponsorships help maintain and improve Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️
|
||||
**GitHub Sponsors**
|
||||
|
||||
---
|
||||
- [TheCodingMachine](https://thecodingmachine.com/)
|
||||
- [pdfme](https://pdfme.com/)
|
||||
|
||||
<p align="center">
|
||||
<strong>Powered by</strong>
|
||||
</p>
|
||||
**Powered By**
|
||||
|
||||
<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>
|
||||
- [Docker](https://docs.docker.com/docker-hub/repos/manage/trusted-content/dsos-program/)
|
||||
- [JetBrains](https://www.jetbrains.com/community/opensource/)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ARG instructions do not create additional layers. Instead, next layers will
|
||||
# concatenate them. Also, we have to repeat ARG instructions in each build
|
||||
# stage that uses them.
|
||||
ARG GOLANG_VERSION=1.25.4
|
||||
ARG GOLANG_VERSION=1.26.0
|
||||
|
||||
# ----------------------------------------------
|
||||
# pdfcpu binary build stage
|
||||
@@ -79,6 +79,9 @@ RUN jlink \
|
||||
# ----------------------------------------------
|
||||
FROM debian:13-slim AS base-image-stage
|
||||
|
||||
ARG TIMEZONE=UTC
|
||||
ENV TZ=$TIMEZONE
|
||||
|
||||
COPY --from=custom-jre-stage /custom-jre /opt/java
|
||||
|
||||
ENV PATH="/opt/java/bin:${PATH}"
|
||||
@@ -212,7 +215,7 @@ RUN \
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
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.2.0/unoconv -o /usr/bin/unoconverter &&\
|
||||
chmod +x /usr/bin/unoconverter &&\
|
||||
# unoconverter will look for the Python binary, which has to be at version 3.
|
||||
ln -s /usr/bin/python3 /usr/bin/python &&\
|
||||
|
||||
@@ -45,10 +45,10 @@ func Run() {
|
||||
fs.Bool("gotenberg-build-debug-data", true, "Set if build data is needed")
|
||||
|
||||
descriptors := gotenberg.GetModuleDescriptors()
|
||||
var modsInfo string
|
||||
var modsInfo strings.Builder
|
||||
for _, desc := range descriptors {
|
||||
fs.AddFlagSet(desc.FlagSet)
|
||||
modsInfo += desc.ID + " "
|
||||
modsInfo.WriteString(desc.ID + " ")
|
||||
}
|
||||
|
||||
// Parse the flags.
|
||||
@@ -94,7 +94,7 @@ func Run() {
|
||||
if !hideBanner {
|
||||
fmt.Printf(banner, Version)
|
||||
}
|
||||
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
|
||||
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo.String())
|
||||
|
||||
ctx := gotenberg.NewContext(parsedFlags, descriptors)
|
||||
|
||||
|
||||
52
go.mod
52
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/gotenberg/gotenberg/v8
|
||||
|
||||
go 1.25.4
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/alexliesenfeld/health v0.8.1
|
||||
@@ -11,23 +11,23 @@ require (
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab
|
||||
github.com/google/uuid v1.6.0
|
||||
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/mholt/archives v0.1.5
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shirou/gopsutil/v4 v4.25.11
|
||||
github.com/shirou/gopsutil/v4 v4.26.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/term v0.37.0
|
||||
golang.org/x/text v0.31.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.40.0
|
||||
golang.org/x/text v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -57,7 +57,7 @@ require (
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -71,34 +71,34 @@ require (
|
||||
github.com/hashicorp/go-memdb v1.3.5 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mikelolasagasti/xz 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/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/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.1 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/common v0.67.5 // 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.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
@@ -109,19 +109,17 @@ require (
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
346
go.sum
346
go.sum
@@ -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/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/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
||||
@@ -45,7 +26,6 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
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/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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
|
||||
@@ -54,10 +34,6 @@ github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZ
|
||||
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/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
@@ -96,16 +72,12 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj6
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
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/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
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-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 h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
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/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.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
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/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
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/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/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/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
@@ -176,24 +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/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.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 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/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
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/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/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.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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/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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -203,14 +144,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24=
|
||||
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/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -227,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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
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/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
@@ -241,20 +182,20 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
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.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nwaples/rardecode/v2 v2.2.1 h1:DgHK/O/fkTQEKBJxBMC5d9IU8IgauifbpG78+rZJMnI=
|
||||
github.com/nwaples/rardecode/v2 v2.2.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
|
||||
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/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
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/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/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -263,22 +204,19 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
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/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
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.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -294,8 +232,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
||||
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/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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
@@ -317,31 +253,26 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
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/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/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -352,201 +283,42 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
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.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-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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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-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-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-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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
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/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
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.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=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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/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=
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -5,7 +5,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"prettier": "3.7.3",
|
||||
"prettier": "3.8.1",
|
||||
"prettier-plugin-gherkin": "^3.1.3",
|
||||
"prettier-plugin-sh": "^0.18.0"
|
||||
}
|
||||
@@ -58,12 +58,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz",
|
||||
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"prettier": "3.7.3",
|
||||
"prettier": "3.8.1",
|
||||
"prettier-plugin-gherkin": "^3.1.3",
|
||||
"prettier-plugin-sh": "^0.18.0"
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
type Context struct {
|
||||
flags ParsedFlags
|
||||
descriptors []ModuleDescriptor
|
||||
moduleInstances map[string]interface{}
|
||||
moduleInstances map[string]any
|
||||
}
|
||||
|
||||
// NewContext creates a [Context].
|
||||
@@ -22,7 +22,7 @@ func NewContext(
|
||||
return &Context{
|
||||
flags: flags,
|
||||
descriptors: descriptors,
|
||||
moduleInstances: make(map[string]interface{}),
|
||||
moduleInstances: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (ctx *Context) ParsedFlags() ParsedFlags {
|
||||
//
|
||||
// If the module has not yet been initialized, this method
|
||||
// initializes it. Otherwise, returns the already initialized instance.
|
||||
func (ctx *Context) Module(kind interface{}) (interface{}, error) {
|
||||
func (ctx *Context) Module(kind any) (any, error) {
|
||||
mods, err := ctx.Modules(kind)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get module: %w", err)
|
||||
@@ -70,10 +70,10 @@ func (ctx *Context) Module(kind interface{}) (interface{}, error) {
|
||||
//
|
||||
// If one or more modules have not yet been initialized, this method
|
||||
// initializes them. Otherwise, returns the already initialized instances.
|
||||
func (ctx *Context) Modules(kind interface{}) ([]interface{}, error) {
|
||||
func (ctx *Context) Modules(kind any) ([]any, error) {
|
||||
realKind := reflect.TypeOf(kind).Elem()
|
||||
|
||||
var mods []interface{}
|
||||
var mods []any
|
||||
|
||||
for _, desc := range ctx.descriptors {
|
||||
newInstance := desc.New()
|
||||
@@ -101,7 +101,7 @@ func (ctx *Context) Modules(kind interface{}) ([]interface{}, error) {
|
||||
|
||||
// loadModule calls the Provision and/or Validate methods of the requested
|
||||
// module if it satisfies the [Provisioner] and/or [Validator] interfaces.
|
||||
func (ctx *Context) loadModule(id string, instance interface{}) error {
|
||||
func (ctx *Context) loadModule(id string, instance any) error {
|
||||
if prov, ok := instance.(Provisioner); ok {
|
||||
// The instance can be provisioned.
|
||||
err := prov.Provision(ctx)
|
||||
|
||||
@@ -9,7 +9,7 @@ func TestContext_Module(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
mods []ModuleDescriptor
|
||||
kind interface{}
|
||||
kind any
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
@@ -80,7 +80,7 @@ func TestContext_Modules(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
mods []ModuleDescriptor
|
||||
kind interface{}
|
||||
kind any
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
@@ -151,12 +151,12 @@ func TestContext_Modules(t *testing.T) {
|
||||
func TestContext_loadModule(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
instance interface{}
|
||||
instance any
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "module with error on provision",
|
||||
instance: func() interface{} {
|
||||
instance: func() any {
|
||||
mod := &struct {
|
||||
ModuleMock
|
||||
ProvisionerMock
|
||||
@@ -171,7 +171,7 @@ func TestContext_loadModule(t *testing.T) {
|
||||
},
|
||||
{
|
||||
scenario: "module with error on validation",
|
||||
instance: func() interface{} {
|
||||
instance: func() any {
|
||||
mod := &struct {
|
||||
ModuleMock
|
||||
ValidatorMock
|
||||
@@ -186,7 +186,7 @@ func TestContext_loadModule(t *testing.T) {
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
instance: func() interface{} {
|
||||
instance: func() any {
|
||||
mod := &struct {
|
||||
ModuleMock
|
||||
ValidatorMock
|
||||
|
||||
@@ -4,17 +4,19 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// DebugInfo gathers data for debugging.
|
||||
type DebugInfo struct {
|
||||
Version string `json:"version"`
|
||||
Architecture string `json:"architecture"`
|
||||
Modules []string `json:"modules"`
|
||||
ModulesAdditionalData map[string]map[string]interface{} `json:"modules_additional_data"`
|
||||
Flags map[string]interface{} `json:"flags"`
|
||||
Version string `json:"version"`
|
||||
Timezone string `json:"timezone"`
|
||||
Architecture string `json:"architecture"`
|
||||
Modules []string `json:"modules"`
|
||||
ModulesAdditionalData map[string]map[string]any `json:"modules_additional_data"`
|
||||
Flags map[string]any `json:"flags"`
|
||||
}
|
||||
|
||||
// BuildDebug builds the debug data from modules.
|
||||
@@ -24,10 +26,11 @@ func BuildDebug(ctx *Context) {
|
||||
|
||||
debug = &DebugInfo{
|
||||
Version: Version,
|
||||
Timezone: time.Now().Location().String(),
|
||||
Architecture: runtime.GOARCH,
|
||||
Modules: make([]string, len(ctx.moduleInstances)),
|
||||
ModulesAdditionalData: make(map[string]map[string]interface{}),
|
||||
Flags: make(map[string]interface{}),
|
||||
ModulesAdditionalData: make(map[string]map[string]any),
|
||||
Flags: make(map[string]any),
|
||||
}
|
||||
|
||||
i := 0
|
||||
|
||||
@@ -13,6 +13,8 @@ func TestBuildDebug(t *testing.T) {
|
||||
t.Errorf("Debug() should return empty debug data")
|
||||
}
|
||||
|
||||
t.Setenv("TZ", "UTC")
|
||||
|
||||
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
||||
fs.String("foo", "bar", "Set foo")
|
||||
ctx := NewContext(ParsedFlags{
|
||||
@@ -31,8 +33,8 @@ func TestBuildDebug(t *testing.T) {
|
||||
mod2.DescriptorMock = func() ModuleDescriptor {
|
||||
return ModuleDescriptor{ID: "bar", New: func() Module { return mod2 }}
|
||||
}
|
||||
mod2.DebugMock = func() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
mod2.DebugMock = func() map[string]any {
|
||||
return map[string]any{
|
||||
"foo": "bar",
|
||||
}
|
||||
}
|
||||
@@ -51,17 +53,18 @@ func TestBuildDebug(t *testing.T) {
|
||||
|
||||
expect := DebugInfo{
|
||||
Version: Version,
|
||||
Timezone: "UTC",
|
||||
Architecture: runtime.GOARCH,
|
||||
Modules: []string{
|
||||
"bar",
|
||||
"foo",
|
||||
},
|
||||
ModulesAdditionalData: map[string]map[string]interface{}{
|
||||
ModulesAdditionalData: map[string]map[string]any{
|
||||
"bar": {
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
Flags: map[string]interface{}{
|
||||
Flags: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -32,22 +32,22 @@ func NewLeveledLogger(logger *zap.Logger) *LeveledLogger {
|
||||
}
|
||||
|
||||
// Error logs a message at the error level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Error(msg string, keysAndValues ...interface{}) {
|
||||
func (leveled LeveledLogger) Error(msg string, keysAndValues ...any) {
|
||||
leveled.logger.Error(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
// Warn logs a message at the warning level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Warn(msg string, keysAndValues ...interface{}) {
|
||||
func (leveled LeveledLogger) Warn(msg string, keysAndValues ...any) {
|
||||
leveled.logger.Warn(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
// Info logs a message at the info level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||
func (leveled LeveledLogger) Info(msg string, keysAndValues ...any) {
|
||||
leveled.logger.Info(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
// Debug logs a message at the debug level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Debug(msg string, keysAndValues ...interface{}) {
|
||||
func (leveled LeveledLogger) Debug(msg string, keysAndValues ...any) {
|
||||
leveled.logger.Debug(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ func (mod *ValidatorMock) Validate() error {
|
||||
}
|
||||
|
||||
type DebuggableMock struct {
|
||||
DebugMock func() map[string]interface{}
|
||||
DebugMock func() map[string]any
|
||||
}
|
||||
|
||||
func (mod *DebuggableMock) Debug() map[string]interface{} {
|
||||
func (mod *DebuggableMock) Debug() map[string]any {
|
||||
return mod.DebugMock()
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ type PdfEngineMock struct {
|
||||
SplitMock func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
||||
FlattenMock func(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
||||
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
||||
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]any, 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
|
||||
}
|
||||
@@ -72,11 +72,11 @@ func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, fo
|
||||
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return engine.ReadMetadataMock(ctx, logger, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ type SystemLogger interface {
|
||||
// Debuggable is a module interface for modules which want to provide
|
||||
// additional debug data.
|
||||
type Debuggable interface {
|
||||
Debug() map[string]interface{}
|
||||
Debug() map[string]any
|
||||
}
|
||||
|
||||
// MustRegisterModule registers a module.
|
||||
|
||||
@@ -133,10 +133,10 @@ type PdfEngine interface {
|
||||
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
|
||||
// ReadMetadata extracts the metadata of a given PDF file.
|
||||
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
||||
|
||||
// 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]any, inputPath string) error
|
||||
|
||||
// Encrypt adds password protection to a PDF file.
|
||||
// The userPassword is required to open the document.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -79,27 +80,38 @@ type processSupervisor struct {
|
||||
process Process
|
||||
maxReqLimit int64
|
||||
maxQueueSize int64
|
||||
mutexChan chan struct{}
|
||||
maxConcurrency int64
|
||||
semaphore chan struct{}
|
||||
firstStart atomic.Bool
|
||||
firstStartOnce sync.Once
|
||||
firstStartErr error
|
||||
reqCounter atomic.Int64
|
||||
reqQueueSize atomic.Int64
|
||||
restartsCounter atomic.Int64
|
||||
isRestarting atomic.Bool
|
||||
activeTasks atomic.Int64
|
||||
restartMutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewProcessSupervisor initializes a new [ProcessSupervisor].
|
||||
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit, maxQueueSize int64) ProcessSupervisor {
|
||||
func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLimit, maxQueueSize, maxConcurrency int64) ProcessSupervisor {
|
||||
if maxConcurrency < 1 {
|
||||
maxConcurrency = 1
|
||||
}
|
||||
|
||||
b := &processSupervisor{
|
||||
logger: logger,
|
||||
process: process,
|
||||
mutexChan: make(chan struct{}, 1),
|
||||
maxReqLimit: maxReqLimit,
|
||||
maxQueueSize: maxQueueSize,
|
||||
logger: logger,
|
||||
process: process,
|
||||
semaphore: make(chan struct{}, maxConcurrency),
|
||||
maxReqLimit: maxReqLimit,
|
||||
maxQueueSize: maxQueueSize,
|
||||
maxConcurrency: maxConcurrency,
|
||||
}
|
||||
b.reqCounter.Store(0)
|
||||
b.reqQueueSize.Store(0)
|
||||
b.restartsCounter.Store(0)
|
||||
b.isRestarting.Store(false)
|
||||
b.activeTasks.Store(0)
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -130,15 +142,7 @@ func (s *processSupervisor) Shutdown() error {
|
||||
}
|
||||
|
||||
func (s *processSupervisor) restart() error {
|
||||
if s.isRestarting.Load() {
|
||||
s.logger.Debug("process already restarting, skip restart")
|
||||
|
||||
return ErrProcessAlreadyRestarting
|
||||
}
|
||||
|
||||
s.logger.Debug("restart process")
|
||||
s.isRestarting.Store(true)
|
||||
defer s.isRestarting.Store(false)
|
||||
|
||||
err := s.Shutdown()
|
||||
if err != nil {
|
||||
@@ -197,33 +201,43 @@ func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task fu
|
||||
for {
|
||||
err := func() error {
|
||||
select {
|
||||
case s.mutexChan <- struct{}{}:
|
||||
case s.semaphore <- struct{}{}:
|
||||
logger.Debug("process lock acquired")
|
||||
|
||||
// If a restart drain is in progress, release the slot
|
||||
// immediately so the drain can acquire it instead.
|
||||
if s.isRestarting.Load() {
|
||||
<-s.semaphore
|
||||
return ErrProcessAlreadyRestarting
|
||||
}
|
||||
|
||||
s.reqQueueSize.Add(-1)
|
||||
s.reqCounter.Add(1)
|
||||
releaseMutexChan := true
|
||||
s.activeTasks.Add(1)
|
||||
releaseSemaphore := true
|
||||
|
||||
defer func() {
|
||||
if releaseMutexChan {
|
||||
s.activeTasks.Add(-1)
|
||||
if releaseSemaphore {
|
||||
logger.Debug("process lock released")
|
||||
<-s.mutexChan
|
||||
<-s.semaphore
|
||||
}
|
||||
}()
|
||||
|
||||
if !s.firstStart.Load() {
|
||||
err := s.runWithDeadline(ctx, func() error {
|
||||
return s.Launch()
|
||||
s.firstStartOnce.Do(func() {
|
||||
s.firstStartErr = s.runWithDeadline(ctx, func() error {
|
||||
return s.Launch()
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("process first start: %w", err)
|
||||
if s.firstStartErr != nil {
|
||||
return fmt.Errorf("process first start: %w", s.firstStartErr)
|
||||
}
|
||||
}
|
||||
|
||||
if !s.Healthy() {
|
||||
s.logger.Debug("process is unhealthy, cannot handle task, restarting...")
|
||||
err := s.runWithDeadline(ctx, func() error {
|
||||
return s.restart()
|
||||
})
|
||||
err := s.doRestart(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("process restart before task: %w", err)
|
||||
}
|
||||
@@ -232,19 +246,21 @@ func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task fu
|
||||
err := s.runWithDeadline(ctx, task)
|
||||
|
||||
if s.maxReqLimit > 0 && s.reqCounter.Load() >= s.maxReqLimit {
|
||||
s.logger.Debug("max request limit reached, restarting eagerly...")
|
||||
releaseMutexChan = false
|
||||
// Only one goroutine should trigger the restart.
|
||||
if s.restartMutex.TryLock() {
|
||||
s.logger.Debug("max request limit reached, restarting eagerly...")
|
||||
releaseSemaphore = false
|
||||
|
||||
go func() {
|
||||
err := s.runWithDeadline(context.Background(), func() error {
|
||||
return s.restart()
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("process restart after task: %v", err))
|
||||
}
|
||||
logger.Debug("process lock released")
|
||||
<-s.mutexChan
|
||||
}()
|
||||
go func() {
|
||||
restartErr := s.doRestartLocked(context.Background())
|
||||
s.restartMutex.Unlock()
|
||||
if restartErr != nil {
|
||||
s.logger.Error(fmt.Sprintf("process restart after task: %v", restartErr))
|
||||
}
|
||||
logger.Debug("process lock released")
|
||||
<-s.semaphore
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Note: no error wrapping because it leaks on Chromium console exceptions output.
|
||||
@@ -259,7 +275,6 @@ func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task fu
|
||||
|
||||
if errors.Is(err, ErrProcessAlreadyRestarting) {
|
||||
logger.Debug("process is already restarting, trying to acquire process lock again...")
|
||||
s.reqQueueSize.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -268,6 +283,47 @@ func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task fu
|
||||
}
|
||||
}
|
||||
|
||||
// doRestart coordinates a process restart, draining all active concurrent
|
||||
// tasks before stopping and restarting the process.
|
||||
func (s *processSupervisor) doRestart(ctx context.Context) error {
|
||||
s.restartMutex.Lock()
|
||||
defer s.restartMutex.Unlock()
|
||||
|
||||
return s.doRestartLocked(ctx)
|
||||
}
|
||||
|
||||
// doRestartLocked performs the restart drain logic. The caller must hold restartMutex.
|
||||
func (s *processSupervisor) doRestartLocked(ctx context.Context) error {
|
||||
s.isRestarting.Store(true)
|
||||
defer s.isRestarting.Store(false)
|
||||
|
||||
// Drain all other active semaphore slots so no other tasks are running during the restart.
|
||||
slotsToAcquire := s.maxConcurrency - 1
|
||||
acquired := make([]struct{}, 0, slotsToAcquire)
|
||||
|
||||
for range slotsToAcquire {
|
||||
select {
|
||||
case s.semaphore <- struct{}{}:
|
||||
acquired = append(acquired, struct{}{})
|
||||
case <-ctx.Done():
|
||||
for range acquired {
|
||||
<-s.semaphore
|
||||
}
|
||||
return fmt.Errorf("drain active tasks before restart: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
err := s.runWithDeadline(ctx, func() error {
|
||||
return s.restart()
|
||||
})
|
||||
|
||||
for range acquired {
|
||||
<-s.semaphore
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *processSupervisor) runWithDeadline(ctx context.Context, task func() error) error {
|
||||
runChan := make(chan error, 1)
|
||||
go func() {
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestProcessSupervisor_Launch(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0, 1).(*processSupervisor)
|
||||
if tc.firstStartSet {
|
||||
ps.firstStart.Store(true)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func TestProcessSupervisor_Shutdown(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0)
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0, 1)
|
||||
err := ps.Shutdown()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
@@ -110,19 +110,11 @@ func TestProcessSupervisor_Shutdown(t *testing.T) {
|
||||
|
||||
func TestProcessSupervisor_restart(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
initiallyRestarting bool
|
||||
startError error
|
||||
stopError error
|
||||
expectError bool
|
||||
expectedError error
|
||||
scenario string
|
||||
startError error
|
||||
stopError error
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "already restarting",
|
||||
initiallyRestarting: true,
|
||||
expectError: true,
|
||||
expectedError: ErrProcessAlreadyRestarting,
|
||||
},
|
||||
{
|
||||
scenario: "successful restart",
|
||||
startError: nil,
|
||||
@@ -154,10 +146,7 @@ func TestProcessSupervisor_restart(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
|
||||
if tc.initiallyRestarting {
|
||||
ps.isRestarting.Store(true)
|
||||
}
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0, 1).(*processSupervisor)
|
||||
|
||||
err := ps.restart()
|
||||
|
||||
@@ -168,10 +157,6 @@ func TestProcessSupervisor_restart(t *testing.T) {
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -217,7 +202,7 @@ func TestProcessSupervisor_Healthy(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0).(*processSupervisor)
|
||||
ps := NewProcessSupervisor(logger, process, 5, 0, 1).(*processSupervisor)
|
||||
if tc.initiallyStarted {
|
||||
ps.firstStart.Store(true)
|
||||
}
|
||||
@@ -402,7 +387,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit, tc.maxQueueSize).(*processSupervisor)
|
||||
ps := NewProcessSupervisor(logger, process, tc.maxReqLimit, tc.maxQueueSize, 1).(*processSupervisor)
|
||||
if tc.initiallyStarted {
|
||||
ps.firstStart.Store(true)
|
||||
}
|
||||
@@ -424,14 +409,12 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
||||
errorChan := make(chan error, tc.tasksToRun)
|
||||
|
||||
for i := 0; i < tc.tasksToRun; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
err := ps.Run(ctx, logger, task)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -452,8 +435,8 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
||||
}
|
||||
|
||||
// Making sure restarts are finished.
|
||||
ps.mutexChan <- struct{}{}
|
||||
<-ps.mutexChan
|
||||
ps.semaphore <- struct{}{}
|
||||
<-ps.semaphore
|
||||
|
||||
if startCalls.Load() != tc.expectedStartCalls {
|
||||
t.Errorf("expected %d process.Start calls, got %d", tc.expectedStartCalls, startCalls.Load())
|
||||
@@ -488,7 +471,7 @@ func TestProcessSupervisor_runWithDeadline(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0, 0).(*processSupervisor)
|
||||
ps := NewProcessSupervisor(zap.NewNop(), new(ProcessMock), 0, 0, 1).(*processSupervisor)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
if tc.ctxDone {
|
||||
@@ -522,10 +505,10 @@ func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
|
||||
return true
|
||||
},
|
||||
}
|
||||
ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
|
||||
ps := NewProcessSupervisor(logger, process, 0, 0, 1).(*processSupervisor)
|
||||
|
||||
// Simulating a lock.
|
||||
ps.mutexChan <- struct{}{}
|
||||
ps.semaphore <- struct{}{}
|
||||
|
||||
if ps.ReqQueueSize() != 0 {
|
||||
t.Fatalf("expected queue size to be 0 but got %d", ps.ReqQueueSize())
|
||||
@@ -537,17 +520,15 @@ func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
errorChan := make(chan error, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range 10 {
|
||||
wg.Go(func() {
|
||||
err := ps.Run(ctx, logger, func() error {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// We have to wait a little bit so that the request queue size may change.
|
||||
@@ -623,7 +604,7 @@ func TestProcessSupervisor_RestartsCount(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ps := NewProcessSupervisor(logger, process, 0, 0).(*processSupervisor)
|
||||
ps := NewProcessSupervisor(logger, process, 0, 0, 1).(*processSupervisor)
|
||||
ps.restartsCounter.Store(tc.initialRestartsCount)
|
||||
|
||||
for i := 0; i < tc.restartAttempts; i++ {
|
||||
@@ -637,3 +618,122 @@ func TestProcessSupervisor_RestartsCount(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_ConcurrentRun(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
var startCalls atomic.Int64
|
||||
process := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
startCalls.Add(1)
|
||||
return nil
|
||||
},
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func(logger *zap.Logger) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
maxConcurrency := int64(3)
|
||||
ps := NewProcessSupervisor(logger, process, 0, 0, maxConcurrency).(*processSupervisor)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var running atomic.Int64
|
||||
var maxRunning atomic.Int64
|
||||
|
||||
var wg sync.WaitGroup
|
||||
tasks := 6
|
||||
|
||||
for range tasks {
|
||||
wg.Go(func() {
|
||||
err := ps.Run(ctx, logger, func() error {
|
||||
cur := running.Add(1)
|
||||
for {
|
||||
old := maxRunning.Load()
|
||||
if cur <= old || maxRunning.CompareAndSwap(old, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
running.Add(-1)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
observed := maxRunning.Load()
|
||||
if observed > maxConcurrency {
|
||||
t.Fatalf("expected at most %d concurrent tasks, but observed %d", maxConcurrency, observed)
|
||||
}
|
||||
if observed < 2 {
|
||||
t.Fatalf("expected concurrent execution (at least 2 tasks running simultaneously), but observed max %d", observed)
|
||||
}
|
||||
|
||||
if startCalls.Load() != 1 {
|
||||
t.Errorf("expected 1 start call, got %d", startCalls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisor_RestartDrainsAllSlots(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
process := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func(logger *zap.Logger) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
maxConcurrency := int64(3)
|
||||
ps := NewProcessSupervisor(logger, process, 3, 0, maxConcurrency).(*processSupervisor)
|
||||
ps.firstStart.Store(true)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
tasks := 3
|
||||
|
||||
for range tasks {
|
||||
wg.Go(func() {
|
||||
err := ps.Run(ctx, logger, func() error {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Wait for the async restart goroutine to complete.
|
||||
deadline := time.After(5 * time.Second)
|
||||
for ps.RestartsCount() < 1 {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("timed out waiting for restart to complete")
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
if ps.RestartsCount() != 1 {
|
||||
t.Fatalf("expected 1 restart, got %d", ps.RestartsCount())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ type downloadFrom struct {
|
||||
|
||||
// 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) {
|
||||
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
|
||||
// limit. We consider: form fields (keys, values, files) and files
|
||||
|
||||
71
pkg/modules/api/context_test.go
Normal file
71
pkg/modules/api/context_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func (form *FormData) append(err error) {
|
||||
// mustValue binds the target interface with a form field. If the value is
|
||||
// empty or the "key" does not exist, it binds the default value. Currently,
|
||||
// only the string, bool, int, float64 and time.Duration types are bindable.
|
||||
func (form *FormData) mustValue(key string, target interface{}, defaultValue interface{}) *FormData {
|
||||
func (form *FormData) mustValue(key string, target any, defaultValue any) *FormData {
|
||||
val, ok := form.values[key]
|
||||
|
||||
if !ok || val[0] == "" {
|
||||
@@ -468,7 +468,7 @@ func (form *FormData) mustValue(key string, target interface{}, defaultValue int
|
||||
// populates an error if the value is empty or the "key" does not exist.
|
||||
// Currently, only the string, bool, int, float64 and time.Duration types are
|
||||
// bindable.
|
||||
func (form *FormData) mustMandatoryField(key string, target interface{}) *FormData {
|
||||
func (form *FormData) mustMandatoryField(key string, target any) *FormData {
|
||||
val, ok := form.values[key]
|
||||
|
||||
if !ok || val[0] == "" {
|
||||
@@ -487,7 +487,7 @@ func (form *FormData) mustMandatoryField(key string, target interface{}) *FormDa
|
||||
// mustAssign parses the string value and tries to convert it to the target
|
||||
// interface real type. Currently, only the string, bool, int, float64 and
|
||||
// time.Duration types are bindable.
|
||||
func (form *FormData) mustAssign(key, value string, target interface{}) *FormData {
|
||||
func (form *FormData) mustAssign(key, value string, target any) *FormData {
|
||||
var err error
|
||||
|
||||
switch t := (target).(type) {
|
||||
|
||||
@@ -288,8 +288,9 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||
forceExactColorsActionFunc(logger, options.PrintBackground),
|
||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType, options.EmulatedMediaFeatures),
|
||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
|
||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||
// PDF specific.
|
||||
printToPdfActionFunc(logger, outputPath, options),
|
||||
@@ -313,8 +314,9 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||
forceExactColorsActionFunc(logger, true),
|
||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType, options.EmulatedMediaFeatures),
|
||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||
waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector),
|
||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||
// Screenshot specific.
|
||||
setDeviceMetricsOverride(logger, options.Width, options.Height),
|
||||
@@ -376,6 +378,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
invalidHttpStatusCode: &invalidHttpStatusCode,
|
||||
invalidHttpStatusCodeMu: &invalidHttpStatusCodeMu,
|
||||
failOnResourceOnHttpStatusCode: options.FailOnResourceHttpStatusCodes,
|
||||
ignoreResourceHttpStatusDomains: options.IgnoreResourceHttpStatusDomains,
|
||||
invalidResourceHttpStatusCode: &invalidResourceHttpStatusCode,
|
||||
invalidResourceHttpStatusCodeMu: &invalidResourceHttpStatusCodeMu,
|
||||
})
|
||||
|
||||
@@ -33,6 +33,10 @@ var (
|
||||
// returns an exception or undefined.
|
||||
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
|
||||
// ChromeDevTools are larger than 100 MB.
|
||||
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
||||
@@ -82,9 +86,10 @@ var (
|
||||
// Chromium is a module that provides both an [Api] and routes for converting
|
||||
// an HTML document to PDF.
|
||||
type Chromium struct {
|
||||
autoStart bool
|
||||
disableRoutes bool
|
||||
args browserArguments
|
||||
autoStart bool
|
||||
disableRoutes bool
|
||||
maxConcurrency int64
|
||||
args browserArguments
|
||||
|
||||
logger *zap.Logger
|
||||
browser browser
|
||||
@@ -108,6 +113,20 @@ type Options struct {
|
||||
// status code from at least one resource matches with one if its entries.
|
||||
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
|
||||
// main page if Chromium fails to load at least one resource.
|
||||
FailOnResourceLoadingFailed bool
|
||||
@@ -128,6 +147,10 @@ type Options struct {
|
||||
// converting an HTML document until it returns true
|
||||
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 []Cookie
|
||||
|
||||
@@ -142,27 +165,47 @@ type Options struct {
|
||||
// "print".
|
||||
EmulatedMediaType string
|
||||
|
||||
// EmulatedMediaFeatures are the media features to emulate, e.g.,
|
||||
// [{"name": "prefers-color-scheme", "value": "dark"}].
|
||||
EmulatedMediaFeatures []EmulatedMediaFeature
|
||||
|
||||
// OmitBackground hides the default white background and allows generating
|
||||
// PDFs with transparency.
|
||||
OmitBackground bool
|
||||
}
|
||||
|
||||
// EmulatedMediaFeature gathers the available entries for emulating a media
|
||||
// feature.
|
||||
type EmulatedMediaFeature struct {
|
||||
// Name is the media feature name (e.g., "prefers-color-scheme",
|
||||
// "prefers-reduced-motion").
|
||||
// Required.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Value is the media feature value (e.g., "dark", "reduce").
|
||||
// Required.
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// DefaultOptions returns the default values for Options.
|
||||
func DefaultOptions() Options {
|
||||
return Options{
|
||||
SkipNetworkIdleEvent: true,
|
||||
FailOnHttpStatusCodes: []int64{499, 599},
|
||||
FailOnResourceHttpStatusCodes: nil,
|
||||
FailOnResourceLoadingFailed: false,
|
||||
FailOnConsoleExceptions: false,
|
||||
WaitDelay: 0,
|
||||
WaitWindowStatus: "",
|
||||
WaitForExpression: "",
|
||||
Cookies: nil,
|
||||
UserAgent: "",
|
||||
ExtraHttpHeaders: nil,
|
||||
EmulatedMediaType: "",
|
||||
OmitBackground: false,
|
||||
SkipNetworkIdleEvent: true,
|
||||
FailOnHttpStatusCodes: []int64{499, 599},
|
||||
FailOnResourceHttpStatusCodes: nil,
|
||||
IgnoreResourceHttpStatusDomains: nil,
|
||||
FailOnResourceLoadingFailed: false,
|
||||
FailOnConsoleExceptions: false,
|
||||
WaitDelay: 0,
|
||||
WaitWindowStatus: "",
|
||||
WaitForExpression: "",
|
||||
WaitForSelector: "",
|
||||
Cookies: nil,
|
||||
UserAgent: "",
|
||||
ExtraHttpHeaders: nil,
|
||||
EmulatedMediaType: "",
|
||||
EmulatedMediaFeatures: nil,
|
||||
OmitBackground: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,8 +410,9 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
||||
ID: "chromium",
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("chromium", flag.ExitOnError)
|
||||
fs.Int64("chromium-restart-after", 10, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
||||
fs.Int64("chromium-restart-after", 100, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
||||
fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for Chromium. Set to 0 to disable this feature")
|
||||
fs.Int64("chromium-max-concurrency", 6, "Maximum number of concurrent conversions. Chromium supports up to 6")
|
||||
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.Bool("chromium-allow-insecure-localhost", false, "Ignore TLS/SSL errors on localhost")
|
||||
@@ -402,6 +446,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
||||
flags := ctx.ParsedFlags()
|
||||
mod.autoStart = flags.MustBool("chromium-auto-start")
|
||||
mod.disableRoutes = flags.MustBool("chromium-disable-routes")
|
||||
mod.maxConcurrency = flags.MustInt64("chromium-max-concurrency")
|
||||
|
||||
binPath, ok := os.LookupEnv("CHROMIUM_BIN_PATH")
|
||||
if !ok {
|
||||
@@ -444,7 +489,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
||||
|
||||
// Process.
|
||||
mod.browser = newChromiumBrowser(mod.args)
|
||||
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"), flags.MustInt64("chromium-max-queue-size"))
|
||||
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"), flags.MustInt64("chromium-max-queue-size"), mod.maxConcurrency)
|
||||
|
||||
// PDF Engine.
|
||||
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||
@@ -462,6 +507,10 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
||||
|
||||
// Validate validates the module properties.
|
||||
func (mod *Chromium) Validate() error {
|
||||
if mod.maxConcurrency < 1 || mod.maxConcurrency > 6 {
|
||||
return fmt.Errorf("chromium-max-concurrency must be between 1 and 6, got %d", mod.maxConcurrency)
|
||||
}
|
||||
|
||||
_, err := os.Stat(mod.args.binPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("chromium binary path does not exist: %w", err)
|
||||
@@ -516,8 +565,8 @@ func (mod *Chromium) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (mod *Chromium) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
func (mod *Chromium) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
|
||||
cmd := exec.Command(mod.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
@@ -21,7 +21,7 @@ func (debug *debugLogger) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
// Printf logs a debug message.
|
||||
func (debug *debugLogger) Printf(format string, v ...interface{}) {
|
||||
func (debug *debugLogger) Printf(format string, v ...any) {
|
||||
debug.logger.Debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/chromedp/cdproto/cdp"
|
||||
@@ -37,7 +39,7 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
|
||||
logger.Debug(fmt.Sprintf("extra HTTP headers: %+v", options.extraHttpHeaders))
|
||||
}
|
||||
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(ctx, func(ev any) {
|
||||
switch e := ev.(type) {
|
||||
case *fetch.EventRequestPaused:
|
||||
go func() {
|
||||
@@ -143,6 +145,7 @@ type eventResponseReceivedOptions struct {
|
||||
invalidHttpStatusCode *error
|
||||
invalidHttpStatusCodeMu *sync.RWMutex
|
||||
failOnResourceOnHttpStatusCode []int64
|
||||
ignoreResourceHttpStatusDomains []string
|
||||
invalidResourceHttpStatusCode *error
|
||||
invalidResourceHttpStatusCodeMu *sync.RWMutex
|
||||
}
|
||||
@@ -157,6 +160,8 @@ func listenForEventResponseReceived(
|
||||
logger *zap.Logger,
|
||||
options eventResponseReceivedOptions,
|
||||
) {
|
||||
normalizedIgnoreDomains := normalizeDomains(options.ignoreResourceHttpStatusDomains)
|
||||
|
||||
for _, code := range []int64{199, 299, 399, 499, 599} {
|
||||
if slices.Contains(options.failOnHttpStatusCodes, code) {
|
||||
for i := code - 99; i <= code; i++ {
|
||||
@@ -171,7 +176,7 @@ func listenForEventResponseReceived(
|
||||
}
|
||||
}
|
||||
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(ctx, func(ev any) {
|
||||
switch ev := ev.(type) {
|
||||
case *network.EventResponseReceived:
|
||||
if ev.Response.URL == options.mainPageUrl {
|
||||
@@ -190,6 +195,11 @@ func listenForEventResponseReceived(
|
||||
logger.Debug(fmt.Sprintf("event EventResponseReceived fired for a resource: %+v", ev.Response))
|
||||
|
||||
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()
|
||||
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 {
|
||||
loadingFailed *error
|
||||
loadingFailedMu *sync.RWMutex
|
||||
@@ -216,7 +299,7 @@ type eventLoadingFailedOptions struct {
|
||||
// https://github.com/gotenberg/gotenberg/issues/959.
|
||||
// https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, options eventLoadingFailedOptions) {
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(ctx, func(ev any) {
|
||||
switch ev := ev.(type) {
|
||||
case *network.EventLoadingFailed:
|
||||
logger.Debug(fmt.Sprintf("event EventLoadingFailed fired: %+v", ev.ErrorText))
|
||||
@@ -272,7 +355,7 @@ func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger, option
|
||||
// appends those exceptions to the given error pointer.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/262.
|
||||
func listenForEventExceptionThrown(ctx context.Context, logger *zap.Logger, consoleExceptions *error, consoleExceptionsMu *sync.RWMutex) {
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(ctx, func(ev any) {
|
||||
switch ev := ev.(type) {
|
||||
case *runtime.EventExceptionThrown:
|
||||
logger.Debug(fmt.Sprintf("event EventExceptionThrown fired: %+v", ev.ExceptionDetails))
|
||||
@@ -291,7 +374,7 @@ func waitForEventDomContentEventFired(ctx context.Context, logger *zap.Logger) f
|
||||
return func() error {
|
||||
ch := make(chan struct{})
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
chromedp.ListenTarget(cctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(cctx, func(ev any) {
|
||||
switch ev.(type) {
|
||||
case *page.EventDomContentEventFired:
|
||||
cancel()
|
||||
@@ -315,7 +398,7 @@ func waitForEventLoadEventFired(ctx context.Context, logger *zap.Logger) func()
|
||||
return func() error {
|
||||
ch := make(chan struct{})
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
chromedp.ListenTarget(cctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(cctx, func(ev any) {
|
||||
switch ev.(type) {
|
||||
case *page.EventLoadEventFired:
|
||||
cancel()
|
||||
@@ -339,7 +422,7 @@ func waitForEventNetworkIdle(ctx context.Context, logger *zap.Logger) func() err
|
||||
return func() error {
|
||||
ch := make(chan struct{})
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
chromedp.ListenTarget(cctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(cctx, func(ev any) {
|
||||
switch e := ev.(type) {
|
||||
case *page.EventLifecycleEvent:
|
||||
if e.Name == "networkIdle" {
|
||||
@@ -365,7 +448,7 @@ func waitForEventLoadingFinished(ctx context.Context, logger *zap.Logger) func()
|
||||
return func() error {
|
||||
ch := make(chan struct{})
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
chromedp.ListenTarget(cctx, func(ev interface{}) {
|
||||
chromedp.ListenTarget(cctx, func(ev any) {
|
||||
switch ev.(type) {
|
||||
case *network.EventLoadingFinished:
|
||||
cancel()
|
||||
|
||||
63
pkg/modules/chromium/events_test.go
Normal file
63
pkg/modules/chromium/events_test.go
Normal 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)")
|
||||
}
|
||||
}
|
||||
@@ -29,25 +29,40 @@ var sameSiteRegexp = regexp2.MustCompile(
|
||||
regexp2.None,
|
||||
)
|
||||
|
||||
// FormDataChromiumOptions creates [Options] from the form data. Fallback to
|
||||
// the default value if the considered key is not present.
|
||||
// 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
|
||||
// - emulatedMediaFeatures: map[string]string
|
||||
//
|
||||
// Domain filtering only applies to resource checks triggered by
|
||||
// "failOnResourceHttpStatusCodes".
|
||||
func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
defaultOptions := DefaultOptions()
|
||||
|
||||
var (
|
||||
skipNetworkIdleEvent bool
|
||||
failOnHttpStatusCodes []int64
|
||||
failOnResourceHttpStatusCodes []int64
|
||||
failOnResourceLoadingFailed bool
|
||||
failOnConsoleExceptions bool
|
||||
waitDelay time.Duration
|
||||
waitWindowStatus string
|
||||
waitForExpression string
|
||||
cookies []Cookie
|
||||
userAgent string
|
||||
extraHttpHeaders []ExtraHttpHeader
|
||||
emulatedMediaType string
|
||||
omitBackground bool
|
||||
skipNetworkIdleEvent bool
|
||||
failOnHttpStatusCodes []int64
|
||||
failOnResourceHttpStatusCodes []int64
|
||||
ignoreResourceHttpStatusDomains []string
|
||||
failOnResourceLoadingFailed bool
|
||||
failOnConsoleExceptions bool
|
||||
waitDelay time.Duration
|
||||
waitWindowStatus string
|
||||
waitForExpression string
|
||||
waitForSelector string
|
||||
cookies []Cookie
|
||||
userAgent string
|
||||
extraHttpHeaders []ExtraHttpHeader
|
||||
emulatedMediaType string
|
||||
emulatedMediaFeatures []EmulatedMediaFeature
|
||||
omitBackground bool
|
||||
)
|
||||
|
||||
form := ctx.FormData().
|
||||
@@ -78,11 +93,25 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
|
||||
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("failOnConsoleExceptions", &failOnConsoleExceptions, defaultOptions.FailOnConsoleExceptions).
|
||||
Duration("waitDelay", &waitDelay, defaultOptions.WaitDelay).
|
||||
String("waitWindowStatus", &waitWindowStatus, defaultOptions.WaitWindowStatus).
|
||||
String("waitForExpression", &waitForExpression, defaultOptions.WaitForExpression).
|
||||
String("waitForSelector", &waitForSelector, defaultOptions.WaitForSelector).
|
||||
Custom("cookies", func(value string) error {
|
||||
if value == "" {
|
||||
cookies = defaultOptions.Cookies
|
||||
@@ -143,8 +172,8 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
var valueTokens []string
|
||||
var invalidScopeToken bool
|
||||
|
||||
tokens := strings.Split(v, ";")
|
||||
for _, token := range tokens {
|
||||
tokens := strings.SplitSeq(v, ";")
|
||||
for token := range tokens {
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(token)), "scope") {
|
||||
tokenNoSpaces := strings.Join(strings.Fields(token), "")
|
||||
parts := strings.SplitN(tokenNoSpaces, "=", 2)
|
||||
@@ -200,22 +229,46 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
|
||||
return nil
|
||||
}).
|
||||
Custom("emulatedMediaFeatures", func(value string) error {
|
||||
if value == "" {
|
||||
emulatedMediaFeatures = defaultOptions.EmulatedMediaFeatures
|
||||
return nil
|
||||
}
|
||||
|
||||
var features map[string]string
|
||||
err := json.Unmarshal([]byte(value), &features)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal emulatedMediaFeatures: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range features {
|
||||
emulatedMediaFeatures = append(emulatedMediaFeatures, EmulatedMediaFeature{
|
||||
Name: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}).
|
||||
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground)
|
||||
|
||||
options := Options{
|
||||
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
||||
FailOnHttpStatusCodes: failOnHttpStatusCodes,
|
||||
FailOnResourceHttpStatusCodes: failOnResourceHttpStatusCodes,
|
||||
FailOnResourceLoadingFailed: failOnResourceLoadingFailed,
|
||||
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||
WaitDelay: waitDelay,
|
||||
WaitWindowStatus: waitWindowStatus,
|
||||
WaitForExpression: waitForExpression,
|
||||
Cookies: cookies,
|
||||
UserAgent: userAgent,
|
||||
ExtraHttpHeaders: extraHttpHeaders,
|
||||
EmulatedMediaType: emulatedMediaType,
|
||||
OmitBackground: omitBackground,
|
||||
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
||||
FailOnHttpStatusCodes: failOnHttpStatusCodes,
|
||||
FailOnResourceHttpStatusCodes: failOnResourceHttpStatusCodes,
|
||||
IgnoreResourceHttpStatusDomains: ignoreResourceHttpStatusDomains,
|
||||
FailOnResourceLoadingFailed: failOnResourceLoadingFailed,
|
||||
FailOnConsoleExceptions: failOnConsoleExceptions,
|
||||
WaitDelay: waitDelay,
|
||||
WaitWindowStatus: waitWindowStatus,
|
||||
WaitForExpression: waitForExpression,
|
||||
WaitForSelector: waitForSelector,
|
||||
Cookies: cookies,
|
||||
UserAgent: userAgent,
|
||||
ExtraHttpHeaders: extraHttpHeaders,
|
||||
EmulatedMediaType: emulatedMediaType,
|
||||
EmulatedMediaFeatures: emulatedMediaFeatures,
|
||||
OmitBackground: omitBackground,
|
||||
}
|
||||
|
||||
return form, options
|
||||
@@ -633,7 +686,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
|
||||
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{}, userPassword, ownerPassword string, embedPaths []string) error {
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]any, userPassword, ownerPassword string, embedPaths []string) error {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1130.
|
||||
filename := ctx.OutputFilename(outputPath)
|
||||
@@ -783,6 +836,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) {
|
||||
return api.WrapError(
|
||||
err,
|
||||
|
||||
@@ -39,11 +39,9 @@ func (reader *streamReader) Read(p []byte) (n int, err error) {
|
||||
// Chromium might have an off-by-one when deciding the maximum size (at
|
||||
// least for base64 encoded data), usually it will overflow. We subtract
|
||||
// one to make sure it fits into p.
|
||||
size := len(p) - 1
|
||||
if size < 1 {
|
||||
size := max(len(p)-1,
|
||||
// Safety-check to avoid crashing Chrome (e.g. via SetSize(-1)).
|
||||
size = 1
|
||||
}
|
||||
1)
|
||||
|
||||
reply, err := reader.next(reader.pos, size)
|
||||
if err != nil {
|
||||
|
||||
@@ -423,26 +423,44 @@ func forceExactColorsActionFunc(logger *zap.Logger, printBackground bool) chrome
|
||||
}
|
||||
}
|
||||
|
||||
func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string) chromedp.ActionFunc {
|
||||
func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string, mediaFeatures []EmulatedMediaFeature) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
if mediaType == "" {
|
||||
logger.Debug("no emulated media type")
|
||||
if mediaType == "" && len(mediaFeatures) == 0 {
|
||||
logger.Debug("no emulated media type or features")
|
||||
return nil
|
||||
}
|
||||
|
||||
if mediaType != "screen" && mediaType != "print" {
|
||||
if mediaType != "" && mediaType != "screen" && mediaType != "print" {
|
||||
return fmt.Errorf("validate emulated media type '%s': %w", mediaType, ErrInvalidEmulatedMediaType)
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("emulate media type '%s'", mediaType))
|
||||
|
||||
emulatedMedia := emulation.SetEmulatedMedia()
|
||||
err := emulatedMedia.WithMedia(mediaType).Do(ctx)
|
||||
|
||||
if mediaType != "" {
|
||||
logger.Debug(fmt.Sprintf("emulate media type '%s'", mediaType))
|
||||
emulatedMedia = emulatedMedia.WithMedia(mediaType)
|
||||
}
|
||||
|
||||
if len(mediaFeatures) > 0 {
|
||||
logger.Debug(fmt.Sprintf("emulate media features %+v", mediaFeatures))
|
||||
|
||||
features := make([]*emulation.MediaFeature, len(mediaFeatures))
|
||||
for i, f := range mediaFeatures {
|
||||
features[i] = &emulation.MediaFeature{
|
||||
Name: f.Name,
|
||||
Value: f.Value,
|
||||
}
|
||||
}
|
||||
|
||||
emulatedMedia = emulatedMedia.WithFeatures(features)
|
||||
}
|
||||
|
||||
err := emulatedMedia.Do(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("emulate media type '%s': %w", mediaType, err)
|
||||
return fmt.Errorf("emulate media: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,3 +530,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ func (engine *ExifTool) Validate() error {
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *ExifTool) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
func (engine *ExifTool) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
|
||||
cmd := exec.Command(engine.binPath, "-ver") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
@@ -94,7 +94,7 @@ func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logger, formats
|
||||
}
|
||||
|
||||
// ReadMetadata extracts the metadata of a given PDF file.
|
||||
func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new ExifTool: %w", err)
|
||||
@@ -116,7 +116,7 @@ func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap.Logger, in
|
||||
}
|
||||
|
||||
// WriteMetadata writes the metadata into a given PDF file.
|
||||
func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("new ExifTool: %w", err)
|
||||
@@ -134,13 +134,40 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
return fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
|
||||
}
|
||||
|
||||
// Define a list of derived, system, or computed tags that ExifTool
|
||||
// extracts but should never be written back. Writing these can break PDF/A
|
||||
// compliance (e.g., PageCount -> prism:pageCount) or cause side effects
|
||||
// (e.g., FileModifyDate).
|
||||
derivedTags := []string{
|
||||
"PageCount", // Causes prism:pageCount injection
|
||||
"Linearized", // Computed status; writing it may invalidate structure
|
||||
"PDFVersion", // Header version; should not be manually forced via metadata
|
||||
"MIMEType", // Read-only derived
|
||||
"FileType", // Read-only derived
|
||||
"FileTypeExtension", // Read-only derived
|
||||
"FileSize", // System attribute
|
||||
"FileModifyDate", // System attribute
|
||||
"FileAccessDate", // System attribute
|
||||
"FileInodeChangeDate", // System attribute
|
||||
"FilePermissions", // System attribute
|
||||
"FileName", // Writing this triggers a file rename in ExifTool
|
||||
"Directory", // System attribute
|
||||
"ExifToolVersion", // Tool metadata
|
||||
"Error", // Extraction error messages
|
||||
"Warning", // Extraction warning messages
|
||||
}
|
||||
|
||||
for _, tag := range derivedTags {
|
||||
delete(fileMetadata[0].Fields, tag)
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
switch val := value.(type) {
|
||||
case string:
|
||||
fileMetadata[0].SetString(key, val)
|
||||
case []string:
|
||||
fileMetadata[0].SetStrings(key, val)
|
||||
case []interface{}:
|
||||
case []any:
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1048.
|
||||
strs := make([]string, len(val))
|
||||
for i, entry := range val {
|
||||
@@ -148,7 +175,7 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
strs[i] = str
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeFor[[]any](), gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
}
|
||||
fileMetadata[0].SetStrings(key, strs)
|
||||
case bool:
|
||||
|
||||
@@ -54,7 +54,7 @@ type Api struct {
|
||||
// See: https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html.
|
||||
type Options struct {
|
||||
// Password specifies the password for opening the source file.
|
||||
Password string
|
||||
Password string // #nosec
|
||||
|
||||
// Landscape allows changing the orientation of the resulting PDF.
|
||||
Landscape bool
|
||||
@@ -253,7 +253,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||
|
||||
// Process.
|
||||
a.libreOffice = newLibreOfficeProcess(a.args)
|
||||
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"))
|
||||
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, a.libreOffice, flags.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"), 1)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -316,8 +316,8 @@ func (a *Api) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (a *Api) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
func (a *Api) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
|
||||
cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
@@ -82,12 +82,12 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
|
||||
}
|
||||
|
||||
// ReadMetadata is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
func (engine *LibreOfficePdfEngine) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("read PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// WriteMetadata is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ func newLogLevel(level string) (zapcore.Level, error) {
|
||||
}
|
||||
|
||||
func newLogEncoder(format string, gcpFields bool) (zapcore.Encoder, error) {
|
||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd())) // #nosec
|
||||
encCfg := zap.NewProductionEncoderConfig()
|
||||
|
||||
// Normalize the log format based on the output device.
|
||||
|
||||
@@ -57,8 +57,8 @@ func (engine *PdfCpu) Validate() error {
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *PdfCpu) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
func (engine *PdfCpu) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
|
||||
cmd := exec.Command(engine.binPath, "version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
@@ -71,10 +71,10 @@ func (engine *PdfCpu) Debug() map[string]interface{} {
|
||||
|
||||
debug["version"] = "Unable to determine pdfcpu version"
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "pdfcpu:") {
|
||||
debug["version"] = strings.TrimSpace(strings.TrimPrefix(line, "pdfcpu:"))
|
||||
lines := strings.SplitSeq(string(output), "\n")
|
||||
for line := range lines {
|
||||
if after, ok := strings.CutPrefix(line, "pdfcpu:"); ok {
|
||||
debug["version"] = strings.TrimSpace(after)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func (engine *PdfCpu) Debug() map[string]interface{} {
|
||||
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var args []string
|
||||
args := make([]string, 0, 2+len(inputPaths))
|
||||
args = append(args, "merge", outputPath)
|
||||
args = append(args, inputPaths...)
|
||||
|
||||
@@ -162,12 +162,12 @@ func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats g
|
||||
}
|
||||
|
||||
// ReadMetadata is not available in this implementation.
|
||||
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("read PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// WriteMetadata is not available in this implementation.
|
||||
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
@@ -180,10 +180,8 @@ func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *zap.Logger, filePa
|
||||
|
||||
logger.Debug(fmt.Sprintf("embedding %d file(s) to %s: %v", len(filePaths), inputPath, filePaths))
|
||||
|
||||
args := []string{
|
||||
"attachments", "add",
|
||||
inputPath,
|
||||
}
|
||||
args := make([]string, 0, 3+len(filePaths))
|
||||
args = append(args, "attachments", "add", inputPath)
|
||||
args = append(args, filePaths...)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
@@ -209,7 +207,7 @@ func (engine *PdfCpu) Encrypt(ctx context.Context, logger *zap.Logger, inputPath
|
||||
ownerPassword = userPassword
|
||||
}
|
||||
|
||||
var args []string
|
||||
args := make([]string, 0, 11)
|
||||
args = append(args, "encrypt")
|
||||
args = append(args, "-mode", "aes")
|
||||
args = append(args, "-upw", userPassword)
|
||||
|
||||
@@ -156,13 +156,13 @@ func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, f
|
||||
}
|
||||
|
||||
type readMetadataResult struct {
|
||||
metadata map[string]interface{}
|
||||
metadata map[string]any
|
||||
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]any, error) {
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
@@ -193,7 +193,7 @@ func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logg
|
||||
|
||||
// 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]any, inputPath string) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
|
||||
@@ -479,8 +479,8 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return make(map[string]any), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -492,13 +492,13 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return make(map[string]any), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -510,12 +510,12 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
@@ -529,8 +529,8 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return make(map[string]any), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -570,7 +570,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -583,12 +583,12 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -601,12 +601,12 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
@@ -620,7 +620,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
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]any, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ package pdfengines
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
@@ -179,13 +180,7 @@ func (mod *PdfEngines) Validate() error {
|
||||
continue
|
||||
}
|
||||
|
||||
alreadyInSlice := false
|
||||
for _, engine := range nonExistingEngines {
|
||||
if engine == name {
|
||||
alreadyInSlice = true
|
||||
break
|
||||
}
|
||||
}
|
||||
alreadyInSlice := slices.Contains(nonExistingEngines, name)
|
||||
|
||||
if !alreadyInSlice {
|
||||
nonExistingEngines = append(nonExistingEngines, name)
|
||||
|
||||
@@ -102,8 +102,8 @@ func FormDataPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||
}
|
||||
|
||||
// FormDataPdfMetadata creates metadata object from the form data.
|
||||
func FormDataPdfMetadata(form *api.FormData, mandatory bool) map[string]interface{} {
|
||||
var metadata map[string]interface{}
|
||||
func FormDataPdfMetadata(form *api.FormData, mandatory bool) map[string]any {
|
||||
var metadata map[string]any
|
||||
|
||||
metadataFunc := func(value string) error {
|
||||
if len(value) > 0 {
|
||||
@@ -239,7 +239,7 @@ func ConvertStub(ctx *api.Context, engine gotenberg.PdfEngine, formats gotenberg
|
||||
|
||||
// WriteMetadataStub writes the metadata into PDF files. If no metadata, it
|
||||
// does nothing.
|
||||
func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata map[string]interface{}, inputPaths []string) error {
|
||||
func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata map[string]any, inputPaths []string) error {
|
||||
if len(metadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -558,7 +558,7 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
res := make(map[string]map[string]interface{}, len(inputPaths))
|
||||
res := make(map[string]map[string]any, len(inputPaths))
|
||||
for _, inputPath := range inputPaths {
|
||||
metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -56,8 +56,8 @@ func (engine *PdfTk) Validate() error {
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *PdfTk) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
func (engine *PdfTk) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
|
||||
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
@@ -108,7 +108,7 @@ func (engine *PdfTk) Split(ctx context.Context, logger *zap.Logger, mode gotenbe
|
||||
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var args []string
|
||||
args := make([]string, 0, 3+len(inputPaths))
|
||||
args = append(args, inputPaths...)
|
||||
args = append(args, "cat", "output", outputPath)
|
||||
|
||||
@@ -136,12 +136,12 @@ func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats go
|
||||
}
|
||||
|
||||
// ReadMetadata is not available in this implementation.
|
||||
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// WriteMetadata is not available in this implementation.
|
||||
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func (engine *PdfTk) Encrypt(ctx context.Context, logger *zap.Logger, inputPath,
|
||||
// Create a temp output file in the same directory.
|
||||
tmpPath := inputPath + ".tmp"
|
||||
|
||||
var args []string
|
||||
args := make([]string, 0, 8)
|
||||
args = append(args, inputPath)
|
||||
args = append(args, "output", tmpPath)
|
||||
args = append(args, "encrypt_128bit")
|
||||
|
||||
@@ -27,6 +27,7 @@ type Prometheus struct {
|
||||
interval time.Duration
|
||||
disableRouteLogging bool
|
||||
disableCollect bool
|
||||
metricsPath string
|
||||
|
||||
metrics []gotenberg.Metric
|
||||
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.Bool("prometheus-disable-route-logging", false, "Disable the route logging")
|
||||
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
|
||||
}(),
|
||||
@@ -56,6 +58,7 @@ func (mod *Prometheus) Provision(ctx *gotenberg.Context) error {
|
||||
mod.interval = flags.MustDuration("prometheus-collect-interval")
|
||||
mod.disableRouteLogging = flags.MustBool("prometheus-disable-route-logging")
|
||||
mod.disableCollect = flags.MustBool("prometheus-disable-collect")
|
||||
mod.metricsPath = flags.MustString("prometheus-metrics-path")
|
||||
|
||||
if mod.disableCollect {
|
||||
// Exit early.
|
||||
@@ -98,6 +101,10 @@ func (mod *Prometheus) Validate() error {
|
||||
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))
|
||||
|
||||
for _, metric := range mod.metrics {
|
||||
@@ -171,7 +178,7 @@ func (mod *Prometheus) Routes() ([]api.Route, error) {
|
||||
return []api.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/prometheus/metrics",
|
||||
Path: mod.metricsPath,
|
||||
DisableLogging: mod.disableRouteLogging,
|
||||
Handler: echo.WrapHandler(
|
||||
promhttp.HandlerFor(mod.registry, promhttp.HandlerOpts{}),
|
||||
|
||||
@@ -59,8 +59,8 @@ func (engine *QPdf) Validate() error {
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *QPdf) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
func (engine *QPdf) Debug() map[string]any {
|
||||
debug := make(map[string]any)
|
||||
|
||||
cmd := exec.Command(engine.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
@@ -114,7 +114,7 @@ func (engine *QPdf) Split(ctx context.Context, logger *zap.Logger, mode gotenber
|
||||
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var args []string
|
||||
args := make([]string, 0, 4+len(engine.globalArgs)+len(inputPaths))
|
||||
args = append(args, "--empty")
|
||||
args = append(args, engine.globalArgs...)
|
||||
args = append(args, "--pages")
|
||||
@@ -137,7 +137,7 @@ func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
|
||||
// Flatten merges annotation appearances with page content, deleting the
|
||||
// original annotations.
|
||||
func (engine *QPdf) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
var args []string
|
||||
args := make([]string, 0, 4+len(engine.globalArgs))
|
||||
args = append(args, inputPath)
|
||||
args = append(args, "--generate-appearances")
|
||||
args = append(args, "--flatten-annotations=all")
|
||||
@@ -163,12 +163,12 @@ func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats got
|
||||
}
|
||||
|
||||
// ReadMetadata is not available in this implementation.
|
||||
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// WriteMetadata is not available in this implementation.
|
||||
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func (engine *QPdf) Encrypt(ctx context.Context, logger *zap.Logger, inputPath,
|
||||
ownerPassword = userPassword
|
||||
}
|
||||
|
||||
var args []string
|
||||
args := make([]string, 0, 7+len(engine.globalArgs))
|
||||
args = append(args, inputPath)
|
||||
args = append(args, engine.globalArgs...)
|
||||
args = append(args, "--replace-input")
|
||||
|
||||
@@ -272,6 +272,29 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
})
|
||||
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
|
||||
// goroutine and return immediately.
|
||||
w.asyncCount.Add(1)
|
||||
|
||||
20
test/integration/features/chromium_concurrent.feature
Normal file
20
test/integration/features/chromium_concurrent.feature
Normal file
@@ -0,0 +1,20 @@
|
||||
@chromium
|
||||
@chromium-concurrent
|
||||
Feature: Chromium concurrent conversions
|
||||
|
||||
Scenario: Concurrent HTML to PDF conversions with max concurrency 3
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| CHROMIUM_MAX_CONCURRENCY | 3 |
|
||||
When I make 3 concurrent "POST" requests 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 |
|
||||
Then all concurrent response status codes should be 200
|
||||
Then all concurrent responses should have 1 PDF(s)
|
||||
|
||||
Scenario: Concurrent conversions exceeding restart-after limit
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| CHROMIUM_MAX_CONCURRENCY | 3 |
|
||||
| CHROMIUM_RESTART_AFTER | 5 |
|
||||
When I make 10 concurrent "POST" requests 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 |
|
||||
Then all concurrent response status codes should be 200
|
||||
Then all concurrent responses should have 1 PDF(s)
|
||||
@@ -195,6 +195,36 @@ Feature: /forms/chromium/convert/html
|
||||
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)
|
||||
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):
|
||||
@@ -251,6 +281,82 @@ Feature: /forms/chromium/convert/html
|
||||
Emulated media type is 'print'.
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/chromium/convert/html (Emulated Media Features)
|
||||
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:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | 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:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaType | screen | field |
|
||||
| emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | 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:
|
||||
"""
|
||||
Emulated media type is 'screen'.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should have the following content at page 1:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should NOT have the following content at page 1:
|
||||
"""
|
||||
Emulated media type is 'print'.
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaType | print | field |
|
||||
| emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | 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:
|
||||
"""
|
||||
Emulated media type is 'print'.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should have the following content at page 1:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should NOT have the following content at page 1:
|
||||
"""
|
||||
Emulated media type is 'screen'.
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/chromium/convert/html (Default Allow / Deny Lists)
|
||||
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):
|
||||
@@ -343,6 +449,16 @@ Feature: /forms/chromium/convert/html
|
||||
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)
|
||||
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):
|
||||
@@ -510,6 +626,15 @@ Feature: /forms/chromium/convert/html
|
||||
"""
|
||||
Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope=*."}', resulting to invalid scope regex pattern for header 'foo': error parsing regexp: missing argument to repetition operator in `*.`)
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaFeatures | foo | 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:
|
||||
"""
|
||||
Invalid form data: form field 'emulatedMediaFeatures' is invalid (got 'foo', resulting to unmarshal emulatedMediaFeatures: invalid character 'o' in literal false (expecting 'a'))
|
||||
"""
|
||||
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 |
|
||||
| splitMode | foo | field |
|
||||
|
||||
@@ -255,6 +255,38 @@ Feature: /forms/chromium/convert/url
|
||||
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)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a static server
|
||||
@@ -314,6 +346,86 @@ Feature: /forms/chromium/convert/url
|
||||
Emulated media type is 'print'.
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/chromium/convert/url (Emulated Media Features)
|
||||
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:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | 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:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaType | screen | field |
|
||||
| emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | 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:
|
||||
"""
|
||||
Emulated media type is 'screen'.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should have the following content at page 1:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should NOT have the following content at page 1:
|
||||
"""
|
||||
Emulated media type is 'print'.
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaType | print | field |
|
||||
| emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | 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:
|
||||
"""
|
||||
Emulated media type is 'print'.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should have the following content at page 1:
|
||||
"""
|
||||
Prefers reduced motion.
|
||||
"""
|
||||
Then the "foo.pdf" PDF should NOT have the following content at page 1:
|
||||
"""
|
||||
Emulated media type is 'screen'.
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/chromium/convert/url (Default Allow / Deny Lists)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a static server
|
||||
@@ -595,6 +707,16 @@ Feature: /forms/chromium/convert/url
|
||||
Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope=*."}', resulting to invalid scope regex pattern for header 'foo': error parsing regexp: missing argument to repetition operator in `*.`)
|
||||
"""
|
||||
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 |
|
||||
| emulatedMediaFeatures | foo | 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:
|
||||
"""
|
||||
Invalid form data: form field 'emulatedMediaFeatures' is invalid (got 'foo', resulting to unmarshal emulatedMediaFeatures: invalid character 'o' in literal false (expecting 'a'))
|
||||
"""
|
||||
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 |
|
||||
| splitMode | foo | field |
|
||||
|
||||
@@ -16,6 +16,127 @@ Feature: /debug
|
||||
"""
|
||||
{
|
||||
"version": "{version}",
|
||||
"timezone": "UTC",
|
||||
"architecture": "ignore",
|
||||
"modules": [
|
||||
"api",
|
||||
"chromium",
|
||||
"exiftool",
|
||||
"libreoffice",
|
||||
"libreoffice-api",
|
||||
"libreoffice-pdfengine",
|
||||
"logging",
|
||||
"pdfcpu",
|
||||
"pdfengines",
|
||||
"pdftk",
|
||||
"prometheus",
|
||||
"qpdf",
|
||||
"webhook"
|
||||
],
|
||||
"modules_additional_data": {
|
||||
"chromium": {
|
||||
"version": "ignore"
|
||||
},
|
||||
"exiftool": {
|
||||
"version": "ignore"
|
||||
},
|
||||
"libreoffice-api": {
|
||||
"version": "ignore"
|
||||
},
|
||||
"pdfcpu": {
|
||||
"version": "ignore"
|
||||
},
|
||||
"pdftk": {
|
||||
"version": "ignore"
|
||||
},
|
||||
"qpdf": {
|
||||
"version": "ignore"
|
||||
}
|
||||
},
|
||||
"flags": {
|
||||
"api-bind-ip": "",
|
||||
"api-body-limit": "",
|
||||
"api-disable-download-from": "false",
|
||||
"api-disable-health-check-logging": "false",
|
||||
"api-download-from-allow-list": "",
|
||||
"api-download-from-deny-list": "",
|
||||
"api-download-from-max-retry": "4",
|
||||
"api-enable-basic-auth": "false",
|
||||
"api-enable-debug-route": "true",
|
||||
"api-port": "3000",
|
||||
"api-port-from-env": "",
|
||||
"api-root-path": "/",
|
||||
"api-start-timeout": "30s",
|
||||
"api-timeout": "30s",
|
||||
"api-tls-cert-file": "",
|
||||
"api-tls-key-file": "",
|
||||
"api-trace-header": "Gotenberg-Trace",
|
||||
"chromium-allow-file-access-from-files": "false",
|
||||
"chromium-allow-insecure-localhost": "false",
|
||||
"chromium-allow-list": "",
|
||||
"chromium-auto-start": "false",
|
||||
"chromium-clear-cache": "false",
|
||||
"chromium-clear-cookies": "false",
|
||||
"chromium-deny-list": "^file:(?!//\\/tmp/).*",
|
||||
"chromium-disable-javascript": "false",
|
||||
"chromium-disable-routes": "false",
|
||||
"chromium-disable-web-security": "false",
|
||||
"chromium-host-resolver-rules": "",
|
||||
"chromium-ignore-certificate-errors": "false",
|
||||
"chromium-incognito": "false",
|
||||
"chromium-max-concurrency": "6",
|
||||
"chromium-max-queue-size": "0",
|
||||
"chromium-proxy-server": "",
|
||||
"chromium-restart-after": "100",
|
||||
"chromium-start-timeout": "20s",
|
||||
"gotenberg-build-debug-data": "true",
|
||||
"gotenberg-graceful-shutdown-duration": "30s",
|
||||
"libreoffice-auto-start": "false",
|
||||
"libreoffice-disable-routes": "false",
|
||||
"libreoffice-max-queue-size": "0",
|
||||
"libreoffice-restart-after": "10",
|
||||
"libreoffice-start-timeout": "20s",
|
||||
"log-fields-prefix": "",
|
||||
"log-format": "auto",
|
||||
"log-level": "info",
|
||||
"pdfengines-convert-engines": "[libreoffice-pdfengine]",
|
||||
"pdfengines-disable-routes": "false",
|
||||
"pdfengines-engines": "[]",
|
||||
"pdfengines-flatten-engines": "[qpdf]",
|
||||
"pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]",
|
||||
"pdfengines-read-metadata-engines": "[exiftool]",
|
||||
"pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]",
|
||||
"pdfengines-write-metadata-engines": "[exiftool]",
|
||||
"prometheus-collect-interval": "1s",
|
||||
"prometheus-disable-collect": "false",
|
||||
"prometheus-disable-route-logging": "false",
|
||||
"prometheus-namespace": "gotenberg",
|
||||
"prometheus-metrics-path": "/prometheus/metrics",
|
||||
"webhook-allow-list": "",
|
||||
"webhook-client-timeout": "30s",
|
||||
"webhook-deny-list": "",
|
||||
"webhook-disable": "false",
|
||||
"webhook-error-allow-list": "",
|
||||
"webhook-error-deny-list": "",
|
||||
"webhook-max-retry": "4",
|
||||
"webhook-retry-max-wait": "30s",
|
||||
"webhook-retry-min-wait": "1s"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: GET /debug (Environment based timezone)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| API_ENABLE_DEBUG_ROUTE | true |
|
||||
| TZ | America/New_York |
|
||||
When I make a "GET" request to Gotenberg at the "/debug" endpoint
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"version": "{version}",
|
||||
"timezone": "America/New_York",
|
||||
"architecture": "ignore",
|
||||
"modules": [
|
||||
"api",
|
||||
@@ -84,8 +205,9 @@ Feature: /debug
|
||||
"chromium-ignore-certificate-errors": "false",
|
||||
"chromium-incognito": "false",
|
||||
"chromium-max-queue-size": "0",
|
||||
"chromium-max-concurrency": "6",
|
||||
"chromium-proxy-server": "",
|
||||
"chromium-restart-after": "10",
|
||||
"chromium-restart-after": "100",
|
||||
"chromium-start-timeout": "20s",
|
||||
"gotenberg-build-debug-data": "true",
|
||||
"gotenberg-graceful-shutdown-duration": "30s",
|
||||
@@ -109,6 +231,7 @@ Feature: /debug
|
||||
"prometheus-disable-collect": "false",
|
||||
"prometheus-disable-route-logging": "false",
|
||||
"prometheus-namespace": "gotenberg",
|
||||
"prometheus-metrics-path": "/prometheus/metrics",
|
||||
"webhook-allow-list": "",
|
||||
"webhook-client-timeout": "30s",
|
||||
"webhook-deny-list": "",
|
||||
@@ -133,6 +256,7 @@ Feature: /debug
|
||||
"""
|
||||
{
|
||||
"version": "",
|
||||
"timezone": "",
|
||||
"architecture": "",
|
||||
"modules": null,
|
||||
"modules_additional_data": null,
|
||||
|
||||
@@ -29,6 +29,31 @@ Feature: /prometheus/metrics
|
||||
Then the Gotenberg container should log the following entries:
|
||||
| "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)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PROMETHEUS_NAMESPACE | foo |
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func compareJson(expected, actual interface{}) error {
|
||||
func compareJson(expected, actual any) error {
|
||||
// Handle maps (JSON objects).
|
||||
expectedMap, ok := expected.(map[string]interface{})
|
||||
expectedMap, ok := expected.(map[string]any)
|
||||
if ok {
|
||||
actualMap, ok := actual.(map[string]interface{})
|
||||
actualMap, ok := actual.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected an object, but actual is: %T", actual)
|
||||
}
|
||||
@@ -31,9 +31,9 @@ func compareJson(expected, actual interface{}) error {
|
||||
}
|
||||
|
||||
// Handle slices (JSON arrays).
|
||||
expectedSlice, ok := expected.([]interface{})
|
||||
expectedSlice, ok := expected.([]any)
|
||||
if ok {
|
||||
actualSlice, ok := actual.([]interface{})
|
||||
actualSlice, ok := actual.([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected an array, but actual is: %T", actual)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ var (
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func (n *noopLogger) Printf(format string, v ...interface{}) {
|
||||
func (n *noopLogger) Printf(format string, v ...any) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ func doRequest(method, url string, headers map[string]string, body io.Reader) (*
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
|
||||
// #nosec
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send a request: %w", err)
|
||||
@@ -74,6 +75,7 @@ func doFormDataRequest(method, url string, fields map[string]string, files map[s
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
// #nosec
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send a request: %w", err)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
|
||||
type scenario struct {
|
||||
resp *httptest.ResponseRecorder
|
||||
concurrentResps []*httptest.ResponseRecorder
|
||||
workdir string
|
||||
gotenbergContainer testcontainers.Container
|
||||
gotenbergContainerNetwork *testcontainers.DockerNetwork
|
||||
@@ -33,6 +35,7 @@ type scenario struct {
|
||||
|
||||
func (s *scenario) reset(ctx context.Context) error {
|
||||
s.resp = httptest.NewRecorder()
|
||||
s.concurrentResps = nil
|
||||
|
||||
err := os.RemoveAll(s.workdir)
|
||||
if err != nil {
|
||||
@@ -281,6 +284,168 @@ func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ct
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iMakeConcurrentRequestsToGotenberg(ctx context.Context, count int, method, endpoint string, dataTable *godog.Table) error {
|
||||
if s.gotenbergContainer == nil {
|
||||
return errors.New("no Gotenberg container")
|
||||
}
|
||||
|
||||
fields := make(map[string]string)
|
||||
files := make(map[string][]string)
|
||||
headers := make(map[string]string)
|
||||
|
||||
for _, row := range dataTable.Rows {
|
||||
name := row.Cells[0].Value
|
||||
value := row.Cells[1].Value
|
||||
kind := row.Cells[2].Value
|
||||
|
||||
switch kind {
|
||||
case "field":
|
||||
fields[name] = value
|
||||
case "file":
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current directory: %w", err)
|
||||
}
|
||||
value = fmt.Sprintf("%s/%s", wd, value)
|
||||
files[name] = append(files[name], value)
|
||||
case "header":
|
||||
headers[name] = value
|
||||
default:
|
||||
return fmt.Errorf("unexpected %q %q", kind, value)
|
||||
}
|
||||
}
|
||||
|
||||
base, err := containerHttpEndpoint(ctx, s.gotenbergContainer, "3000")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get container HTTP endpoint: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
s.concurrentResps = make([]*httptest.ResponseRecorder, 0, count)
|
||||
errs := make([]error, 0)
|
||||
|
||||
for range count {
|
||||
wg.Go(func() {
|
||||
resp, reqErr := doFormDataRequest(method, fmt.Sprintf("%s%s", base, endpoint), fields, files, headers)
|
||||
if reqErr != nil {
|
||||
mu.Lock()
|
||||
errs = append(errs, fmt.Errorf("do request: %w", reqErr))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, reqErr := io.ReadAll(resp.Body)
|
||||
if reqErr != nil {
|
||||
mu.Lock()
|
||||
errs = append(errs, fmt.Errorf("read response body: %w", reqErr))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
rec.Code = resp.StatusCode
|
||||
for key, values := range resp.Header {
|
||||
for _, v := range values {
|
||||
rec.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
_, _ = rec.Body.Write(body)
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
cd := resp.Header.Get("Content-Disposition")
|
||||
if cd != "" {
|
||||
_, params, parseErr := mime.ParseMediaType(cd)
|
||||
if parseErr == nil {
|
||||
if filename, ok := params["filename"]; ok {
|
||||
traceID := resp.Header.Get("Gotenberg-Trace")
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, traceID)
|
||||
|
||||
mu.Lock()
|
||||
mkErr := os.MkdirAll(dirPath, 0o755)
|
||||
mu.Unlock()
|
||||
|
||||
if mkErr == nil {
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
f, fErr := os.Create(fpath)
|
||||
if fErr == nil {
|
||||
_, _ = f.Write(body)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
s.concurrentResps = append(s.concurrentResps, rec)
|
||||
mu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("concurrent requests failed: %v", errs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) allConcurrentResponseStatusCodesShouldBe(expected int) error {
|
||||
if len(s.concurrentResps) == 0 {
|
||||
return errors.New("no concurrent responses recorded")
|
||||
}
|
||||
|
||||
for i, resp := range s.concurrentResps {
|
||||
if resp.Code != expected {
|
||||
return fmt.Errorf("concurrent response %d: expected status %d, got %d %q", i+1, expected, resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) allConcurrentResponsesShouldHavePdfs(expected int) error {
|
||||
if len(s.concurrentResps) == 0 {
|
||||
return errors.New("no concurrent responses recorded")
|
||||
}
|
||||
|
||||
for i, resp := range s.concurrentResps {
|
||||
traceID := resp.Header().Get("Gotenberg-Trace")
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, traceID)
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("concurrent response %d: directory %q does not exist", i+1, 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("concurrent response %d: walk %q: %w", i+1, dirPath, err)
|
||||
}
|
||||
|
||||
if len(paths) != expected {
|
||||
return fmt.Errorf("concurrent response %d: expected %d PDF(s), got %d", i+1, expected, len(paths))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iWaitForTheAsynchronousRequestToWebhook(ctx context.Context) error {
|
||||
if s.server == nil {
|
||||
return errors.New("server not initialized")
|
||||
@@ -327,7 +492,7 @@ func (s *scenario) theGotenbergContainerShouldLogTheFollowingEntries(ctx context
|
||||
}
|
||||
|
||||
var err error
|
||||
for i := 0; i < 3; i++ {
|
||||
for range 3 {
|
||||
err = check()
|
||||
if err != nil && !invert {
|
||||
// We have to retry as not all logs may have been produced.
|
||||
@@ -450,7 +615,7 @@ func (s *scenario) theBodyShouldMatchJSON(kind string, expectedDoc *godog.DocStr
|
||||
body = s.server.bodyCopy
|
||||
}
|
||||
|
||||
var expected, actual interface{}
|
||||
var expected, actual any
|
||||
|
||||
content := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion)
|
||||
err := json.Unmarshal([]byte(content), &expected)
|
||||
@@ -965,9 +1130,12 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
|
||||
ctx.When(`^I make a "(GET|HEAD)" request to Gotenberg at the "([^"]*)" endpoint$`, s.iMakeARequestToGotenberg)
|
||||
ctx.When(`^I make a "(GET|HEAD)" request to Gotenberg at the "([^"]*)" endpoint with the following header\(s\):$`, s.iMakeARequestToGotenbergWithTheFollowingHeaders)
|
||||
ctx.When(`^I make a "(POST)" request to Gotenberg at the "([^"]*)" endpoint with the following form data and header\(s\):$`, s.iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders)
|
||||
ctx.When(`^I make (\d+) concurrent "(POST)" requests to Gotenberg at the "([^"]*)" endpoint with the following form data and header\(s\):$`, s.iMakeConcurrentRequestsToGotenberg)
|
||||
ctx.When(`^I wait for the asynchronous request to the webhook$`, s.iWaitForTheAsynchronousRequestToWebhook)
|
||||
ctx.Then(`^the Gotenberg container (should|should NOT) log the following entries:$`, s.theGotenbergContainerShouldLogTheFollowingEntries)
|
||||
ctx.Then(`^the response status code should be (\d+)$`, s.theResponseStatusCodeShouldBe)
|
||||
ctx.Then(`^all concurrent response status codes should be (\d+)$`, s.allConcurrentResponseStatusCodesShouldBe)
|
||||
ctx.Then(`^all concurrent responses should have (\d+) PDF\(s\)$`, s.allConcurrentResponsesShouldHavePdfs)
|
||||
ctx.Then(`^the (response|webhook request|file request|server request) header "([^"]*)" should be "([^"]*)"$`, s.theHeaderValueShouldBe)
|
||||
ctx.Then(`^the (response|webhook request|file request|server request) cookie "([^"]*)" should be "([^"]*)"$`, s.theCookieValueShouldBe)
|
||||
ctx.Then(`^the (response|webhook request) body should match string:$`, s.theBodyShouldMatchString)
|
||||
|
||||
@@ -79,12 +79,14 @@ func newServer(ctx context.Context, workdir string) (*server, error) {
|
||||
}
|
||||
|
||||
dirPath := fmt.Sprintf("%s/%s", workdir, s.req.Header.Get("Gotenberg-Trace"))
|
||||
// #nosec
|
||||
err = os.MkdirAll(dirPath, 0o755)
|
||||
if err != nil {
|
||||
return webhookErr(fmt.Errorf("create working directory: %w", err))
|
||||
}
|
||||
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
// #nosec
|
||||
file, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return webhookErr(fmt.Errorf("create file %q: %w", fpath, err))
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#reduced-motion {
|
||||
display: none;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#reduced-motion {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -29,6 +37,7 @@
|
||||
</p>
|
||||
<p id="print">Emulated media type is 'print'.</p>
|
||||
<p id="screen">Emulated media type is 'screen'.</p>
|
||||
<p id="reduced-motion">Prefers reduced motion.</p>
|
||||
<p id="javascript" style="display: none">JavaScript is enabled.</p>
|
||||
<iframe src="file:///etc/passwd"></iframe>
|
||||
|
||||
@@ -37,8 +46,14 @@
|
||||
|
||||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
delay(2000).then(() => {
|
||||
document.getElementById("wait").style.display = "contents";
|
||||
const waitText = document.getElementById("wait");
|
||||
waitText.style.display = "contents";
|
||||
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>
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#reduced-motion {
|
||||
display: none;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#reduced-motion {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -37,6 +45,7 @@
|
||||
</p>
|
||||
<p id="print">Emulated media type is 'print'.</p>
|
||||
<p id="screen">Emulated media type is 'screen'.</p>
|
||||
<p id="reduced-motion">Prefers reduced motion.</p>
|
||||
<p id="javascript" style="display: none">JavaScript is enabled.</p>
|
||||
<iframe src="/etc/passwd"></iframe>
|
||||
<iframe src="\\localhost/etc/passwd"></iframe>
|
||||
@@ -46,8 +55,14 @@
|
||||
|
||||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
delay(2000).then(() => {
|
||||
document.getElementById("wait").style.display = "contents";
|
||||
const waitText = document.getElementById("wait");
|
||||
waitText.style.display = "contents";
|
||||
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>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<span class="date"></span>
|
||||
<span class="title"></span>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user