mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffeb4703fd | ||
|
|
fcf41187fa | ||
|
|
f0fc704efc | ||
|
|
aa58615650 | ||
|
|
b0a84bcbf7 | ||
|
|
cf2d413a28 | ||
|
|
9226b78b0c | ||
|
|
a635fd8a7b | ||
|
|
6f714277f7 | ||
|
|
4aff4982b0 | ||
|
|
8634989497 | ||
|
|
a50e60b2a0 | ||
|
|
8875f4dfc4 | ||
|
|
61c8217ed5 | ||
|
|
f75912033e | ||
|
|
1d8630810b | ||
|
|
ce155fb43e | ||
|
|
67ebe17fcb | ||
|
|
f13a6c0a3b | ||
|
|
e1870429b9 | ||
|
|
5aa1cd7f40 | ||
|
|
de1b887d29 | ||
|
|
e662f5ce0b | ||
|
|
790b69a32e | ||
|
|
9aaab97d6a | ||
|
|
66317197b6 | ||
|
|
91757335ac | ||
|
|
be3145aae9 | ||
|
|
79425af848 | ||
|
|
45b47a1071 | ||
|
|
4b30567c39 | ||
|
|
1ed33a7400 | ||
|
|
ca1d11006c | ||
|
|
71f1f3dd22 | ||
|
|
64add9a542 | ||
|
|
a7c02438e4 | ||
|
|
910af7a44c | ||
|
|
05f9ac3745 | ||
|
|
5c43eefe46 | ||
|
|
b63f4c53d2 | ||
|
|
3d652bc829 | ||
|
|
c78ef8771d | ||
|
|
9a38f16219 | ||
|
|
e5363205a7 | ||
|
|
088b3274f2 | ||
|
|
fabe323e34 | ||
|
|
a731bf034e | ||
|
|
3cbf772acf | ||
|
|
9701f88ae3 | ||
|
|
7ca1d118a1 | ||
|
|
a7dfa21cbf | ||
|
|
ab81444773 | ||
|
|
02c4343dfd | ||
|
|
1bfe86738f | ||
|
|
7fe0e05050 | ||
|
|
10588efe25 | ||
|
|
bf4607d368 | ||
|
|
45f04ae8d4 | ||
|
|
674f1de183 | ||
|
|
368c4fa1a7 | ||
|
|
c737579b7f | ||
|
|
0b7c51a323 | ||
|
|
e7c77b7bf3 | ||
|
|
6c727b7e51 | ||
|
|
a9754d6a1c | ||
|
|
218052d5dd | ||
|
|
f112cb748f | ||
|
|
67a65c66b6 | ||
|
|
cd6add14a0 | ||
|
|
33f9f1ddf4 | ||
|
|
2131de77b0 | ||
|
|
0febb60a3f | ||
|
|
820b914291 | ||
|
|
536d3ee70d | ||
|
|
2544d82486 | ||
|
|
ea6854fadc | ||
|
|
147e3b1173 | ||
|
|
8252142e65 | ||
|
|
6e2270632e | ||
|
|
7d14546d3b | ||
|
|
b31024c362 | ||
|
|
85eaef05ad | ||
|
|
16e4c10862 | ||
|
|
904e0f61a8 | ||
|
|
70953aa66e | ||
|
|
d41de5d270 | ||
|
|
d8603f4f34 | ||
|
|
b13a8094ea | ||
|
|
28e07bad54 | ||
|
|
81cc483166 | ||
|
|
8d7cc55c28 | ||
|
|
083e0e307a | ||
|
|
80f3f89a89 | ||
|
|
3dc8c18849 | ||
|
|
0a6a97cf07 | ||
|
|
7676d7a239 | ||
|
|
679715f896 | ||
|
|
d5aa880336 | ||
|
|
08daa86766 | ||
|
|
f0c1952dfe | ||
|
|
198d9115cc | ||
|
|
6b0c01bd3e | ||
|
|
a1848d3c75 | ||
|
|
a1596f7c62 | ||
|
|
740e701ad3 | ||
|
|
71d571a25c | ||
|
|
2293300182 | ||
|
|
85c7f2806f | ||
|
|
27ecb4fe49 | ||
|
|
7afeefe84f |
@@ -1 +1 @@
|
||||
.git
|
||||
.git
|
||||
|
||||
10
.env
10
.env
@@ -1,14 +1,6 @@
|
||||
GOLANG_VERSION=1.23
|
||||
GOTENBERG_VERSION=snapshot
|
||||
DOCKER_REGISTRY=gotenberg
|
||||
DOCKER_REPOSITORY=gotenberg
|
||||
GOTENBERG_VERSION=snapshot
|
||||
GOTENBERG_USER_GID=1001
|
||||
GOTENBERG_USER_UID=1001
|
||||
NOTO_COLOR_EMOJI_VERSION=v2.047 # See https://github.com/googlefonts/noto-emoji/releases.
|
||||
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||
PDFCPU_VERSION=v0.8.1 # See https://github.com/pdfcpu/pdfcpu/releases.
|
||||
GOLANGCI_LINT_VERSION=v1.63.4 # See https://github.com/golangci/golangci-lint/releases.
|
||||
GOTENBERG_VERSION=snapshot
|
||||
DOCKERFILE=build/Dockerfile
|
||||
DOCKERFILE_CLOUDRUN=build/Dockerfile.cloudrun
|
||||
DOCKER_BUILD_CONTEXT='.'
|
||||
|
||||
89
.github/actions/build-test-push/action.yml
vendored
Normal file
89
.github/actions/build-test-push/action.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: Build Test Push
|
||||
description: Build, test and push Docker images for a given platform
|
||||
author: Julien Neuhart
|
||||
|
||||
inputs:
|
||||
github_token:
|
||||
description: The GitHub token
|
||||
required: true
|
||||
default: ${{ github.token }}
|
||||
docker_hub_username:
|
||||
description: The Docker Hub username
|
||||
required: true
|
||||
docker_hub_password:
|
||||
description: The Docker Hub password
|
||||
required: true
|
||||
platform:
|
||||
description: linux/amd64, linux/386, linux/arm64, linux/arm/v7
|
||||
required: true
|
||||
version:
|
||||
description: Gotenberg version
|
||||
required: true
|
||||
skip_integrations_tests:
|
||||
description: Define whether to skip integration testing
|
||||
default: false
|
||||
alternate_repository:
|
||||
description: Alternate repository to push the tags to
|
||||
dry_run:
|
||||
description: Dry run this action
|
||||
|
||||
outputs:
|
||||
tags:
|
||||
description: Comma separated list of tag
|
||||
value: ${{ steps.build.outputs.tags }}
|
||||
tags_cloud_run:
|
||||
description: Comma separated list of Cloud Run tags (linux/amd64 only)
|
||||
value: ${{ steps.build.outputs.tags_cloud_run }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: inputs.docker_hub_username != ''
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ inputs.docker_hub_username }}
|
||||
password: ${{ inputs.docker_hub_password }}
|
||||
|
||||
- name: Build ${{ inputs.platform }}
|
||||
id: build
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/build-test-push/build.sh \
|
||||
--version "${{ inputs.version }}" \
|
||||
--platform "${{ inputs.platform }}" \
|
||||
--alternate-repository "${{ inputs.alternate_repository }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
|
||||
- name: Run integration tests
|
||||
if: inputs.skip_integrations_tests != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/build-test-push/test.sh \
|
||||
--version "${{ inputs.version }}" \
|
||||
--platform "${{ inputs.platform }}" \
|
||||
--alternate-repository "${{ inputs.alternate_repository }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
|
||||
- name: Push
|
||||
if: inputs.docker_hub_username != ''
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/build-test-push/push.sh \
|
||||
--tags "${{ steps.build.outputs.tags }},${{ steps.build.outputs.tags_cloud_run }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
run: |
|
||||
echo "tags=${{ steps.build.outputs.tags }}"
|
||||
echo "tags_cloud_run=${{ steps.build.outputs.tags_cloud_run }}"
|
||||
161
.github/actions/build-test-push/build.sh
vendored
Executable file
161
.github/actions/build-test-push/build.sh
vendored
Executable file
@@ -0,0 +1,161 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit early.
|
||||
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||
set -e
|
||||
|
||||
# Source dot env file.
|
||||
source .env
|
||||
|
||||
# Arguments.
|
||||
version=""
|
||||
platform=""
|
||||
alternate_repository=""
|
||||
dry_run=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--version)
|
||||
version="${2//v/}"
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
platform="$2"
|
||||
shift 2
|
||||
;;
|
||||
--alternate-repository)
|
||||
alternate_repository="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Build and push 👷"
|
||||
echo
|
||||
|
||||
echo "Gotenberg version: $version"
|
||||
echo "Target platform: $platform"
|
||||
|
||||
if [ -n "$alternate_repository" ]; then
|
||||
DOCKER_REPOSITORY=$alternate_repository
|
||||
echo "⚠️ Using $alternate_repository for DOCKER_REPOSITORY"
|
||||
fi
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run"
|
||||
fi
|
||||
|
||||
# Build tags arrays.
|
||||
tags=()
|
||||
tags_cloud_run=()
|
||||
|
||||
IFS='/' read -ra arch <<< "$platform"
|
||||
IFS='.' read -ra semver <<< "$version"
|
||||
|
||||
if [ "${#semver[@]}" -eq 3 ]; then
|
||||
echo
|
||||
echo "Semver version detected"
|
||||
|
||||
major="${semver[0]}"
|
||||
minor="${semver[1]}"
|
||||
patch="${semver[2]}"
|
||||
|
||||
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-${arch[1]}")
|
||||
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-${arch[1]}")
|
||||
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-${arch[1]}")
|
||||
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-${arch[1]}")
|
||||
|
||||
if [ "$platform" = "linux/amd64" ]; then
|
||||
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-cloudrun")
|
||||
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-cloudrun")
|
||||
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-cloudrun")
|
||||
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-cloudrun")
|
||||
fi
|
||||
else
|
||||
echo
|
||||
echo "Non-semver version detected, fallback to $version"
|
||||
|
||||
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-${arch[1]}")
|
||||
if [ "$platform" = "linux/amd64" ]; then
|
||||
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-cloudrun")
|
||||
fi
|
||||
fi
|
||||
|
||||
tags_flags=()
|
||||
tags_cloud_run_flags=()
|
||||
|
||||
echo "Will use the following tags:"
|
||||
for tag in "${tags[@]}"; do
|
||||
tags_flags+=("-t" "$tag")
|
||||
echo "- $tag"
|
||||
done
|
||||
for tag in "${tags_cloud_run[@]}"; do
|
||||
tags_cloud_run_flags+=("-t" "$tag")
|
||||
echo "- $tag"
|
||||
done
|
||||
echo
|
||||
|
||||
# Build images.
|
||||
run_cmd() {
|
||||
local cmd="$1"
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run - would run the following command:"
|
||||
echo "$cmd"
|
||||
echo
|
||||
else
|
||||
echo "⚙️ Running command:"
|
||||
echo "$cmd"
|
||||
eval "$cmd"
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
join() {
|
||||
local delimiter="$1"
|
||||
shift
|
||||
local IFS="$delimiter"
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
no_arch_tag="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version"
|
||||
|
||||
cmd="docker buildx build \
|
||||
--build-arg GOTENBERG_VERSION=$version \
|
||||
--platform $platform \
|
||||
--load \
|
||||
${tags_flags[*]} \
|
||||
-t $no_arch_tag \
|
||||
-f $DOCKERFILE $DOCKER_BUILD_CONTEXT
|
||||
"
|
||||
run_cmd "$cmd"
|
||||
|
||||
if [ "$platform" != "linux/amd64" ]; then
|
||||
echo "⚠️ Skip Cloud Run variant(s)"
|
||||
echo "✅ Done!"
|
||||
echo "tags=$(join "," "${tags[@]}")" >> "$GITHUB_OUTPUT"
|
||||
echo "tags_cloud_run=$(join "," "${tags_cloud_run[@]}")" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cmd="docker build \
|
||||
--build-arg DOCKER_REGISTRY=$DOCKER_REGISTRY \
|
||||
--build-arg DOCKER_REPOSITORY=$DOCKER_REPOSITORY \
|
||||
--build-arg GOTENBERG_VERSION=$version \
|
||||
${tags_cloud_run_flags[*]} \
|
||||
-f $DOCKERFILE_CLOUDRUN $DOCKER_BUILD_CONTEXT
|
||||
"
|
||||
run_cmd "$cmd"
|
||||
|
||||
echo "✅ Done!"
|
||||
echo "tags=$(join "," "${tags[@]}")" >> "$GITHUB_OUTPUT"
|
||||
echo "tags_cloud_run=$(join "," "${tags_cloud_run[@]}")" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
70
.github/actions/build-test-push/push.sh
vendored
Executable file
70
.github/actions/build-test-push/push.sh
vendored
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit early.
|
||||
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||
set -e
|
||||
|
||||
# Source dot env file.
|
||||
source .env
|
||||
|
||||
# Arguments.
|
||||
tags=""
|
||||
dry_run=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--tags)
|
||||
tags="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Push tag(s) 📦"
|
||||
echo
|
||||
|
||||
echo "Tag(s) to push:"
|
||||
IFS=',' read -ra tags_to_push <<< "$tags"
|
||||
for tag in "${tags_to_push[@]}"; do
|
||||
echo "- $tag"
|
||||
done
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Push tags.
|
||||
run_cmd() {
|
||||
local cmd="$1"
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run - would run the following command:"
|
||||
echo "$cmd"
|
||||
echo
|
||||
else
|
||||
echo "⚙️ Running command:"
|
||||
echo "$cmd"
|
||||
eval "$cmd"
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
for tag in "${tags_to_push[@]}"; do
|
||||
cmd="docker push $tag"
|
||||
run_cmd "$cmd"
|
||||
|
||||
echo "➡️ $tag pushed"
|
||||
echo
|
||||
done
|
||||
|
||||
echo "✅ Done!"
|
||||
exit 0
|
||||
79
.github/actions/build-test-push/test.sh
vendored
Executable file
79
.github/actions/build-test-push/test.sh
vendored
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit early.
|
||||
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||
set -e
|
||||
|
||||
# Source dot env file.
|
||||
source .env
|
||||
|
||||
# Arguments.
|
||||
version=""
|
||||
platform=""
|
||||
alternate_repository=""
|
||||
dry_run=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--version)
|
||||
version="${2//v/}"
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
platform="$2"
|
||||
shift 2
|
||||
;;
|
||||
--alternate-repository)
|
||||
alternate_repository="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Integration testing 🧪"
|
||||
echo
|
||||
|
||||
echo "Gotenberg version: $version"
|
||||
echo "Target platform: $platform"
|
||||
|
||||
repository=$DOCKER_REPOSITORY
|
||||
|
||||
if [ -n "$alternate_repository" ]; then
|
||||
echo "⚠️ Using $alternate_repository for DOCKER_REPOSITORY"
|
||||
repository=$alternate_repository
|
||||
fi
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Test image.
|
||||
run_cmd() {
|
||||
local cmd="$1"
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run - would run the following command:"
|
||||
echo "$cmd"
|
||||
echo
|
||||
else
|
||||
echo "⚙️ Running command:"
|
||||
echo "$cmd"
|
||||
eval "$cmd"
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
cmd="make test-integration DOCKER_REPOSITORY=$repository GOTENBERG_VERSION=$version PLATFORM=$platform NO_CONCURRENCY=true"
|
||||
run_cmd "$cmd"
|
||||
|
||||
echo "✅ Done!"
|
||||
exit 0
|
||||
31
.github/actions/clean/action.yml
vendored
Normal file
31
.github/actions/clean/action.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Clean
|
||||
description: Clean tags from Docker Hub
|
||||
author: Julien Neuhart
|
||||
|
||||
inputs:
|
||||
docker_hub_username:
|
||||
description: The Docker Hub username
|
||||
required: true
|
||||
docker_hub_password:
|
||||
description: The Docker Hub password
|
||||
required: true
|
||||
tags:
|
||||
description: Comma separated list of tags to clean
|
||||
snapshot_version:
|
||||
description: Snapshot version to clean
|
||||
dry_run:
|
||||
description: Dry run this action
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Clean tags from Docker Hub
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ inputs.docker_hub_username }}
|
||||
DOCKERHUB_TOKEN: ${{ inputs.docker_hub_password }}
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/clean/clean.sh \
|
||||
--tags "${{ inputs.tags }}" \
|
||||
--snapshot-version "${{ inputs.snapshot_version }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
122
.github/actions/clean/clean.sh
vendored
Executable file
122
.github/actions/clean/clean.sh
vendored
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit early.
|
||||
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||
set -e
|
||||
|
||||
# Source dot env file.
|
||||
source .env
|
||||
|
||||
# Arguments.
|
||||
tags=""
|
||||
snapshot_version=""
|
||||
dry_run=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--tags)
|
||||
tags="$2"
|
||||
shift 2
|
||||
;;
|
||||
--snapshot-version)
|
||||
snapshot_version="${2//v/}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Clean tag(s) from Docker Hub 🧹"
|
||||
echo
|
||||
|
||||
IFS=',' read -ra tags_to_delete <<< "$tags"
|
||||
if [ -n "$snapshot_version" ]; then
|
||||
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version")
|
||||
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version-cloudrun")
|
||||
fi
|
||||
|
||||
echo "Will delete the following tag(s):"
|
||||
for tag in "${tags_to_delete[@]}"; do
|
||||
echo "- $tag"
|
||||
done
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Delete tags.
|
||||
base_url="https://hub.docker.com/v2"
|
||||
token=""
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
token="placeholder"
|
||||
echo "🚧 Dry run - would call $base_url to get a token"
|
||||
echo
|
||||
else
|
||||
echo "🌐 Get token from $base_url"
|
||||
|
||||
readarray -t lines < <(
|
||||
curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$DOCKERHUB_USERNAME\", \"password\":\"$DOCKERHUB_TOKEN\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
"$base_url/users/login"
|
||||
)
|
||||
|
||||
http_code="${lines[-1]}"
|
||||
unset 'lines[-1]'
|
||||
json_body=$(printf "%s\n" "${lines[@]}")
|
||||
|
||||
if [ "$http_code" -ne "200" ]; then
|
||||
echo "❌ Wrong HTTP status - $http_code"
|
||||
echo "$json_body"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
token=$(jq -r '.token' <<< "$json_body")
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
echo "❌ No token from Docker Hub"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for tag in "${tags_to_delete[@]}"; do
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run - would call $base_url to delete tag $tag"
|
||||
echo
|
||||
else
|
||||
echo "🌐 Delete tag $tag"
|
||||
IFS=':' read -ra tag_parts <<< "$tag"
|
||||
|
||||
readarray -t lines < <(
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-w "\n%{http_code}" \
|
||||
"$base_url/repositories/${tag_parts[0]}/tags/${tag_parts[1]}/"
|
||||
)
|
||||
|
||||
http_code="${lines[-1]}"
|
||||
unset 'lines[-1]'
|
||||
|
||||
if [ "$http_code" -ne "200" ] && [ "$http_code" -ne "204" ]; then
|
||||
echo "❌ Wrong HTTP status - $http_code"
|
||||
printf '%s\n' "${lines[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Done!"
|
||||
exit 0
|
||||
48
.github/actions/merge/action.yml
vendored
Normal file
48
.github/actions/merge/action.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Merge
|
||||
description: Merge tags to single multi-platform tags
|
||||
author: Julien Neuhart
|
||||
|
||||
inputs:
|
||||
github_token:
|
||||
description: The GitHub token
|
||||
required: true
|
||||
default: ${{ github.token }}
|
||||
docker_hub_username:
|
||||
description: The Docker Hub username
|
||||
required: true
|
||||
docker_hub_password:
|
||||
description: The Docker Hub password
|
||||
required: true
|
||||
tags:
|
||||
description: Comma separated tags to merge
|
||||
required: true
|
||||
alternate_registry:
|
||||
description: Alternate registry to also push resulting tags
|
||||
dry_run:
|
||||
description: Dry run this action
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ inputs.docker_hub_username }}
|
||||
password: ${{ inputs.docker_hub_password }}
|
||||
|
||||
- name: Merge
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/merge/merge.sh \
|
||||
--tags "${{ inputs.tags }}" \
|
||||
--alternate-registry "${{ inputs.alternate_registry }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
107
.github/actions/merge/merge.sh
vendored
Executable file
107
.github/actions/merge/merge.sh
vendored
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit early.
|
||||
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||
set -e
|
||||
|
||||
# Source dot env file.
|
||||
source .env
|
||||
|
||||
# Arguments.
|
||||
tags=""
|
||||
alternate_registry=""
|
||||
dry_run=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--tags)
|
||||
tags="$2"
|
||||
shift 2
|
||||
;;
|
||||
--alternate-registry)
|
||||
alternate_registry="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Merge tag(s) 👷"
|
||||
echo
|
||||
|
||||
echo "Tag(s) to merge:"
|
||||
IFS=',' read -ra tags_to_merge <<< "$tags"
|
||||
for tag in "${tags_to_merge[@]}"; do
|
||||
echo "- $tag"
|
||||
done
|
||||
|
||||
if [ -n "$alternate_registry" ]; then
|
||||
echo "⚠️ Will also push to $alternate_registry registry"
|
||||
fi
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Build merge map.
|
||||
declare -A merge_map
|
||||
|
||||
for tag in "${tags_to_merge[@]}"; do
|
||||
target_tag="${tag//-amd64/}"
|
||||
target_tag="${target_tag//-arm64/}"
|
||||
target_tag="${target_tag//-arm/}"
|
||||
target_tag="${target_tag//-386/}"
|
||||
|
||||
merge_map["$target_tag"]+="$tag "
|
||||
done
|
||||
|
||||
# Merge tags.
|
||||
run_cmd() {
|
||||
local cmd="$1"
|
||||
|
||||
if [ "$dry_run" = "true" ]; then
|
||||
echo "🚧 Dry run - would run the following command:"
|
||||
echo "$cmd"
|
||||
echo
|
||||
else
|
||||
echo "⚙️ Running command:"
|
||||
echo "$cmd"
|
||||
eval "$cmd"
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
for target in "${!merge_map[@]}"; do
|
||||
IFS=' ' read -ra source_tags <<< "${merge_map[$target]}"
|
||||
|
||||
cmd="docker buildx imagetools create \
|
||||
-t $target \
|
||||
${source_tags[*]}
|
||||
"
|
||||
run_cmd "$cmd"
|
||||
|
||||
echo "➡️ $target pushed"
|
||||
echo
|
||||
if [ -n "$alternate_registry" ]; then
|
||||
alternate_target="${target/$DOCKER_REGISTRY/$alternate_registry}"
|
||||
cmd="docker buildx imagetools create \
|
||||
-t $alternate_target \
|
||||
$target
|
||||
"
|
||||
run_cmd "$cmd"
|
||||
|
||||
echo "➡️ $alternate_target pushed"
|
||||
echo
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Done!"
|
||||
exit 0
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -1,8 +1,18 @@
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/build"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
@@ -15,4 +15,4 @@ markComment: >
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
closeComment: false
|
||||
|
||||
51
.github/workflows/__build-push.yml
vendored
51
.github/workflows/__build-push.yml
vendored
@@ -1,51 +0,0 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
runs_on:
|
||||
type: string
|
||||
required: true
|
||||
platform:
|
||||
type: string
|
||||
required: true
|
||||
gotenberg_version:
|
||||
type: string
|
||||
required: true
|
||||
alternate_repository:
|
||||
type: string
|
||||
default: ''
|
||||
outputs:
|
||||
tags:
|
||||
value: ${{ jobs.build_push.outputs.tags }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build_push:
|
||||
name: Build and push Docker images
|
||||
runs-on: ${{ inputs.runs_on }}
|
||||
outputs:
|
||||
tags: ${{ steps.build_push.outputs.tags }}
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push ${{ inputs.platform }}
|
||||
id: build_push
|
||||
uses: gotenberg/gotenberg-release-action/actions/build-push@main
|
||||
with:
|
||||
version: ${{ inputs.gotenberg_version }}
|
||||
platform: ${{ inputs.platform }}
|
||||
alternate_repository: ${{ inputs.alternate_repository }}
|
||||
28
.github/workflows/__delete.yml
vendored
28
.github/workflows/__delete.yml
vendored
@@ -1,28 +0,0 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
gotenberg_version:
|
||||
type: string
|
||||
required: true
|
||||
alternate_repository:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
name: Delete Docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Delete
|
||||
uses: gotenberg/gotenberg-release-action/actions/delete@main
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ inputs.gotenberg_version }}
|
||||
alternate_repository: ${{ inputs.alternate_repository }}
|
||||
40
.github/workflows/__merge-clean.yml
vendored
40
.github/workflows/__merge-clean.yml
vendored
@@ -1,40 +0,0 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tags:
|
||||
type: string
|
||||
required: true
|
||||
alternate_registry:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
merge_clean:
|
||||
name: Merge and clean Docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Merge and clean
|
||||
uses: gotenberg/gotenberg-release-action/actions/merge-clean@main
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: ${{ inputs.tags }}
|
||||
alternate_registry: ${{ inputs.alternate_registry }}
|
||||
118
.github/workflows/continuous-delivery.yml
vendored
118
.github/workflows/continuous-delivery.yml
vendored
@@ -2,7 +2,7 @@ name: Continuous Delivery
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -10,39 +10,83 @@ permissions:
|
||||
jobs:
|
||||
release_amd64:
|
||||
name: Release linux/amd64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tags: ${{ steps.build_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/amd64
|
||||
skip_integrations_tests: true
|
||||
|
||||
release_386:
|
||||
name: Release linux/386
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/386
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tags: ${{ steps.build_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/386
|
||||
skip_integrations_tests: true
|
||||
|
||||
release_arm64:
|
||||
name: Release linux/arm64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
tags: ${{ steps.build_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/arm64
|
||||
skip_integrations_tests: true
|
||||
|
||||
release_arm_v7:
|
||||
name: Release linux/arm/v7
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm/v7
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
tags: ${{ steps.build_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and push
|
||||
id: build_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/arm/v7
|
||||
skip_integrations_tests: true
|
||||
|
||||
merge_clean_release_tags:
|
||||
needs:
|
||||
@@ -51,8 +95,22 @@ jobs:
|
||||
- release_arm64
|
||||
- release_arm_v7
|
||||
name: Merge and clean release tags
|
||||
uses: ./.github/workflows/__merge-clean.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}"
|
||||
alternate_registry: thecodingmachine
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Merge
|
||||
uses: ./.github/actions/merge
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}"
|
||||
alternate_registry: thecodingmachine
|
||||
|
||||
- name: Clean
|
||||
uses: ./.github/actions/clean
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}"
|
||||
|
||||
333
.github/workflows/continuous-integration.yml
vendored
333
.github/workflows/continuous-integration.yml
vendored
@@ -16,163 +16,286 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
name: Lint Golang codebase
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
cache: false
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v1.63.4
|
||||
version: v2.1.6
|
||||
|
||||
tests:
|
||||
needs:
|
||||
- lint
|
||||
name: Tests
|
||||
lint-prettier:
|
||||
name: Lint non-Golang codebase
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build testing environment
|
||||
run: make build build-tests
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Run linters
|
||||
run: make lint-prettier
|
||||
|
||||
test-unit:
|
||||
needs:
|
||||
- lint
|
||||
- lint-prettier
|
||||
name: Run unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Run tests
|
||||
run: make tests-once
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
run: make test-unit
|
||||
|
||||
snapshot_amd64:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Snapshot linux/amd64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: pr-${{ github.event.pull_request.number }}
|
||||
platform: linux/amd64
|
||||
alternate_repository: snapshot
|
||||
|
||||
snapshot_386:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Snapshot linux/386
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/386
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: pr-${{ github.event.pull_request.number }}
|
||||
platform: linux/386
|
||||
alternate_repository: snapshot
|
||||
|
||||
snapshot_arm64:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Snapshot linux/arm64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: pr-${{ github.event.pull_request.number }}
|
||||
platform: linux/arm64
|
||||
alternate_repository: snapshot
|
||||
|
||||
snapshot_arm_v7:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Snapshot linux/arm/v7
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm/v7
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
merge_clean_snapshot_tags:
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: pr-${{ github.event.pull_request.number }}
|
||||
platform: linux/arm/v7
|
||||
alternate_repository: snapshot
|
||||
|
||||
merge_clean_snapshot_guard:
|
||||
needs:
|
||||
- snapshot_amd64
|
||||
- snapshot_386
|
||||
- snapshot_arm64
|
||||
- snapshot_arm_v7
|
||||
name: Secrets access check
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
continue: ${{ steps.check.outputs.continue }}
|
||||
steps:
|
||||
- name: Check
|
||||
id: check
|
||||
run: |
|
||||
if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]; then
|
||||
echo "continue=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "continue=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
merge_clean_snapshot_tags:
|
||||
if: needs.merge_clean_snapshot_guard.outputs.continue == 'true'
|
||||
needs:
|
||||
- merge_clean_snapshot_guard
|
||||
- snapshot_amd64
|
||||
- snapshot_386
|
||||
- snapshot_arm64
|
||||
- snapshot_arm_v7
|
||||
name: Merge and clean snapshot tags
|
||||
uses: ./.github/workflows/__merge-clean.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Merge
|
||||
uses: ./.github/actions/merge
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}"
|
||||
|
||||
- name: Clean
|
||||
uses: ./.github/actions/clean
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}"
|
||||
|
||||
edge_amd64:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Edge linux/amd64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
gotenberg_version: edge
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: edge
|
||||
platform: linux/amd64
|
||||
|
||||
edge_386:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Edge linux/386
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/386
|
||||
gotenberg_version: edge
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: edge
|
||||
platform: linux/386
|
||||
|
||||
edge_arm64:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Edge linux/arm64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
gotenberg_version: edge
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: edge
|
||||
platform: linux/arm64
|
||||
|
||||
edge_arm_v7:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
- test-unit
|
||||
name: Edge linux/arm/v7
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm/v7
|
||||
gotenberg_version: edge
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build, test and push
|
||||
id: build_test_push
|
||||
uses: ./.github/actions/build-test-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: edge
|
||||
platform: linux/arm/v7
|
||||
|
||||
merge_clean_edge_tags:
|
||||
needs:
|
||||
@@ -181,8 +304,22 @@ jobs:
|
||||
- edge_arm64
|
||||
- edge_arm_v7
|
||||
name: Merge and clean edge tags
|
||||
uses: ./.github/workflows/__merge-clean.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}"
|
||||
alternate_registry: thecodingmachine
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Merge
|
||||
uses: ./.github/actions/merge
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}"
|
||||
alternate_registry: thecodingmachine
|
||||
|
||||
- name: Clean
|
||||
uses: ./.github/actions/clean
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}"
|
||||
|
||||
22
.github/workflows/pull-request-cleanup.yml
vendored
22
.github/workflows/pull-request-cleanup.yml
vendored
@@ -2,17 +2,23 @@ name: Pull Request Cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
uses: ./.github/workflows/__delete.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
name: Cleanup Docker images
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
uses: ./.github/actions/clean
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
snapshot_version: pr-${{ github.event.pull_request.number }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
/coverage.html
|
||||
/coverage.txt
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
|
||||
@@ -1,56 +1,65 @@
|
||||
linters-settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/gotenberg/gotenberg/v8)
|
||||
skip-generated: true
|
||||
custom-order: true
|
||||
|
||||
version: "2"
|
||||
run:
|
||||
issues-exit-code: 1
|
||||
tests: false
|
||||
linters:
|
||||
disable-all: true
|
||||
default: none
|
||||
enable:
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- copyloopvar
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- copyloopvar
|
||||
- errcheck
|
||||
- errname
|
||||
- exhaustive
|
||||
- gci
|
||||
- gofmt
|
||||
- goimports
|
||||
- gofumpt
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- importas
|
||||
- ineffassign
|
||||
- misspell
|
||||
- prealloc
|
||||
- promlinter
|
||||
#- sloglint
|
||||
- staticcheck
|
||||
- tenv
|
||||
- testableexamples
|
||||
- tparallel
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- usetesting
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
issues-exit-code: 1
|
||||
tests: false
|
||||
|
||||
output:
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/gotenberg/gotenberg/v8)
|
||||
custom-order: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
23.9.0
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-gherkin", "prettier-plugin-sh"],
|
||||
"escapeBackslashes": true
|
||||
}
|
||||
79
Makefile
79
Makefile
@@ -2,25 +2,16 @@ include .env
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show the help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: it
|
||||
it: build build-tests ## Initialize the development environment
|
||||
@grep -hE '^[A-Za-z0-9_ \-]*?:.*##.*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the Gotenberg's Docker image
|
||||
docker build \
|
||||
--build-arg GOLANG_VERSION=$(GOLANG_VERSION) \
|
||||
--build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) \
|
||||
--build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) \
|
||||
--build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) \
|
||||
--build-arg NOTO_COLOR_EMOJI_VERSION=$(NOTO_COLOR_EMOJI_VERSION) \
|
||||
--build-arg PDFTK_VERSION=$(PDFTK_VERSION) \
|
||||
--build-arg PDFCPU_VERSION=$(PDFCPU_VERSION) \
|
||||
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||
-f $(DOCKERFILE) $(DOCKER_BUILD_CONTEXT)
|
||||
|
||||
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
||||
GOTENBERG_BUILD_DEBUG_DATA=true
|
||||
API_PORT=3000
|
||||
API_PORT_FROM_ENV=
|
||||
API_BIND_IP=
|
||||
@@ -37,6 +28,7 @@ API-DOWNLOAD-FROM-DENY-LIST=
|
||||
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_MAX_QUEUE_SIZE=0
|
||||
CHROMIUM_AUTO_START=false
|
||||
@@ -62,7 +54,7 @@ LIBREOFFICE_DISABLE_ROUTES=false
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=auto
|
||||
LOG_FIELDS_PREFIX=
|
||||
PDFENGINES_ENGINES=
|
||||
LOG_ENABLE_GCP_FIELDS=false
|
||||
PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk
|
||||
PDFENGINES_SPLIT_ENGINES=pdfcpu,qpdf,pdftk
|
||||
PDFENGINES_FLATTEN_ENGINES=qpdf
|
||||
@@ -93,6 +85,7 @@ run: ## Start a Gotenberg container
|
||||
$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||
gotenberg \
|
||||
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||
--gotenberg-build-debug-data="$(GOTENBERG_BUILD_DEBUG_DATA)" \
|
||||
--api-port=$(API_PORT) \
|
||||
--api-port-from-env=$(API_PORT_FROM_ENV) \
|
||||
--api-bind-ip=$(API_BIND_IP) \
|
||||
@@ -107,6 +100,7 @@ run: ## Start a Gotenberg container
|
||||
--api-download-from-max-retry=$(API-DOWNLOAD-FROM-FROM-MAX-RETRY) \
|
||||
--api-disable-download-from=$(API-DISABLE-DOWNLOAD-FROM) \
|
||||
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||
--api-enable-debug-route=$(API_ENABLE_DEBUG_ROUTE) \
|
||||
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
|
||||
@@ -132,10 +126,10 @@ run: ## Start a Gotenberg container
|
||||
--log-level=$(LOG_LEVEL) \
|
||||
--log-format=$(LOG_FORMAT) \
|
||||
--log-fields-prefix=$(LOG_FIELDS_PREFIX) \
|
||||
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
||||
--log-enable-gcp-fields=$(LOG_ENABLE_GCP_FIELDS) \
|
||||
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
||||
--pdfengines-split-engines=$(PDFENGINES_SPLIT_ENGINES) \
|
||||
--pdfengines-convert-engines=$(PDFENGINES_FLATTEN_ENGINES) \
|
||||
--pdfengines-flatten-engines=$(PDFENGINES_FLATTEN_ENGINES) \
|
||||
--pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \
|
||||
--pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \
|
||||
--pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \
|
||||
@@ -154,39 +148,42 @@ run: ## Start a Gotenberg container
|
||||
--webhook-client-timeout=$(WEBHOOK_CLIENT_TIMEOUT) \
|
||||
--webhook-disable=$(WEBHOOK_DISABLE)
|
||||
|
||||
.PHONY: build-tests
|
||||
build-tests: ## Build the tests' Docker image
|
||||
docker build \
|
||||
--build-arg GOLANG_VERSION=$(GOLANG_VERSION) \
|
||||
--build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \
|
||||
--build-arg DOCKER_REPOSITORY=$(DOCKER_REPOSITORY) \
|
||||
--build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) \
|
||||
--build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) \
|
||||
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION)-tests \
|
||||
-f test/Dockerfile .
|
||||
.PHONY: test-unit
|
||||
test-unit: ## Run unit tests
|
||||
go test -race ./...
|
||||
|
||||
.PHONY: tests
|
||||
tests: ## Start the testing environment
|
||||
docker run --rm -it \
|
||||
-v $(PWD):/tests \
|
||||
$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION)-tests \
|
||||
bash
|
||||
PLATFORM=
|
||||
NO_CONCURRENCY=false
|
||||
|
||||
.PHONY: tests-once
|
||||
tests-once: ## Run the tests once (prefer the "tests" command while developing)
|
||||
docker run --rm \
|
||||
-v $(PWD):/tests \
|
||||
$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION)-tests \
|
||||
gotest
|
||||
.PHONY: test-integration
|
||||
test-integration: ## Run integration tests
|
||||
go test -timeout 20m -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \
|
||||
--gotenberg-docker-repository=$(DOCKER_REPOSITORY) \
|
||||
--gotenberg-version=$(GOTENBERG_VERSION) \
|
||||
--gotenberg-container-platform=$(PLATFORM) \
|
||||
--no-concurrency=$(NO_CONCURRENCY)
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Lint Golang codebase
|
||||
golangci-lint run
|
||||
|
||||
.PHONY: lint-prettier
|
||||
lint-prettier: ## Lint non-Golang codebase
|
||||
npx prettier --check .
|
||||
|
||||
.PHONY: lint-todo
|
||||
lint-todo: ## Find TODOs in Golang codebase
|
||||
golangci-lint run --no-config --disable-all --enable godox
|
||||
|
||||
# go install mvdan.cc/gofumpt@latest
|
||||
# go install github.com/daixiang0/gci@latest
|
||||
.PHONY: fmt
|
||||
fmt: ## Format the code and "optimize" the dependencies
|
||||
gofumpt -l -w .
|
||||
gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v8)" --skip-generated --skip-vendor --custom-order .
|
||||
fmt: ## Format Golang codebase and "optimize" the dependencies
|
||||
golangci-lint fmt
|
||||
go mod tidy
|
||||
|
||||
.PHONY: prettify
|
||||
prettify: ## Format non-Golang codebase
|
||||
npx prettier --write .
|
||||
|
||||
# go install golang.org/x/tools/cmd/godoc@latest
|
||||
.PHONY: godoc
|
||||
godoc: ## Run a webserver with Gotenberg godoc
|
||||
|
||||
15
README.md
15
README.md
@@ -1,21 +1,22 @@
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
|
||||
<h3 align="center">Gotenberg</h3>
|
||||
<p align="center">A Docker-powered stateless API for PDF files</p>
|
||||
<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>
|
||||
<br>
|
||||
<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>
|
||||
<a href="https://codecov.io/gh/gotenberg/gotenberg/branch/main/graph/badge.svg"><img alt="Code coverage" src="https://codecov.io/gh/gotenberg/gotenberg/branch/main/graph/badge.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>
|
||||
|
||||
---
|
||||
|
||||
**Gotenberg** provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice for converting
|
||||
**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!
|
||||
|
||||
## Quick Start
|
||||
@@ -42,9 +43,9 @@ Head to the [documentation](https://gotenberg.dev/docs/getting-started/introduct
|
||||
<a href="https://thecodingmachine.com">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/130324668-9d6e7b35-53a3-49c7-a574-38190d2bd6b0.png" alt="TheCodingMachine Logo" width="333" height="163" />
|
||||
</a>
|
||||
<a href="https://zolsec.com?utm_source=gotenberg_github&utm_medium=website" target="_blank">
|
||||
<img src="https://github.com/gotenberg/gotenberg/assets/8983173/707ccc97-a79b-4dcb-8fc8-6827366e5be3" alt="Zolsec Logo" width="333" height="163" />
|
||||
<a href="https://pdfme.com?utm_source=gotenberg_github&utm_medium=website" target="_blank">
|
||||
<img src="https://github.com/user-attachments/assets/2a75dd40-ca18-4d34-acd5-5dd474595168" alt="pdfme Logo" width="333" height="163" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Sponsorships help maintaining and improving Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️
|
||||
Sponsorships help maintain and improve Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️
|
||||
|
||||
30
SECURITY.md
30
SECURITY.md
@@ -2,25 +2,25 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Please ensure to keep your environment up-to-date and use only the latest version of Gotenberg.
|
||||
Please ensure to keep your environment up to date and use only the latest version of Gotenberg.
|
||||
Security updates and patches will be applied only to the most recent version.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Your help in identifying vulnerabilities in our project is much appreciated.
|
||||
Your help in identifying vulnerabilities in our project is much appreciated.
|
||||
We take all reports regarding security seriously.
|
||||
|
||||
If you discover a security vulnerability, please refrain from publishing it publicly.
|
||||
Instead, kindly send us the details via email to *neuhart [dot] julien [at] gmail [dot] com*.
|
||||
If you discover a security vulnerability, please refrain from publishing it publicly.
|
||||
Instead, kindly send us the details via email to _neuhart [dot] julien [at] gmail [dot] com_.
|
||||
|
||||
In the subject of your email, please indicate that it's a security vulnerability report for Gotenberg.
|
||||
In the subject of your email, please indicate that it's a security vulnerability report for Gotenberg.
|
||||
In your message, please include:
|
||||
|
||||
* A detailed description of the vulnerability.
|
||||
* The steps to reproduce the issue.
|
||||
* Any potential impact of the vulnerability on the users or system.
|
||||
- A detailed description of the vulnerability.
|
||||
- The steps to reproduce the issue.
|
||||
- Any potential impact of the vulnerability on the users or system.
|
||||
|
||||
Please remember that this process is done in a *'best-effort'* manner.
|
||||
Please remember that this process is done in a _'best-effort'_ manner.
|
||||
This means we strive to respond and act as quickly as possible, but the speed may vary depending on the severity of
|
||||
the issue and our resources.
|
||||
|
||||
@@ -28,14 +28,14 @@ Thank you in advance for helping to keep our project safe!
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
Once we have received your vulnerability report, we will work to validate and reproduce the issue.
|
||||
Once we have received your vulnerability report, we will work to validate and reproduce the issue.
|
||||
If we can confirm the vulnerability, we will proceed to:
|
||||
|
||||
* Work on a fix and a release timeline.
|
||||
* Notify you when the fix has been implemented and released.
|
||||
* Credit you for discovering the vulnerability (unless you request anonymity).
|
||||
* Please note that we will do our best to keep you informed about the progress towards resolving the issue.
|
||||
- Work on a fix and a release timeline.
|
||||
- Notify you when the fix has been implemented and released.
|
||||
- Credit you for discovering the vulnerability (unless you request anonymity).
|
||||
- Please note that we will do our best to keep you informed about the progress towards resolving the issue.
|
||||
|
||||
## Comments on this Policy
|
||||
|
||||
If you have suggestions on how this process could be improved, please submit a pull request.
|
||||
If you have suggestions on how this process could be improved, please submit a pull request.
|
||||
|
||||
197
build/Dockerfile
197
build/Dockerfile
@@ -1,17 +1,17 @@
|
||||
# ARG instructions do not create additional layers. Instead, next layers will
|
||||
# concatenate them. Also, we have to repeat ARG instructions in each build
|
||||
# stage that uses them.
|
||||
ARG GOLANG_VERSION
|
||||
ARG GOLANG_VERSION=1.24
|
||||
|
||||
# ----------------------------------------------
|
||||
# pdfcpu binary build stage
|
||||
# ----------------------------------------------
|
||||
# Note: this stage is required as pdfcpu does not release an armhf variant by
|
||||
# default.
|
||||
|
||||
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
|
||||
|
||||
ARG PDFCPU_VERSION
|
||||
# See https://github.com/pdfcpu/pdfcpu/releases.
|
||||
ARG PDFCPU_VERSION=v0.8.1
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Define the working directory outside of $GOPATH (we're using go modules).
|
||||
@@ -33,7 +33,7 @@ RUN go build -o pdfcpu -ldflags "-s -w -X 'main.version=$PDFCPU_VERSION' -X 'git
|
||||
# ----------------------------------------------
|
||||
FROM golang:$GOLANG_VERSION AS gotenberg-binary-stage
|
||||
|
||||
ARG GOTENBERG_VERSION
|
||||
ARG GOTENBERG_VERSION=snapshot
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Define the working directory outside of $GOPATH (we're using go modules).
|
||||
@@ -49,25 +49,77 @@ RUN go mod download &&\
|
||||
COPY cmd ./cmd
|
||||
COPY pkg ./pkg
|
||||
|
||||
RUN go build -o gotenberg -ldflags "-X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
||||
RUN go build -o gotenberg -ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
||||
|
||||
# ----------------------------------------------
|
||||
# Compress Golang binaries stage
|
||||
# ----------------------------------------------
|
||||
FROM debian:12-slim AS compress-go-binaries-stage
|
||||
|
||||
RUN \
|
||||
echo "deb http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bookworm-backports upx-ucl
|
||||
|
||||
COPY --from=pdfcpu-binary-stage /home/pdfcpu /home/pdfcpu
|
||||
COPY --from=gotenberg-binary-stage /home/gotenberg /home/gotenberg
|
||||
|
||||
RUN \
|
||||
upx-ucl --best /home/pdfcpu &&\
|
||||
upx-ucl --best /home/gotenberg
|
||||
|
||||
# ----------------------------------------------
|
||||
# Custom JRE stage
|
||||
# Credits: https://github.com/jodconverter/docker-image-jodconverter-runtime
|
||||
# ----------------------------------------------
|
||||
FROM debian:12-slim AS custom-jre-stage
|
||||
|
||||
RUN \
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends default-jdk-headless binutils
|
||||
|
||||
# Note: jdeps helps finding which modules a JAR requires.
|
||||
# Currently only for PDFtk, as we don't rely on LibreOffice UNO Java SDK.
|
||||
ENV JAVA_MODULES=java.base,java.desktop,java.naming,java.sql
|
||||
|
||||
RUN jlink \
|
||||
--add-modules $JAVA_MODULES \
|
||||
--strip-debug \
|
||||
--no-man-pages \
|
||||
--no-header-files \
|
||||
--compress=2 \
|
||||
--output /custom-jre
|
||||
|
||||
# ----------------------------------------------
|
||||
# Base image stage
|
||||
# ----------------------------------------------
|
||||
FROM debian:12-slim AS base-image-stage
|
||||
|
||||
COPY --from=custom-jre-stage /custom-jre /opt/java
|
||||
|
||||
ENV PATH="/opt/java/bin:${PATH}"
|
||||
|
||||
# ----------------------------------------------
|
||||
# Final stage
|
||||
# ----------------------------------------------
|
||||
FROM debian:12-slim
|
||||
FROM base-image-stage
|
||||
|
||||
ARG GOTENBERG_VERSION
|
||||
ARG GOTENBERG_USER_GID
|
||||
ARG GOTENBERG_USER_UID
|
||||
ARG NOTO_COLOR_EMOJI_VERSION
|
||||
ARG PDFTK_VERSION
|
||||
ARG GOTENBERG_VERSION=snapshot
|
||||
ARG GOTENBERG_USER_GID=1001
|
||||
ARG GOTENBERG_USER_UID=1001
|
||||
# See https://github.com/googlefonts/noto-emoji/releases.
|
||||
ARG NOTO_COLOR_EMOJI_VERSION=v2.047
|
||||
# See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||
ARG PDFTK_VERSION=v3.3.3
|
||||
|
||||
LABEL org.opencontainers.image.title="Gotenberg" \
|
||||
org.opencontainers.image.description="A Docker-powered stateless API for PDF files." \
|
||||
org.opencontainers.image.version="$GOTENBERG_VERSION" \
|
||||
org.opencontainers.image.authors="Julien Neuhart <neuhart.julien@gmail.com>" \
|
||||
org.opencontainers.image.documentation="https://gotenberg.dev" \
|
||||
org.opencontainers.image.source="https://github.com/gotenberg/gotenberg"
|
||||
org.opencontainers.image.description="A containerized API for seamless PDF conversion." \
|
||||
org.opencontainers.image.version="$GOTENBERG_VERSION" \
|
||||
org.opencontainers.image.authors="Julien Neuhart <neuhart.julien@gmail.com>" \
|
||||
org.opencontainers.image.documentation="https://gotenberg.dev" \
|
||||
org.opencontainers.image.source="https://github.com/gotenberg/gotenberg"
|
||||
|
||||
RUN \
|
||||
# Create a non-root user.
|
||||
@@ -82,7 +134,7 @@ RUN \
|
||||
# Note: tini is a helper for reaping zombie processes.
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg tini python3 default-jre-headless &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg=2.2.40-1.1 tini=0.19.0-1 python3=3.11.2-1+b1 &&\
|
||||
# Cleanup.
|
||||
# Note: the Debian image does automatically a clean after each install thanks to a hook.
|
||||
# Therefore, there is no need for apt-get clean.
|
||||
@@ -99,40 +151,39 @@ RUN \
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||
./ttf-mscorefonts-installer_3.8.1_all.deb \
|
||||
culmus \
|
||||
fonts-beng \
|
||||
fonts-hosny-amiri \
|
||||
fonts-lklug-sinhala \
|
||||
fonts-lohit-guru \
|
||||
fonts-lohit-knda \
|
||||
fonts-samyak-gujr \
|
||||
fonts-samyak-mlym \
|
||||
fonts-samyak-taml \
|
||||
fonts-sarai \
|
||||
fonts-sil-abyssinica \
|
||||
fonts-sil-padauk \
|
||||
fonts-telu \
|
||||
fonts-thai-tlwg \
|
||||
culmus=0.133-1 \
|
||||
fonts-beng=2:1.3 \
|
||||
fonts-hosny-amiri=0.113-1 \
|
||||
fonts-lklug-sinhala=0.6-4 \
|
||||
fonts-lohit-guru=2.91.2-3 \
|
||||
fonts-lohit-knda=2.5.4-3 \
|
||||
fonts-samyak-gujr=1.2.2-6 \
|
||||
fonts-samyak-mlym=1.2.2-6 \
|
||||
fonts-samyak-taml=1.2.2-6 \
|
||||
fonts-sarai=1.0-3 \
|
||||
fonts-sil-abyssinica=2.100-3 \
|
||||
fonts-sil-padauk=5.000-3 \
|
||||
fonts-telu=2:1.3 \
|
||||
fonts-thai-tlwg=1:0.7.3-1 \
|
||||
ttf-wqy-zenhei \
|
||||
fonts-arphic-ukai \
|
||||
fonts-arphic-uming \
|
||||
fonts-ipafont-mincho \
|
||||
fonts-ipafont-gothic \
|
||||
fonts-unfonts-core \
|
||||
fonts-arphic-ukai=0.2.20080216.2-5 \
|
||||
fonts-arphic-uming=0.2.20080216.2-11 \
|
||||
fonts-ipafont-mincho=00303-23 \
|
||||
fonts-ipafont-gothic=00303-23 \
|
||||
fonts-unfonts-core=1:1.0.2-080608-18 \
|
||||
# LibreOffice recommends.
|
||||
fonts-crosextra-caladea \
|
||||
fonts-crosextra-carlito \
|
||||
fonts-dejavu \
|
||||
fonts-dejavu-extra \
|
||||
fonts-liberation \
|
||||
fonts-liberation2 \
|
||||
fonts-linuxlibertine \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-core \
|
||||
fonts-noto-mono \
|
||||
fonts-noto-ui-core \
|
||||
fonts-sil-gentium \
|
||||
fonts-sil-gentium-basic &&\
|
||||
fonts-crosextra-caladea=20200211-1 \
|
||||
fonts-crosextra-carlito=20220224-1 \
|
||||
fonts-dejavu=2.37-6 \
|
||||
fonts-liberation=1:1.07.4-11 \
|
||||
fonts-liberation2=2.1.5-1 \
|
||||
fonts-linuxlibertine=5.3.0-6 \
|
||||
fonts-noto-cjk=1:20220127+repack1-1 \
|
||||
fonts-noto-core=20201225-1 \
|
||||
fonts-noto-mono=20201225-1 \
|
||||
fonts-noto-ui-core=20201225-1 \
|
||||
fonts-sil-gentium=20081126:1.03-4 \
|
||||
fonts-sil-gentium-basic=1.102-1.1 &&\
|
||||
rm -f ./ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||
# Add Color and Black-and-White Noto emoji font.
|
||||
# Credits:
|
||||
@@ -149,29 +200,36 @@ RUN \
|
||||
/bin/bash -c \
|
||||
'set -e &&\
|
||||
if [[ "$(dpkg --print-architecture)" == "amd64" ]]; then \
|
||||
curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends --allow-unauthenticated google-chrome-stable &&\
|
||||
mv /usr/bin/google-chrome-stable /usr/bin/chromium; \
|
||||
curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends --allow-unauthenticated google-chrome-stable &&\
|
||||
mv /usr/bin/google-chrome-stable /usr/bin/chromium; \
|
||||
else \
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium; \
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium; \
|
||||
fi' &&\
|
||||
# Verify installation.
|
||||
chromium --version &&\
|
||||
# Cleanup.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Set default characterset encoding to UTF-8.
|
||||
# See:
|
||||
# https://github.com/gotenberg/gotenberg/issues/104
|
||||
# https://github.com/gotenberg/gotenberg/issues/730
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
RUN \
|
||||
# Install LibreOffice & unoconverter.
|
||||
echo "deb http://deb.debian.org/debian bookworm-backports main" >> /etc/apt/sources.list &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t bookworm-backports libreoffice &&\
|
||||
curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.0.1/unoconv -o /usr/bin/unoconverter &&\
|
||||
curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.1.1/unoconv -o /usr/bin/unoconverter &&\
|
||||
chmod +x /usr/bin/unoconverter &&\
|
||||
# unoconverter will look for the Python binary, which has to be at version 3.
|
||||
ln -s /usr/bin/python3 /usr/bin/python &&\
|
||||
@@ -186,11 +244,11 @@ RUN \
|
||||
# See https://github.com/gotenberg/gotenberg/pull/273.
|
||||
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
|
||||
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||
echo '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
||||
printf '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
||||
chmod +x /usr/bin/pdftk &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf exiftool &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf=11.3.0-1+deb12u1 exiftool &&\
|
||||
# See https://github.com/nextcloud/docker/issues/380.
|
||||
mkdir -p /usr/share/man/man1 &&\
|
||||
# Verify installations.
|
||||
@@ -200,17 +258,24 @@ RUN \
|
||||
# Cleanup.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Support for arbitrary user IDs (OpenShift).
|
||||
# See:
|
||||
# https://github.com/gotenberg/gotenberg/issues/1049.
|
||||
# https://docs.redhat.com/en/documentation/openshift_container_platform/4.15/html/images/creating-images#use-uid_create-images.
|
||||
RUN \
|
||||
usermod -aG root gotenberg &&\
|
||||
chgrp -R 0 /home/gotenberg &&\
|
||||
chmod -R g=u /home/gotenberg
|
||||
|
||||
# Improve fonts subpixel hinting and smoothing.
|
||||
# Credits:
|
||||
# https://github.com/arachnys/athenapdf/issues/69.
|
||||
# https://github.com/arachnys/athenapdf/commit/ba25a8d80a25d08d58865519c4cd8756dc9a336d.
|
||||
COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||
|
||||
# Copy the pdfcpu binary from the pdfcpu-binary-stage.
|
||||
COPY --from=pdfcpu-binary-stage /home/pdfcpu /usr/bin/
|
||||
|
||||
# Copy the Gotenberg binary from the gotenberg-binary-stage.
|
||||
COPY --from=gotenberg-binary-stage /home/gotenberg /usr/bin/
|
||||
# Copy the Golang binaries.
|
||||
COPY --from=compress-go-binaries-stage /home/pdfcpu /usr/bin/
|
||||
COPY --from=compress-go-binaries-stage /home/gotenberg /usr/bin/
|
||||
|
||||
# Environment variables required by modules or else.
|
||||
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium
|
||||
|
||||
@@ -2,9 +2,11 @@ package gotenbergcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -23,7 +25,7 @@ const banner = `
|
||||
\___/\___/\__/\__/_//_/_.__/\__/_/ \_, /
|
||||
/___/
|
||||
|
||||
A Docker-powered stateless API for PDF files.
|
||||
A containerized API for seamless PDF conversion.
|
||||
Version: %s
|
||||
-------------------------------------------------------
|
||||
`
|
||||
@@ -40,6 +42,7 @@ func Run() {
|
||||
// Create the root FlagSet and adds the modules flags to it.
|
||||
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
||||
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
|
||||
fs.Bool("gotenberg-build-debug-data", true, "Set if build data is needed")
|
||||
|
||||
descriptors := gotenberg.GetModuleDescriptors()
|
||||
var modsInfo string
|
||||
@@ -50,14 +53,42 @@ func Run() {
|
||||
|
||||
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo)
|
||||
|
||||
// Parse the flags...
|
||||
// Parse the flags.
|
||||
err := fs.Parse(os.Args[1:])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// ...and create a wrapper around those.
|
||||
// Override their values if the corresponding environment variables are
|
||||
// set.
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
envName := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
|
||||
val, ok := os.LookupEnv(envName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
sliceVal, ok := f.Value.(flag.SliceValue)
|
||||
if ok {
|
||||
// We don't want to append the values (default pflag behavior).
|
||||
items := strings.Split(val, ",")
|
||||
err = sliceVal.Replace(items)
|
||||
if err != nil {
|
||||
fmt.Printf("[FATAL] invalid overriding value '%s' from %s: %v\n", val, envName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = f.Value.Set(val)
|
||||
if err != nil {
|
||||
fmt.Printf("[FATAL] invalid overriding value '%s' from %s: %v\n", val, envName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Create a wrapper around our flags.
|
||||
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
|
||||
|
||||
// Get the graceful shutdown duration.
|
||||
@@ -108,6 +139,11 @@ func Run() {
|
||||
}(l.(gotenberg.SystemLogger))
|
||||
}
|
||||
|
||||
if parsedFlags.MustBool("gotenberg-build-debug-data") {
|
||||
// Build the debug data.
|
||||
gotenberg.BuildDebug(ctx)
|
||||
}
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
|
||||
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) or SIGTERM (Kubernetes).
|
||||
@@ -138,7 +174,9 @@ func Run() {
|
||||
id := app.(gotenberg.Module).Descriptor().ID
|
||||
|
||||
err = app.Stop(gracefulShutdownCtx)
|
||||
if err != nil {
|
||||
if errors.Is(err, gotenberg.ErrCancelGracefulShutdownContext) {
|
||||
cancel()
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("stopping %s: %w", id, err)
|
||||
}
|
||||
|
||||
|
||||
121
go.mod
121
go.mod
@@ -1,72 +1,131 @@
|
||||
module github.com/gotenberg/gotenberg/v8
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/alexliesenfeld/health v0.8.0
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
||||
github.com/barasher/go-exiftool v1.10.0
|
||||
github.com/chromedp/cdproto v0.0.0-20250126231910-1730200a0f74
|
||||
github.com/chromedp/chromedp v0.12.1
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20250416210000-d7e4d624041a
|
||||
github.com/chromedp/chromedp v0.13.6
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/labstack/gommon v0.4.2
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.26.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4
|
||||
github.com/shirou/gopsutil/v4 v4.24.12
|
||||
github.com/cucumber/godog v0.15.0
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/docker/docker v28.2.2+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/mholt/archives v0.1.2
|
||||
github.com/shirou/gopsutil/v4 v4.25.5
|
||||
github.com/testcontainers/testcontainers-go v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.0 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
|
||||
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.5 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/minio/minlz v1.0.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.1.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // 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.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.7 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
543
go.sum
543
go.sum
@@ -1,34 +1,116 @@
|
||||
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.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-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=
|
||||
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||
github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk=
|
||||
github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
|
||||
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=
|
||||
github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc=
|
||||
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-20250126231910-1730200a0f74 h1:oul0R+ZyorVxVNr7bALbPolKvbXlpIefB4lDv5sjt00=
|
||||
github.com/chromedp/cdproto v0.0.0-20250126231910-1730200a0f74/go.mod h1:RTGuBeCeabAJGi3OZf71a6cGa7oYBfBP75VJZFLv6SU=
|
||||
github.com/chromedp/chromedp v0.12.1 h1:kBMblXk7xH5/6j3K9uk8d7/c+fzXWiUsCsPte0VMwOA=
|
||||
github.com/chromedp/chromedp v0.12.1/go.mod h1:F6+wdq9LKFDMoyxhq46ZLz4VLXrsrCAR3sFqJz4Nqc0=
|
||||
github.com/chromedp/cdproto v0.0.0-20250416210000-d7e4d624041a h1:TVjyMfX22UgV/jP0A4UwrrCmI9zfvp6ZS5UBZMy3HDw=
|
||||
github.com/chromedp/cdproto v0.0.0-20250416210000-d7e4d624041a/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||
github.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk=
|
||||
github.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
|
||||
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
|
||||
github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo=
|
||||
github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
|
||||
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
|
||||
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
|
||||
github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
|
||||
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/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-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY=
|
||||
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -38,130 +120,447 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
|
||||
github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo=
|
||||
github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.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=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A=
|
||||
github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=
|
||||
github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
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/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 v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew=
|
||||
github.com/nwaples/rardecode/v2 v2.1.1/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.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
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/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=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
|
||||
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sorairolake/lzip-go v0.3.7 h1:vP2uiD/NoklLyzYMdgOWkZME0ulkSfVTTE4MNRKCwNs=
|
||||
github.com/sorairolake/lzip-go v0.3.7/go.mod h1:THOHr0FlNVCw2eOIEE9shFJAG1QxQg/pf2XUPAmNIqg=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.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=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
|
||||
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
|
||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
|
||||
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
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.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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 v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
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.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
|
||||
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
|
||||
183
package-lock.json
generated
Normal file
183
package-lock.json
generated
Normal file
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"name": "gotenberg",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-gherkin": "^3.1.2",
|
||||
"prettier-plugin-sh": "^0.17.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@cucumber/gherkin": {
|
||||
"version": "27.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-27.0.0.tgz",
|
||||
"integrity": "sha512-j5rCsjqzRiC3iVTier3sa0kzyNbkcAmF7xr7jKnyO7qDeK3Z8Ye1P3KSVpeQRMY+KCDJ3WbTDdyxH0FwfA/fIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cucumber/messages": ">=19.1.4 <=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-22.0.0.tgz",
|
||||
"integrity": "sha512-EuaUtYte9ilkxcKmfqGF9pJsHRUU0jwie5ukuZ/1NPTuHS1LxHPsGEODK17RPRbZHOFhqybNzG2rHAwThxEymg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/uuid": "9.0.1",
|
||||
"class-transformer": "0.5.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"uuid": "9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cucumber/gherkin/node_modules/@types/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cucumber/gherkin/node_modules/uuid": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@cucumber/messages": {
|
||||
"version": "23.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-23.0.0.tgz",
|
||||
"integrity": "sha512-2ZWKNnikNMBnle3P3cgy+fgzhABrY4oBbiWp9wcI8ANdT4LlYRN6jQ6j/9CQGTDGRfkyRtr/5VkYq7q24XCsuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/uuid": "9.0.6",
|
||||
"class-transformer": "0.5.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"uuid": "9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@reteps/dockerfmt": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@reteps/dockerfmt/-/dockerfmt-0.3.6.tgz",
|
||||
"integrity": "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^v12.20.0 || ^14.13.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.6.tgz",
|
||||
"integrity": "sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-gherkin": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-gherkin/-/prettier-plugin-gherkin-3.1.2.tgz",
|
||||
"integrity": "sha512-LgehRNKCAqZsoE/yblmqXYfMpEK2mhVWQKNj/TFzeLR+0rhFGHqq3RlYcYhPgzbr0n6w2Bv+0Gm1yxNdzkdacw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cucumber/gherkin": "^27.0.0",
|
||||
"@cucumber/messages": "^23.0.0",
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-sh": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.17.4.tgz",
|
||||
"integrity": "sha512-aAVKXZ7GTEMZdZsIPSwMwddwPvt2ibMbRGd4OJAP0G7QoeYZV+mPNg2Oln3R53sZ4PVjeAA7Xzi/PuI0QlHHfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reteps/dockerfmt": "^0.3.5",
|
||||
"sh-syntax": "^0.5.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
||||
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/sh-syntax": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.5.7.tgz",
|
||||
"integrity": "sha512-74m9dt91konrF5+m0kASugzi37VxKsnTJQ6yvdDZu3IijG5/vIZpImP6FadsJLWNt2X2YD0VaTwW5W7Ox7mFVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/sh-syntax"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-gherkin": "^3.1.2",
|
||||
"prettier-plugin-sh": "^0.17.4"
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (cmd *Cmd) Start() error {
|
||||
}
|
||||
|
||||
// Wait waits for the command to complete. It should be called when using the
|
||||
// Start method, so that the command does not leak zombies.
|
||||
// Start method so that the command does not leak zombies.
|
||||
func (cmd *Cmd) Wait() error {
|
||||
err := cmd.process.Wait()
|
||||
if err != nil {
|
||||
@@ -84,7 +84,7 @@ func (cmd *Cmd) Wait() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec executes the command and wait for its completion or until the context
|
||||
// Exec executes the command and waits for its completion or until the context
|
||||
// is done. In any case, it kills the unix process and all its children.
|
||||
func (cmd *Cmd) Exec() (int, error) {
|
||||
if cmd.ctx == nil {
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestCommand(t *testing.T) {
|
||||
cmd := Command(zap.NewNop(), "foo")
|
||||
if !cmd.process.SysProcAttr.Setpgid {
|
||||
t.Error("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
expectCommandContextError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
ctx: context.Background(),
|
||||
expectCommandContextError: false,
|
||||
},
|
||||
{
|
||||
scenario: "nil context",
|
||||
ctx: nil,
|
||||
expectCommandContextError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
|
||||
|
||||
if err == nil && !cmd.process.SysProcAttr.Setpgid {
|
||||
t.Fatal("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||
}
|
||||
|
||||
if !tc.expectCommandContextError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectCommandContextError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmd_Start(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
cmd *Cmd
|
||||
expectStartError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||
expectStartError: false,
|
||||
},
|
||||
{
|
||||
scenario: "start error",
|
||||
cmd: Command(zap.NewNop(), "foo"),
|
||||
expectStartError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.cmd.Start()
|
||||
|
||||
if !tc.expectStartError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectStartError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmd_Wait(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
cmd *Cmd
|
||||
expectWaitError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
cmd: func() *Cmd {
|
||||
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}(),
|
||||
expectWaitError: false,
|
||||
},
|
||||
{
|
||||
scenario: "wait error",
|
||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||
expectWaitError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.cmd.Wait()
|
||||
|
||||
if !tc.expectWaitError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectWaitError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmd_Exec(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
cmd *Cmd
|
||||
timeout time.Duration
|
||||
expectExecError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
cmd: func() *Cmd {
|
||||
cmd, err := CommandContext(context.Background(), zap.NewNop(), "echo", "Hello", "World")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}(),
|
||||
expectExecError: false,
|
||||
},
|
||||
{
|
||||
scenario: "nil context",
|
||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||
expectExecError: true,
|
||||
},
|
||||
{
|
||||
scenario: "start error",
|
||||
cmd: func() *Cmd {
|
||||
cmd, err := CommandContext(context.Background(), zap.NewNop(), "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}(),
|
||||
expectExecError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context done",
|
||||
cmd: Command(zap.NewNop(), "sleep", "2"),
|
||||
timeout: time.Duration(1) * time.Second,
|
||||
expectExecError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
if tc.timeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
|
||||
defer cancel()
|
||||
|
||||
tc.cmd.ctx = ctx
|
||||
}
|
||||
|
||||
_, err := tc.cmd.Exec()
|
||||
|
||||
if !tc.expectExecError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectExecError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmd_pipeOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
cmd *Cmd
|
||||
run bool
|
||||
expectPipeOutputError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
|
||||
run: true,
|
||||
expectPipeOutputError: false,
|
||||
},
|
||||
{
|
||||
scenario: "no debug, no pipe",
|
||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||
run: false,
|
||||
expectPipeOutputError: false,
|
||||
},
|
||||
{
|
||||
scenario: "stdout already piped",
|
||||
cmd: func() *Cmd {
|
||||
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||
_, err := cmd.process.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}(),
|
||||
run: false,
|
||||
expectPipeOutputError: true,
|
||||
},
|
||||
{
|
||||
scenario: "stderr already piped",
|
||||
cmd: func() *Cmd {
|
||||
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||
_, err := cmd.process.StderrPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}(),
|
||||
run: false,
|
||||
expectPipeOutputError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.cmd.pipeOutput()
|
||||
|
||||
if tc.run {
|
||||
errStart := tc.cmd.process.Start()
|
||||
if errStart != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !tc.expectPipeOutputError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectPipeOutputError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmd_Kill(t *testing.T) {
|
||||
tests := []struct {
|
||||
scenario string
|
||||
cmd *Cmd
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
cmd: func() *Cmd {
|
||||
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||
err := cmd.process.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}(),
|
||||
},
|
||||
{
|
||||
scenario: "no process",
|
||||
cmd: &Cmd{logger: zap.NewNop()},
|
||||
},
|
||||
{
|
||||
scenario: "process already killed",
|
||||
cmd: func() *Cmd {
|
||||
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||
err := cmd.process.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
err = cmd.Kill()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.cmd.Kill()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Context is a struct which helps to initialize modules. When provisioning, a
|
||||
// Context is a struct that helps to initialize modules. When provisioning, a
|
||||
// module may use the context to get other modules that it needs internally.
|
||||
type Context struct {
|
||||
flags ParsedFlags
|
||||
@@ -58,7 +58,7 @@ func (ctx *Context) Module(kind interface{}) (interface{}, error) {
|
||||
return mods[0], nil
|
||||
}
|
||||
|
||||
// Modules returns the list of modules which satisfies the requested interface.
|
||||
// Modules return the list of modules which satisfies the requested interface.
|
||||
//
|
||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||
// mods, _ := ctx.Modules(new(ModuleInterface))
|
||||
|
||||
@@ -5,23 +5,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
if NewContext(ParsedFlags{}, nil) == nil {
|
||||
t.Error("expected a non-nil value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_ParsedFlags(t *testing.T) {
|
||||
ctx := NewContext(ParsedFlags{}, nil)
|
||||
|
||||
actual := ctx.ParsedFlags()
|
||||
expect := ParsedFlags{}
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected %v but got %v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_Module(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
|
||||
68
pkg/gotenberg/debug.go
Normal file
68
pkg/gotenberg/debug.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// BuildDebug builds the debug data from modules.
|
||||
func BuildDebug(ctx *Context) {
|
||||
debugMu.Lock()
|
||||
defer debugMu.Unlock()
|
||||
|
||||
debug = &DebugInfo{
|
||||
Version: Version,
|
||||
Architecture: runtime.GOARCH,
|
||||
Modules: make([]string, len(ctx.moduleInstances)),
|
||||
ModulesAdditionalData: make(map[string]map[string]interface{}),
|
||||
Flags: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
i := 0
|
||||
for ID, mod := range ctx.moduleInstances {
|
||||
debug.Modules[i] = ID
|
||||
i++
|
||||
|
||||
debuggable, ok := mod.(Debuggable)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
debug.ModulesAdditionalData[ID] = debuggable.Debug()
|
||||
}
|
||||
|
||||
sort.Sort(AlphanumericSort(debug.Modules))
|
||||
|
||||
ctx.ParsedFlags().VisitAll(func(f *flag.Flag) {
|
||||
debug.Flags[f.Name] = f.Value.String()
|
||||
})
|
||||
}
|
||||
|
||||
// Debug returns the debug data.
|
||||
func Debug() DebugInfo {
|
||||
debugMu.Lock()
|
||||
defer debugMu.Unlock()
|
||||
|
||||
if debug == nil {
|
||||
return DebugInfo{}
|
||||
}
|
||||
|
||||
return *debug
|
||||
}
|
||||
|
||||
var (
|
||||
debug *DebugInfo
|
||||
debugMu sync.Mutex
|
||||
)
|
||||
72
pkg/gotenberg/debug_test.go
Normal file
72
pkg/gotenberg/debug_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func TestBuildDebug(t *testing.T) {
|
||||
if !reflect.DeepEqual(Debug(), DebugInfo{}) {
|
||||
t.Errorf("Debug() should return empty debug data")
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
|
||||
fs.String("foo", "bar", "Set foo")
|
||||
ctx := NewContext(ParsedFlags{
|
||||
FlagSet: fs,
|
||||
}, func() []ModuleDescriptor {
|
||||
mod1 := &struct {
|
||||
ModuleMock
|
||||
}{}
|
||||
mod1.DescriptorMock = func() ModuleDescriptor {
|
||||
return ModuleDescriptor{ID: "foo", New: func() Module { return mod1 }}
|
||||
}
|
||||
mod2 := &struct {
|
||||
ModuleMock
|
||||
DebuggableMock
|
||||
}{}
|
||||
mod2.DescriptorMock = func() ModuleDescriptor {
|
||||
return ModuleDescriptor{ID: "bar", New: func() Module { return mod2 }}
|
||||
}
|
||||
mod2.DebugMock = func() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
}
|
||||
|
||||
return []ModuleDescriptor{mod1.Descriptor(), mod2.Descriptor()}
|
||||
}())
|
||||
|
||||
// Load modules.
|
||||
_, err := ctx.Modules(new(Module))
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
// Build debug data.
|
||||
BuildDebug(ctx)
|
||||
|
||||
expect := DebugInfo{
|
||||
Version: Version,
|
||||
Architecture: runtime.GOARCH,
|
||||
Modules: []string{
|
||||
"bar",
|
||||
"foo",
|
||||
},
|
||||
ModulesAdditionalData: map[string]map[string]interface{}{
|
||||
"bar": {
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
Flags: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expect, Debug()) {
|
||||
t.Errorf("expected '%+v', bug got '%+v'", expect, Debug())
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package gotenberg
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -25,7 +23,7 @@ func (o *OsMkdirAll) MkdirAll(path string, perm os.FileMode) error { return os.M
|
||||
|
||||
// PathRename defines the method signature for renaming files. Implement this
|
||||
// interface if you don't want to rely on [os.Rename], notably for testing
|
||||
// purpose.
|
||||
// purposes.
|
||||
type PathRename interface {
|
||||
// Rename uses the same signature as [os.Rename].
|
||||
Rename(oldpath, newpath string) error
|
||||
@@ -86,25 +84,6 @@ func (fs *FileSystem) MkdirAll() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// WalkDir walks through the root level of a directory and returns a list of
|
||||
// files paths that match the specified file extension.
|
||||
func WalkDir(dir, ext string) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(filepath.Ext(info.Name()), ext) {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ MkdirAll = (*OsMkdirAll)(nil)
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestOsMkdirAll_MkdirAll(t *testing.T) {
|
||||
dirPath, err := NewFileSystem(new(OsMkdirAll)).MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("create working directory: %v", err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("remove working directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOsPathRename_Rename(t *testing.T) {
|
||||
dirPath, err := NewFileSystem(new(OsMkdirAll)).MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("create working directory: %v", err)
|
||||
}
|
||||
|
||||
path := "/tests/test/testdata/api/sample1.txt"
|
||||
copyPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open file: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := in.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := os.Create(copyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create new file: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := out.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close new file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
t.Fatalf("copy file to new file: %v", err)
|
||||
}
|
||||
|
||||
rename := new(OsPathRename)
|
||||
newPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||
|
||||
err = rename.Rename(copyPath, newPath)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("remove working directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystem_WorkingDir(t *testing.T) {
|
||||
fs := NewFileSystem(new(MkdirAllMock))
|
||||
dirName := fs.WorkingDir()
|
||||
|
||||
if dirName == "" {
|
||||
t.Error("expected directory name but got empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystem_WorkingDirPath(t *testing.T) {
|
||||
fs := NewFileSystem(new(MkdirAllMock))
|
||||
expectedPath := fmt.Sprintf("%s/%s", os.TempDir(), fs.WorkingDir())
|
||||
|
||||
if fs.WorkingDirPath() != expectedPath {
|
||||
t.Errorf("expected path '%s' but got '%s'", expectedPath, fs.WorkingDirPath())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystem_NewDirPath(t *testing.T) {
|
||||
fs := NewFileSystem(new(MkdirAllMock))
|
||||
newDir := fs.NewDirPath()
|
||||
expectedPrefix := fs.WorkingDirPath()
|
||||
|
||||
if !strings.HasPrefix(newDir, expectedPrefix) {
|
||||
t.Errorf("expected new directory to start with '%s' but got '%s'", expectedPrefix, newDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystem_MkdirAll(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
mkdirAll MkdirAll
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "error",
|
||||
mkdirAll: &MkdirAllMock{
|
||||
MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
mkdirAll: &MkdirAllMock{
|
||||
MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
fs := NewFileSystem(tc.mkdirAll)
|
||||
|
||||
_, err := fs.MkdirAll()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDir(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
dir string
|
||||
ext string
|
||||
expectError bool
|
||||
expectFiles []string
|
||||
}{
|
||||
{
|
||||
scenario: "directory does not exist",
|
||||
dir: uuid.NewString(),
|
||||
ext: ".pdf",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "find PDF files",
|
||||
dir: func() string {
|
||||
path := fmt.Sprintf("%s/a_directory", os.TempDir())
|
||||
|
||||
err := os.MkdirAll(path, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file.pdf", path), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_bar_file.PDF", path), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_baz_file.txt", path), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return path
|
||||
}(),
|
||||
ext: ".pdf",
|
||||
expectError: false,
|
||||
expectFiles: []string{"/tmp/a_directory/a_bar_file.PDF", "/tmp/a_directory/a_foo_file.pdf"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
defer func() {
|
||||
err := os.RemoveAll(tc.dir)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
files, err := WalkDir(tc.dir, tc.ext)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectError && err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(files, tc.expectFiles) {
|
||||
t.Errorf("expected files %+v, but got %+v", tc.expectFiles, files)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// GarbageCollect scans the root path and deletes files or directories with
|
||||
// names containing specific substrings and before a given experiation time.
|
||||
// names containing specific substrings and before a given expiration time.
|
||||
func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr []string, expirationTime time.Time) error {
|
||||
logger = logger.Named("gc")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package gotenberg
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,31 +28,31 @@ func TestGarbageCollect(t *testing.T) {
|
||||
{
|
||||
scenario: "remove include substrings",
|
||||
rootPath: func() string {
|
||||
path := fmt.Sprintf("%s/a_directory", os.TempDir())
|
||||
p := fmt.Sprintf("%s/a_directory", os.TempDir())
|
||||
|
||||
err := os.MkdirAll(path, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file", path), []byte{1}, 0o755)
|
||||
err := os.MkdirAll(p, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_bar_file", path), []byte{1}, 0o755)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file", p), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_baz_file", path), []byte{1}, 0o755)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_bar_file", p), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return path
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_baz_file", p), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return p
|
||||
}(),
|
||||
includeSubstr: []string{"foo", fmt.Sprintf("%s/a_directory/a_bar_file", os.TempDir())},
|
||||
includeSubstr: []string{"foo", path.Join(os.TempDir(), "/a_directory/a_bar_file")},
|
||||
expectError: false,
|
||||
expectExists: []string{"a_baz_file"},
|
||||
expectNotExists: []string{"a_foo_file", "a_bar_file"},
|
||||
@@ -80,18 +81,18 @@ func TestGarbageCollect(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, name := range tc.expectNotExists {
|
||||
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
|
||||
_, err = os.Stat(path)
|
||||
p := fmt.Sprintf("%s/%s", tc.rootPath, name)
|
||||
_, err = os.Stat(p)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("expected '%s' not to exist but it does: %v", path, err)
|
||||
t.Errorf("expected '%s' not to exist but it does: %v", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range tc.expectExists {
|
||||
path := fmt.Sprintf("%s/%s", tc.rootPath, name)
|
||||
_, err = os.Stat(path)
|
||||
p := fmt.Sprintf("%s/%s", tc.rootPath, name)
|
||||
_, err = os.Stat(p)
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("expected '%s' to exist but it does not: %v", path, err)
|
||||
t.Errorf("expected '%s' to exist but it does not: %v", p, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ type LoggerProvider interface {
|
||||
Logger(mod Module) (*zap.Logger, error)
|
||||
}
|
||||
|
||||
// LeveledLogger is wrapper around a [zap.Logger] so that it may be used by a
|
||||
// LeveledLogger is a wrapper around a [zap.Logger] so that it may be used by a
|
||||
// [retryablehttp.Client].
|
||||
type LeveledLogger struct {
|
||||
logger *zap.Logger
|
||||
@@ -31,22 +31,22 @@ func NewLeveledLogger(logger *zap.Logger) *LeveledLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Error logs a message at error level using the wrapped zap.Logger.
|
||||
// Error logs a message at the error level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Error(msg string, keysAndValues ...interface{}) {
|
||||
leveled.logger.Error(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
// Warn logs a message at warning level using the wrapped zap.Logger.
|
||||
// Warn logs a message at the warning level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Warn(msg string, keysAndValues ...interface{}) {
|
||||
leveled.logger.Warn(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
// Info logs a message at info level using the wrapped zap.Logger.
|
||||
// Info logs a message at the info level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||
leveled.logger.Info(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
// Debug logs a message at debug level using the wrapped zap.Logger.
|
||||
// Debug logs a message at the debug level using the wrapped zap.Logger.
|
||||
func (leveled LeveledLogger) Debug(msg string, keysAndValues ...interface{}) {
|
||||
leveled.logger.Debug(fmt.Sprintf("%s: %+v", msg, keysAndValues))
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestLeveledLogger_Error(t *testing.T) {
|
||||
NewLeveledLogger(zap.NewNop()).Error("foo")
|
||||
}
|
||||
|
||||
func TestLeveledLogger_Warn(t *testing.T) {
|
||||
NewLeveledLogger(zap.NewNop()).Warn("foo")
|
||||
}
|
||||
|
||||
func TestLeveledLogger_Info(t *testing.T) {
|
||||
NewLeveledLogger(zap.NewNop()).Info("foo")
|
||||
}
|
||||
|
||||
func TestLeveledLogger_Debug(t *testing.T) {
|
||||
NewLeveledLogger(zap.NewNop()).Debug("foo")
|
||||
}
|
||||
@@ -34,6 +34,14 @@ func (mod *ValidatorMock) Validate() error {
|
||||
return mod.ValidateMock()
|
||||
}
|
||||
|
||||
type DebuggableMock struct {
|
||||
DebugMock func() map[string]interface{}
|
||||
}
|
||||
|
||||
func (mod *DebuggableMock) Debug() map[string]interface{} {
|
||||
return mod.DebugMock()
|
||||
}
|
||||
|
||||
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
||||
//
|
||||
//nolint:dupl
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestModuleMock(t *testing.T) {
|
||||
mock := &ModuleMock{
|
||||
DescriptorMock: func() ModuleDescriptor {
|
||||
return ModuleDescriptor{ID: "foo", New: func() Module {
|
||||
return nil
|
||||
}}
|
||||
},
|
||||
}
|
||||
|
||||
if mock.Descriptor().ID != "foo" {
|
||||
t.Errorf("expected ID '%s' from ModuleMock.Descriptor, but got '%s'", "foo", mock.Descriptor().ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerMock(t *testing.T) {
|
||||
mock := &ProvisionerMock{
|
||||
ProvisionMock: func(*Context) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Provision(&Context{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProvisionerMock.Provision, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatorMock(t *testing.T) {
|
||||
mock := &ValidatorMock{
|
||||
ValidateMock: func() error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Validate()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ValidatorMock.Validate, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngineMock(t *testing.T) {
|
||||
mock := &PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
},
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.Merge, but got: %v", err)
|
||||
}
|
||||
|
||||
_, err = mock.Split(context.Background(), zap.NewNop(), SplitMode{}, "", "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.Split, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Flatten(context.Background(), zap.NewNop(), "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||
}
|
||||
|
||||
_, err = mock.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.ReadMetadata, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.WriteMetadata(context.Background(), zap.NewNop(), map[string]interface{}{}, "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.WriteMetadata but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngineProviderMock(t *testing.T) {
|
||||
mock := &PdfEngineProviderMock{
|
||||
PdfEngineMock: func() (PdfEngine, error) {
|
||||
return new(PdfEngineMock), nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.PdfEngine()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineProviderMock.PdfEngine, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessMock(t *testing.T) {
|
||||
mock := &ProcessMock{
|
||||
StartMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
StopMock: func(logger *zap.Logger) error {
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func(logger *zap.Logger) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Start(zap.NewNop())
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessMock.Start, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Stop(zap.NewNop())
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessMock.Stop, but got: %v", err)
|
||||
}
|
||||
|
||||
healthy := mock.Healthy(zap.NewNop())
|
||||
if !healthy {
|
||||
t.Error("expected true from ProcessMock.Healthy, but got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSupervisorMock(t *testing.T) {
|
||||
mock := &ProcessSupervisorMock{
|
||||
LaunchMock: func() error {
|
||||
return nil
|
||||
},
|
||||
ShutdownMock: func() error {
|
||||
return nil
|
||||
},
|
||||
HealthyMock: func() bool {
|
||||
return true
|
||||
},
|
||||
RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
return nil
|
||||
},
|
||||
ReqQueueSizeMock: func() int64 {
|
||||
return 0
|
||||
},
|
||||
RestartsCountMock: func() int64 {
|
||||
return 0
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Launch()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessSupervisorMock.Launch, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Shutdown()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessSupervisorMock.Shutdown, but got: %v", err)
|
||||
}
|
||||
|
||||
healthy := mock.Healthy()
|
||||
if !healthy {
|
||||
t.Error("expected true from ProcessSupervisorMock.Healthy, but got false")
|
||||
}
|
||||
|
||||
err = mock.Run(context.TODO(), zap.NewNop(), nil)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProcessSupervisorMock.Run, but got: %v", err)
|
||||
}
|
||||
|
||||
size := mock.ReqQueueSize()
|
||||
if size != 0 {
|
||||
t.Errorf("expected 0 from ProcessSupervisorMock.ReqQueueSize, but got: %d", size)
|
||||
}
|
||||
|
||||
restarts := mock.RestartsCount()
|
||||
if restarts != 0 {
|
||||
t.Errorf("expected 0 from ProcessSupervisorMock.RestartsCount, but got: %d", restarts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerProviderMock(t *testing.T) {
|
||||
mock := &LoggerProviderMock{
|
||||
LoggerMock: func(mod Module) (*zap.Logger, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.Logger(new(ModuleMock))
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from LoggerProviderMock.Logger, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsProviderMock(t *testing.T) {
|
||||
mock := &MetricsProviderMock{
|
||||
MetricsMock: func() ([]Metric, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.Metrics()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from MetricsProviderMock.Metrics, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkdirAllMock(t *testing.T) {
|
||||
mock := &MkdirAllMock{
|
||||
MkdirAllMock: func(dir string, perm os.FileMode) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.MkdirAll("/foo", 0o755)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from MkdirAllMock.MkdirAll, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathRenameMock(t *testing.T) {
|
||||
mock := &PathRenameMock{
|
||||
RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Rename("", "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PathRenameMock.Rename, but got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,12 @@ type SystemLogger interface {
|
||||
SystemMessages() []string
|
||||
}
|
||||
|
||||
// Debuggable is a module interface for modules which want to provide
|
||||
// additional debug data.
|
||||
type Debuggable interface {
|
||||
Debug() map[string]interface{}
|
||||
}
|
||||
|
||||
// MustRegisterModule registers a module.
|
||||
//
|
||||
// To register a module, create an init() method in the module main go file:
|
||||
|
||||
@@ -13,7 +13,7 @@ var (
|
||||
ErrPdfEngineMethodNotSupported = errors.New("method not supported")
|
||||
|
||||
// ErrPdfSplitModeNotSupported is returned when the Split method of the
|
||||
// PdfEngine interface does not sumport a requested PDF split mode.
|
||||
// PdfEngine interface does not support a requested PDF split mode.
|
||||
ErrPdfSplitModeNotSupported = errors.New("split mode not supported")
|
||||
|
||||
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
||||
@@ -86,7 +86,7 @@ type PdfFormats struct {
|
||||
}
|
||||
|
||||
// PdfEngine provides an interface for operations on PDFs. Implementations
|
||||
// can utilize various tools like PDFtk, or implement functionality directly in
|
||||
// can use various tools like PDFtk, or implement functionality directly in
|
||||
// Go.
|
||||
//
|
||||
//nolint:dupl
|
||||
|
||||
8
pkg/gotenberg/shutdown.go
Normal file
8
pkg/gotenberg/shutdown.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package gotenberg
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrCancelGracefulShutdownContext tells that a module wants to abort a
|
||||
// graceful shutdown and stops Gotenberg right away as there are no more
|
||||
// ongoing processes.
|
||||
var ErrCancelGracefulShutdownContext = errors.New("cancel graceful shutdown's context")
|
||||
@@ -1,13 +1,15 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// AlphanumericSort implements sort.Interface and helps to sort strings
|
||||
// alphanumerically.
|
||||
// alphanumerically by either a numeric prefix or, if missing, a numeric
|
||||
// suffix.
|
||||
//
|
||||
// See: https://github.com/gotenberg/gotenberg/issues/805.
|
||||
type AlphanumericSort []string
|
||||
@@ -21,20 +23,20 @@ func (s AlphanumericSort) Swap(i, j int) {
|
||||
}
|
||||
|
||||
func (s AlphanumericSort) Less(i, j int) bool {
|
||||
numI, restI := extractPrefix(s[i])
|
||||
numJ, restJ := extractPrefix(s[j])
|
||||
numI, restI := extractNumber(s[i])
|
||||
numJ, restJ := extractNumber(s[j])
|
||||
|
||||
// Compares numerical prefixes if they exist.
|
||||
// If both strings contain a number, compare them numerically.
|
||||
if numI != -1 && numJ != -1 {
|
||||
if numI != numJ {
|
||||
return numI < numJ
|
||||
}
|
||||
// If numbers are equal, falls back to string comparison of the rest.
|
||||
// If the numbers are equal, compare the "rest" strings.
|
||||
return restI < restJ
|
||||
}
|
||||
|
||||
// If one has a numerical prefix and the other doesn't, the one with the
|
||||
// number comes first.
|
||||
// If one contains a number and the other doesn't, the one with the number
|
||||
// comes first.
|
||||
if numI != -1 {
|
||||
return true
|
||||
}
|
||||
@@ -42,28 +44,55 @@ func (s AlphanumericSort) Less(i, j int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// If neither has a numerical prefix, compare as strings
|
||||
// Neither has a number; fall back to lexicographical order.
|
||||
return s[i] < s[j]
|
||||
}
|
||||
|
||||
// extractPrefix attempts to extract a numerical prefix and the rest of the filename
|
||||
func extractPrefix(filename string) (int, string) {
|
||||
matches := numPrefixRegexp.FindStringSubmatch(filename)
|
||||
if len(matches) > 2 {
|
||||
prefix, err := strconv.Atoi(matches[1])
|
||||
if err == nil {
|
||||
return prefix, matches[2]
|
||||
// extractNumber attempts to extract a numeric portion from the filename.
|
||||
// It first checks for a numeric prefix (digits at the beginning).
|
||||
// If none is found, it next attempts to match a number immediately before the
|
||||
// extension (for filenames such as "sample1_1.pdf").
|
||||
// If that fails, it then attempts a trailing numeric pattern.
|
||||
// If no number is found, it returns -1 and the original string.
|
||||
func extractNumber(str string) (int, string) {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1168.
|
||||
str = filepath.Base(str)
|
||||
|
||||
// Check for a numeric prefix.
|
||||
if matches := prefixRegexp.FindStringSubmatch(str); len(matches) > 2 {
|
||||
if num, err := strconv.Atoi(matches[1]); err == nil {
|
||||
return num, matches[2]
|
||||
}
|
||||
}
|
||||
|
||||
// Returns -1 if no numerical prefix is found, indicating to just compare
|
||||
// as strings.
|
||||
return -1, filename
|
||||
// Check for a number immediately before an extension.
|
||||
if matches := extensionSuffixRegexp.FindStringSubmatch(str); len(matches) > 3 {
|
||||
if num, err := strconv.Atoi(matches[2]); err == nil {
|
||||
// Remove the numeric block but keep the extension.
|
||||
return num, matches[1] + matches[3]
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a trailing number (with no extension following).
|
||||
if matches := suffixRegexp.FindStringSubmatch(str); len(matches) > 2 {
|
||||
if num, err := strconv.Atoi(matches[2]); err == nil {
|
||||
return num, matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
// No numeric portion found.
|
||||
return -1, str
|
||||
}
|
||||
|
||||
var numPrefixRegexp = regexp.MustCompile(`^(\d+)(.*)$`)
|
||||
// Regular expressions used by extractNumber.
|
||||
var (
|
||||
// Matches a numeric prefix: one or more digits at the start.
|
||||
prefixRegexp = regexp.MustCompile(`^(\d+)(.*)$`)
|
||||
// Matches a numeric block immediately before a file extension.
|
||||
extensionSuffixRegexp = regexp.MustCompile(`^(.*?)(\d+)(\.[^.]+)$`)
|
||||
// Matches a trailing numeric sequence when there is no extension.
|
||||
suffixRegexp = regexp.MustCompile(`^(.*?)(\d+)$`)
|
||||
)
|
||||
|
||||
// Interface guard.
|
||||
var (
|
||||
_ sort.Interface = (*AlphanumericSort)(nil)
|
||||
)
|
||||
var _ sort.Interface = (*AlphanumericSort)(nil)
|
||||
|
||||
@@ -17,6 +17,16 @@ func TestAlphanumericSort(t *testing.T) {
|
||||
values: []string{"10qux.pdf", "2_baz.txt", "2_aza.txt", "1bar.pdf", "Afoo.txt", "Bbar.docx", "25zeta.txt", "3.pdf", "4_foo.pdf"},
|
||||
expectedSort: []string{"1bar.pdf", "2_aza.txt", "2_baz.txt", "3.pdf", "4_foo.pdf", "10qux.pdf", "25zeta.txt", "Afoo.txt", "Bbar.docx"},
|
||||
},
|
||||
{
|
||||
scenario: "numeric suffixes with extensions",
|
||||
values: []string{"sample1_10.pdf", "sample1_11.pdf", "sample1_4.pdf", "sample1_3.pdf", "sample1_1.pdf", "sample1_2.pdf"},
|
||||
expectedSort: []string{"sample1_1.pdf", "sample1_2.pdf", "sample1_3.pdf", "sample1_4.pdf", "sample1_10.pdf", "sample1_11.pdf"},
|
||||
},
|
||||
{
|
||||
scenario: "numeric suffixes",
|
||||
values: []string{"sample1_10", "sample1_11", "sample1_4", "sample1_3", "sample1_1", "sample1_2"},
|
||||
expectedSort: []string{"sample1_1", "sample1_2", "sample1_3", "sample1_4", "sample1_10", "sample1_11"},
|
||||
},
|
||||
{
|
||||
scenario: "hrtime (PHP library)",
|
||||
values: []string{"245654773395259", "245654773395039", "245654773395149", "245654773394919", "245654773394369"},
|
||||
|
||||
@@ -26,7 +26,7 @@ func init() {
|
||||
gotenberg.MustRegisterModule(new(Api))
|
||||
}
|
||||
|
||||
// Api is a module which provides an HTTP server. Other modules may add routes,
|
||||
// Api is a module that provides an HTTP server. Other modules may add routes,
|
||||
// middlewares or health checks.
|
||||
type Api struct {
|
||||
port int
|
||||
@@ -42,11 +42,13 @@ type Api struct {
|
||||
basicAuthPassword string
|
||||
downloadFromCfg downloadFromConfig
|
||||
disableHealthCheckLogging bool
|
||||
enableDebugRoute bool
|
||||
|
||||
routes []Route
|
||||
externalMiddlewares []Middleware
|
||||
healthChecks []health.CheckerOption
|
||||
readyFn []func() error
|
||||
asyncCounters []AsynchronousCounter
|
||||
fs *gotenberg.FileSystem
|
||||
logger *zap.Logger
|
||||
srv *echo.Echo
|
||||
@@ -59,7 +61,7 @@ type downloadFromConfig struct {
|
||||
disable bool
|
||||
}
|
||||
|
||||
// Router is a module interface which adds routes to the [Api].
|
||||
// Router is a module interface that adds routes to the [Api].
|
||||
type Router interface {
|
||||
Routes() ([]Route, error)
|
||||
}
|
||||
@@ -82,17 +84,17 @@ type Route struct {
|
||||
// Optional.
|
||||
DisableLogging bool
|
||||
|
||||
// Handler is the function which handles the request.
|
||||
// Handler is the function that handles the request.
|
||||
// Required.
|
||||
Handler echo.HandlerFunc
|
||||
}
|
||||
|
||||
// MiddlewareProvider is a module interface which adds middlewares to the [Api].
|
||||
// MiddlewareProvider is a module interface that adds middlewares to the [Api].
|
||||
type MiddlewareProvider interface {
|
||||
Middlewares() ([]Middleware, error)
|
||||
}
|
||||
|
||||
// MiddlewareStack is a type which helps to determine in which stack the
|
||||
// MiddlewareStack is a type that helps to determine in which stack the
|
||||
// middlewares provided by the [MiddlewareProvider] modules should be located.
|
||||
type MiddlewareStack uint32
|
||||
|
||||
@@ -102,7 +104,7 @@ const (
|
||||
MultipartStack
|
||||
)
|
||||
|
||||
// MiddlewarePriority is a type which helps to determine the execution order of
|
||||
// MiddlewarePriority is a type that helps to determine the execution order of
|
||||
// middlewares provided by the [MiddlewareProvider] modules in a stack.
|
||||
type MiddlewarePriority uint32
|
||||
|
||||
@@ -114,7 +116,7 @@ const (
|
||||
VeryHighPriority
|
||||
)
|
||||
|
||||
// Middleware is a middleware which can be added to the [Api]'s middlewares
|
||||
// Middleware is a middleware that can be added to the [Api]'s middlewares
|
||||
// chain.
|
||||
//
|
||||
// middleware := Middleware{
|
||||
@@ -156,7 +158,7 @@ type Middleware struct {
|
||||
Handler echo.MiddlewareFunc
|
||||
}
|
||||
|
||||
// HealthChecker is a module interface which allows adding health checks to the
|
||||
// HealthChecker is a module interface that allows adding health checks to the
|
||||
// API.
|
||||
//
|
||||
// See https://github.com/alexliesenfeld/health for more details.
|
||||
@@ -165,6 +167,14 @@ type HealthChecker interface {
|
||||
Ready() error
|
||||
}
|
||||
|
||||
// AsynchronousCounter is a module interface that returns the number of active
|
||||
// asynchronous requests.
|
||||
//
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1022.
|
||||
type AsynchronousCounter interface {
|
||||
AsyncCount() int64
|
||||
}
|
||||
|
||||
// Descriptor returns an [Api]'s module descriptor.
|
||||
func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{
|
||||
@@ -187,6 +197,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.Int("api-download-from-max-retry", 4, "Set the maximum number of retries for the download from feature")
|
||||
fs.Bool("api-disable-download-from", false, "Disable the download from feature")
|
||||
fs.Bool("api-disable-health-check-logging", false, "Disable health check logging")
|
||||
fs.Bool("api-enable-debug-route", false, "Enable the debug route")
|
||||
return fs
|
||||
}(),
|
||||
New: func() gotenberg.Module { return new(Api) },
|
||||
@@ -212,6 +223,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||
disable: flags.MustBool("api-disable-download-from"),
|
||||
}
|
||||
a.disableHealthCheckLogging = flags.MustBool("api-disable-health-check-logging")
|
||||
a.enableDebugRoute = flags.MustBool("api-enable-debug-route")
|
||||
|
||||
// Port from env?
|
||||
portEnvVar := flags.MustString("api-port-from-env")
|
||||
@@ -304,6 +316,17 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||
a.readyFn = append(a.readyFn, healthChecker.Ready)
|
||||
}
|
||||
|
||||
// Get asynchronous counters.
|
||||
mods, err = ctx.Modules(new(AsynchronousCounter))
|
||||
if err != nil {
|
||||
return fmt.Errorf("get asynchronous counters: %w", err)
|
||||
}
|
||||
|
||||
a.asyncCounters = make([]AsynchronousCounter, len(mods))
|
||||
for i, asyncCounter := range mods {
|
||||
a.asyncCounters[i] = asyncCounter.(AsynchronousCounter)
|
||||
}
|
||||
|
||||
// Logger.
|
||||
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
||||
if err != nil {
|
||||
@@ -365,9 +388,10 @@ func (a *Api) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
routesMap := make(map[string]string, len(a.routes)+2)
|
||||
routesMap := make(map[string]string, len(a.routes)+3)
|
||||
routesMap["/health"] = "/health"
|
||||
routesMap["/version"] = "/version"
|
||||
routesMap["/debug"] = "/debug"
|
||||
|
||||
for _, route := range a.routes {
|
||||
if route.Path == "" {
|
||||
@@ -427,7 +451,7 @@ func (a *Api) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user wish to add logging entries related to the health
|
||||
// Check if the user wishes to add logging entries related to the health
|
||||
// check route.
|
||||
if a.disableHealthCheckLogging {
|
||||
disableLoggingForPaths = append(disableLoggingForPaths, "health")
|
||||
@@ -438,6 +462,7 @@ func (a *Api) Start() error {
|
||||
latencyMiddleware(),
|
||||
rootPathMiddleware(a.rootPath),
|
||||
traceMiddleware(a.traceHeader),
|
||||
outputFilenameMiddleware(),
|
||||
loggerMiddleware(a.logger, disableLoggingForPaths),
|
||||
)
|
||||
|
||||
@@ -529,7 +554,7 @@ func (a *Api) Start() error {
|
||||
hardTimeoutMiddleware(hardTimeout),
|
||||
)
|
||||
|
||||
// ...and the version route.
|
||||
// ...the version route.
|
||||
a.srv.GET(
|
||||
fmt.Sprintf("%s%s", a.rootPath, "version"),
|
||||
func(c echo.Context) error {
|
||||
@@ -538,6 +563,17 @@ func (a *Api) Start() error {
|
||||
securityMiddleware,
|
||||
)
|
||||
|
||||
// ...and the debug route.
|
||||
if a.enableDebugRoute {
|
||||
a.srv.GET(
|
||||
fmt.Sprintf("%s%s", a.rootPath, "debug"),
|
||||
func(c echo.Context) error {
|
||||
return c.JSONPretty(http.StatusOK, gotenberg.Debug(), " ")
|
||||
},
|
||||
securityMiddleware,
|
||||
)
|
||||
}
|
||||
|
||||
// Wait for all modules to be ready.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), a.startTimeout)
|
||||
defer cancel()
|
||||
@@ -582,7 +618,28 @@ func (a *Api) StartupMessage() string {
|
||||
|
||||
// Stop stops the HTTP server.
|
||||
func (a *Api) Stop(ctx context.Context) error {
|
||||
return a.srv.Shutdown(ctx)
|
||||
for {
|
||||
count := int64(0)
|
||||
for _, asyncCounter := range a.asyncCounters {
|
||||
count += asyncCounter.AsyncCount()
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return a.srv.Shutdown(ctx)
|
||||
default:
|
||||
a.logger.Debug(fmt.Sprintf("%d asynchronous requests", count))
|
||||
if count > 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
a.logger.Debug("no more asynchronous requests, continue with shutdown")
|
||||
err := a.srv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutdown: %w", err)
|
||||
}
|
||||
return gotenberg.ErrCancelGracefulShutdownContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -19,7 +18,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/mholt/archives"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
@@ -410,6 +409,13 @@ func (ctx *Context) GeneratePath(extension string) string {
|
||||
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New().String(), extension)
|
||||
}
|
||||
|
||||
// GeneratePathFromFilename generates a path within the context's working
|
||||
// directory, using the given filename (with extension). It does not create
|
||||
// a file.
|
||||
func (ctx *Context) GeneratePathFromFilename(filename string) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.dirPath, filename)
|
||||
}
|
||||
|
||||
// CreateSubDirectory creates a subdirectory within the context's working
|
||||
// directory.
|
||||
func (ctx *Context) CreateSubDirectory(dirName string) (string, error) {
|
||||
@@ -468,22 +474,33 @@ func (ctx *Context) BuildOutputFile() (string, error) {
|
||||
|
||||
if len(ctx.outputPaths) == 1 {
|
||||
ctx.logger.Debug(fmt.Sprintf("only one output file '%s', skip archive creation", ctx.outputPaths[0]))
|
||||
|
||||
return ctx.outputPaths[0], nil
|
||||
}
|
||||
|
||||
z := archiver.Zip{
|
||||
CompressionLevel: flate.DefaultCompression,
|
||||
MkdirAll: true,
|
||||
SelectiveCompression: true,
|
||||
ContinueOnError: false,
|
||||
OverwriteExisting: false,
|
||||
ImplicitTopLevelFolder: false,
|
||||
filesInfo, err := archives.FilesFromDisk(ctx.Context, nil, func() map[string]string {
|
||||
f := make(map[string]string)
|
||||
for _, outputPath := range ctx.outputPaths {
|
||||
f[outputPath] = ""
|
||||
}
|
||||
return f
|
||||
}())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create files info: %w", err)
|
||||
}
|
||||
|
||||
archivePath := ctx.GeneratePath(".zip")
|
||||
out, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create zip file: %w", err)
|
||||
}
|
||||
defer func(out *os.File) {
|
||||
err := out.Close()
|
||||
if err != nil {
|
||||
ctx.logger.Error(fmt.Sprintf("close zip file: %s", err))
|
||||
}
|
||||
}(out)
|
||||
|
||||
err := z.Archive(ctx.outputPaths, archivePath)
|
||||
err = archives.Zip{}.Archive(ctx.Context, out, filesInfo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("archive output files: %w", err)
|
||||
}
|
||||
@@ -496,7 +513,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
|
||||
// OutputFilename returns the filename based on the given output path or the
|
||||
// "Gotenberg-Output-Filename" header's value.
|
||||
func (ctx *Context) OutputFilename(outputPath string) string {
|
||||
filename := ctx.echoCtx.Request().Header.Get("Gotenberg-Output-Filename")
|
||||
filename := ctx.echoCtx.Get("outputFilename").(string)
|
||||
|
||||
if filename == "" {
|
||||
return filepath.Base(outputPath)
|
||||
|
||||
@@ -1,836 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
defaultAllowList, err := regexp2.Compile("", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
defaultDenyList, err := regexp2.Compile("", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
defaultDownloadFromCfg := downloadFromConfig{
|
||||
allowList: defaultAllowList,
|
||||
denyList: defaultDenyList,
|
||||
maxRetry: 1,
|
||||
disable: false,
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
request *http.Request
|
||||
bodyLimit int64
|
||||
downloadFromCfg downloadFromConfig
|
||||
downloadFromSrv *echo.Echo
|
||||
expectContext *Context
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
expectHttpStatus int
|
||||
}{
|
||||
{
|
||||
scenario: "http.ErrNotMultipart",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
scenario: "http.ErrMissingBoundary",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm)
|
||||
return req
|
||||
}(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
scenario: "malformed body",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("foo", "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "request entity too large: form values",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("key", "value")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
bodyLimit: 1,
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusRequestEntityTooLarge,
|
||||
},
|
||||
{
|
||||
scenario: "request entity too large: downloadFrom",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
bodyLimit: 45, // form values = 44 bytes.
|
||||
downloadFromSrv: func() *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.GET("/", func(c echo.Context) error {
|
||||
c.Response().Header().Set(echo.HeaderContentDisposition, `attachment; filename="bar.txt"`)
|
||||
c.Response().Header().Set(echo.HeaderContentType, "text/plain")
|
||||
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
|
||||
})
|
||||
return srv
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusRequestEntityTooLarge,
|
||||
},
|
||||
{
|
||||
scenario: "request entity too large: form files",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
_, err = part.Write([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
bodyLimit: 1,
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusRequestEntityTooLarge,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: cannot unmarshal",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: no URL",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: filtered URL",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{"url":"https://foo.bar"}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromCfg: func() downloadFromConfig {
|
||||
denyList, err := regexp2.Compile("https://foo.bar", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
return downloadFromConfig{allowList: defaultAllowList, denyList: denyList, maxRetry: 1, disable: false}
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: unreachable URL",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: invalid status code",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromSrv: func() *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.GET("/", func(c echo.Context) error {
|
||||
return c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
})
|
||||
return srv
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: no 'Content-Disposition' header",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromSrv: func() *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.GET("/", func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
|
||||
})
|
||||
return srv
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: malformed 'Content-Disposition' header",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromSrv: func() *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.GET("/", func(c echo.Context) error {
|
||||
c.Response().Header().Set(echo.HeaderContentDisposition, ";;")
|
||||
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
|
||||
})
|
||||
return srv
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "invalid downloadFrom form field: no filename parameter in 'Content-Disposition' header",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromSrv: func() *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.GET("/", func(c echo.Context) error {
|
||||
c.Response().Header().Set(echo.HeaderContentDisposition, "inline;")
|
||||
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
|
||||
})
|
||||
return srv
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
request: func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
err := writer.WriteField("foo", "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
part, err := writer.CreateFormFile("foo.txt", "foo.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
_, err = part.Write([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
err = writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/","extraHttpHeaders":{"X-Foo":"Bar"}}]`)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
return req
|
||||
}(),
|
||||
downloadFromSrv: func() *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.GET("/", func(c echo.Context) error {
|
||||
if c.Request().Header.Get("User-Agent") != "Gotenberg" {
|
||||
t.Fatalf("expected 'Gotenberg' from header 'User-Agent', but got '%s'", c.Request().Header.Get("User-Agent"))
|
||||
}
|
||||
if c.Request().Header.Get("X-Foo") != "Bar" {
|
||||
t.Fatalf("expected 'Bar' from header 'X-Foo', but got '%s'", c.Request().Header.Get("X-Foo"))
|
||||
}
|
||||
if c.Request().Header.Get("Gotenberg-Trace") != "123" {
|
||||
t.Fatalf("expected '123' from header 'Gotenberg-Trace', but got '%s'", c.Request().Header.Get("Gotenberg-Trace"))
|
||||
}
|
||||
c.Response().Header().Set(echo.HeaderContentDisposition, `attachment; filename="bar.txt"`)
|
||||
c.Response().Header().Set(echo.HeaderContentType, "text/plain")
|
||||
return c.String(http.StatusOK, http.StatusText(http.StatusOK))
|
||||
})
|
||||
return srv
|
||||
}(),
|
||||
downloadFromCfg: defaultDownloadFromCfg,
|
||||
expectContext: &Context{
|
||||
values: map[string][]string{
|
||||
"foo": {"foo"},
|
||||
"downloadFrom": {
|
||||
`[{"url":"http://localhost:80/","extraHttpHeaders":{"X-Foo":"Bar"}}]`,
|
||||
},
|
||||
},
|
||||
files: map[string]string{
|
||||
"foo.txt": "foo.txt",
|
||||
"bar.txt": "bar.txt", // downloadFrom.
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
if tc.downloadFromSrv != nil {
|
||||
go func() {
|
||||
err := tc.downloadFromSrv.Start(":80")
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
err := tc.downloadFromSrv.Shutdown(context.TODO())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
handler := func(c echo.Context) error {
|
||||
ctx, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, tc.bodyLimit, tc.downloadFromCfg, "Gotenberg-Trace", "123")
|
||||
defer cancel()
|
||||
// Context already cancelled.
|
||||
defer cancel()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tc.expectContext != nil {
|
||||
if !reflect.DeepEqual(tc.expectContext.values, ctx.values) {
|
||||
t.Fatalf("expected context.values to be %v but got %v", tc.expectContext.values, ctx.values)
|
||||
}
|
||||
if len(tc.expectContext.files) != len(ctx.files) {
|
||||
t.Fatalf("expected context.files to contain %d items but got %d", len(tc.expectContext.files), len(ctx.files))
|
||||
}
|
||||
for key, value := range tc.expectContext.files {
|
||||
if !strings.HasSuffix(ctx.files[key], value) {
|
||||
t.Fatalf("expected context.files to contain '%s' but got '%s'", value, ctx.files[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(tc.request, recorder)
|
||||
err := handler(c)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
var httpErr HttpError
|
||||
isHttpError := errors.As(err, &httpErr)
|
||||
|
||||
if tc.expectHttpError && !isHttpError {
|
||||
t.Errorf("expected an HTTP error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectHttpError && isHttpError {
|
||||
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||
}
|
||||
|
||||
if err != nil && tc.expectHttpError && isHttpError {
|
||||
status, _ := httpErr.HttpError()
|
||||
if status != tc.expectHttpStatus {
|
||||
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_Request(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
c := echo.New().NewContext(request, recorder)
|
||||
|
||||
ctx := &Context{
|
||||
echoCtx: c,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ctx.Request(), c.Request()) {
|
||||
t.Errorf("expected %v but got %v", ctx.Request(), c.Request())
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_FormData(t *testing.T) {
|
||||
ctx := &Context{
|
||||
values: map[string][]string{
|
||||
"foo": {"foo"},
|
||||
},
|
||||
files: map[string]string{
|
||||
"foo.txt": "/foo.txt",
|
||||
},
|
||||
}
|
||||
|
||||
actual := ctx.FormData()
|
||||
expect := &FormData{
|
||||
values: ctx.values,
|
||||
files: ctx.files,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expect) {
|
||||
t.Errorf("expected %+v but got %+v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_CreateSubDirectory(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "failure",
|
||||
ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return errors.New("cannot rename")
|
||||
}}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}}},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.logger = zap.NewNop()
|
||||
_, err := tc.ctx.CreateSubDirectory("foo")
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_GeneratePath(t *testing.T) {
|
||||
ctx := &Context{
|
||||
dirPath: "/foo",
|
||||
}
|
||||
|
||||
path := ctx.GeneratePath(".pdf")
|
||||
if !strings.HasPrefix(path, ctx.dirPath) {
|
||||
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_Rename(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "failure",
|
||||
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return errors.New("cannot rename")
|
||||
}}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}}},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.logger = zap.NewNop()
|
||||
err := tc.ctx.Rename("", "")
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_AddOutputPaths(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *Context
|
||||
path string
|
||||
expectCount int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "ErrContextAlreadyClosed",
|
||||
ctx: &Context{cancelled: true},
|
||||
expectCount: 0,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "ErrOutOfBoundsOutputPath",
|
||||
ctx: &Context{dirPath: "/foo"},
|
||||
path: "/bar/foo.txt",
|
||||
expectCount: 0,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
ctx: &Context{dirPath: "/foo"},
|
||||
path: "/foo/foo.txt",
|
||||
expectCount: 1,
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.ctx.AddOutputPaths(tc.path)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if len(tc.ctx.outputPaths) != tc.expectCount {
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectCount, len(tc.ctx.outputPaths))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_Log(t *testing.T) {
|
||||
expect := zap.NewNop()
|
||||
ctx := Context{logger: expect}
|
||||
actual := ctx.Log()
|
||||
|
||||
if !reflect.DeepEqual(actual, expect) {
|
||||
t.Errorf("expected %v but got %v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_BuildOutputFile(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "ErrContextAlreadyClosed",
|
||||
ctx: &Context{cancelled: true},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no output path",
|
||||
ctx: &Context{},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success: one output path",
|
||||
ctx: &Context{outputPaths: []string{"foo.txt"}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "cannot archive: invalid output paths",
|
||||
ctx: &Context{outputPaths: []string{"foo.txt", "foo.pdf"}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success: many output paths",
|
||||
ctx: &Context{
|
||||
outputPaths: []string{
|
||||
"/tests/test/testdata/api/sample1.txt",
|
||||
"/tests/test/testdata/api/sample1.txt",
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
dirPath, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no erro but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
tc.ctx.dirPath = dirPath
|
||||
tc.ctx.logger = zap.NewNop()
|
||||
|
||||
_, err = tc.ctx.BuildOutputFile()
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_OutputFilename(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *Context
|
||||
outputPath string
|
||||
expectOutputFilename string
|
||||
}{
|
||||
{
|
||||
scenario: "with Gotenberg-Output-Filename header",
|
||||
ctx: func() *Context {
|
||||
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
|
||||
c.Request().Header.Set("Gotenberg-Output-Filename", "foo")
|
||||
return &Context{echoCtx: c}
|
||||
}(),
|
||||
outputPath: "/foo/bar.txt",
|
||||
expectOutputFilename: "foo.txt",
|
||||
},
|
||||
{
|
||||
scenario: "without custom filename",
|
||||
ctx: func() *Context {
|
||||
c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
|
||||
return &Context{echoCtx: c}
|
||||
}(),
|
||||
outputPath: "/foo/foo.txt",
|
||||
expectOutputFilename: "foo.txt",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
actual := tc.ctx.OutputFilename(tc.outputPath)
|
||||
|
||||
if actual != tc.expectOutputFilename {
|
||||
t.Errorf("expected '%s' but got '%s'", tc.expectOutputFilename, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
// Package api provides a module which is an HTTP server. Other modules may
|
||||
// Package api provides a module, which is an HTTP server. Other modules may
|
||||
// add multipart/form-data routes, middlewares, and health checks.
|
||||
package api
|
||||
|
||||
@@ -27,7 +27,7 @@ type FormData struct {
|
||||
}
|
||||
|
||||
// Validate returns nil or an error related to the [FormData] values, with a
|
||||
// [SentinelHttpError] (status code 400, errors' details as message) wrapped
|
||||
// [SentinelHttpError] (status code 400, errors' details as a message) wrapped
|
||||
// inside.
|
||||
//
|
||||
// var foo string
|
||||
@@ -96,7 +96,7 @@ func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
|
||||
}
|
||||
|
||||
// MandatoryInt binds a form field to an int variable. It populates an
|
||||
// error if the value is not int, is empty, or the "key" does not exist.
|
||||
// error if the value is not int, or is empty, or the "key" does not exist.
|
||||
//
|
||||
// var foo int
|
||||
//
|
||||
@@ -116,7 +116,7 @@ func (form *FormData) Float64(key string, target *float64, defaultValue float64)
|
||||
}
|
||||
|
||||
// MandatoryFloat64 binds a form field to a float64 variable. It populates
|
||||
// an error if the is not float64, is empty, or the "key" does not exist.
|
||||
// an error if the value is not float64, is empty, or the "key" does not exist.
|
||||
//
|
||||
// var foo float64
|
||||
//
|
||||
@@ -136,8 +136,8 @@ func (form *FormData) Duration(key string, target *time.Duration, defaultValue t
|
||||
}
|
||||
|
||||
// MandatoryDuration binds a form field to a time.Duration variable. It
|
||||
// populates an error if the value is not time.Duration, is empty, or the "key"
|
||||
// does not exist.
|
||||
// populates an error if the value is not time.Duration, or is empty, or the
|
||||
// "key" does not exist.
|
||||
//
|
||||
// var foo time.Duration
|
||||
//
|
||||
@@ -146,7 +146,7 @@ func (form *FormData) MandatoryDuration(key string, target *time.Duration) *Form
|
||||
return form.mustMandatoryField(key, target)
|
||||
}
|
||||
|
||||
// Inches binds a form field to a float64 variable. It populates an error
|
||||
// Inches bind a form field to a float64 variable. It populates an error
|
||||
// if the value cannot be computed back to inches.
|
||||
//
|
||||
// var foo float64
|
||||
@@ -303,7 +303,7 @@ func (form *FormData) Path(filename string, target *string) *FormData {
|
||||
return form.path(filename, target)
|
||||
}
|
||||
|
||||
// MandatoryPath binds the absolute path ofa form data file to a string
|
||||
// MandatoryPath binds the absolute path of a form data file to a string
|
||||
// variable. It populates an error if the file does not exist.
|
||||
//
|
||||
// var path string
|
||||
@@ -348,7 +348,7 @@ func (form *FormData) MandatoryContent(filename string, target *string) *FormDat
|
||||
return form.readFile(path, filename, target)
|
||||
}
|
||||
|
||||
// Paths binds the absolute paths of form data files, according to a list of
|
||||
// Paths bind the absolute paths of form data files, according to a list of
|
||||
// file extensions, to a string slice variable.
|
||||
//
|
||||
// var paths []string
|
||||
@@ -379,7 +379,7 @@ func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *For
|
||||
return form
|
||||
}
|
||||
|
||||
// paths binds the absolute paths of form data files, according to a list of
|
||||
// paths bind the absolute paths of form data files, according to a list of
|
||||
// file extensions, to a string slice variable.
|
||||
func (form *FormData) paths(extensions []string, target *[]string) *FormData {
|
||||
for filename, path := range form.files {
|
||||
|
||||
@@ -1425,36 +1425,36 @@ func TestFormData_Content(t *testing.T) {
|
||||
scenario: "file does exist without file extension",
|
||||
form: &FormData{
|
||||
files: map[string]string{
|
||||
"foo": "/tests/test/testdata/api/sample1.txt",
|
||||
"foo": "testdata/sample.txt",
|
||||
},
|
||||
},
|
||||
filename: "foo",
|
||||
defaultValue: "",
|
||||
expect: "foo",
|
||||
expect: "This is a text from a text file.",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "file does exist with an uppercase file extension",
|
||||
form: &FormData{
|
||||
files: map[string]string{
|
||||
"foo.TXT": "/tests/test/testdata/api/sample1.txt",
|
||||
"foo.TXT": "testdata/sample.txt",
|
||||
},
|
||||
},
|
||||
filename: "foo.txt",
|
||||
defaultValue: "",
|
||||
expect: "foo",
|
||||
expect: "This is a text from a text file.",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "file does exist without a lowercase file extension",
|
||||
form: &FormData{
|
||||
files: map[string]string{
|
||||
"foo.txt": "/tests/test/testdata/api/sample1.txt",
|
||||
"foo.txt": "testdata/sample.txt",
|
||||
},
|
||||
},
|
||||
filename: "foo.txt",
|
||||
defaultValue: "",
|
||||
expect: "foo",
|
||||
expect: "This is a text from a text file.",
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
@@ -1519,33 +1519,33 @@ func TestFormData_MandatoryContent(t *testing.T) {
|
||||
scenario: "mandatory file does exist without file extension",
|
||||
form: &FormData{
|
||||
files: map[string]string{
|
||||
"foo": "/tests/test/testdata/api/sample1.txt",
|
||||
"foo": "testdata/sample.txt",
|
||||
},
|
||||
},
|
||||
filename: "foo",
|
||||
expect: "foo",
|
||||
expect: "This is a text from a text file.",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "mandatory file does exist with an uppercase file extension",
|
||||
form: &FormData{
|
||||
files: map[string]string{
|
||||
"foo.TXT": "/tests/test/testdata/api/sample1.txt",
|
||||
"foo.TXT": "testdata/sample.txt",
|
||||
},
|
||||
},
|
||||
filename: "foo.txt",
|
||||
expect: "foo",
|
||||
expect: "This is a text from a text file.",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "mandatory file does exist without a lowercase file extension",
|
||||
form: &FormData{
|
||||
files: map[string]string{
|
||||
"foo.txt": "/tests/test/testdata/api/sample1.txt",
|
||||
"foo.txt": "testdata/sample.txt",
|
||||
},
|
||||
},
|
||||
filename: "foo.txt",
|
||||
expect: "foo",
|
||||
expect: "This is a text from a text file.",
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -86,7 +87,7 @@ func httpErrorHandler() echo.HTTPErrorHandler {
|
||||
}
|
||||
|
||||
// latencyMiddleware sets the start time in the [echo.Context] under
|
||||
// "startTime". Its value will be used later to calculate a request latency.
|
||||
// "startTime". Its value will be used later to calculate request latency.
|
||||
//
|
||||
// startTime := c.Get("startTime").(time.Time)
|
||||
func latencyMiddleware() echo.MiddlewareFunc {
|
||||
@@ -109,7 +110,7 @@ func latencyMiddleware() echo.MiddlewareFunc {
|
||||
// rootPath := c.Get("rootPath").(string)
|
||||
// healthURI := fmt.Sprintf("%s/health", rootPath)
|
||||
//
|
||||
// // Skip the middleware if health check URI.
|
||||
// // Skip the middleware if it's the health check URI.
|
||||
// if c.Request().RequestURI == healthURI {
|
||||
// // Call the next middleware in the chain.
|
||||
// return next(c)
|
||||
@@ -150,6 +151,25 @@ func traceMiddleware(header string) echo.MiddlewareFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// outputFilenameMiddleware sets the output filename in the [echo.Context]
|
||||
// under "outputFilename".
|
||||
//
|
||||
// outputFilename := c.Get("outputFilename").(string)
|
||||
func outputFilenameMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
filename := c.Request().Header.Get("Gotenberg-Output-Filename")
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1227.
|
||||
if filename != "" {
|
||||
filename = filepath.Base(filename)
|
||||
}
|
||||
c.Set("outputFilename", filename)
|
||||
// Call the next middleware in the chain.
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loggerMiddleware sets the logger in the [echo.Context] under "logger" and
|
||||
// logs a synchronous request result.
|
||||
//
|
||||
@@ -241,7 +261,7 @@ func basicAuthMiddleware(username, password string) echo.MiddlewareFunc {
|
||||
})
|
||||
}
|
||||
|
||||
// contextMiddleware, a middleware for "multipart/form-data" requests, sets the
|
||||
// contextMiddleware, middleware for "multipart/form-data" requests, sets the
|
||||
// [Context] and related context.CancelFunc in the [echo.Context] under
|
||||
// "context" and "cancel". If the process is synchronous, it also handles the
|
||||
// result of a "multipart/form-data" request.
|
||||
@@ -256,7 +276,7 @@ func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration, bodyLimi
|
||||
trace := c.Get("trace").(string)
|
||||
|
||||
// We create a context with a timeout so that underlying processes are
|
||||
// able to stop early and handle correctly a timeout scenario.
|
||||
// able to stop early and correctly handle a timeout scenario.
|
||||
ctx, cancel, err := newContext(c, logger, fs, timeout, bodyLimit, downloadFromCfg, traceHeader, trace)
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
@@ -1,584 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
err error
|
||||
expectStatus int
|
||||
expectMessage string
|
||||
}{
|
||||
{
|
||||
err: echo.ErrInternalServerError,
|
||||
expectStatus: http.StatusInternalServerError,
|
||||
expectMessage: http.StatusText(http.StatusInternalServerError),
|
||||
},
|
||||
{
|
||||
err: gotenberg.ErrFiltered,
|
||||
expectStatus: http.StatusForbidden,
|
||||
expectMessage: http.StatusText(http.StatusForbidden),
|
||||
},
|
||||
{
|
||||
err: gotenberg.ErrMaximumQueueSizeExceeded,
|
||||
expectStatus: http.StatusTooManyRequests,
|
||||
expectMessage: http.StatusText(http.StatusTooManyRequests),
|
||||
},
|
||||
{
|
||||
err: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues",
|
||||
},
|
||||
{
|
||||
err: gotenberg.ErrPdfFormatNotSupported,
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues",
|
||||
},
|
||||
{
|
||||
err: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues",
|
||||
},
|
||||
{
|
||||
err: WrapError(
|
||||
errors.New("foo"),
|
||||
NewSentinelHttpError(http.StatusBadRequest, "foo"),
|
||||
),
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "foo",
|
||||
},
|
||||
} {
|
||||
actualStatus, actualMessage := ParseError(tc.err)
|
||||
|
||||
if actualStatus != tc.expectStatus {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, actualStatus)
|
||||
}
|
||||
|
||||
if actualMessage != tc.expectMessage {
|
||||
t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, actualMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpErrorHandler(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
err error
|
||||
expectStatus int
|
||||
expectMessage string
|
||||
}{
|
||||
{
|
||||
err: echo.ErrInternalServerError,
|
||||
expectStatus: http.StatusInternalServerError,
|
||||
expectMessage: http.StatusText(http.StatusInternalServerError),
|
||||
},
|
||||
{
|
||||
err: context.DeadlineExceeded,
|
||||
expectStatus: http.StatusServiceUnavailable,
|
||||
expectMessage: http.StatusText(http.StatusServiceUnavailable),
|
||||
},
|
||||
{
|
||||
err: WrapError(
|
||||
errors.New("foo"),
|
||||
NewSentinelHttpError(http.StatusBadRequest, "foo"),
|
||||
),
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "foo",
|
||||
},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(request, recorder)
|
||||
c.Set("logger", zap.NewNop())
|
||||
|
||||
handler := httpErrorHandler()
|
||||
handler(tc.err, c)
|
||||
|
||||
contentType := recorder.Header().Get(echo.HeaderContentType)
|
||||
if contentType != echo.MIMETextPlainCharsetUTF8 {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, echo.MIMETextPlainCharsetUTF8, contentType)
|
||||
}
|
||||
|
||||
// Note: we cannot test the trace header in the response here, as it is set in the trace middleware.
|
||||
|
||||
if recorder.Code != tc.expectStatus {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code)
|
||||
}
|
||||
|
||||
if recorder.Body.String() != tc.expectMessage {
|
||||
t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatencyMiddleware(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(request, recorder)
|
||||
|
||||
err := latencyMiddleware()(
|
||||
func(c echo.Context) error {
|
||||
return nil
|
||||
},
|
||||
)(c)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
startTime := c.Get("startTime").(time.Time)
|
||||
now := time.Now()
|
||||
|
||||
if now.Before(startTime) {
|
||||
t.Errorf("expected start time %s to be < %s", startTime, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootPathMiddleware(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(request, recorder)
|
||||
|
||||
err := rootPathMiddleware("foo")(
|
||||
func(c echo.Context) error {
|
||||
return nil
|
||||
},
|
||||
)(c)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
rootPath := c.Get("rootPath").(string)
|
||||
|
||||
if rootPath != "foo" {
|
||||
t.Errorf("expected '%s' but got '%s", "foo", rootPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceMiddleware(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
trace string
|
||||
}{
|
||||
{
|
||||
trace: "foo",
|
||||
},
|
||||
{
|
||||
trace: "",
|
||||
},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(request, recorder)
|
||||
|
||||
if tc.trace != "" {
|
||||
c.Request().Header.Set("Gotenberg-Trace", tc.trace)
|
||||
}
|
||||
|
||||
err := traceMiddleware("Gotenberg-Trace")(
|
||||
func(c echo.Context) error {
|
||||
return nil
|
||||
},
|
||||
)(c)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
|
||||
trace := c.Get("trace").(string)
|
||||
|
||||
if trace == "" {
|
||||
t.Errorf("test %d: expected non empty trace in context", i)
|
||||
}
|
||||
|
||||
if tc.trace != "" && trace != tc.trace {
|
||||
t.Errorf("test %d: expected context trace '%s' but got '%s'", i, tc.trace, trace)
|
||||
}
|
||||
|
||||
if tc.trace == "" && trace == tc.trace {
|
||||
t.Errorf("test %d: expected context trace different from '%s' but got '%s'", i, tc.trace, trace)
|
||||
}
|
||||
|
||||
responseTrace := recorder.Header().Get("Gotenberg-Trace")
|
||||
|
||||
if tc.trace != "" && responseTrace != tc.trace {
|
||||
t.Errorf("test %d: expected header trace '%s' but got '%s'", i, tc.trace, responseTrace)
|
||||
}
|
||||
|
||||
if tc.trace == "" && responseTrace == tc.trace {
|
||||
t.Errorf("test %d: expected header trace different from '%s' but got '%s'", i, tc.trace, responseTrace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthMiddleware(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
request *http.Request
|
||||
username string
|
||||
password string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid basic auth",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.SetBasicAuth("invalid", "invalid")
|
||||
return req
|
||||
}(),
|
||||
username: "foo",
|
||||
password: "bar",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid basic auth",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.SetBasicAuth("foo", "bar")
|
||||
return req
|
||||
}(),
|
||||
username: "foo",
|
||||
password: "bar",
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
c := srv.NewContext(tc.request, recorder)
|
||||
err := basicAuthMiddleware(tc.username, tc.password)(func(c echo.Context) error {
|
||||
return nil
|
||||
})(c)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerMiddleware(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
request *http.Request
|
||||
next echo.HandlerFunc
|
||||
skipLogging bool
|
||||
}{
|
||||
{
|
||||
request: httptest.NewRequest(http.MethodGet, "/", nil),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
}(),
|
||||
},
|
||||
{
|
||||
request: httptest.NewRequest(http.MethodGet, "/health", nil),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
skipLogging: true,
|
||||
},
|
||||
{
|
||||
request: httptest.NewRequest(http.MethodGet, "/health", nil),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(tc.request, recorder)
|
||||
c.Set("startTime", time.Now())
|
||||
c.Set("trace", "foo")
|
||||
c.Set("rootPath", "/")
|
||||
|
||||
var disableLoggingForPaths []string
|
||||
if tc.skipLogging {
|
||||
disableLoggingForPaths = append(disableLoggingForPaths, tc.request.RequestURI)
|
||||
}
|
||||
|
||||
err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c)
|
||||
if err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMiddleware(t *testing.T) {
|
||||
buildMultipartFormDataRequest := func() *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
defer func() {
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err := writer.WriteField("foo", "foo")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
for i, tc := range []struct {
|
||||
request *http.Request
|
||||
next echo.HandlerFunc
|
||||
expectErr bool
|
||||
expectStatus int
|
||||
expectContentType string
|
||||
expectFilename string
|
||||
}{
|
||||
{
|
||||
request: httptest.NewRequest(http.MethodGet, "/", nil),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return ErrAsyncProcess
|
||||
}
|
||||
}(),
|
||||
expectStatus: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return ErrNoOutputFile
|
||||
}
|
||||
}(),
|
||||
expectStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
}(),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
request: func() *http.Request {
|
||||
req := buildMultipartFormDataRequest()
|
||||
req.Header.Set("Gotenberg-Output-Filename", "foo")
|
||||
|
||||
return req
|
||||
}(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*Context)
|
||||
ctx.outputPaths = []string{
|
||||
"/tests/test/testdata/api/sample2.pdf",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
expectStatus: http.StatusOK,
|
||||
expectContentType: "application/pdf",
|
||||
expectFilename: "foo.pdf",
|
||||
},
|
||||
{
|
||||
request: buildMultipartFormDataRequest(),
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*Context)
|
||||
ctx.outputPaths = []string{
|
||||
"/tests/test/testdata/api/sample1.txt",
|
||||
"/tests/test/testdata/api/sample2.pdf",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
expectStatus: http.StatusOK,
|
||||
expectContentType: "application/zip",
|
||||
},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(tc.request, recorder)
|
||||
c.Set("logger", zap.NewNop())
|
||||
c.Set("traceHeader", "Gotenberg-Trace")
|
||||
c.Set("trace", "foo")
|
||||
c.Set("startTime", time.Now())
|
||||
|
||||
err := contextMiddleware(gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, 0, downloadFromConfig{})(tc.next)(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if recorder.Code != tc.expectStatus {
|
||||
t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code)
|
||||
}
|
||||
|
||||
if tc.expectStatus == http.StatusNoContent {
|
||||
continue
|
||||
}
|
||||
|
||||
contentType := recorder.Header().Get(echo.HeaderContentType)
|
||||
if contentType != tc.expectContentType {
|
||||
t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, tc.expectContentType, contentType)
|
||||
}
|
||||
|
||||
contentDisposition := recorder.Header().Get(echo.HeaderContentDisposition)
|
||||
if !strings.Contains(contentDisposition, tc.expectFilename) {
|
||||
t.Errorf("test %d: expected %s '%s' to contain '%s'", i, echo.HeaderContentDisposition, contentDisposition, tc.expectFilename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHardTimeoutMiddleware(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
next echo.HandlerFunc
|
||||
timeout time.Duration
|
||||
expectErr bool
|
||||
expectHardTimeout bool
|
||||
}{
|
||||
{
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
timeout: time.Duration(100) * time.Millisecond,
|
||||
},
|
||||
{
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
panic("foo")
|
||||
}
|
||||
}(),
|
||||
timeout: time.Duration(100) * time.Millisecond,
|
||||
expectErr: true,
|
||||
expectHardTimeout: true,
|
||||
},
|
||||
{
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
}(),
|
||||
timeout: time.Duration(100) * time.Millisecond,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
next: func() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
|
||||
return nil
|
||||
}
|
||||
}(),
|
||||
timeout: time.Duration(100) * time.Millisecond,
|
||||
expectErr: true,
|
||||
expectHardTimeout: true,
|
||||
},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
|
||||
c := srv.NewContext(request, recorder)
|
||||
c.Set("logger", zap.NewNop())
|
||||
|
||||
err := hardTimeoutMiddleware(tc.timeout)(tc.next)(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
}
|
||||
|
||||
var isHardTimeout bool
|
||||
if err != nil {
|
||||
isHardTimeout = strings.Contains(err.Error(), "hard timeout")
|
||||
}
|
||||
|
||||
if tc.expectHardTimeout && !isHardTimeout {
|
||||
t.Errorf("test %d: expected hard timeout error but got: %v", i, err)
|
||||
}
|
||||
|
||||
if !tc.expectHardTimeout && isHardTimeout {
|
||||
t.Errorf("test %d: expected no hard timeout error but got one: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (ctx *ContextMock) SetFiles(files map[string]string) {
|
||||
ctx.files = files
|
||||
}
|
||||
|
||||
// SetCancelled sets if the context is cancelled or not.
|
||||
// SetCancelled sets if the context is canceled or not.
|
||||
//
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
// ctx.SetCancelled(true)
|
||||
@@ -83,7 +83,7 @@ func (ctx *ContextMock) SetLogger(logger *zap.Logger) {
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
// ctx.setEchoContext(c)
|
||||
func (ctx *ContextMock) SetEchoContext(c echo.Context) {
|
||||
ctx.Context.echoCtx = c
|
||||
ctx.echoCtx = c
|
||||
}
|
||||
|
||||
// SetMkdirAll sets the [gotenberg.MkdirAll].
|
||||
@@ -91,7 +91,7 @@ func (ctx *ContextMock) SetEchoContext(c echo.Context) {
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
// ctx.SetMkdirAll(mkdirAll)
|
||||
func (ctx *ContextMock) SetMkdirAll(mkdirAll gotenberg.MkdirAll) {
|
||||
ctx.Context.mkdirAll = mkdirAll
|
||||
ctx.mkdirAll = mkdirAll
|
||||
}
|
||||
|
||||
// SetPathRename sets the [gotenberg.PathRename].
|
||||
@@ -99,7 +99,7 @@ func (ctx *ContextMock) SetMkdirAll(mkdirAll gotenberg.MkdirAll) {
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
// ctx.setPathRename(rename)
|
||||
func (ctx *ContextMock) SetPathRename(rename gotenberg.PathRename) {
|
||||
ctx.Context.pathRename = rename
|
||||
ctx.pathRename = rename
|
||||
}
|
||||
|
||||
// RouterMock is a mock for the [Router] interface.
|
||||
@@ -120,7 +120,7 @@ func (provider *MiddlewareProviderMock) Middlewares() ([]Middleware, error) {
|
||||
return provider.MiddlewaresMock()
|
||||
}
|
||||
|
||||
// HealthCheckerMock is mock for the [HealthChecker] interface.
|
||||
// HealthCheckerMock is a mock for the [HealthChecker] interface.
|
||||
type HealthCheckerMock struct {
|
||||
ChecksMock func() ([]health.CheckerOption, error)
|
||||
ReadyMock func() error
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestContextMock_SetDirPath(t *testing.T) {
|
||||
mock := &ContextMock{&Context{}}
|
||||
mock.SetDirPath("/foo")
|
||||
|
||||
actual := mock.dirPath
|
||||
expect := "/foo"
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_DirPath(t *testing.T) {
|
||||
mock := &ContextMock{&Context{}}
|
||||
mock.SetDirPath("/foo")
|
||||
|
||||
actual := mock.DirPath()
|
||||
expect := "/foo"
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetValues(t *testing.T) {
|
||||
mock := &ContextMock{&Context{}}
|
||||
mock.SetValues(map[string][]string{
|
||||
"foo": {"foo"},
|
||||
})
|
||||
|
||||
actual := mock.values
|
||||
expect := map[string][]string{
|
||||
"foo": {"foo"},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expect) {
|
||||
t.Errorf("expected %+v but got: %+v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetFiles(t *testing.T) {
|
||||
mock := &ContextMock{&Context{}}
|
||||
mock.SetFiles(map[string]string{
|
||||
"foo": "/foo",
|
||||
})
|
||||
|
||||
actual := mock.files
|
||||
expect := map[string]string{
|
||||
"foo": "/foo",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expect) {
|
||||
t.Errorf("expected %+v but got: %+v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetCancelled(t *testing.T) {
|
||||
mock := &ContextMock{&Context{}}
|
||||
mock.SetCancelled(true)
|
||||
|
||||
actual := mock.cancelled
|
||||
|
||||
if !actual {
|
||||
t.Errorf("expected %t but got %t", true, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_OutputPaths(t *testing.T) {
|
||||
mock := ContextMock{
|
||||
&Context{
|
||||
outputPaths: []string{"/foo"},
|
||||
},
|
||||
}
|
||||
|
||||
actual := mock.OutputPaths()
|
||||
expect := []string{"/foo"}
|
||||
|
||||
if !reflect.DeepEqual(actual, expect) {
|
||||
t.Errorf("expected %+v but got: %+v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetLogger(t *testing.T) {
|
||||
mock := ContextMock{&Context{}}
|
||||
|
||||
expect := zap.NewNop()
|
||||
mock.SetLogger(expect)
|
||||
|
||||
actual := mock.logger
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected %v but got %v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetEchoContext(t *testing.T) {
|
||||
mock := ContextMock{&Context{}}
|
||||
|
||||
expect := echo.New().NewContext(nil, nil)
|
||||
mock.SetEchoContext(expect)
|
||||
|
||||
actual := mock.echoCtx
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected %v but got %v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetMkdirAll(t *testing.T) {
|
||||
mock := ContextMock{&Context{}}
|
||||
|
||||
expect := new(gotenberg.OsMkdirAll)
|
||||
mock.SetMkdirAll(expect)
|
||||
|
||||
actual := mock.mkdirAll
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected %v but got %v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetPathRename(t *testing.T) {
|
||||
mock := ContextMock{&Context{}}
|
||||
|
||||
expect := new(gotenberg.OsPathRename)
|
||||
mock.SetPathRename(expect)
|
||||
|
||||
actual := mock.pathRename
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected %v but got %v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterMock(t *testing.T) {
|
||||
mock := &RouterMock{
|
||||
RoutesMock: func() ([]Route, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.Routes()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from RouterMock.Routes, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareProviderMock(t *testing.T) {
|
||||
mock := &MiddlewareProviderMock{
|
||||
MiddlewaresMock: func() ([]Middleware, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.Middlewares()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from MiddlewareProviderMock.Middlewares, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthCheckerMock(t *testing.T) {
|
||||
mock := &HealthCheckerMock{
|
||||
ChecksMock: func() ([]health.CheckerOption, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadyMock: func() error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.Checks()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from HealthCheckerMock.Checks, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Ready()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from HealthCheckerMock.Ready, but got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/chromedp/cdproto/fetch"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/dlclark/regexp2"
|
||||
@@ -93,6 +94,9 @@ func (b *chromiumBrowser) Start(logger *zap.Logger) error {
|
||||
chromedp.UserDataDir(b.userProfileDirPath),
|
||||
// See https://github.com/gotenberg/gotenberg/issues/831.
|
||||
chromedp.Flag("disable-pdf-tagging", true),
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1177.
|
||||
chromedp.Flag("no-zygote", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
)
|
||||
|
||||
if b.arguments.incognito {
|
||||
@@ -211,7 +215,7 @@ func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
|
||||
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
|
||||
}
|
||||
|
||||
// Also remove Chromium specific files in the temporary directory.
|
||||
// Also, remove Chromium-specific files in the temporary directory.
|
||||
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{".org.chromium.Chromium", ".com.google.Chrome"}, expirationTime)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
@@ -271,12 +275,14 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||
forceExactColorsActionFunc(),
|
||||
forceExactColorsActionFunc(logger, options.PrintBackground),
|
||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||
// PDF specific.
|
||||
printToPdfActionFunc(logger, outputPath, options),
|
||||
// Teardown.
|
||||
page.Close(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,13 +300,15 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur
|
||||
userAgentOverride(logger, options.UserAgent),
|
||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||
forceExactColorsActionFunc(),
|
||||
forceExactColorsActionFunc(logger, true),
|
||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||
// Screenshot specific.
|
||||
setDeviceMetricsOverride(logger, options.Width, options.Height),
|
||||
captureScreenshotActionFunc(logger, outputPath, options),
|
||||
// Teardown.
|
||||
page.Close(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -314,7 +322,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
return errors.New("context has no deadline")
|
||||
}
|
||||
|
||||
// We validate the "main" URL against our allow / deny lists.
|
||||
// We validate the "main" URL against our allowed / deny lists.
|
||||
err := gotenberg.FilterDeadline(b.arguments.allowList, b.arguments.denyList, url, deadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("filter URL: %w", err)
|
||||
@@ -329,7 +337,7 @@ func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string
|
||||
taskCtx, taskCancel := chromedp.NewContext(timeoutCtx)
|
||||
defer taskCancel()
|
||||
|
||||
// We validate all others requests against our allow / deny lists.
|
||||
// We validate all other requests against our allowed / deny lists.
|
||||
// If a request does not pass the validation, we make it fail. It also set
|
||||
// the extra HTTP headers, if any.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1011.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
@@ -23,7 +26,7 @@ func init() {
|
||||
|
||||
var (
|
||||
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
||||
// "screen" nor "print". Empty value are allowed though.
|
||||
// "screen" nor "print". Empty value is allowed, though.
|
||||
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
||||
|
||||
// ErrInvalidEvaluationExpression happens if an evaluation expression
|
||||
@@ -35,11 +38,11 @@ var (
|
||||
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
||||
|
||||
// ErrInvalidHttpStatusCode happens when the status code from the main page
|
||||
// matches with one of the entry in [Options.FailOnHttpStatusCodes].
|
||||
// matches with one of the entries in [Options.FailOnHttpStatusCodes].
|
||||
ErrInvalidHttpStatusCode = errors.New("invalid HTTP status code")
|
||||
|
||||
// ErrInvalidResourceHttpStatusCode happens when the status code from one
|
||||
// or more resources matches with one of the entry in
|
||||
// or more resources matches with one of the entries in
|
||||
// [Options.FailOnResourceHttpStatusCodes].
|
||||
ErrInvalidResourceHttpStatusCode = errors.New("invalid resource HTTP status code")
|
||||
|
||||
@@ -65,12 +68,12 @@ var (
|
||||
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
||||
|
||||
// ErrPageRangesSyntaxError happens if the PdfOptions have an invalid page
|
||||
// ranges.
|
||||
// range.
|
||||
ErrPageRangesSyntaxError = errors.New("page ranges syntax error")
|
||||
)
|
||||
|
||||
// Chromium is a module which provides both an [Api] and routes for converting
|
||||
// HTML document to PDF.
|
||||
// Chromium is a module that provides both an [Api] and routes for converting
|
||||
// an HTML document to PDF.
|
||||
type Chromium struct {
|
||||
autoStart bool
|
||||
disableRoutes bool
|
||||
@@ -125,15 +128,15 @@ type Options struct {
|
||||
UserAgent string
|
||||
|
||||
// ExtraHttpHeaders are extra HTTP headers to send by Chromium while
|
||||
// loading he HTML document.
|
||||
// loading the HTML document.
|
||||
ExtraHttpHeaders []ExtraHttpHeader
|
||||
|
||||
// EmulatedMediaType is the media type to emulate, either "screen" or
|
||||
// "print".
|
||||
EmulatedMediaType string
|
||||
|
||||
// OmitBackground hides default white background and allows generating PDFs
|
||||
// with transparency.
|
||||
// OmitBackground hides the default white background and allows generating
|
||||
// PDFs with transparency.
|
||||
OmitBackground bool
|
||||
}
|
||||
|
||||
@@ -194,9 +197,9 @@ type PdfOptions struct {
|
||||
// Page ranges to print, e.g., '1-5, 8, 11-13'. Empty means all pages.
|
||||
PageRanges string
|
||||
|
||||
// HeaderTemplate is the HTML template of the header. It should be valid
|
||||
// HTML markup with following classes used to inject printing values into
|
||||
// them:
|
||||
// HeaderTemplate is the HTML template of the header. It should be a valid
|
||||
// HTML markup with the following classes used to inject printing values
|
||||
// into them:
|
||||
// - date: formatted print date
|
||||
// - title: document title
|
||||
// - url: document location
|
||||
@@ -217,6 +220,10 @@ type PdfOptions struct {
|
||||
// GenerateDocumentOutline defines whether the document outline should be
|
||||
// embedded into the PDF.
|
||||
GenerateDocumentOutline bool
|
||||
|
||||
// GenerateTaggedPdf defines whether to generate tagged (accessible)
|
||||
// PDF.
|
||||
GenerateTaggedPdf bool
|
||||
}
|
||||
|
||||
// DefaultPdfOptions returns the default values for PdfOptions.
|
||||
@@ -238,6 +245,7 @@ func DefaultPdfOptions() PdfOptions {
|
||||
FooterTemplate: "<html><head></head><body></body></html>",
|
||||
PreferCssPageSize: false,
|
||||
GenerateDocumentOutline: false,
|
||||
GenerateTaggedPdf: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +343,7 @@ type Api interface {
|
||||
Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||
}
|
||||
|
||||
// Provider is a module interface which exposes a method for creating an [Api]
|
||||
// Provider is a module interface that exposes a method for creating an [Api]
|
||||
// for other modules.
|
||||
//
|
||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||
@@ -470,8 +478,8 @@ func (mod *Chromium) StartupMessage() string {
|
||||
|
||||
// Stop stops the current browser instance.
|
||||
func (mod *Chromium) Stop(ctx context.Context) error {
|
||||
// Block until the context is done so that other module may gracefully stop
|
||||
// before we do a shutdown.
|
||||
// Block until the context is done so that another module may gracefully
|
||||
// stop before we do a shutdown.
|
||||
mod.logger.Debug("wait for the end of grace duration")
|
||||
|
||||
<-ctx.Done()
|
||||
@@ -484,6 +492,23 @@ func (mod *Chromium) Stop(ctx context.Context) error {
|
||||
return fmt.Errorf("stop Chromium: %w", err)
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (mod *Chromium) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
|
||||
cmd := exec.Command(mod.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
}
|
||||
|
||||
debug["version"] = strings.TrimSpace(string(output))
|
||||
return debug
|
||||
}
|
||||
|
||||
// Metrics returns the metrics.
|
||||
func (mod *Chromium) Metrics() ([]gotenberg.Metric, error) {
|
||||
return []gotenberg.Metric{
|
||||
@@ -593,6 +618,7 @@ var (
|
||||
_ gotenberg.Provisioner = (*Chromium)(nil)
|
||||
_ gotenberg.Validator = (*Chromium)(nil)
|
||||
_ gotenberg.App = (*Chromium)(nil)
|
||||
_ gotenberg.Debuggable = (*Chromium)(nil)
|
||||
_ gotenberg.MetricsProvider = (*Chromium)(nil)
|
||||
_ api.HealthChecker = (*Chromium)(nil)
|
||||
_ api.Router = (*Chromium)(nil)
|
||||
|
||||
@@ -1,589 +0,0 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestDefaultOptions(t *testing.T) {
|
||||
actual := DefaultOptions()
|
||||
notExpect := Options{}
|
||||
|
||||
if reflect.DeepEqual(actual, notExpect) {
|
||||
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultPdfOptions(t *testing.T) {
|
||||
actual := DefaultPdfOptions()
|
||||
notExpect := PdfOptions{}
|
||||
|
||||
if reflect.DeepEqual(actual, notExpect) {
|
||||
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultScreenshotOptions(t *testing.T) {
|
||||
actual := DefaultScreenshotOptions()
|
||||
notExpect := ScreenshotOptions{}
|
||||
|
||||
if reflect.DeepEqual(actual, notExpect) {
|
||||
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Descriptor(t *testing.T) {
|
||||
descriptor := new(Chromium).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(Chromium))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Chromium).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no logger from logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Chromium).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Chromium).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no PDF engine from PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Chromium).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "provision success",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return new(gotenberg.PdfEngineMock), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Chromium).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
err := mod.Provision(tc.ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "empty bin path",
|
||||
binPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "bin path does not exist",
|
||||
binPath: "/foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "validate success",
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.args = browserArguments{
|
||||
binPath: tc.binPath,
|
||||
}
|
||||
err := mod.Validate()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Start(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
autoStart bool
|
||||
supervisor *gotenberg.ProcessSupervisorMock
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no auto-start",
|
||||
autoStart: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start success",
|
||||
autoStart: true,
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start failed",
|
||||
autoStart: true,
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||
return errors.New("foo")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.autoStart = tc.autoStart
|
||||
mod.supervisor = tc.supervisor
|
||||
|
||||
err := mod.Start()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_StartupMessage(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
|
||||
mod.autoStart = true
|
||||
autoStartMsg := mod.StartupMessage()
|
||||
|
||||
mod.autoStart = false
|
||||
noAutoStartMsg := mod.StartupMessage()
|
||||
|
||||
if autoStartMsg == noAutoStartMsg {
|
||||
t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Stop(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor *gotenberg.ProcessSupervisorMock
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "stop success",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "stop failed",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||
return errors.New("foo")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.logger = zap.NewNop()
|
||||
mod.supervisor = tc.supervisor
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second)
|
||||
cancel()
|
||||
|
||||
err := mod.Stop(ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Metrics(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.supervisor = &gotenberg.ProcessSupervisorMock{
|
||||
ReqQueueSizeMock: func() int64 {
|
||||
return 10
|
||||
},
|
||||
RestartsCountMock: func() int64 {
|
||||
return 0
|
||||
},
|
||||
}
|
||||
|
||||
metrics, err := mod.Metrics()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if len(metrics) != 2 {
|
||||
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
||||
}
|
||||
|
||||
actual := metrics[0].Read()
|
||||
if actual != float64(10) {
|
||||
t.Errorf("expected %f for chromium_requests_queue_size, but got %f", float64(10), actual)
|
||||
}
|
||||
|
||||
actual = metrics[1].Read()
|
||||
if actual != float64(0) {
|
||||
t.Errorf("expected %f for chromium_restarts_count, but got %f", float64(0), actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Checks(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
expectAvailabilityStatus health.AvailabilityStatus
|
||||
}{
|
||||
{
|
||||
scenario: "healthy module",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||
return true
|
||||
}},
|
||||
expectAvailabilityStatus: health.StatusUp,
|
||||
},
|
||||
{
|
||||
scenario: "unhealthy module",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||
return false
|
||||
}},
|
||||
expectAvailabilityStatus: health.StatusDown,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.supervisor = tc.supervisor
|
||||
|
||||
checks, err := mod.Checks()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
checker := health.NewChecker(checks...)
|
||||
result := checker.Check(context.Background())
|
||||
|
||||
if result.Status != tc.expectAvailabilityStatus {
|
||||
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Ready(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
autoStart bool
|
||||
startTimeout time.Duration
|
||||
browser browser
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no auto-start",
|
||||
autoStart: false,
|
||||
startTimeout: time.Duration(30) * time.Second,
|
||||
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||
return false
|
||||
}}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start: context done",
|
||||
autoStart: true,
|
||||
startTimeout: time.Duration(200) * time.Millisecond,
|
||||
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||
return false
|
||||
}}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start success",
|
||||
autoStart: true,
|
||||
startTimeout: time.Duration(30) * time.Second,
|
||||
browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||
return true
|
||||
}}},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.autoStart = tc.autoStart
|
||||
mod.args = browserArguments{wsUrlReadTimeout: tc.startTimeout}
|
||||
mod.browser = tc.browser
|
||||
|
||||
err := mod.Ready()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Chromium(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
|
||||
_, err := mod.Chromium()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Routes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
expectRoutes int
|
||||
disableRoutes bool
|
||||
}{
|
||||
{
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 6,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
scenario: "routes disabled",
|
||||
expectRoutes: 0,
|
||||
disableRoutes: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.disableRoutes = tc.disableRoutes
|
||||
|
||||
routes, err := mod.Routes()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectRoutes != len(routes) {
|
||||
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Pdf(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
browser browser
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "PDF task success",
|
||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "PDF task error",
|
||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return errors.New("PDF task error")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
return task()
|
||||
}}
|
||||
mod.browser = tc.browser
|
||||
|
||||
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Screenshot(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
browser browser
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "Screenshot task success",
|
||||
browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "Screenshot task error",
|
||||
browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||
return errors.New("screenshot task error")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
return task()
|
||||
}}
|
||||
mod.browser = tc.browser
|
||||
|
||||
err := mod.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestDebugLogger_Write(t *testing.T) {
|
||||
actual, err := (&debugLogger{logger: zap.NewNop()}).Write([]byte("foo"))
|
||||
expected := len([]byte("foo"))
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf("expected %d but got %d", expected, actual)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expected not error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugLogger_Printf(t *testing.T) {
|
||||
(&debugLogger{logger: zap.NewNop()}).Printf("%s", "foo")
|
||||
}
|
||||
@@ -72,10 +72,10 @@ func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, option
|
||||
|
||||
var extraHttpHeadersToSet []ExtraHttpHeader
|
||||
if len(options.extraHttpHeaders) > 0 {
|
||||
// The user want to set extra HTTP headers.
|
||||
// The user wants to set extra HTTP headers.
|
||||
|
||||
// First, we have to check if at least one header has to be
|
||||
// set for current request.
|
||||
// set for the current request.
|
||||
for _, header := range options.extraHttpHeaders {
|
||||
if header.Scope == nil {
|
||||
// Non-scoped header.
|
||||
@@ -147,8 +147,8 @@ type eventResponseReceivedOptions struct {
|
||||
invalidResourceHttpStatusCodeMu *sync.RWMutex
|
||||
}
|
||||
|
||||
// listenForEventResponseReceived listens for an invalid HTTP status code that
|
||||
// is returned by the main page or by one or more resources.
|
||||
// listenForEventResponseReceived listens for an invalid HTTP status code
|
||||
// returned by the main page or by one or more resources.
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/613.
|
||||
// https://github.com/gotenberg/gotenberg/issues/1021.
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestApiMock(t *testing.T) {
|
||||
mock := &ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return nil
|
||||
},
|
||||
ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ApiMock.Screenshot, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowserMock(t *testing.T) {
|
||||
mock := &browserMock{
|
||||
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return nil
|
||||
},
|
||||
screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from browserMock.pdf, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from browserMock.screenshot, but got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// FormDataChromiumOptions creates [Options] from the form data. Fallback to
|
||||
// default value if the considered key is not present.
|
||||
// the default value if the considered key is not present.
|
||||
func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
defaultOptions := DefaultOptions()
|
||||
|
||||
@@ -194,7 +194,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
}
|
||||
|
||||
// FormDataChromiumPdfOptions creates [PdfOptions] from the form data. Fallback to
|
||||
// default value if the considered key is not present.
|
||||
// the default value if the considered key is not present.
|
||||
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||
form, options := FormDataChromiumOptions(ctx)
|
||||
defaultPdfOptions := DefaultPdfOptions()
|
||||
@@ -207,6 +207,7 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||
headerTemplate, footerTemplate string
|
||||
preferCssPageSize bool
|
||||
generateDocumentOutline bool
|
||||
generateTaggedPdf bool
|
||||
)
|
||||
|
||||
form.
|
||||
@@ -224,7 +225,8 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||
Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate).
|
||||
Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).
|
||||
Bool("preferCssPageSize", &preferCssPageSize, defaultPdfOptions.PreferCssPageSize).
|
||||
Bool("generateDocumentOutline", &generateDocumentOutline, defaultPdfOptions.GenerateDocumentOutline)
|
||||
Bool("generateDocumentOutline", &generateDocumentOutline, defaultPdfOptions.GenerateDocumentOutline).
|
||||
Bool("generateTaggedPdf", &generateTaggedPdf, defaultPdfOptions.GenerateTaggedPdf)
|
||||
|
||||
pdfOptions := PdfOptions{
|
||||
Options: options,
|
||||
@@ -243,13 +245,14 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||
FooterTemplate: footerTemplate,
|
||||
PreferCssPageSize: preferCssPageSize,
|
||||
GenerateDocumentOutline: generateDocumentOutline,
|
||||
GenerateTaggedPdf: generateTaggedPdf,
|
||||
}
|
||||
|
||||
return form, pdfOptions
|
||||
}
|
||||
|
||||
// FormDataChromiumScreenshotOptions creates [ScreenshotOptions] from the form
|
||||
// data. Fallback to default value if the considered key is not present.
|
||||
// data. Fallback to the default value if the considered key is not present.
|
||||
func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, ScreenshotOptions) {
|
||||
form, options := FormDataChromiumOptions(ctx)
|
||||
defaultScreenshotOptions := DefaultScreenshotOptions()
|
||||
@@ -483,7 +486,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
|
||||
// screenshotMarkdownRoute returns an [api.Route] which can take a screenshot
|
||||
// from markdown files.
|
||||
// from Markdown files.
|
||||
func screenshotMarkdownRoute(chromium Api) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
@@ -522,7 +525,7 @@ func screenshotMarkdownRoute(chromium Api) api.Route {
|
||||
}
|
||||
|
||||
func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string) (string, error) {
|
||||
// We have to convert each markdown file referenced in the HTML
|
||||
// We have to convert each Markdown file referenced in the HTML
|
||||
// file to... HTML. Thanks to the "html/template" package, we are
|
||||
// able to provide the "toHTML" function which the user may call
|
||||
// directly inside the HTML file.
|
||||
@@ -598,6 +601,9 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
|
||||
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}) error {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1130.
|
||||
filename := ctx.OutputFilename(outputPath)
|
||||
outputPath = ctx.GeneratePathFromFilename(filename)
|
||||
|
||||
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
||||
err = handleChromiumError(err, options.Options)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ type streamReader struct {
|
||||
// Read a chunk of the stream.
|
||||
func (reader *streamReader) Read(p []byte) (n int, err error) {
|
||||
if reader.r != nil {
|
||||
// Continue reading from buffer.
|
||||
// Continue reading from the buffer.
|
||||
return reader.read(p)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,9 +49,8 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOpti
|
||||
WithPageRanges(pageRanges).
|
||||
WithPreferCSSPageSize(options.PreferCssPageSize).
|
||||
WithGenerateDocumentOutline(options.GenerateDocumentOutline).
|
||||
// Does not seem to work.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/831.
|
||||
WithGenerateTaggedPDF(false)
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1210.
|
||||
WithGenerateTaggedPDF(options.GenerateTaggedPdf)
|
||||
|
||||
hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
|
||||
options.FooterTemplate != DefaultPdfOptions().FooterTemplate
|
||||
@@ -392,26 +391,30 @@ func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackground, pr
|
||||
}
|
||||
}
|
||||
|
||||
func forceExactColorsActionFunc() chromedp.ActionFunc {
|
||||
func forceExactColorsActionFunc(logger *zap.Logger, printBackground bool) chromedp.ActionFunc {
|
||||
return func(ctx context.Context) error {
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/354
|
||||
// https://github.com/puppeteer/puppeteer/issues/2685
|
||||
// https://github.com/chromedp/chromedp/issues/520
|
||||
script := `
|
||||
(() => {
|
||||
const css = 'html { -webkit-print-color-adjust: exact !important; }';
|
||||
css := "html { -webkit-print-color-adjust: exact !important; }"
|
||||
if !printBackground {
|
||||
// The -webkit-print-color-adjust: exact CSS property forces the
|
||||
// print of the background, whatever the printToPDF args.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1154.
|
||||
additionalCss := "html, body { background: none !important; }"
|
||||
logger.Debug(fmt.Sprintf("inject %s as printBackground is %t", additionalCss, printBackground))
|
||||
css += additionalCss
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(`
|
||||
(() => {
|
||||
const css = '%s';
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
`
|
||||
`, css)
|
||||
|
||||
evaluate := chromedp.Evaluate(script, nil)
|
||||
err := evaluate.Do(ctx)
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/barasher/go-exiftool"
|
||||
"go.uber.org/zap"
|
||||
@@ -53,6 +56,23 @@ func (engine *ExifTool) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *ExifTool) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
|
||||
cmd := exec.Command(engine.binPath, "-ver") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
}
|
||||
|
||||
debug["version"] = strings.TrimSpace(string(output))
|
||||
return debug
|
||||
}
|
||||
|
||||
// Merge is not available in this implementation.
|
||||
func (engine *ExifTool) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return fmt.Errorf("merge PDFs with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
@@ -122,15 +142,15 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
fileMetadata[0].SetStrings(key, val)
|
||||
case []interface{}:
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1048.
|
||||
strings := make([]string, len(val))
|
||||
strs := make([]string, len(val))
|
||||
for i, entry := range val {
|
||||
if str, ok := entry.(string); ok {
|
||||
strings[i] = str
|
||||
strs[i] = str
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
}
|
||||
fileMetadata[0].SetStrings(key, strings)
|
||||
fileMetadata[0].SetStrings(key, strs)
|
||||
case bool:
|
||||
fileMetadata[0].SetString(key, fmt.Sprintf("%t", val))
|
||||
case int:
|
||||
@@ -161,5 +181,6 @@ var (
|
||||
_ gotenberg.Module = (*ExifTool)(nil)
|
||||
_ gotenberg.Provisioner = (*ExifTool)(nil)
|
||||
_ gotenberg.Validator = (*ExifTool)(nil)
|
||||
_ gotenberg.Debuggable = (*ExifTool)(nil)
|
||||
_ gotenberg.PdfEngine = (*ExifTool)(nil)
|
||||
)
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
package exiftool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestExifTool_Descriptor(t *testing.T) {
|
||||
descriptor := new(ExifTool).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(ExifTool))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExifTool_Provision(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||
|
||||
err := engine.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExifTool_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "empty bin path",
|
||||
binPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "bin path does not exist",
|
||||
binPath: "/foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "validate success",
|
||||
binPath: os.Getenv("EXIFTOOL_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
engine.binPath = tc.binPath
|
||||
err := engine.Validate()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Merge(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Split(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
_, err := engine.Split(context.Background(), zap.NewNop(), gotenberg.SplitMode{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Flatten(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Flatten(context.Background(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Convert(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_ReadMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
inputPath string
|
||||
expectMetadata map[string]interface{}
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
inputPath: "foo",
|
||||
expectMetadata: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectMetadata: map[string]interface{}{
|
||||
"FileName": "sample1.pdf",
|
||||
"FileTypeExtension": "pdf",
|
||||
"MIMEType": "application/pdf",
|
||||
"PDFVersion": 1.4,
|
||||
"PageCount": float64(3),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), tc.inputPath)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectMetadata != nil && err == nil {
|
||||
for k, v := range tc.expectMetadata {
|
||||
if v2, ok := metadata[k]; !ok || v != v2 {
|
||||
t.Errorf("expected entry %s with value %v to exists", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_WriteMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
createCopy bool
|
||||
inputPath string
|
||||
metadata map[string]interface{}
|
||||
expectMetadata map[string]interface{}
|
||||
expectError bool
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
createCopy: false,
|
||||
inputPath: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (not string array)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Unsupported": []interface{}{
|
||||
"foo",
|
||||
1,
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (default)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Unsupported": map[string]interface{}{},
|
||||
},
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "success (interface array to string array)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Keywords": []interface{}{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
},
|
||||
expectMetadata: map[string]interface{}{
|
||||
"Keywords": []interface{}{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreationDate": "2006-09-18T16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": []string{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
"Marked": true,
|
||||
"ModDate": "2006-09-18T16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
"Title": "Sample",
|
||||
"Trapped": "Unknown",
|
||||
// Those are not valid PDF metadata.
|
||||
"int": 1,
|
||||
"int64": int64(2),
|
||||
"float32": float32(2.2),
|
||||
"float64": 3.3,
|
||||
},
|
||||
expectMetadata: map[string]interface{}{
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreationDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": []interface{}{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
"Title": "Sample",
|
||||
"Trapped": "Unknown",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
var destinationPath string
|
||||
if tc.createCopy {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error no but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
destinationPath = fmt.Sprintf("%s/copy_temp.pdf", outputDir)
|
||||
source, err := os.Open(tc.inputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open source file: %v", err)
|
||||
}
|
||||
|
||||
defer func(source *os.File) {
|
||||
err := source.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close file: %v", err)
|
||||
}
|
||||
}(source)
|
||||
|
||||
destination, err := os.Create(destinationPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create destination file: %v", err)
|
||||
}
|
||||
|
||||
defer func(destination *os.File) {
|
||||
err := destination.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close file: %v", err)
|
||||
}
|
||||
}(destination)
|
||||
|
||||
_, err = io.Copy(destination, source)
|
||||
if err != nil {
|
||||
t.Fatalf("copy source into destination: %v", err)
|
||||
}
|
||||
} else {
|
||||
destinationPath = tc.inputPath
|
||||
}
|
||||
|
||||
err = engine.WriteMetadata(context.Background(), zap.NewNop(), tc.metadata, destinationPath)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
|
||||
if tc.expectError {
|
||||
return
|
||||
}
|
||||
|
||||
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), destinationPath)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectMetadata != nil && err == nil {
|
||||
for k, v := range tc.expectMetadata {
|
||||
v2, ok := metadata[k]
|
||||
if !ok {
|
||||
t.Errorf("expected entry %s with value %v to exists, but got none", k, v)
|
||||
continue
|
||||
}
|
||||
|
||||
switch v2.(type) {
|
||||
case []interface{}:
|
||||
for i, entry := range v.([]interface{}) {
|
||||
if entry != v2.([]interface{})[i] {
|
||||
t.Errorf("expected entry %s to contain value %v, but got %v", k, entry, v2.([]interface{})[i])
|
||||
}
|
||||
}
|
||||
default:
|
||||
if v != v2 {
|
||||
t.Errorf("expected entry %s with value %v to exists, but got %v", k, v, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
@@ -21,23 +24,23 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidPdfFormats happens if the PDF formats option cannot be handled
|
||||
// by LibreOffice.
|
||||
// ErrInvalidPdfFormats happens if LibreOffice cannot handle the PDF
|
||||
// formats option.
|
||||
ErrInvalidPdfFormats = errors.New("invalid PDF formats")
|
||||
|
||||
// ErrUnoException happens when unoconverter returns an exit code 5.
|
||||
// ErrUnoException happens when unoconverter returns exit code 5.
|
||||
ErrUnoException = errors.New("uno exception")
|
||||
|
||||
// ErrRuntimeException happens when unoconverter returns an exit code 6.
|
||||
// ErrRuntimeException happens when unoconverter returns exit code 6.
|
||||
ErrRuntimeException = errors.New("uno exception")
|
||||
|
||||
// ErrCoreDumped happens randomly; sometime a conversion will work as
|
||||
// ErrCoreDumped happens randomly; sometimes a conversion will work as
|
||||
// expected, and some other time the same conversion will fail.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/639.
|
||||
ErrCoreDumped = errors.New("core dumped")
|
||||
)
|
||||
|
||||
// Api is a module which provides a [Uno] to interact with LibreOffice.
|
||||
// Api is a module that provides a [Uno] to interact with LibreOffice.
|
||||
type Api struct {
|
||||
autoStart bool
|
||||
args libreOfficeArguments
|
||||
@@ -53,12 +56,17 @@ type Options struct {
|
||||
// Password specifies the password for opening the source file.
|
||||
Password string
|
||||
|
||||
// Landscape allows to change the orientation of the resulting PDF.
|
||||
// Landscape allows changing the orientation of the resulting PDF.
|
||||
Landscape bool
|
||||
|
||||
// PageRanges allows to select the pages to convert.
|
||||
// PageRanges allows selecting the pages to convert.
|
||||
PageRanges string
|
||||
|
||||
// UpdateIndexes specifies whether to update the indexes before conversion,
|
||||
// keeping in mind that doing so might result in missing links in the final
|
||||
// PDF.
|
||||
UpdateIndexes bool
|
||||
|
||||
// ExportFormFields specifies whether form fields are exported as widgets
|
||||
// or only their fixed print representation is exported.
|
||||
ExportFormFields bool
|
||||
@@ -75,7 +83,7 @@ type Options struct {
|
||||
// Named Destination.
|
||||
ExportBookmarksToPdfDestination bool
|
||||
|
||||
// ExportPlaceholders exports the placeholders fields visual markings only.
|
||||
// ExportPlaceholders exports the placeholder fields visual markings only.
|
||||
// The exported placeholder is ineffective.
|
||||
ExportPlaceholders bool
|
||||
|
||||
@@ -86,15 +94,16 @@ type Options struct {
|
||||
// Notes pages are available in Impress documents only.
|
||||
ExportNotesPages bool
|
||||
|
||||
// ExportOnlyNotesPages specifies, if the property ExportNotesPages is set
|
||||
// to true, if only notes pages are exported to PDF.
|
||||
// ExportOnlyNotesPages specifies if the property ExportNotesPages is set
|
||||
// to true if only notes pages are exported to PDF.
|
||||
ExportOnlyNotesPages bool
|
||||
|
||||
// ExportNotesInMargin specifies if notes in margin are exported to PDF.
|
||||
// ExportNotesInMargin specifies if notes in the margin are exported to
|
||||
// PDF.
|
||||
ExportNotesInMargin bool
|
||||
|
||||
// ConvertOooTargetToPdfTarget specifies that the target documents with
|
||||
// .od[tpgs] extension, will have that extension changed to .pdf when the
|
||||
// .od[tpgs] extension will have that extension changed to .pdf when the
|
||||
// link is exported to PDF. The source document remains untouched.
|
||||
ConvertOooTargetToPdfTarget bool
|
||||
|
||||
@@ -149,6 +158,7 @@ func DefaultOptions() Options {
|
||||
Password: "",
|
||||
Landscape: false,
|
||||
PageRanges: "",
|
||||
UpdateIndexes: true,
|
||||
ExportFormFields: true,
|
||||
AllowDuplicateFieldNames: false,
|
||||
ExportBookmarks: true,
|
||||
@@ -181,7 +191,7 @@ type Uno interface {
|
||||
Extensions() []string
|
||||
}
|
||||
|
||||
// Provider is a module interface which exposes a method for creating a
|
||||
// Provider is a module interface that exposes a method for creating a
|
||||
// [Uno] for other modules.
|
||||
//
|
||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||
@@ -291,8 +301,8 @@ func (a *Api) StartupMessage() string {
|
||||
|
||||
// Stop stops the current browser instance.
|
||||
func (a *Api) Stop(ctx context.Context) error {
|
||||
// Block until the context is done so that other module may gracefully stop
|
||||
// before we do a shutdown.
|
||||
// Block until the context is done so that another module may gracefully
|
||||
// stop before we do a shutdown.
|
||||
a.logger.Debug("wait for the end of grace duration")
|
||||
|
||||
<-ctx.Done()
|
||||
@@ -305,6 +315,23 @@ func (a *Api) Stop(ctx context.Context) error {
|
||||
return fmt.Errorf("stop LibreOffice: %w", err)
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (a *Api) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
|
||||
cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
}
|
||||
|
||||
debug["version"] = strings.TrimSpace(string(output))
|
||||
return debug
|
||||
}
|
||||
|
||||
// Metrics returns the metrics.
|
||||
func (a *Api) Metrics() ([]gotenberg.Metric, error) {
|
||||
return []gotenberg.Metric{
|
||||
@@ -536,6 +563,7 @@ var (
|
||||
_ gotenberg.Provisioner = (*Api)(nil)
|
||||
_ gotenberg.Validator = (*Api)(nil)
|
||||
_ gotenberg.App = (*Api)(nil)
|
||||
_ gotenberg.Debuggable = (*Api)(nil)
|
||||
_ gotenberg.MetricsProvider = (*Api)(nil)
|
||||
_ api.HealthChecker = (*Api)(nil)
|
||||
_ Uno = (*Api)(nil)
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestDefaultOptions(t *testing.T) {
|
||||
actual := DefaultOptions()
|
||||
notExpect := Options{}
|
||||
|
||||
if reflect.DeepEqual(actual, notExpect) {
|
||||
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Descriptor(t *testing.T) {
|
||||
descriptor := new(Api).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(Api))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Api).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no logger from logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Api).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "provision success",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(Api).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
a := new(Api)
|
||||
err := a.Provision(tc.ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
unoBinPath string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "empty LibreOffice bin path",
|
||||
binPath: "",
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "LibreOffice bin path does not exist",
|
||||
binPath: "/foo",
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "empty uno bin path",
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
unoBinPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "uno bin path does not exist",
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
unoBinPath: "/foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "validate success",
|
||||
binPath: os.Getenv("CHROMIUM_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.args = libreOfficeArguments{
|
||||
binPath: tc.binPath,
|
||||
unoBinPath: tc.unoBinPath,
|
||||
}
|
||||
err := a.Validate()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Start(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
autoStart bool
|
||||
supervisor *gotenberg.ProcessSupervisorMock
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no auto-start",
|
||||
autoStart: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start success",
|
||||
autoStart: true,
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start failed",
|
||||
autoStart: true,
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error {
|
||||
return errors.New("foo")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.autoStart = tc.autoStart
|
||||
a.supervisor = tc.supervisor
|
||||
|
||||
err := a.Start()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_StartupMessage(t *testing.T) {
|
||||
a := new(Api)
|
||||
|
||||
a.autoStart = true
|
||||
autoStartMsg := a.StartupMessage()
|
||||
|
||||
a.autoStart = false
|
||||
noAutoStartMsg := a.StartupMessage()
|
||||
|
||||
if autoStartMsg == noAutoStartMsg {
|
||||
t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Stop(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor *gotenberg.ProcessSupervisorMock
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "stop success",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "stop failed",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error {
|
||||
return errors.New("foo")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.logger = zap.NewNop()
|
||||
a.supervisor = tc.supervisor
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second)
|
||||
cancel()
|
||||
|
||||
err := a.Stop(ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Metrics(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.supervisor = &gotenberg.ProcessSupervisorMock{
|
||||
ReqQueueSizeMock: func() int64 {
|
||||
return 10
|
||||
},
|
||||
RestartsCountMock: func() int64 {
|
||||
return 0
|
||||
},
|
||||
}
|
||||
|
||||
metrics, err := a.Metrics()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if len(metrics) != 2 {
|
||||
t.Fatalf("expected %d metrics, but got %d", 2, len(metrics))
|
||||
}
|
||||
|
||||
actual := metrics[0].Read()
|
||||
if actual != float64(10) {
|
||||
t.Errorf("expected %f for libreoffice_requests_queue_size, but got %f", float64(10), actual)
|
||||
}
|
||||
|
||||
actual = metrics[1].Read()
|
||||
if actual != float64(0) {
|
||||
t.Errorf("expected %f for libreoffice_restarts_count, but got %f", float64(0), actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Checks(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
expectAvailabilityStatus health.AvailabilityStatus
|
||||
}{
|
||||
{
|
||||
scenario: "healthy module",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||
return true
|
||||
}},
|
||||
expectAvailabilityStatus: health.StatusUp,
|
||||
},
|
||||
{
|
||||
scenario: "unhealthy module",
|
||||
supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool {
|
||||
return false
|
||||
}},
|
||||
expectAvailabilityStatus: health.StatusDown,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.supervisor = tc.supervisor
|
||||
|
||||
checks, err := a.Checks()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
checker := health.NewChecker(checks...)
|
||||
result := checker.Check(context.Background())
|
||||
|
||||
if result.Status != tc.expectAvailabilityStatus {
|
||||
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Ready(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
autoStart bool
|
||||
startTimeout time.Duration
|
||||
libreOffice libreOffice
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no auto-start",
|
||||
autoStart: false,
|
||||
startTimeout: time.Duration(30) * time.Second,
|
||||
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||
return false
|
||||
}}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start: context done",
|
||||
autoStart: true,
|
||||
startTimeout: time.Duration(200) * time.Millisecond,
|
||||
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||
return false
|
||||
}}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "auto-start success",
|
||||
autoStart: true,
|
||||
startTimeout: time.Duration(30) * time.Second,
|
||||
libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool {
|
||||
return true
|
||||
}}},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.autoStart = tc.autoStart
|
||||
a.args = libreOfficeArguments{startTimeout: tc.startTimeout}
|
||||
a.libreOffice = tc.libreOffice
|
||||
|
||||
err := a.Ready()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_LibreOffice(t *testing.T) {
|
||||
a := new(Api)
|
||||
|
||||
_, err := a.LibreOffice()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Pdf(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
libreOffice libreOffice
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "PDF task success",
|
||||
libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||
return nil
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "PDF task error",
|
||||
libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||
return errors.New("PDF task error")
|
||||
}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "ErrCoreDumped",
|
||||
libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||
return ErrCoreDumped
|
||||
}},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||
return task()
|
||||
}}
|
||||
a.libreOffice = tc.libreOffice
|
||||
|
||||
err := a.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Extensions(t *testing.T) {
|
||||
a := new(Api)
|
||||
extensions := a.Extensions()
|
||||
|
||||
actual := len(extensions)
|
||||
expect := 130
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected %d extensions, but got %d", expect, actual)
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
@@ -200,7 +197,7 @@ func (p *libreOfficeProcess) Stop(logger *zap.Logger) error {
|
||||
logger.Debug(fmt.Sprintf("'%s' LibreOffice's user profile directory removed", userProfileDirPath))
|
||||
}
|
||||
|
||||
// Also remove LibreOffice specific files in the temporary directory.
|
||||
// Also, remove LibreOffice specific files in the temporary directory.
|
||||
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{"OSL_PIPE", ".tmp"}, expirationTime)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
@@ -275,10 +272,15 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
args = append(args, "--printer", "PaperOrientation=landscape")
|
||||
}
|
||||
|
||||
// See: https://github.com/gotenberg/gotenberg/issues/1149.
|
||||
if options.PageRanges != "" {
|
||||
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
||||
}
|
||||
|
||||
if !options.UpdateIndexes {
|
||||
args = append(args, "--disable-update-indexes")
|
||||
}
|
||||
|
||||
args = append(args, "--export", fmt.Sprintf("ExportFormFields=%t", options.ExportFormFields))
|
||||
args = append(args, "--export", fmt.Sprintf("AllowDuplicateFieldNames=%t", options.AllowDuplicateFieldNames))
|
||||
args = append(args, "--export", fmt.Sprintf("ExportBookmarks=%t", options.ExportBookmarks))
|
||||
@@ -328,11 +330,6 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
)
|
||||
}
|
||||
|
||||
inputPath, err := nonBasicLatinCharactersGuard(logger, inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("non-basic latin characters guard: %w", err)
|
||||
}
|
||||
|
||||
args = append(args, "--output", outputPath, inputPath)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.unoBinPath, args...)
|
||||
@@ -348,10 +345,10 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
}
|
||||
|
||||
// LibreOffice's errors are not explicit.
|
||||
// For instance, an exit code 5 may be explained by a malformed page
|
||||
// ranges, but also by a not required password.
|
||||
// For instance, exit code 5 may be explained by a malformed page range
|
||||
// but also by a not required password.
|
||||
|
||||
// We may want to retry in case of a core dumped event.
|
||||
// We may want to retry in case of a core-dumped event.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/639.
|
||||
if strings.Contains(err.Error(), "core dumped") {
|
||||
return ErrCoreDumped
|
||||
@@ -369,65 +366,6 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
return fmt.Errorf("convert to PDF: %w", err)
|
||||
}
|
||||
|
||||
// LibreOffice cannot convert a file with a name containing non-basic Latin
|
||||
// characters.
|
||||
// See:
|
||||
// https://github.com/gotenberg/gotenberg/issues/104
|
||||
// https://github.com/gotenberg/gotenberg/issues/730
|
||||
func nonBasicLatinCharactersGuard(logger *zap.Logger, inputPath string) (string, error) {
|
||||
hasNonBasicLatinChars := func(str string) bool {
|
||||
for _, r := range str {
|
||||
// Check if the character is outside basic Latin.
|
||||
if r != '.' && (r < ' ' || r > '~') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
filename := filepath.Base(inputPath)
|
||||
if !hasNonBasicLatinChars(filename) {
|
||||
logger.Debug("no non-basic latin characters in filename, skip copy")
|
||||
return inputPath, nil
|
||||
}
|
||||
|
||||
logger.Warn("non-basic latin characters in filename, copy to a file with a valid filename")
|
||||
basePath := filepath.Dir(inputPath)
|
||||
ext := filepath.Ext(inputPath)
|
||||
newInputPath := filepath.Join(basePath, fmt.Sprintf("%s%s", uuid.NewString(), ext))
|
||||
|
||||
in, err := os.Open(inputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := in.Close()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("close file: %s", err))
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := os.Create(newInputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create new file: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := out.Close()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("close new file: %s", err))
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("copy file to new file: %w", err)
|
||||
}
|
||||
|
||||
return newInputPath, nil
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Process = (*libreOfficeProcess)(nil)
|
||||
|
||||
@@ -1,699 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestLibreOfficeProcess_Start(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
libreOffice libreOffice
|
||||
expectError bool
|
||||
cleanup bool
|
||||
}{
|
||||
{
|
||||
scenario: "successful start",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
expectError: false,
|
||||
cleanup: true,
|
||||
},
|
||||
{
|
||||
scenario: "LibreOffice already started",
|
||||
libreOffice: func() libreOffice {
|
||||
p := new(libreOfficeProcess)
|
||||
p.isStarted.Store(true)
|
||||
return p
|
||||
}(),
|
||||
expectError: true,
|
||||
cleanup: false,
|
||||
},
|
||||
{
|
||||
scenario: "non-exit code 81 on first start",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: "foo",
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
expectError: true,
|
||||
cleanup: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
err := tc.libreOffice.Start(logger)
|
||||
|
||||
if tc.cleanup {
|
||||
defer func(p libreOffice, logger *zap.Logger) {
|
||||
err = p.Stop(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||
}
|
||||
}(tc.libreOffice, logger)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficeProcess_Stop(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
libreOffice libreOffice
|
||||
setup func(libreOffice libreOffice, logger *zap.Logger) error
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "successful stop",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
setup: func(p libreOffice, logger *zap.Logger) error {
|
||||
return p.Start(logger)
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "LibreOffice already stopped",
|
||||
libreOffice: func() libreOffice {
|
||||
p := new(libreOfficeProcess)
|
||||
p.isStarted.Store(false)
|
||||
return p
|
||||
}(),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
if tc.setup != nil {
|
||||
err := tc.setup(tc.libreOffice, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("setup error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := tc.libreOffice.Stop(logger)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficeProcess_Healthy(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
libreOffice libreOffice
|
||||
setup func(libreOffice libreOffice, logger *zap.Logger) error
|
||||
expectHealthy bool
|
||||
cleanup bool
|
||||
}{
|
||||
{
|
||||
scenario: "healthy LibreOffice",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
setup: func(p libreOffice, logger *zap.Logger) error {
|
||||
return p.Start(logger)
|
||||
},
|
||||
expectHealthy: true,
|
||||
cleanup: true,
|
||||
},
|
||||
{
|
||||
scenario: "LibreOffice not started",
|
||||
libreOffice: func() libreOffice {
|
||||
p := new(libreOfficeProcess)
|
||||
p.isStarted.Store(false)
|
||||
return p
|
||||
}(),
|
||||
expectHealthy: false,
|
||||
cleanup: false,
|
||||
},
|
||||
{
|
||||
scenario: "unhealthy LibreOffice",
|
||||
libreOffice: func() libreOffice {
|
||||
p := new(libreOfficeProcess)
|
||||
p.isStarted.Store(true)
|
||||
p.socketPort = 12345
|
||||
return p
|
||||
}(),
|
||||
expectHealthy: false,
|
||||
cleanup: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
if tc.setup != nil {
|
||||
err := tc.setup(tc.libreOffice, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("setup error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.cleanup {
|
||||
defer func(p libreOffice, logger *zap.Logger) {
|
||||
err := p.Stop(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||
}
|
||||
}(tc.libreOffice, logger)
|
||||
}
|
||||
|
||||
healthy := tc.libreOffice.Healthy(logger)
|
||||
|
||||
if !tc.expectHealthy && healthy {
|
||||
t.Fatal("expected unhealthy LibreOffice but got an healthy one")
|
||||
}
|
||||
|
||||
if tc.expectHealthy && !healthy {
|
||||
t.Fatal("expected a healthy LibreOffice but got an unhealthy one")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
libreOffice libreOffice
|
||||
fs *gotenberg.FileSystem
|
||||
options Options
|
||||
cancelledCtx bool
|
||||
start bool
|
||||
expectError bool
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
scenario: "LibreOffice not started",
|
||||
libreOffice: func() libreOffice {
|
||||
p := new(libreOfficeProcess)
|
||||
p.isStarted.Store(false)
|
||||
return p
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
cancelledCtx: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "ErrInvalidPdfFormats",
|
||||
libreOffice: func() libreOffice {
|
||||
p := new(libreOfficeProcess)
|
||||
p.socketPort = 12345
|
||||
p.isStarted.Store(true)
|
||||
return p
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: "foo"}},
|
||||
cancelledCtx: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
expectedError: ErrInvalidPdfFormats,
|
||||
},
|
||||
{
|
||||
scenario: "ErrUnoException",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
options: Options{PageRanges: "foo"},
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Context done"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: true,
|
||||
expectedError: ErrUnoException,
|
||||
},
|
||||
{
|
||||
scenario: "ErrRuntimeException",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
options: Options{Password: "foo"},
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
in, err := os.Open("/tests/test/testdata/libreoffice/protected.docx")
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := in.Close()
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := os.Create(fmt.Sprintf("%s/protected.docx", fs.WorkingDirPath()))
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := out.Close()
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: true,
|
||||
expectedError: ErrRuntimeException,
|
||||
},
|
||||
{
|
||||
scenario: "context done",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Context done"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
cancelledCtx: true,
|
||||
start: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success (default options)",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Success"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success (not default options)",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Success"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{
|
||||
Password: "", // Ok, the only exception in this list.
|
||||
Landscape: true,
|
||||
PageRanges: "1",
|
||||
ExportFormFields: false,
|
||||
AllowDuplicateFieldNames: true,
|
||||
ExportBookmarks: false,
|
||||
ExportBookmarksToPdfDestination: true,
|
||||
ExportPlaceholders: true,
|
||||
ExportNotes: true,
|
||||
ExportNotesPages: true,
|
||||
ExportOnlyNotesPages: true,
|
||||
ExportNotesInMargin: true,
|
||||
ConvertOooTargetToPdfTarget: true,
|
||||
ExportLinksRelativeFsys: true,
|
||||
ExportHiddenSlides: true,
|
||||
SkipEmptyPages: true,
|
||||
AddOriginalDocumentAsStream: true,
|
||||
SinglePageSheets: true,
|
||||
LosslessImageCompression: true,
|
||||
Quality: 100,
|
||||
ReduceImageResolution: true,
|
||||
MaxImageResolution: 600,
|
||||
},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success (PDF/A-1b)",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success (PDF/A-2b)",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA2b}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success (PDF/A-3b)",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA3b}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success (PDF/UA)",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfUa: true}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
// Force the debug level.
|
||||
logger := zap.NewExample()
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(tc.fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if tc.start {
|
||||
err := tc.libreOffice.Start(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("setup error: %v", err)
|
||||
}
|
||||
|
||||
defer func(p libreOffice, logger *zap.Logger) {
|
||||
err = p.Stop(logger)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||
}
|
||||
}(tc.libreOffice, logger)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(5)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if tc.cancelledCtx {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := tc.libreOffice.pdf(
|
||||
ctx,
|
||||
logger,
|
||||
fmt.Sprintf("%s/document.txt", tc.fs.WorkingDirPath()),
|
||||
fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()),
|
||||
tc.options,
|
||||
)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonBasicLatinCharactersGuard(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
fs *gotenberg.FileSystem
|
||||
filename string
|
||||
expectSameInputPath bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "basic latin characters",
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Basic latin characters"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
filename: "document.txt",
|
||||
expectSameInputPath: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "non-basic latin characters",
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/éèßàùä.txt", fs.WorkingDirPath()), []byte("Non-basic latin characters"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
filename: "éèßàùä.txt",
|
||||
expectSameInputPath: false,
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
defer func() {
|
||||
err := os.RemoveAll(tc.fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up, but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
inputPath := fmt.Sprintf("%s/%s", tc.fs.WorkingDirPath(), tc.filename)
|
||||
newInputPath, err := nonBasicLatinCharactersGuard(
|
||||
zap.NewNop(),
|
||||
inputPath,
|
||||
)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectSameInputPath && newInputPath != inputPath {
|
||||
t.Fatalf("expected same input path, but got '%s'", newInputPath)
|
||||
}
|
||||
|
||||
if !tc.expectSameInputPath && newInputPath == inputPath {
|
||||
t.Fatalf("expected different input path, but got same '%s'", newInputPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestApiMock(t *testing.T) {
|
||||
mock := &ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
||||
}
|
||||
|
||||
ext := mock.Extensions()
|
||||
if ext != nil {
|
||||
t.Errorf("expected nil result from ApiMock.Extensions, but got: %v", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderMock(t *testing.T) {
|
||||
mock := &ProviderMock{
|
||||
LibreOfficeMock: func() (Uno, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.LibreOffice()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from ProviderMock.LibreOffice, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficeMock(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
mock *libreOfficeMock
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "success",
|
||||
mock: &libreOfficeMock{
|
||||
pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "ErrCoreDumped (first call)",
|
||||
mock: &libreOfficeMock{
|
||||
pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||
return ErrCoreDumped
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "ErrCoreDumped (second call)",
|
||||
mock: func() *libreOfficeMock {
|
||||
m := &libreOfficeMock{
|
||||
pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||
return ErrCoreDumped
|
||||
},
|
||||
}
|
||||
m.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||
return m
|
||||
}(),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error from libreOfficeMock.pdf but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error from libreOfficeMock.pdf but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ func init() {
|
||||
gotenberg.MustRegisterModule(new(LibreOffice))
|
||||
}
|
||||
|
||||
// LibreOffice is a module which provides a route for converting documents to
|
||||
// LibreOffice is a module that provides a route for converting documents to
|
||||
// PDF with LibreOffice.
|
||||
type LibreOffice struct {
|
||||
api libeofficeapi.Uno
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
package libreoffice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||
)
|
||||
|
||||
func TestLibreOffice_Descriptor(t *testing.T) {
|
||||
descriptor := new(LibreOffice).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(LibreOffice))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOffice_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no LibreOffice API provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no LibreOffice API from LibreOffice API provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
libreofficeapi.ProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
libreofficeapi.ProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||
return new(libreofficeapi.ApiMock), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no PDF engine from PDF engine provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
libreofficeapi.ProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||
return new(libreofficeapi.ApiMock), nil
|
||||
}
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "provision success",
|
||||
ctx: func() *gotenberg.Context {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
libreofficeapi.ProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
}
|
||||
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||
return new(libreofficeapi.ApiMock), nil
|
||||
}
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return new(gotenberg.PdfEngineMock), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
mod.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(LibreOffice)
|
||||
err := mod.Provision(tc.ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOffice_Routes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
expectRoutes int
|
||||
disableRoutes bool
|
||||
}{
|
||||
{
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 1,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
scenario: "routes disabled",
|
||||
expectRoutes: 0,
|
||||
disableRoutes: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(LibreOffice)
|
||||
mod.disableRoutes = tc.disableRoutes
|
||||
|
||||
routes, err := mod.Routes()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectRoutes != len(routes) {
|
||||
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (engine *LibreOfficePdfEngine) Split(ctx context.Context, logger *zap.Logge
|
||||
|
||||
// Flatten is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return fmt.Errorf("Flatten PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
return fmt.Errorf("flatten PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
package pdfengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||
)
|
||||
|
||||
func TestLibreOfficePdfEngine_Descriptor(t *testing.T) {
|
||||
descriptor := new(LibreOfficePdfEngine).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(LibreOfficePdfEngine))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Provider(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no LibreOffice API provider",
|
||||
ctx: gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{},
|
||||
),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no API from LibreOffice API provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
api.ProviderMock
|
||||
}{}
|
||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||
return provider
|
||||
}}
|
||||
}
|
||||
provider.LibreOfficeMock = func() (api.Uno, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "provision success",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
api.ProviderMock
|
||||
}{}
|
||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||
return provider
|
||||
}}
|
||||
}
|
||||
provider.LibreOfficeMock = func() (api.Uno, error) {
|
||||
return new(api.ApiMock), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
err := engine.Provision(tc.ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Merge(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Split(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
_, err := engine.Split(context.Background(), zap.NewNop(), gotenberg.SplitMode{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Flatten(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
err := engine.Flatten(context.Background(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
api api.Uno
|
||||
expectError bool
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
scenario: "convert success",
|
||||
api: &api.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "ErrInvalidPdfFormats",
|
||||
api: &api.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||
return api.ErrInvalidPdfFormats
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfFormatNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "convert fail",
|
||||
api: &api.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := &LibreOfficePdfEngine{unoApi: tc.api}
|
||||
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package libreoffice
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -39,6 +38,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
password string
|
||||
landscape bool
|
||||
nativePageRanges string
|
||||
updateIndexes bool
|
||||
exportFormFields bool
|
||||
allowDuplicateFieldNames bool
|
||||
exportBookmarks bool
|
||||
@@ -68,6 +68,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
String("password", &password, defaultOptions.Password).
|
||||
Bool("landscape", &landscape, defaultOptions.Landscape).
|
||||
String("nativePageRanges", &nativePageRanges, defaultOptions.PageRanges).
|
||||
Bool("updateIndexes", &updateIndexes, defaultOptions.UpdateIndexes).
|
||||
Bool("exportFormFields", &exportFormFields, defaultOptions.ExportFormFields).
|
||||
Bool("allowDuplicateFieldNames", &allowDuplicateFieldNames, defaultOptions.AllowDuplicateFieldNames).
|
||||
Bool("exportBookmarks", &exportBookmarks, defaultOptions.ExportBookmarks).
|
||||
@@ -127,15 +128,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
}).
|
||||
Bool("nativePdfFormats", &nativePdfFormats, true).
|
||||
Bool("merge", &merge, false).
|
||||
Custom("metadata", func(value string) error {
|
||||
if len(value) > 0 {
|
||||
err := json.Unmarshal([]byte(value), &metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Bool("flatten", &flatten, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
@@ -149,6 +141,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
Password: password,
|
||||
Landscape: landscape,
|
||||
PageRanges: nativePageRanges,
|
||||
UpdateIndexes: updateIndexes,
|
||||
ExportFormFields: exportFormFields,
|
||||
AllowDuplicateFieldNames: allowDuplicateFieldNames,
|
||||
ExportBookmarks: exportBookmarks,
|
||||
@@ -171,7 +164,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
}
|
||||
|
||||
if nativePdfFormats && splitMode == zeroValuedSplitMode {
|
||||
// Only apply natively given PDF formats if we're not
|
||||
// Only natively apply given PDF formats if we're not
|
||||
// splitting the PDF later.
|
||||
options.PdfFormats = pdfFormats
|
||||
}
|
||||
|
||||
@@ -1,817 +0,0 @@
|
||||
package libreoffice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||
)
|
||||
|
||||
func TestConvertRoute(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
libreOffice libreofficeapi.Uno
|
||||
engine gotenberg.PdfEngine
|
||||
expectOptions libreofficeapi.Options
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
expectHttpStatus int
|
||||
expectOutputPathsCount int
|
||||
expectOutputPaths []string
|
||||
}{
|
||||
{
|
||||
scenario: "missing at least one mandatory file",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
}},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (not an integer)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"quality": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
}},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (< 1)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"quality": {
|
||||
"0",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
}},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid quality form field (> 100)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"quality": {
|
||||
"101",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
}},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid maxImageResolution form field (not an integer)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"maxImageResolution": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
}},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid maxImageResolution form field (not in range)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"maxImageResolution": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
}},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid metadata form field",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"metadata": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
}},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrPdfFormatNotSupported (nativePdfFormats)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return libreofficeapi.ErrInvalidPdfFormats
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrUnoException",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"nativePageRanges": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return libreofficeapi.ErrUnoException
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrRuntimeException",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"password": {
|
||||
"invalid",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return libreofficeapi.ErrRuntimeException
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from LibreOffice",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine merge error",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"merge": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine split error",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine convert error",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1b,
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine write metadata error",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine flatten error",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
ctx.SetCancelled(true)
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "cannot add output paths",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetCancelled(true)
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success (single file)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"quality": {
|
||||
"100",
|
||||
},
|
||||
"maxImageResolution": {
|
||||
"1200",
|
||||
},
|
||||
"merge": {
|
||||
"true",
|
||||
},
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1b,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success (many files)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
"document2.doc": "/document2.doc",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1b,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx", ".doc"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 3,
|
||||
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
|
||||
},
|
||||
{
|
||||
scenario: "success with native PDF/A & PDF/UA",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1b,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success with split (many files)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
return []string{
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 0,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 1,
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 4,
|
||||
expectOutputPaths: []string{"/document_docx/document.docx_0.pdf", "/document_docx/document.docx_1.pdf", "/document2_docx/document2.docx_0.pdf", "/document2_docx/document2.docx_1.pdf"},
|
||||
},
|
||||
{
|
||||
scenario: "success with merge and split",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"merge": {
|
||||
"true",
|
||||
},
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
return []string{
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 0,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 1,
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 2,
|
||||
},
|
||||
{
|
||||
scenario: "success with split and native PDF/A & PDF/UA (many files)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1b,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
return []string{
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 0,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 1,
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 4,
|
||||
expectOutputPaths: []string{"/document_docx/document.docx_0.pdf", "/document_docx/document.docx_1.pdf", "/document2_docx/document2.docx_0.pdf", "/document2_docx/document2.docx_1.pdf"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
tc.ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
tc.ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}})
|
||||
c := echo.New().NewContext(nil, nil)
|
||||
c.Set("context", tc.ctx.Context)
|
||||
|
||||
err := convertRoute(tc.libreOffice, tc.engine).Handler(c)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
var httpErr api.HttpError
|
||||
isHttpError := errors.As(err, &httpErr)
|
||||
|
||||
if tc.expectHttpError && !isHttpError {
|
||||
t.Errorf("expected an HTTP error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectHttpError && isHttpError {
|
||||
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||
}
|
||||
|
||||
if err != nil && tc.expectHttpError && isHttpError {
|
||||
status, _ := httpErr.HttpError()
|
||||
if status != tc.expectHttpStatus {
|
||||
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||
}
|
||||
|
||||
for _, path := range tc.expectOutputPaths {
|
||||
if !slices.Contains(tc.ctx.OutputPaths(), path) {
|
||||
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
49
pkg/modules/logging/color.go
Normal file
49
pkg/modules/logging/color.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Foreground colors.
|
||||
// Copy pasted from go.uber.org/zap/internal/color/color.go
|
||||
const (
|
||||
black color = iota + 30
|
||||
red
|
||||
green
|
||||
yellow
|
||||
blue
|
||||
magenta
|
||||
cyan
|
||||
white
|
||||
)
|
||||
|
||||
type color uint8
|
||||
|
||||
func (c color) Add(s string) string {
|
||||
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(c), s)
|
||||
}
|
||||
|
||||
func levelToColor(l zapcore.Level) color {
|
||||
switch l {
|
||||
case zapcore.DebugLevel:
|
||||
return cyan
|
||||
case zapcore.InfoLevel:
|
||||
return blue
|
||||
case zapcore.WarnLevel:
|
||||
return yellow
|
||||
case zapcore.ErrorLevel:
|
||||
return red
|
||||
case zapcore.DPanicLevel:
|
||||
return red
|
||||
case zapcore.PanicLevel:
|
||||
return red
|
||||
case zapcore.FatalLevel:
|
||||
return red
|
||||
case zapcore.InvalidLevel:
|
||||
return red
|
||||
default:
|
||||
return red
|
||||
}
|
||||
}
|
||||
36
pkg/modules/logging/gcp.go
Normal file
36
pkg/modules/logging/gcp.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package logging
|
||||
|
||||
import "go.uber.org/zap/zapcore"
|
||||
|
||||
func gcpSeverity(l zapcore.Level) string {
|
||||
switch l {
|
||||
case zapcore.DebugLevel:
|
||||
return "DEBUG"
|
||||
case zapcore.InfoLevel:
|
||||
return "INFO"
|
||||
case zapcore.WarnLevel:
|
||||
return "WARNING"
|
||||
case zapcore.ErrorLevel:
|
||||
return "ERROR"
|
||||
case zapcore.DPanicLevel:
|
||||
return "CRITICAL"
|
||||
case zapcore.PanicLevel:
|
||||
return "ALERT"
|
||||
case zapcore.FatalLevel:
|
||||
return "EMERGENCY"
|
||||
case zapcore.InvalidLevel:
|
||||
return "DEFAULT"
|
||||
default:
|
||||
return "DEFAULT"
|
||||
}
|
||||
}
|
||||
|
||||
func gcpSeverityEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(gcpSeverity(l))
|
||||
}
|
||||
|
||||
func gcpSeverityColorEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
||||
severity := gcpSeverity(l)
|
||||
c := levelToColor(l)
|
||||
enc.AppendString(c.Add(severity))
|
||||
}
|
||||
@@ -31,12 +31,13 @@ const (
|
||||
textLoggingFormat = "text"
|
||||
)
|
||||
|
||||
// Logging is a module which implements the [gotenberg.LoggerProvider]
|
||||
// Logging is a module that implements the [gotenberg.LoggerProvider]
|
||||
// interface.
|
||||
type Logging struct {
|
||||
level string
|
||||
format string
|
||||
fieldsPrefix string
|
||||
level string
|
||||
format string
|
||||
fieldsPrefix string
|
||||
enableGcpFields bool
|
||||
}
|
||||
|
||||
// Descriptor returns a [Logging]'s module descriptor.
|
||||
@@ -48,6 +49,14 @@ func (log *Logging) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.String("log-level", infoLoggingLevel, fmt.Sprintf("Choose the level of logging detail. Options include %s, %s, %s, or %s", errorLoggingLevel, warnLoggingLevel, infoLoggingLevel, debugLoggingLevel))
|
||||
fs.String("log-format", autoLoggingFormat, fmt.Sprintf("Specify the format of logging. Options include %s, %s, or %s", autoLoggingFormat, jsonLoggingFormat, textLoggingFormat))
|
||||
fs.String("log-fields-prefix", "", "Prepend a specified prefix to each field in the logs")
|
||||
fs.Bool("log-enable-gcp-fields", false, "Enable Google Cloud Platform fields - namely: time, message, severity")
|
||||
|
||||
// Deprecated flags.
|
||||
fs.Bool("log-enable-gcp-severity", false, "Enable Google Cloud Platform severity mapping")
|
||||
err := fs.MarkDeprecated("log-enable-gcp-severity", "use log-enable-gcp-fields instead")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
@@ -62,6 +71,7 @@ func (log *Logging) Provision(ctx *gotenberg.Context) error {
|
||||
log.level = flags.MustString("log-level")
|
||||
log.format = flags.MustString("log-format")
|
||||
log.fieldsPrefix = flags.MustString("log-fields-prefix")
|
||||
log.enableGcpFields = flags.MustDeprecatedBool("log-enable-gcp-severity", "log-enable-gcp-fields")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -101,7 +111,7 @@ func (log *Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return nil, fmt.Errorf("get log level: %w", err)
|
||||
}
|
||||
|
||||
encoder, err := newLogEncoder(log.format)
|
||||
encoder, err := newLogEncoder(log.format, log.enableGcpFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get log encoder: %w", err)
|
||||
}
|
||||
@@ -166,26 +176,44 @@ func newLogLevel(level string) (zapcore.Level, error) {
|
||||
return lvl, nil
|
||||
}
|
||||
|
||||
func newLogEncoder(format string) (zapcore.Encoder, error) {
|
||||
func newLogEncoder(format string, gcpFields bool) (zapcore.Encoder, error) {
|
||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
encCfg := zap.NewProductionEncoderConfig()
|
||||
|
||||
// Normalize the log format based on the output device.
|
||||
if format == autoLoggingFormat {
|
||||
if isTerminal {
|
||||
format = textLoggingFormat
|
||||
} else {
|
||||
format = jsonLoggingFormat
|
||||
}
|
||||
}
|
||||
|
||||
// Use a human-readable time format if running in a terminal.
|
||||
if isTerminal {
|
||||
// If interactive terminal, make output more human-readable by default.
|
||||
// Credits: https://github.com/caddyserver/caddy/blob/v2.1.1/logging.go#L671.
|
||||
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
|
||||
encoder.AppendString(ts.Local().Format("2006/01/02 15:04:05.000"))
|
||||
}
|
||||
}
|
||||
|
||||
if format == textLoggingFormat || format == autoLoggingFormat {
|
||||
// Configure level encoding based on format and GCP settings.
|
||||
if format == textLoggingFormat && isTerminal {
|
||||
if gcpFields {
|
||||
encCfg.EncodeLevel = gcpSeverityColorEncoder
|
||||
} else {
|
||||
encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
}
|
||||
}
|
||||
|
||||
if format == autoLoggingFormat && isTerminal {
|
||||
format = textLoggingFormat
|
||||
} else if format == autoLoggingFormat {
|
||||
format = jsonLoggingFormat
|
||||
// For non-text (JSON) or when GCP fields are requested outside a terminal text output,
|
||||
// adjust the configuration to use GCP-specific field names and encoders.
|
||||
if gcpFields && format != textLoggingFormat {
|
||||
encCfg.EncodeLevel = gcpSeverityEncoder
|
||||
encCfg.TimeKey = "time"
|
||||
encCfg.LevelKey = "severity"
|
||||
encCfg.MessageKey = "message"
|
||||
encCfg.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encCfg.EncodeDuration = zapcore.MillisDurationEncoder
|
||||
}
|
||||
|
||||
switch format {
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestLogging_Descriptor(t *testing.T) {
|
||||
descriptor := new(Logging).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(Logging))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogging_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
level string
|
||||
format string
|
||||
fieldsPrefix string
|
||||
expectLevel string
|
||||
expectFormat string
|
||||
expectFieldsPrefix string
|
||||
}{
|
||||
{
|
||||
scenario: "default values",
|
||||
expectLevel: infoLoggingLevel,
|
||||
expectFormat: autoLoggingFormat,
|
||||
expectFieldsPrefix: "",
|
||||
},
|
||||
{
|
||||
scenario: "explicit values",
|
||||
level: "debug",
|
||||
format: "json",
|
||||
fieldsPrefix: "gotenberg",
|
||||
expectLevel: debugLoggingLevel,
|
||||
expectFormat: jsonLoggingFormat,
|
||||
expectFieldsPrefix: "gotenberg",
|
||||
},
|
||||
{
|
||||
scenario: "wrong values", // no validation at this point.
|
||||
level: "foo",
|
||||
format: "foo",
|
||||
expectLevel: "foo",
|
||||
expectFormat: "foo",
|
||||
expectFieldsPrefix: "",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
var flags []string
|
||||
|
||||
if tc.level != "" {
|
||||
flags = append(flags, "--log-level", tc.level)
|
||||
}
|
||||
|
||||
if tc.format != "" {
|
||||
flags = append(flags, "--log-format", tc.format)
|
||||
}
|
||||
|
||||
if tc.fieldsPrefix != "" {
|
||||
flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
|
||||
}
|
||||
|
||||
logging := new(Logging)
|
||||
fs := logging.Descriptor().FlagSet
|
||||
|
||||
err := fs.Parse(flags)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while parsing flags but got: %v", err)
|
||||
}
|
||||
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
|
||||
|
||||
err = logging.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while provisioning but got: %v", err)
|
||||
}
|
||||
|
||||
if logging.level != tc.expectLevel {
|
||||
t.Errorf("expected logging level '%s' but got '%s'", tc.expectLevel, logging.level)
|
||||
}
|
||||
|
||||
if logging.format != tc.expectFormat {
|
||||
t.Errorf("expected logging format '%s' but got '%s'", tc.expectFormat, logging.format)
|
||||
}
|
||||
|
||||
if logging.fieldsPrefix != tc.expectFieldsPrefix {
|
||||
t.Errorf("expected logging fields prefix '%s' but got '%s'", tc.expectFieldsPrefix, logging.fieldsPrefix)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogging_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
level string
|
||||
format string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format",
|
||||
level: debugLoggingLevel,
|
||||
format: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid level and format",
|
||||
level: debugLoggingLevel,
|
||||
format: autoLoggingFormat,
|
||||
},
|
||||
} {
|
||||
logging := new(Logging)
|
||||
logging.level = tc.level
|
||||
logging.format = tc.format
|
||||
|
||||
err := logging.Validate()
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Errorf("%s: expected error but got: %v", tc.scenario, err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogging_Logger(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
level string
|
||||
format string
|
||||
fieldsPrefix string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format",
|
||||
level: debugLoggingLevel,
|
||||
format: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid level and format",
|
||||
level: debugLoggingLevel,
|
||||
format: autoLoggingFormat,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
logging := new(Logging)
|
||||
logging.level = tc.level
|
||||
logging.format = tc.format
|
||||
logging.fieldsPrefix = tc.fieldsPrefix
|
||||
|
||||
_, err := logging.Logger(&gotenberg.ModuleMock{
|
||||
DescriptorMock: func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "mock", New: nil}
|
||||
},
|
||||
})
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomCore(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
level zapcore.Level
|
||||
fieldsPrefix string
|
||||
expectEntry bool
|
||||
}{
|
||||
{
|
||||
scenario: "level enabled",
|
||||
level: zapcore.DebugLevel,
|
||||
fieldsPrefix: "gotenberg",
|
||||
expectEntry: true,
|
||||
},
|
||||
{
|
||||
scenario: "no fields prefix",
|
||||
level: zapcore.DebugLevel,
|
||||
expectEntry: true,
|
||||
},
|
||||
{
|
||||
scenario: "level disabled",
|
||||
level: zapcore.ErrorLevel,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
core, obsvr := observer.New(tc.level)
|
||||
lgr := zap.New(customCore{
|
||||
Core: core,
|
||||
fieldsPrefix: tc.fieldsPrefix,
|
||||
}).With(zap.String("a_field", "a value"))
|
||||
|
||||
lgr.Debug("a debug message", zap.String("another_field", "another value"))
|
||||
|
||||
entries := obsvr.TakeAll()
|
||||
|
||||
if tc.expectEntry && len(entries) == 0 {
|
||||
t.Fatal("expected an entry")
|
||||
}
|
||||
|
||||
if !tc.expectEntry && len(entries) != 0 {
|
||||
t.Fatal("expected no entry")
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if tc.fieldsPrefix != "" {
|
||||
prefix = tc.fieldsPrefix + "_"
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
fields := entry.Context
|
||||
|
||||
if len(fields) != 2 {
|
||||
t.Fatalf("expected 2 fields but got %d", len(fields))
|
||||
}
|
||||
|
||||
if fields[0].Key != fmt.Sprintf("%sa_field", prefix) {
|
||||
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key)
|
||||
}
|
||||
|
||||
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) {
|
||||
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newLogLevel(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
level string
|
||||
expectZapLevel zapcore.Level
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "error level",
|
||||
level: errorLoggingLevel,
|
||||
expectZapLevel: zapcore.ErrorLevel,
|
||||
},
|
||||
{
|
||||
scenario: "warning level",
|
||||
level: warnLoggingLevel,
|
||||
expectZapLevel: zapcore.WarnLevel,
|
||||
},
|
||||
{
|
||||
scenario: "info level",
|
||||
level: infoLoggingLevel,
|
||||
expectZapLevel: zapcore.InfoLevel,
|
||||
},
|
||||
{
|
||||
scenario: "debug level",
|
||||
level: debugLoggingLevel,
|
||||
expectZapLevel: zapcore.DebugLevel,
|
||||
},
|
||||
{
|
||||
scenario: "invalid level",
|
||||
level: "foo",
|
||||
expectZapLevel: zapcore.InvalidLevel,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
actual, err := newLogLevel(tc.level)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectZapLevel != actual {
|
||||
t.Errorf("expected %d level but got %d", tc.expectZapLevel, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newLogEncoder(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
format string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "auto format",
|
||||
format: autoLoggingFormat,
|
||||
},
|
||||
{
|
||||
scenario: "text format",
|
||||
format: textLoggingFormat,
|
||||
},
|
||||
{
|
||||
scenario: "json format",
|
||||
format: jsonLoggingFormat,
|
||||
},
|
||||
{
|
||||
scenario: "invalid format",
|
||||
format: "foo",
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
_, err := newLogEncoder(tc.format)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -52,6 +56,32 @@ func (engine *PdfCpu) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *PdfCpu) Debug() map[string]interface{} {
|
||||
debug := make(map[string]interface{})
|
||||
|
||||
cmd := exec.Command(engine.binPath, "version") //nolint:gosec
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
debug["version"] = err.Error()
|
||||
return debug
|
||||
}
|
||||
|
||||
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:"))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return debug
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -99,11 +129,25 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenb
|
||||
return nil, fmt.Errorf("split PDFs with pdfcpu: %w", err)
|
||||
}
|
||||
|
||||
outputPaths, err := gotenberg.WalkDir(outputDirPath, ".pdf")
|
||||
var outputPaths []string
|
||||
err = filepath.Walk(outputDirPath, func(path string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(filepath.Ext(info.Name()), ".pdf") {
|
||||
outputPaths = append(outputPaths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("walk directory to find resulting PDFs from split with pdfcpu: %w", err)
|
||||
}
|
||||
|
||||
sort.Sort(digitSuffixSort(outputPaths))
|
||||
|
||||
return outputPaths, nil
|
||||
}
|
||||
|
||||
@@ -132,5 +176,6 @@ var (
|
||||
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||
_ gotenberg.Provisioner = (*PdfCpu)(nil)
|
||||
_ gotenberg.Validator = (*PdfCpu)(nil)
|
||||
_ gotenberg.Debuggable = (*PdfCpu)(nil)
|
||||
_ gotenberg.PdfEngine = (*PdfCpu)(nil)
|
||||
)
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
package pdfcpu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestPdfCpu_Descriptor(t *testing.T) {
|
||||
descriptor := new(PdfCpu).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(PdfCpu))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Provision(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||
|
||||
err := engine.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "empty bin path",
|
||||
binPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "bin path does not exist",
|
||||
binPath: "/foo",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "validate success",
|
||||
binPath: os.Getenv("PDFTK_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
engine.binPath = tc.binPath
|
||||
err := engine.Validate()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
inputPaths []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "single file success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "many files success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Split(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
mode gotenberg.SplitMode
|
||||
inputPath string
|
||||
expectError bool
|
||||
expectedError error
|
||||
expectOutputPathsCount int
|
||||
}{
|
||||
{
|
||||
scenario: "ErrPdfSplitModeNotSupported",
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
inputPath: "",
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success (intervals)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 3,
|
||||
},
|
||||
{
|
||||
scenario: "success (pages)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1"},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success (pages & unify)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
outputPaths, err := engine.Split(tc.ctx, zap.NewNop(), tc.mode, tc.inputPath, outputDir)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(outputPaths) {
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(outputPaths))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Flatten(t *testing.T) {
|
||||
mod := new(PdfCpu)
|
||||
err := mod.Flatten(context.TODO(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Convert(t *testing.T) {
|
||||
mod := new(PdfCpu)
|
||||
err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
_, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
68
pkg/modules/pdfcpu/sort.go
Normal file
68
pkg/modules/pdfcpu/sort.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package pdfcpu
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type digitSuffixSort []string
|
||||
|
||||
func (s digitSuffixSort) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s digitSuffixSort) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s digitSuffixSort) Less(i, j int) bool {
|
||||
numI, restI := extractNumber(s[i])
|
||||
numJ, restJ := extractNumber(s[j])
|
||||
|
||||
// If both strings contain a number, compare them numerically.
|
||||
if numI != -1 && numJ != -1 {
|
||||
if numI != numJ {
|
||||
return numI < numJ
|
||||
}
|
||||
// If the numbers are equal, compare the "rest" strings.
|
||||
return restI < restJ
|
||||
}
|
||||
|
||||
// If one contains a number and the other doesn't, the one with the number
|
||||
// comes first.
|
||||
if numI != -1 {
|
||||
return true
|
||||
}
|
||||
if numJ != -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Neither has a number; fall back to lexicographical order.
|
||||
return s[i] < s[j]
|
||||
}
|
||||
|
||||
func extractNumber(str string) (int, string) {
|
||||
str = filepath.Base(str)
|
||||
|
||||
// Check for a number immediately before an extension.
|
||||
if matches := extensionSuffixRegexp.FindStringSubmatch(str); len(matches) > 3 {
|
||||
if num, err := strconv.Atoi(matches[2]); err == nil {
|
||||
// Remove the numeric block but keep the extension.
|
||||
return num, matches[1] + matches[3]
|
||||
}
|
||||
}
|
||||
|
||||
// No numeric portion found.
|
||||
return -1, str
|
||||
}
|
||||
|
||||
// Regular expressions used by extractNumber.
|
||||
var (
|
||||
// Matches a numeric block immediately before a file extension.
|
||||
extensionSuffixRegexp = regexp.MustCompile(`^(.*?)(\d+)(\.[^.]+)$`)
|
||||
)
|
||||
|
||||
// Interface guard.
|
||||
var _ sort.Interface = (*digitSuffixSort)(nil)
|
||||
29
pkg/modules/pdfcpu/sort_test.go
Normal file
29
pkg/modules/pdfcpu/sort_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package pdfcpu
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDigitSuffixSort(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
values []string
|
||||
expectedSort []string
|
||||
}{
|
||||
{
|
||||
scenario: "UUIDs with digit suffixes",
|
||||
values: []string{"2521a33d-1fb4-4279-80fe-8a945285b8f4_12.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_1.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_10.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_3.pdf"},
|
||||
expectedSort: []string{"2521a33d-1fb4-4279-80fe-8a945285b8f4_1.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_3.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_10.pdf", "2521a33d-1fb4-4279-80fe-8a945285b8f4_12.pdf"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
sort.Sort(digitSuffixSort(tc.values))
|
||||
|
||||
if !reflect.DeepEqual(tc.values, tc.expectedSort) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedSort, tc.values)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -74,16 +74,14 @@ func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mod
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
resultChan := make(chan splitResult, len(multi.splitEngines))
|
||||
|
||||
for _, engine := range multi.splitEngines {
|
||||
resultChan := make(chan splitResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
outputPaths, err := engine.Split(ctx, logger, mode, inputPath, outputDirPath)
|
||||
resultChan <- splitResult{outputPaths: outputPaths, err: err}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
for range multi.splitEngines {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
@@ -126,7 +124,7 @@ func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, i
|
||||
return fmt.Errorf("flatten PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format. thanks to its
|
||||
// Convert converts the given PDF to a specific PDF format, thanks to its
|
||||
// children. If the context is done, it stops and returns an error.
|
||||
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
var err error
|
||||
@@ -160,16 +158,14 @@ func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logg
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
resultChan := make(chan readMetadataResult, len(multi.readMetadataEngines))
|
||||
|
||||
for _, engine := range multi.readMetadataEngines {
|
||||
resultChan := make(chan readMetadataResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
|
||||
resultChan <- readMetadataResult{metadata: metadata, err: err}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
for range multi.readMetadataEngines {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
|
||||
@@ -51,6 +51,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
||||
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
||||
|
||||
// Deprecated flags.
|
||||
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the default PDF engines and their default order - all by default")
|
||||
err := fs.MarkDeprecated("pdfengines-engines", "use other flags for a more granular selection of PDF engines per method")
|
||||
if err != nil {
|
||||
@@ -91,7 +92,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
defaultNames[i] = engine.(gotenberg.Module).Descriptor().ID
|
||||
}
|
||||
|
||||
// Example in case of deprecated module name.
|
||||
// Example in the case of deprecated module name.
|
||||
//for i, name := range defaultNames {
|
||||
// if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
||||
// logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
package pdfengines
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestPdfEngines_Descriptor(t *testing.T) {
|
||||
descriptor := new(PdfEngines).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(PdfEngines))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfEngines_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectedMergePdfEngines []string
|
||||
expectedSplitPdfEngines []string
|
||||
expectedFlattenPdfEngines []string
|
||||
expectedConvertPdfEngines []string
|
||||
expectedReadMetadataPdfEngines []string
|
||||
expectedWriteMetadataPdfEngines []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no selection from user",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
}{}
|
||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||
return provider
|
||||
}}
|
||||
}
|
||||
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||
}
|
||||
engine.ValidateMock = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
engine.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"},
|
||||
expectedSplitPdfEngines: []string{"pdfcpu", "qpdf", "pdftk"},
|
||||
expectedFlattenPdfEngines: []string{"qpdf"},
|
||||
expectedConvertPdfEngines: []string{"libreoffice-pdfengine"},
|
||||
expectedReadMetadataPdfEngines: []string{"exiftool"},
|
||||
expectedWriteMetadataPdfEngines: []string{"exiftool"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "selection from user",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
}{}
|
||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||
return provider
|
||||
}}
|
||||
}
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
||||
}
|
||||
engine1.ValidateMock = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
||||
}
|
||||
engine2.ValidateMock = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fs := new(PdfEngines).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-split-engines=a", "--pdfengines-flatten-engines=c", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: fs,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
engine1.Descriptor(),
|
||||
engine2.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
|
||||
expectedMergePdfEngines: []string{"b"},
|
||||
expectedSplitPdfEngines: []string{"a"},
|
||||
expectedFlattenPdfEngines: []string{"c"},
|
||||
expectedConvertPdfEngines: []string{"b"},
|
||||
expectedReadMetadataPdfEngines: []string{"a"},
|
||||
expectedWriteMetadataPdfEngines: []string{"a"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "no valid PDF engine",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
}{}
|
||||
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||
return provider
|
||||
}}
|
||||
}
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||
}
|
||||
engine.ValidateMock = func() error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
engine.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
err := mod.Provision(tc.ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if len(tc.expectedMergePdfEngines) != len(mod.mergeNames) {
|
||||
t.Fatalf("expected %d merge names but got %d", len(tc.expectedMergePdfEngines), len(mod.mergeNames))
|
||||
}
|
||||
|
||||
if len(tc.expectedFlattenPdfEngines) != len(mod.flattenNames) {
|
||||
t.Fatalf("expected %d flatten names but got %d", len(tc.expectedFlattenPdfEngines), len(mod.flattenNames))
|
||||
}
|
||||
|
||||
if len(tc.expectedConvertPdfEngines) != len(mod.convertNames) {
|
||||
t.Fatalf("expected %d convert names but got %d", len(tc.expectedConvertPdfEngines), len(mod.convertNames))
|
||||
}
|
||||
|
||||
if len(tc.expectedReadMetadataPdfEngines) != len(mod.readMetadataNames) {
|
||||
t.Fatalf("expected %d read metadata names but got %d", len(tc.expectedReadMetadataPdfEngines), len(mod.readMetadataNames))
|
||||
}
|
||||
|
||||
if len(tc.expectedWriteMetadataPdfEngines) != len(mod.writeMetadataNames) {
|
||||
t.Fatalf("expected %d write metadata names but got %d", len(tc.expectedWriteMetadataPdfEngines), len(mod.writeMetadataNames))
|
||||
}
|
||||
|
||||
for index, name := range mod.mergeNames {
|
||||
if name != tc.expectedMergePdfEngines[index] {
|
||||
t.Fatalf("expected merge name at index %d to be %s, but got: %s", index, name, tc.expectedMergePdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.splitNames {
|
||||
if name != tc.expectedSplitPdfEngines[index] {
|
||||
t.Fatalf("expected split name at index %d to be %s, but got: %s", index, name, tc.expectedSplitPdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.convertNames {
|
||||
if name != tc.expectedConvertPdfEngines[index] {
|
||||
t.Fatalf("expected convert name at index %d to be %s, but got: %s", index, name, tc.expectedConvertPdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.readMetadataNames {
|
||||
if name != tc.expectedReadMetadataPdfEngines[index] {
|
||||
t.Fatalf("expected read metadata name at index %d to be %s, but got: %s", index, name, tc.expectedReadMetadataPdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.writeMetadataNames {
|
||||
if name != tc.expectedWriteMetadataPdfEngines[index] {
|
||||
t.Fatalf("expected write metadat name at index %d to be %s, but got: %s", index, name, tc.expectedWriteMetadataPdfEngines[index])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfEngines_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
names []string
|
||||
engines []gotenberg.PdfEngine
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "existing PDF engine",
|
||||
names: []string{"foo"},
|
||||
engines: func() []gotenberg.PdfEngine {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
||||
}
|
||||
|
||||
return []gotenberg.PdfEngine{
|
||||
engine,
|
||||
}
|
||||
}(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "non-existing bar PDF engine",
|
||||
names: []string{"foo", "bar", "baz"},
|
||||
engines: func() []gotenberg.PdfEngine {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||
}
|
||||
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
|
||||
}
|
||||
|
||||
return []gotenberg.PdfEngine{
|
||||
engine1,
|
||||
engine2,
|
||||
}
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "no PDF engine",
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
mergeNames: tc.names,
|
||||
convertNames: tc.names,
|
||||
readMetadataNames: tc.names,
|
||||
writeMetadataNames: tc.names,
|
||||
engines: tc.engines,
|
||||
}
|
||||
|
||||
err := mod.Validate()
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
mod.mergeNames = []string{"foo", "bar"}
|
||||
mod.splitNames = []string{"foo", "bar"}
|
||||
mod.convertNames = []string{"foo", "bar"}
|
||||
mod.readMetadataNames = []string{"foo", "bar"}
|
||||
mod.writeMetadataNames = []string{"foo", "bar"}
|
||||
|
||||
expectedMessages := 6
|
||||
messages := mod.SystemMessages()
|
||||
if len(messages) != expectedMessages {
|
||||
t.Errorf("expected %d message(s), but got %d", expectedMessages, len(messages))
|
||||
}
|
||||
|
||||
expect := []string{
|
||||
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
||||
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")),
|
||||
fmt.Sprintf("flatten engines - %s", strings.Join(mod.flattenNames[:], " ")),
|
||||
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
||||
}
|
||||
|
||||
for i, message := range messages {
|
||||
if message != expect[i] {
|
||||
t.Errorf("expected message at index %d to be %s, but got %s", i, message, expect[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfEngines_PdfEngine(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
mergeNames: []string{"foo", "bar"},
|
||||
splitNames: []string{"foo", "bar"},
|
||||
convertNames: []string{"foo", "bar"},
|
||||
readMetadataNames: []string{"foo", "bar"},
|
||||
writeMetadataNames: []string{"foo", "bar"},
|
||||
engines: func() []gotenberg.PdfEngine {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||
}
|
||||
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
||||
}
|
||||
|
||||
return []gotenberg.PdfEngine{
|
||||
engine1,
|
||||
engine2,
|
||||
}
|
||||
}(),
|
||||
}
|
||||
|
||||
_, err := mod.PdfEngine()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfEngines_Routes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
expectRoutes int
|
||||
disableRoutes bool
|
||||
}{
|
||||
{
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 6,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
scenario: "routes disabled",
|
||||
expectRoutes: 0,
|
||||
disableRoutes: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
mod.disableRoutes = tc.disableRoutes
|
||||
|
||||
routes, err := mod.Routes()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectRoutes != len(routes) {
|
||||
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -500,6 +500,11 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
|
||||
err = c.JSON(http.StatusOK, res)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "request method or response status code does not allow body") {
|
||||
// High probability that the user is using the webhook
|
||||
// feature. It does not make sense for this route.
|
||||
return api.ErrNoOutputFile
|
||||
}
|
||||
return fmt.Errorf("return JSON response: %w", err)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user