mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a6a97cf07 | ||
|
|
7676d7a239 | ||
|
|
679715f896 | ||
|
|
d5aa880336 | ||
|
|
08daa86766 | ||
|
|
f0c1952dfe | ||
|
|
198d9115cc | ||
|
|
6b0c01bd3e | ||
|
|
a1848d3c75 | ||
|
|
a1596f7c62 | ||
|
|
740e701ad3 | ||
|
|
71d571a25c | ||
|
|
2293300182 | ||
|
|
85c7f2806f | ||
|
|
27ecb4fe49 | ||
|
|
7afeefe84f | ||
|
|
651922194f | ||
|
|
1d1b4fb04d | ||
|
|
b418f1eb05 | ||
|
|
a9b44ee39e | ||
|
|
cb48355898 | ||
|
|
6ce47e3a20 | ||
|
|
09e511bc49 | ||
|
|
66ade27383 | ||
|
|
d96058d589 | ||
|
|
8631a15933 | ||
|
|
c0bf81ad7c | ||
|
|
8fd1fa203e | ||
|
|
51b907bcf5 | ||
|
|
ecb5d974c5 | ||
|
|
19c91aff19 | ||
|
|
8e0cc7dd2f | ||
|
|
6478528ebc | ||
|
|
3e3b24f611 | ||
|
|
6dc7d70a1f | ||
|
|
a6f4c5651c | ||
|
|
291a4d39fe | ||
|
|
e639a1dd73 | ||
|
|
e3a961623b | ||
|
|
1ca27fdba2 | ||
|
|
f72428056a | ||
|
|
bdb3f11ac7 | ||
|
|
2f69af1a52 | ||
|
|
984cf77ee0 | ||
|
|
1762596fce | ||
|
|
910eb9b770 |
14
.env
Normal file
14
.env
Normal file
@@ -0,0 +1,14 @@
|
||||
GOLANG_VERSION=1.23
|
||||
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='.'
|
||||
67
.github/actions/build-push/action.yml
vendored
Normal file
67
.github/actions/build-push/action.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Build and Push
|
||||
description: Build and push Docker images
|
||||
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
|
||||
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_push.outputs.tags }}
|
||||
tags_cloud_run:
|
||||
description: Comma separated list of Cloud Run tags (linux/amd64 only)
|
||||
value: ${{ steps.build_push.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
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ inputs.docker_hub_username }}
|
||||
password: ${{ inputs.docker_hub_password }}
|
||||
|
||||
- name: Build and push ${{ inputs.platform }}
|
||||
id: build_push
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/build-push/build-push.sh \
|
||||
--version "${{ inputs.version }}" \
|
||||
--platform "${{ inputs.platform }}" \
|
||||
--alternate-repository "${{ inputs.alternate_repository }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
run: |
|
||||
echo "tags=${{ steps.build_push.outputs.tags }}"
|
||||
echo "tags_cloud_run=${{ steps.build_push.outputs.tags_cloud_run }}"
|
||||
173
.github/actions/build-push/build-push.sh
vendored
Executable file
173
.github/actions/build-push/build-push.sh
vendored
Executable file
@@ -0,0 +1,173 @@
|
||||
#!/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 push 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 and push 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 "$*"
|
||||
}
|
||||
|
||||
cmd="docker buildx build \
|
||||
--build-arg GOLANG_VERSION=$GOLANG_VERSION \
|
||||
--build-arg GOTENBERG_VERSION=$version \
|
||||
--build-arg GOTENBERG_USER_GID=$GOTENBERG_USER_GID \
|
||||
--build-arg GOTENBERG_USER_UID=$GOTENBERG_USER_UID \
|
||||
--build-arg NOTO_COLOR_EMOJI_VERSION=$NOTO_COLOR_EMOJI_VERSION \
|
||||
--build-arg PDFTK_VERSION=$PDFTK_VERSION \
|
||||
--build-arg PDFCPU_VERSION=$PDFCPU_VERSION \
|
||||
--push \
|
||||
--platform $platform \
|
||||
${tags_flags[*]} \
|
||||
-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
|
||||
|
||||
source_tag_cloud_run="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-${arch[1]}"
|
||||
cmd="docker pull $source_tag_cloud_run"
|
||||
run_cmd "$cmd"
|
||||
|
||||
target_tag_cloud_run="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version"
|
||||
cmd="docker image tag $source_tag_cloud_run $target_tag_cloud_run"
|
||||
run_cmd "$cmd"
|
||||
|
||||
cmd="docker build \
|
||||
--build-arg DOCKER_REGISTRY=$DOCKER_REGISTRY \
|
||||
--build-arg DOCKER_REPOSITORY=$DOCKER_REPOSITORY \
|
||||
--build-arg GOTENBERG_VERSION=$version \
|
||||
--push \
|
||||
${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
|
||||
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 }}"
|
||||
109
.github/actions/merge/merge.sh
vendored
Executable file
109
.github/actions/merge/merge.sh
vendored
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/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
|
||||
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -1,8 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "weekly"
|
||||
114
.github/workflows/continuous-delivery.yml
vendored
Normal file
114
.github/workflows/continuous-delivery.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Continuous Delivery
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release_amd64:
|
||||
name: Release linux/amd64
|
||||
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-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/amd64
|
||||
|
||||
release_386:
|
||||
name: Release linux/386
|
||||
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-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/386
|
||||
|
||||
release_arm64:
|
||||
name: Release linux/arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
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-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/arm64
|
||||
|
||||
release_arm_v7:
|
||||
name: Release linux/arm/v7
|
||||
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-push
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
platform: linux/arm/v7
|
||||
|
||||
merge_clean_release_tags:
|
||||
needs:
|
||||
- release_amd64
|
||||
- release_386
|
||||
- release_arm64
|
||||
- release_arm_v7
|
||||
name: Merge and clean release 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.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 }}"
|
||||
295
.github/workflows/continuous-integration.yml
vendored
Normal file
295
.github/workflows/continuous-integration.yml
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ (github.event_name == 'pull_request' && github.event.pull_request.number) || 'main' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23"
|
||||
cache: false
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.63.4
|
||||
|
||||
tests:
|
||||
needs:
|
||||
- lint
|
||||
name: Tests
|
||||
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: Run tests
|
||||
run: make tests-once
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
|
||||
snapshot_amd64:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
name: Snapshot linux/amd64
|
||||
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-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
|
||||
name: Snapshot linux/386
|
||||
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-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
|
||||
name: Snapshot linux/arm64
|
||||
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-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
|
||||
name: Snapshot linux/arm/v7
|
||||
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-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_tags:
|
||||
needs:
|
||||
- snapshot_amd64
|
||||
- snapshot_386
|
||||
- snapshot_arm64
|
||||
- snapshot_arm_v7
|
||||
name: Merge and clean snapshot 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
|
||||
name: Edge linux/amd64
|
||||
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-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
|
||||
name: Edge linux/386
|
||||
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-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
|
||||
name: Edge linux/arm64
|
||||
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-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
|
||||
name: Edge linux/arm/v7
|
||||
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-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:
|
||||
- edge_amd64
|
||||
- edge_386
|
||||
- edge_arm64
|
||||
- edge_arm_v7
|
||||
name: Merge and clean edge 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.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 }}"
|
||||
26
.github/workflows/continuous_delivery.yml
vendored
26
.github/workflows/continuous_delivery.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Continuous Delivery
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release Docker image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Log in to Docker Hub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push Docker image for release
|
||||
run: |
|
||||
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }}
|
||||
make release GOTENBERG_VERSION=${{ github.event.release.tag_name }} DOCKER_REGISTRY=thecodingmachine
|
||||
95
.github/workflows/continuous_integration.yml
vendored
95
.github/workflows/continuous_integration.yml
vendored
@@ -1,95 +0,0 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
cache: false
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.61.0
|
||||
|
||||
tests:
|
||||
needs:
|
||||
- lint
|
||||
name: Tests
|
||||
# TODO: once arm64 actions are available, also run the tests on this architecture.
|
||||
# See: https://github.com/actions/virtual-environments/issues/2552#issuecomment-771478000.
|
||||
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: Run tests
|
||||
run: make tests-once
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
|
||||
snapshot_release:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
name: Snapshot release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Log in to Docker Hub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push snapshot Docker image (linux/amd64)
|
||||
run: make release GOTENBERG_VERSION=${{ github.head_ref }} DOCKER_REPOSITORY=snapshot LINUX_AMD64_RELEASE=true
|
||||
|
||||
edge_release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
name: Edge release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Log in to Docker Hub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push Docker image for main branch
|
||||
run: |
|
||||
make release GOTENBERG_VERSION=edge
|
||||
make release GOTENBERG_VERSION=edge DOCKER_REGISTRY=thecodingmachine
|
||||
23
.github/workflows/pull-request-cleanup.yml
vendored
Normal file
23
.github/workflows/pull-request-cleanup.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Pull Request Cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Cleanup Docker images
|
||||
runs-on: ubuntu-latest
|
||||
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 }}
|
||||
@@ -6,10 +6,6 @@ linters-settings:
|
||||
- prefix(github.com/gotenberg/gotenberg/v8)
|
||||
skip-generated: true
|
||||
custom-order: true
|
||||
# Until https://github.com/securego/gosec/issues/1187 is resolved.
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Julien Neuhart
|
||||
Copyright (c) Julien Neuhart
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
40
Makefile
40
Makefile
@@ -1,3 +1,5 @@
|
||||
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}'
|
||||
@@ -5,17 +7,6 @@ help: ## Show the help
|
||||
.PHONY: it
|
||||
it: build build-tests ## Initialize the development environment
|
||||
|
||||
GOLANG_VERSION=1.23
|
||||
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.61.0 # See https://github.com/golangci/golangci-lint/releases.
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the Gotenberg's Docker image
|
||||
docker build \
|
||||
@@ -27,7 +18,7 @@ build: ## Build the Gotenberg's Docker image
|
||||
--build-arg PDFTK_VERSION=$(PDFTK_VERSION) \
|
||||
--build-arg PDFCPU_VERSION=$(PDFCPU_VERSION) \
|
||||
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||
-f build/Dockerfile .
|
||||
-f $(DOCKERFILE) $(DOCKER_BUILD_CONTEXT)
|
||||
|
||||
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
||||
API_PORT=3000
|
||||
@@ -36,7 +27,7 @@ API_BIND_IP=
|
||||
API_START_TIMEOUT=30s
|
||||
API_TIMEOUT=30s
|
||||
API_BODY_LIMIT=
|
||||
API_ROOT_PATH=/
|
||||
API_ROOT_PATH="/"
|
||||
API_TRACE_HEADER=Gotenberg-Trace
|
||||
API_ENABLE_BASIC_AUTH=false
|
||||
GOTENBERG_API_BASIC_AUTH_USERNAME=
|
||||
@@ -46,7 +37,8 @@ API-DOWNLOAD-FROM-DENY-LIST=
|
||||
API-DOWNLOAD-FROM-FROM-MAX-RETRY=4
|
||||
API-DISABLE-DOWNLOAD-FROM=false
|
||||
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||
CHROMIUM_RESTART_AFTER=0
|
||||
API_ENABLE_DEBUG_ROUTE=false
|
||||
CHROMIUM_RESTART_AFTER=10
|
||||
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||
CHROMIUM_AUTO_START=false
|
||||
CHROMIUM_START_TIMEOUT=20s
|
||||
@@ -74,6 +66,7 @@ LOG_FIELDS_PREFIX=
|
||||
PDFENGINES_ENGINES=
|
||||
PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk
|
||||
PDFENGINES_SPLIT_ENGINES=pdfcpu,qpdf,pdftk
|
||||
PDFENGINES_FLATTEN_ENGINES=qpdf
|
||||
PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine
|
||||
PDFENGINES_READ_METADATA_ENGINES=exiftool
|
||||
PDFENGINES_WRITE_METADATA_ENGINES=exiftool
|
||||
@@ -115,6 +108,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) \
|
||||
@@ -143,6 +137,7 @@ run: ## Start a Gotenberg container
|
||||
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
||||
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
||||
--pdfengines-split-engines=$(PDFENGINES_SPLIT_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) \
|
||||
@@ -199,20 +194,3 @@ fmt: ## Format the code and "optimize" the dependencies
|
||||
godoc: ## Run a webserver with Gotenberg godoc
|
||||
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v8)
|
||||
godoc -http=:6060
|
||||
|
||||
LINUX_AMD64_RELEASE=false
|
||||
|
||||
.PHONY: release
|
||||
release: ## Build the Gotenberg's Docker image and push it to a Docker repository
|
||||
./scripts/release.sh \
|
||||
$(GOLANG_VERSION) \
|
||||
$(GOTENBERG_VERSION) \
|
||||
$(GOTENBERG_USER_GID) \
|
||||
$(GOTENBERG_USER_UID) \
|
||||
$(NOTO_COLOR_EMOJI_VERSION) \
|
||||
$(PDFTK_VERSION) \
|
||||
$(PDFCPU_VERSION) \
|
||||
$(DOCKER_REGISTRY) \
|
||||
$(DOCKER_REPOSITORY) \
|
||||
$(LINUX_AMD64_RELEASE)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<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://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>
|
||||
|
||||
@@ -61,7 +61,6 @@ ARG GOTENBERG_USER_GID
|
||||
ARG GOTENBERG_USER_UID
|
||||
ARG NOTO_COLOR_EMOJI_VERSION
|
||||
ARG PDFTK_VERSION
|
||||
ARG TMP_CHOMIUM_VERSION_ARMHF="116.0.5845.180-1~deb12u1"
|
||||
|
||||
LABEL org.opencontainers.image.title="Gotenberg" \
|
||||
org.opencontainers.image.description="A Docker-powered stateless API for PDF files." \
|
||||
@@ -82,6 +81,7 @@ RUN \
|
||||
# Install system dependencies required for the next instructions or debugging.
|
||||
# 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 &&\
|
||||
# Cleanup.
|
||||
# Note: the Debian image does automatically a clean after each install thanks to a hook.
|
||||
@@ -96,6 +96,7 @@ RUN \
|
||||
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html.
|
||||
curl -o ./ttf-mscorefonts-installer_3.8.1_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||
./ttf-mscorefonts-installer_3.8.1_all.deb \
|
||||
culmus \
|
||||
@@ -145,27 +146,18 @@ RUN \
|
||||
# Install either Google Chrome stable on amd64 architecture or
|
||||
# Chromium on other architectures.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/328.
|
||||
# FIXME:
|
||||
# armhf is currently not working with the latest version of Chromium.
|
||||
# See: https://github.com/gotenberg/gotenberg/issues/709.
|
||||
/bin/bash -c \
|
||||
'set -e &&\
|
||||
if [[ "$(dpkg --print-architecture)" == "amd64" ]]; then \
|
||||
curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
||||
apt-get update -qq &&\
|
||||
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; \
|
||||
elif [[ "$(dpkg --print-architecture)" == "armhf" ]]; then \
|
||||
apt-get update -qq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends devscripts &&\
|
||||
debsnap chromium-common "$TMP_CHOMIUM_VERSION_ARMHF" -v --force --binary --architecture armhf &&\
|
||||
debsnap chromium "$TMP_CHOMIUM_VERSION_ARMHF" -v --force --binary --architecture armhf &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install --fix-broken -y -qq --no-install-recommends "./binary-chromium-common/chromium-common_${TMP_CHOMIUM_VERSION_ARMHF}_armhf.deb" "./binary-chromium/chromium_${TMP_CHOMIUM_VERSION_ARMHF}_armhf.deb" &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq devscripts &&\
|
||||
rm -rf ./binary-chromium-common/* ./binary-chromium/*; \
|
||||
else \
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium; \
|
||||
fi' &&\
|
||||
# Verify installation.
|
||||
@@ -177,6 +169,7 @@ 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 &&\
|
||||
chmod +x /usr/bin/unoconverter &&\
|
||||
@@ -196,6 +189,7 @@ RUN \
|
||||
echo '#!/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 &&\
|
||||
# See https://github.com/nextcloud/docker/issues/380.
|
||||
mkdir -p /usr/share/man/man1 &&\
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -50,14 +51,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 +137,9 @@ func Run() {
|
||||
}(l.(gotenberg.SystemLogger))
|
||||
}
|
||||
|
||||
// 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).
|
||||
|
||||
56
go.mod
56
go.mod
@@ -6,9 +6,8 @@ require (
|
||||
github.com/alexliesenfeld/health v0.8.0
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/barasher/go-exiftool v1.10.0
|
||||
github.com/chromedp/cdproto v0.0.0-20241208230723-d1c7de7e5dd2
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20250203011601-a3c71a042730
|
||||
github.com/chromedp/chromedp v0.12.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
@@ -17,46 +16,65 @@ require (
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
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/russross/blackfriday/v2 v2.1.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
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.31.0 // indirect
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0
|
||||
golang.org/x/text v0.22.0
|
||||
)
|
||||
|
||||
require github.com/dlclark/regexp2 v1.11.4
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4
|
||||
github.com/mholt/archives v0.1.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.1 // 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/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/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/ebitengine/purego v0.8.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/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // 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/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.1.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.61.0 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.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
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
google.golang.org/protobuf v1.36.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
|
||||
354
go.sum
354
go.sum
@@ -1,6 +1,26 @@
|
||||
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=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
||||
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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
@@ -9,56 +29,112 @@ github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEX
|
||||
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/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-20241208230723-d1c7de7e5dd2 h1:fJob5N/Eprtd427U84kFpQhAHIEqJYuDzveaL6T4Xsk=
|
||||
github.com/chromedp/cdproto v0.0.0-20241208230723-d1c7de7e5dd2/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
|
||||
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
|
||||
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
|
||||
github.com/chromedp/cdproto v0.0.0-20250203011601-a3c71a042730 h1:IEa+Va47x06CJQaLKFoce5iPTRRR5uI/GbeZbxdnYdc=
|
||||
github.com/chromedp/cdproto v0.0.0-20250203011601-a3c71a042730/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/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/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/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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/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-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=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/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/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/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/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-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/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/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/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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.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/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/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/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=
|
||||
@@ -67,83 +143,271 @@ 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q=
|
||||
github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I=
|
||||
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/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.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U=
|
||||
github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
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/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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
||||
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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
|
||||
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
|
||||
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.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/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.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.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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-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-20220520151302-bc2c85ada10a/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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
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.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
|
||||
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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/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/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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/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=
|
||||
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=
|
||||
|
||||
65
pkg/gotenberg/debug.go
Normal file
65
pkg/gotenberg/debug.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"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()
|
||||
}
|
||||
|
||||
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{
|
||||
"foo",
|
||||
"bar",
|
||||
},
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// GarbageCollect scans the root path and deletes files or directories with
|
||||
// names containing specific substrings.
|
||||
func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr []string) error {
|
||||
// names containing specific substrings and before a given experiation time.
|
||||
func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr []string, expirationTime time.Time) error {
|
||||
logger = logger.Named("gc")
|
||||
|
||||
// To make sure that the next Walk method stays on
|
||||
@@ -36,7 +37,7 @@ func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr []string)
|
||||
}
|
||||
|
||||
for _, substr := range includeSubstr {
|
||||
if strings.Contains(info.Name(), substr) || path == substr {
|
||||
if (strings.Contains(info.Name(), substr) || path == substr) && info.ModTime().Before(expirationTime) {
|
||||
err := os.RemoveAll(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("garbage collect '%s': %w", path, err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
@@ -64,7 +65,7 @@ func TestGarbageCollect(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
err := GarbageCollect(zap.NewNop(), tc.rootPath, tc.includeSubstr)
|
||||
err := GarbageCollect(zap.NewNop(), tc.rootPath, tc.includeSubstr, time.Now())
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
|
||||
@@ -34,10 +34,21 @@ 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
|
||||
type PdfEngineMock struct {
|
||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||
SplitMock func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
||||
FlattenMock func(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
||||
@@ -51,6 +62,10 @@ func (engine *PdfEngineMock) Split(ctx context.Context, logger *zap.Logger, mode
|
||||
return engine.SplitMock(ctx, logger, mode, inputPath, outputDirPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return engine.FlattenMock(ctx, logger, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,21 @@ func TestValidatorMock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebuggableMock(t *testing.T) {
|
||||
mock := &DebuggableMock{
|
||||
DebugMock: func() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
d := mock.Debug()
|
||||
if d == nil {
|
||||
t.Errorf("expected debug data, but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngineMock(t *testing.T) {
|
||||
mock := &PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
@@ -56,6 +71,9 @@ func TestPDFEngineMock(t *testing.T) {
|
||||
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
|
||||
},
|
||||
@@ -77,6 +95,11 @@ func TestPDFEngineMock(t *testing.T) {
|
||||
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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -88,6 +88,8 @@ type PdfFormats struct {
|
||||
// PdfEngine provides an interface for operations on PDFs. Implementations
|
||||
// can utilize various tools like PDFtk, or implement functionality directly in
|
||||
// Go.
|
||||
//
|
||||
//nolint:dupl
|
||||
type PdfEngine interface {
|
||||
// Merge combines multiple PDFs into a single PDF. The resulting page order
|
||||
// is determined by the order of files provided in inputPaths.
|
||||
@@ -96,6 +98,12 @@ type PdfEngine interface {
|
||||
// Split splits a given PDF file.
|
||||
Split(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
||||
|
||||
// Flatten merges existing annotation appearances with page content,
|
||||
// effectively deleting the original annotations. This process can flatten
|
||||
// forms as well, as forms share a relationship with annotations. Note that
|
||||
// this operation is irreversible.
|
||||
Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||
|
||||
// Convert transforms a given PDF to the specified formats defined in
|
||||
// PdfFormats. If no format, it does nothing.
|
||||
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
|
||||
@@ -42,6 +42,7 @@ type Api struct {
|
||||
basicAuthPassword string
|
||||
downloadFromCfg downloadFromConfig
|
||||
disableHealthCheckLogging bool
|
||||
enableDebugRoute bool
|
||||
|
||||
routes []Route
|
||||
externalMiddlewares []Middleware
|
||||
@@ -187,6 +188,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 +214,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")
|
||||
@@ -365,9 +368,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 == "" {
|
||||
@@ -456,14 +460,22 @@ func (a *Api) Start() error {
|
||||
|
||||
hardTimeout := a.timeout + (time.Duration(5) * time.Second)
|
||||
|
||||
// Basic auth?
|
||||
var securityMiddleware echo.MiddlewareFunc
|
||||
if a.basicAuthUsername != "" {
|
||||
securityMiddleware = basicAuthMiddleware(a.basicAuthUsername, a.basicAuthPassword)
|
||||
} else {
|
||||
securityMiddleware = func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the modules' routes and their specific middlewares.
|
||||
for _, route := range a.routes {
|
||||
var middlewares []echo.MiddlewareFunc
|
||||
|
||||
// Basic auth?
|
||||
if a.basicAuthUsername != "" {
|
||||
middlewares = append(middlewares, basicAuthMiddleware(a.basicAuthUsername, a.basicAuthPassword))
|
||||
}
|
||||
middlewares = append(middlewares, securityMiddleware)
|
||||
|
||||
if route.IsMultipart {
|
||||
middlewares = append(middlewares, contextMiddleware(a.fs, a.timeout, a.bodyLimit, a.downloadFromCfg))
|
||||
@@ -483,6 +495,24 @@ func (a *Api) Start() error {
|
||||
)
|
||||
}
|
||||
|
||||
// Root route.
|
||||
a.srv.GET(
|
||||
a.rootPath,
|
||||
func(c echo.Context) error {
|
||||
return c.HTML(http.StatusOK, `Hey, Gotenberg has no UI, it's an API. Head to the <a href="https://gotenberg.dev">documentation</a> to learn how to interact with it 🚀`)
|
||||
},
|
||||
securityMiddleware,
|
||||
)
|
||||
|
||||
// Favicon route.
|
||||
a.srv.GET(
|
||||
fmt.Sprintf("%s%s", a.rootPath, "favicon.ico"),
|
||||
func(c echo.Context) error {
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
},
|
||||
securityMiddleware,
|
||||
)
|
||||
|
||||
// Let's not forget the health check routes...
|
||||
checks := append(a.healthChecks, health.WithTimeout(a.timeout))
|
||||
checker := health.NewChecker(checks...)
|
||||
@@ -503,14 +533,26 @@ 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 {
|
||||
return c.String(http.StatusOK, gotenberg.Version)
|
||||
},
|
||||
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()
|
||||
|
||||
@@ -786,6 +786,7 @@ func TestApi_Start(t *testing.T) {
|
||||
mod.basicAuthUsername = "foo"
|
||||
mod.basicAuthPassword = "bar"
|
||||
mod.disableHealthCheckLogging = true
|
||||
mod.enableDebugRoute = true
|
||||
mod.routes = []Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
@@ -866,15 +867,33 @@ func TestApi_Start(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
// health requests.
|
||||
// root request.
|
||||
recorder := httptest.NewRecorder()
|
||||
rootRequest := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rootRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||
mod.srv.ServeHTTP(recorder, rootRequest)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
// favicon request.
|
||||
recorder = httptest.NewRecorder()
|
||||
faviconRequest := httptest.NewRequest(http.MethodGet, "/favicon.ico", nil)
|
||||
faviconRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||
mod.srv.ServeHTTP(recorder, faviconRequest)
|
||||
if recorder.Code != http.StatusNoContent {
|
||||
t.Errorf("expected %d status code but got %d", http.StatusNoContent, recorder.Code)
|
||||
}
|
||||
|
||||
// health requests.
|
||||
recorder = httptest.NewRecorder()
|
||||
healthGetRequest := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
mod.srv.ServeHTTP(recorder, healthGetRequest)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
healthHeadRequest := httptest.NewRequest(http.MethodHead, "/health", nil)
|
||||
mod.srv.ServeHTTP(recorder, healthHeadRequest)
|
||||
if recorder.Code != http.StatusOK {
|
||||
@@ -882,12 +901,23 @@ func TestApi_Start(t *testing.T) {
|
||||
}
|
||||
|
||||
// version request.
|
||||
recorder = httptest.NewRecorder()
|
||||
versionRequest := httptest.NewRequest(http.MethodGet, "/version", nil)
|
||||
versionRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||
mod.srv.ServeHTTP(recorder, versionRequest)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
// debug request.
|
||||
recorder = httptest.NewRecorder()
|
||||
debugRequest := httptest.NewRequest(http.MethodGet, "/debug", nil)
|
||||
debugRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword)
|
||||
mod.srv.ServeHTTP(recorder, debugRequest)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
// "multipart/form-data" request.
|
||||
multipartRequest := func(url string) *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
@@ -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"
|
||||
@@ -468,22 +467,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)
|
||||
}
|
||||
|
||||
@@ -783,6 +783,7 @@ func TestContext_BuildOutputFile(t *testing.T) {
|
||||
}()
|
||||
|
||||
tc.ctx.dirPath = dirPath
|
||||
tc.ctx.Context = context.Background()
|
||||
tc.ctx.logger = zap.NewNop()
|
||||
|
||||
_, err = tc.ctx.BuildOutputFile()
|
||||
|
||||
@@ -161,9 +161,12 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
||||
trace := c.Get("trace").(string)
|
||||
rootPath := c.Get("rootPath").(string)
|
||||
|
||||
// Create the request logger and add it to our locals.
|
||||
reqLogger := logger.With(zap.String("trace", trace))
|
||||
c.Set("logger", reqLogger.Named(func() string {
|
||||
// Create the application logger and add it to our locals.
|
||||
appLogger := logger.
|
||||
With(zap.String("log_type", "application")).
|
||||
With(zap.String("trace", trace))
|
||||
|
||||
c.Set("logger", appLogger.Named(func() string {
|
||||
return strings.ReplaceAll(
|
||||
strings.ReplaceAll(c.Request().URL.Path, rootPath, ""),
|
||||
"/",
|
||||
@@ -177,6 +180,11 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
||||
c.Error(err)
|
||||
}
|
||||
|
||||
// Create the access logger.
|
||||
accessLogger := logger.
|
||||
With(zap.String("log_type", "access")).
|
||||
With(zap.String("trace", trace))
|
||||
|
||||
for _, path := range disableLoggingForPaths {
|
||||
URI := fmt.Sprintf("%s%s", rootPath, path)
|
||||
|
||||
@@ -212,9 +220,9 @@ func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []string) echo.
|
||||
fields[11] = zap.Int64("bytes_out", c.Response().Size)
|
||||
|
||||
if err != nil {
|
||||
reqLogger.Error(err.Error(), fields...)
|
||||
accessLogger.Error(err.Error(), fields...)
|
||||
} else {
|
||||
reqLogger.Info("request handled", fields...)
|
||||
accessLogger.Info("request handled", fields...)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
@@ -162,21 +163,61 @@ func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
|
||||
|
||||
// Always remove the user profile directory created by Chromium.
|
||||
copyUserProfileDirPath := b.userProfileDirPath
|
||||
defer func(userProfileDirPath string) {
|
||||
expirationTime := time.Now()
|
||||
defer func(userProfileDirPath string, expirationTime time.Time) {
|
||||
// See:
|
||||
// https://github.com/SeleniumHQ/docker-selenium/blob/7216d060d86872afe853ccda62db0dfab5118dc7/NodeChrome/chrome-cleanup.sh
|
||||
// https://github.com/SeleniumHQ/docker-selenium/blob/7216d060d86872afe853ccda62db0dfab5118dc7/NodeChromium/chrome-cleanup.sh
|
||||
|
||||
// Clean up stuck processes.
|
||||
ps, err := process.Processes()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("list processes: %v", err))
|
||||
} else {
|
||||
for _, p := range ps {
|
||||
func() {
|
||||
cmdline, err := p.Cmdline()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(cmdline, "chromium/chromium") && !strings.Contains(cmdline, "chrome/chrome") {
|
||||
return
|
||||
}
|
||||
|
||||
killCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
err = p.KillWithContext(killCtx)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("kill process: %v", err))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("Chromium process %d killed", p.Pid))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
// FIXME: Chromium seems to recreate the user profile directory
|
||||
// right after its deletion if we do not wait a certain amount
|
||||
// of time before deleting it.
|
||||
<-time.After(10 * time.Second)
|
||||
|
||||
err := os.RemoveAll(userProfileDirPath)
|
||||
err = os.RemoveAll(userProfileDirPath)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("remove Chromium's user profile directory: %s", err))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("'%s' Chromium's user profile directory removed", userProfileDirPath))
|
||||
// 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())
|
||||
}
|
||||
}()
|
||||
}(copyUserProfileDirPath)
|
||||
}(copyUserProfileDirPath, expirationTime)
|
||||
|
||||
b.ctxMu.Lock()
|
||||
defer b.ctxMu.Unlock()
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
@@ -352,7 +355,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
||||
ID: "chromium",
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("chromium", flag.ExitOnError)
|
||||
fs.Int64("chromium-restart-after", 0, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
||||
fs.Int64("chromium-restart-after", 10, "Number of conversions after which Chromium will automatically restart. Set to 0 to disable this feature")
|
||||
fs.Int64("chromium-max-queue-size", 0, "Maximum request queue size for Chromium. Set to 0 to disable this feature")
|
||||
fs.Bool("chromium-auto-start", false, "Automatically launch Chromium upon initialization if set to true; otherwise, Chromium will start at the time of the first conversion")
|
||||
fs.Duration("chromium-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for Chromium to start or restart")
|
||||
@@ -484,6 +487,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 +613,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)
|
||||
|
||||
@@ -336,6 +336,54 @@ func TestChromium_Stop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Debug(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
mod *Chromium
|
||||
expect map[string]interface{}
|
||||
doNotExpect map[string]interface{}
|
||||
}{
|
||||
{
|
||||
scenario: "cannot determine version",
|
||||
mod: &Chromium{
|
||||
args: browserArguments{
|
||||
binPath: "foo",
|
||||
},
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"version": `exec: "foo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
mod: &Chromium{
|
||||
args: browserArguments{
|
||||
binPath: "echo",
|
||||
},
|
||||
},
|
||||
doNotExpect: map[string]interface{}{
|
||||
"version": `exec: "echo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
d := tc.mod.Debug()
|
||||
|
||||
if tc.expect != nil {
|
||||
if !reflect.DeepEqual(d, tc.expect) {
|
||||
t.Errorf("expected '%v' but got '%v'", tc.expect, d)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.doNotExpect != nil {
|
||||
if reflect.DeepEqual(d, tc.doNotExpect) {
|
||||
t.Errorf("did not expect '%v'", d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromium_Metrics(t *testing.T) {
|
||||
mod := new(Chromium)
|
||||
mod.supervisor = &gotenberg.ProcessSupervisorMock{
|
||||
|
||||
@@ -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)
|
||||
@@ -63,6 +83,11 @@ func (engine *ExifTool) Split(ctx context.Context, logger *zap.Logger, mode gote
|
||||
return nil, fmt.Errorf("split PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Flatten is not available in this implementation.
|
||||
func (engine *ExifTool) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return fmt.Errorf("flatten PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Convert is not available in this implementation.
|
||||
func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
@@ -156,5 +181,6 @@ var (
|
||||
_ gotenberg.Module = (*ExifTool)(nil)
|
||||
_ gotenberg.Provisioner = (*ExifTool)(nil)
|
||||
_ gotenberg.Validator = (*ExifTool)(nil)
|
||||
_ gotenberg.Debuggable = (*ExifTool)(nil)
|
||||
_ gotenberg.PdfEngine = (*ExifTool)(nil)
|
||||
)
|
||||
|
||||
@@ -73,6 +73,50 @@ func TestExifTool_Validate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExifTool_Debug(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *ExifTool
|
||||
expect map[string]interface{}
|
||||
doNotExpect map[string]interface{}
|
||||
}{
|
||||
{
|
||||
scenario: "cannot determine version",
|
||||
engine: &ExifTool{
|
||||
binPath: "foo",
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"version": `exec: "foo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
engine: &ExifTool{
|
||||
binPath: "echo",
|
||||
},
|
||||
doNotExpect: map[string]interface{}{
|
||||
"version": `exec: "echo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
d := tc.engine.Debug()
|
||||
|
||||
if tc.expect != nil {
|
||||
if !reflect.DeepEqual(d, tc.expect) {
|
||||
t.Errorf("expected '%v' but got '%v'", tc.expect, d)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.doNotExpect != nil {
|
||||
if reflect.DeepEqual(d, tc.doNotExpect) {
|
||||
t.Errorf("did not expect '%v'", d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Merge(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
@@ -91,6 +135,15 @@ func TestExiftool_Split(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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{}, "", "")
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexliesenfeld/health"
|
||||
@@ -305,6 +308,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 +556,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)
|
||||
|
||||
@@ -277,6 +277,54 @@ func TestApi_Stop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfTk_Debug(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
a *Api
|
||||
expect map[string]interface{}
|
||||
doNotExpect map[string]interface{}
|
||||
}{
|
||||
{
|
||||
scenario: "cannot determine version",
|
||||
a: &Api{
|
||||
args: libreOfficeArguments{
|
||||
binPath: "foo",
|
||||
},
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"version": `exec: "foo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
a: &Api{
|
||||
args: libreOfficeArguments{
|
||||
binPath: "echo",
|
||||
},
|
||||
},
|
||||
doNotExpect: map[string]interface{}{
|
||||
"version": `exec: "echo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
d := tc.a.Debug()
|
||||
|
||||
if tc.expect != nil {
|
||||
if !reflect.DeepEqual(d, tc.expect) {
|
||||
t.Errorf("expected '%v' but got '%v'", tc.expect, d)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.doNotExpect != nil {
|
||||
if reflect.DeepEqual(d, tc.doNotExpect) {
|
||||
t.Errorf("did not expect '%v'", d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApi_Metrics(t *testing.T) {
|
||||
a := new(Api)
|
||||
a.supervisor = &gotenberg.ProcessSupervisorMock{
|
||||
|
||||
@@ -190,22 +190,23 @@ func (p *libreOfficeProcess) Stop(logger *zap.Logger) error {
|
||||
|
||||
// Always remove the user profile directory created by LibreOffice.
|
||||
copyUserProfileDirPath := p.userProfileDirPath
|
||||
defer func(userProfileDirPath string) {
|
||||
expirationTime := time.Now()
|
||||
defer func(userProfileDirPath string, expirationTime time.Time) {
|
||||
go func() {
|
||||
err := os.RemoveAll(userProfileDirPath)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("remove LibreOffice's user profile directory: %v", err))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("'%s' LibreOffice's user profile directory removed", userProfileDirPath))
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("'%s' LibreOffice's user profile directory removed", userProfileDirPath))
|
||||
|
||||
// Also remove LibreOffice specific files in the temporary directory.
|
||||
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{"OSL_PIPE", ".tmp"})
|
||||
err = gotenberg.GarbageCollect(logger, os.TempDir(), []string{"OSL_PIPE", ".tmp"}, expirationTime)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
}(copyUserProfileDirPath)
|
||||
}(copyUserProfileDirPath, expirationTime)
|
||||
|
||||
p.cfgMu.Lock()
|
||||
defer p.cfgMu.Unlock()
|
||||
|
||||
@@ -56,6 +56,11 @@ func (engine *LibreOfficePdfEngine) Split(ctx context.Context, logger *zap.Logge
|
||||
return nil, fmt.Errorf("split PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
||||
// PDF/A-1b, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
|
||||
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
|
||||
|
||||
@@ -127,6 +127,15 @@ func TestLibreOfficePdfEngine_Split(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -60,6 +60,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
maxImageResolution int
|
||||
nativePdfFormats bool
|
||||
merge bool
|
||||
flatten bool
|
||||
)
|
||||
|
||||
err := form.
|
||||
@@ -135,6 +136,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Bool("flatten", &flatten, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
@@ -261,6 +263,13 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
err = pdfengines.FlattenStub(ctx, engine, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(outputPaths) > 1 && splitMode == zeroValuedSplitMode {
|
||||
// If .zip archive, document.docx -> document.docx.pdf.
|
||||
for i, inputPath := range inputPaths {
|
||||
|
||||
@@ -402,6 +402,38 @@ func TestConvertRoute(t *testing.T) {
|
||||
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 {
|
||||
@@ -454,6 +486,9 @@ func TestConvertRoute(t *testing.T) {
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
@@ -475,6 +510,9 @@ func TestConvertRoute(t *testing.T) {
|
||||
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,
|
||||
@@ -502,6 +540,9 @@ func TestConvertRoute(t *testing.T) {
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
@@ -526,6 +567,9 @@ func TestConvertRoute(t *testing.T) {
|
||||
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,
|
||||
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -52,6 +55,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
|
||||
@@ -107,6 +136,11 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenb
|
||||
return outputPaths, nil
|
||||
}
|
||||
|
||||
// Flatten is not available in this implementation.
|
||||
func (engine *PdfCpu) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return fmt.Errorf("flatten PDF with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Convert is not available in this implementation.
|
||||
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with pdfcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
@@ -127,5 +161,6 @@ var (
|
||||
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||
_ gotenberg.Provisioner = (*PdfCpu)(nil)
|
||||
_ gotenberg.Validator = (*PdfCpu)(nil)
|
||||
_ gotenberg.Debuggable = (*PdfCpu)(nil)
|
||||
_ gotenberg.PdfEngine = (*PdfCpu)(nil)
|
||||
)
|
||||
|
||||
@@ -71,6 +71,59 @@ func TestPdfCpu_Validate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Debug(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *PdfCpu
|
||||
expect map[string]interface{}
|
||||
doNotExpect map[string]interface{}
|
||||
}{
|
||||
{
|
||||
scenario: "cannot determine version (command error)",
|
||||
engine: &PdfCpu{
|
||||
binPath: "foo",
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"version": `exec: "foo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "cannot determine version (no pdfcpu)",
|
||||
engine: &PdfCpu{
|
||||
binPath: "echo",
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"version": "Unable to determine pdfcpu version",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
engine: &PdfCpu{
|
||||
binPath: "pdfcpu",
|
||||
},
|
||||
doNotExpect: map[string]interface{}{
|
||||
"version": "Unable to determine pdfcpu version",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
d := tc.engine.Debug()
|
||||
|
||||
if tc.expect != nil {
|
||||
if !reflect.DeepEqual(d, tc.expect) {
|
||||
t.Errorf("expected '%v' but got '%v'", tc.expect, d)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.doNotExpect != nil {
|
||||
if reflect.DeepEqual(d, tc.doNotExpect) {
|
||||
t.Errorf("did not expect '%v'", d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -239,6 +292,15 @@ func TestPdfCpu_Split(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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{}, "", "")
|
||||
|
||||
@@ -12,26 +12,29 @@ import (
|
||||
)
|
||||
|
||||
type multiPdfEngines struct {
|
||||
mergeEngines []gotenberg.PdfEngine
|
||||
splitEngines []gotenberg.PdfEngine
|
||||
convertEngines []gotenberg.PdfEngine
|
||||
readMedataEngines []gotenberg.PdfEngine
|
||||
writeMedataEngines []gotenberg.PdfEngine
|
||||
mergeEngines []gotenberg.PdfEngine
|
||||
splitEngines []gotenberg.PdfEngine
|
||||
flattenEngines []gotenberg.PdfEngine
|
||||
convertEngines []gotenberg.PdfEngine
|
||||
readMetadataEngines []gotenberg.PdfEngine
|
||||
writeMetadataEngines []gotenberg.PdfEngine
|
||||
}
|
||||
|
||||
func newMultiPdfEngines(
|
||||
mergeEngines,
|
||||
splitEngines,
|
||||
flattenEngines,
|
||||
convertEngines,
|
||||
readMetadataEngines,
|
||||
writeMedataEngines []gotenberg.PdfEngine,
|
||||
writeMetadataEngines []gotenberg.PdfEngine,
|
||||
) *multiPdfEngines {
|
||||
return &multiPdfEngines{
|
||||
mergeEngines: mergeEngines,
|
||||
splitEngines: splitEngines,
|
||||
convertEngines: convertEngines,
|
||||
readMedataEngines: readMetadataEngines,
|
||||
writeMedataEngines: writeMedataEngines,
|
||||
mergeEngines: mergeEngines,
|
||||
splitEngines: splitEngines,
|
||||
flattenEngines: flattenEngines,
|
||||
convertEngines: convertEngines,
|
||||
readMetadataEngines: readMetadataEngines,
|
||||
writeMetadataEngines: writeMetadataEngines,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +101,31 @@ func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mod
|
||||
return nil, fmt.Errorf("split PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Flatten merges existing annotation appearances with page content, thanks to
|
||||
// its children. If the context is done, it stops and returns an error
|
||||
func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.flattenEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Flatten(ctx, logger, inputPath)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case mergeErr := <-errChan:
|
||||
errored := multierr.AppendInto(&err, mergeErr)
|
||||
if !errored {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("flatten PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format. thanks to its
|
||||
// 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 {
|
||||
@@ -132,16 +160,16 @@ 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.readMedataEngines))
|
||||
resultChan := make(chan readMetadataResult, len(multi.readMetadataEngines))
|
||||
|
||||
for _, engine := range multi.readMedataEngines {
|
||||
for _, engine := range multi.readMetadataEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
|
||||
resultChan <- readMetadataResult{metadata: metadata, err: err}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
for range multi.readMedataEngines {
|
||||
for range multi.readMetadataEngines {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
@@ -163,7 +191,7 @@ func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Log
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.writeMedataEngines {
|
||||
for _, engine := range multi.writeMetadataEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
|
||||
}(engine)
|
||||
|
||||
@@ -194,6 +194,97 @@ func TestMultiPdfEngines_Split(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Flatten(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.Flatten(tc.ctx, zap.NewNop(), "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -295,7 +386,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
@@ -308,7 +399,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
@@ -326,7 +417,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
@@ -345,7 +436,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
@@ -386,7 +477,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
@@ -399,7 +490,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -417,7 +508,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -436,7 +527,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
|
||||
@@ -27,13 +27,14 @@ func init() {
|
||||
// the [api.Router] interface to expose relevant PDF processing routes if
|
||||
// enabled.
|
||||
type PdfEngines struct {
|
||||
mergeNames []string
|
||||
splitNames []string
|
||||
convertNames []string
|
||||
readMetadataNames []string
|
||||
writeMedataNames []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
mergeNames []string
|
||||
splitNames []string
|
||||
flattenNames []string
|
||||
convertNames []string
|
||||
readMetadataNames []string
|
||||
writeMetadataNames []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
}
|
||||
|
||||
// Descriptor returns a PdfEngines' module descriptor.
|
||||
@@ -44,6 +45,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
||||
fs.StringSlice("pdfengines-merge-engines", []string{"qpdf", "pdfcpu", "pdftk"}, "Set the PDF engines and their order for the merge feature - empty means all")
|
||||
fs.StringSlice("pdfengines-split-engines", []string{"pdfcpu", "qpdf", "pdftk"}, "Set the PDF engines and their order for the split feature - empty means all")
|
||||
fs.StringSlice("pdfengines-flatten-engines", []string{"qpdf"}, "Set the PDF engines and their order for the flatten feature - empty means all")
|
||||
fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all")
|
||||
fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all")
|
||||
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
||||
@@ -67,6 +69,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
flags := ctx.ParsedFlags()
|
||||
mergeNames := flags.MustStringSlice("pdfengines-merge-engines")
|
||||
splitNames := flags.MustStringSlice("pdfengines-split-engines")
|
||||
flattenNames := flags.MustStringSlice("pdfengines-flatten-engines")
|
||||
convertNames := flags.MustStringSlice("pdfengines-convert-engines")
|
||||
readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines")
|
||||
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
|
||||
@@ -106,6 +109,11 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.splitNames = splitNames
|
||||
}
|
||||
|
||||
mod.flattenNames = defaultNames
|
||||
if len(flattenNames) > 0 {
|
||||
mod.flattenNames = flattenNames
|
||||
}
|
||||
|
||||
mod.convertNames = defaultNames
|
||||
if len(convertNames) > 0 {
|
||||
mod.convertNames = convertNames
|
||||
@@ -116,9 +124,9 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.readMetadataNames = readMetadataNames
|
||||
}
|
||||
|
||||
mod.writeMedataNames = defaultNames
|
||||
mod.writeMetadataNames = defaultNames
|
||||
if len(writeMetadataNames) > 0 {
|
||||
mod.writeMedataNames = writeMetadataNames
|
||||
mod.writeMetadataNames = writeMetadataNames
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -170,9 +178,10 @@ func (mod *PdfEngines) Validate() error {
|
||||
|
||||
findNonExistingEngines(mod.mergeNames)
|
||||
findNonExistingEngines(mod.splitNames)
|
||||
findNonExistingEngines(mod.flattenNames)
|
||||
findNonExistingEngines(mod.convertNames)
|
||||
findNonExistingEngines(mod.readMetadataNames)
|
||||
findNonExistingEngines(mod.writeMedataNames)
|
||||
findNonExistingEngines(mod.writeMetadataNames)
|
||||
|
||||
if len(nonExistingEngines) == 0 {
|
||||
return nil
|
||||
@@ -187,9 +196,10 @@ func (mod *PdfEngines) SystemMessages() []string {
|
||||
return []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 medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")),
|
||||
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,9 +222,10 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||
return newMultiPdfEngines(
|
||||
engines(mod.mergeNames),
|
||||
engines(mod.splitNames),
|
||||
engines(mod.flattenNames),
|
||||
engines(mod.convertNames),
|
||||
engines(mod.readMetadataNames),
|
||||
engines(mod.writeMedataNames),
|
||||
engines(mod.writeMetadataNames),
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -234,6 +245,7 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||
return []api.Route{
|
||||
mergeRoute(engine),
|
||||
splitRoute(engine),
|
||||
flattenRoute(engine),
|
||||
convertRoute(engine),
|
||||
readMetadataRoute(engine),
|
||||
writeMetadataRoute(engine),
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
ctx *gotenberg.Context
|
||||
expectedMergePdfEngines []string
|
||||
expectedSplitPdfEngines []string
|
||||
expectedFlattenPdfEngines []string
|
||||
expectedConvertPdfEngines []string
|
||||
expectedReadMetadataPdfEngines []string
|
||||
expectedWriteMetadataPdfEngines []string
|
||||
@@ -68,6 +69,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}(),
|
||||
expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"},
|
||||
expectedSplitPdfEngines: []string{"pdfcpu", "qpdf", "pdftk"},
|
||||
expectedFlattenPdfEngines: []string{"qpdf"},
|
||||
expectedConvertPdfEngines: []string{"libreoffice-pdfengine"},
|
||||
expectedReadMetadataPdfEngines: []string{"exiftool"},
|
||||
expectedWriteMetadataPdfEngines: []string{"exiftool"},
|
||||
@@ -109,7 +111,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}
|
||||
|
||||
fs := new(PdfEngines).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-split-engines=a", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"})
|
||||
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)
|
||||
}
|
||||
@@ -128,6 +130,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
|
||||
expectedMergePdfEngines: []string{"b"},
|
||||
expectedSplitPdfEngines: []string{"a"},
|
||||
expectedFlattenPdfEngines: []string{"c"},
|
||||
expectedConvertPdfEngines: []string{"b"},
|
||||
expectedReadMetadataPdfEngines: []string{"a"},
|
||||
expectedWriteMetadataPdfEngines: []string{"a"},
|
||||
@@ -185,6 +188,10 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
@@ -193,8 +200,8 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
t.Fatalf("expected %d read metadata names but got %d", len(tc.expectedReadMetadataPdfEngines), len(mod.readMetadataNames))
|
||||
}
|
||||
|
||||
if len(tc.expectedWriteMetadataPdfEngines) != len(mod.writeMedataNames) {
|
||||
t.Fatalf("expected %d write metadata names but got %d", len(tc.expectedWriteMetadataPdfEngines), len(mod.writeMedataNames))
|
||||
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 {
|
||||
@@ -221,7 +228,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.writeMedataNames {
|
||||
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])
|
||||
}
|
||||
@@ -289,11 +296,11 @@ func TestPdfEngines_Validate(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
mergeNames: tc.names,
|
||||
convertNames: tc.names,
|
||||
readMetadataNames: tc.names,
|
||||
writeMedataNames: tc.names,
|
||||
engines: tc.engines,
|
||||
mergeNames: tc.names,
|
||||
convertNames: tc.names,
|
||||
readMetadataNames: tc.names,
|
||||
writeMetadataNames: tc.names,
|
||||
engines: tc.engines,
|
||||
}
|
||||
|
||||
err := mod.Validate()
|
||||
@@ -315,19 +322,21 @@ func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
mod.splitNames = []string{"foo", "bar"}
|
||||
mod.convertNames = []string{"foo", "bar"}
|
||||
mod.readMetadataNames = []string{"foo", "bar"}
|
||||
mod.writeMedataNames = []string{"foo", "bar"}
|
||||
mod.writeMetadataNames = []string{"foo", "bar"}
|
||||
|
||||
expectedMessages := 6
|
||||
messages := mod.SystemMessages()
|
||||
if len(messages) != 5 {
|
||||
t.Errorf("expected one and only one message, but got %d", len(messages))
|
||||
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 medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")),
|
||||
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
||||
}
|
||||
|
||||
for i, message := range messages {
|
||||
@@ -339,11 +348,11 @@ func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
|
||||
func TestPdfEngines_PdfEngine(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
mergeNames: []string{"foo", "bar"},
|
||||
splitNames: []string{"foo", "bar"},
|
||||
convertNames: []string{"foo", "bar"},
|
||||
readMetadataNames: []string{"foo", "bar"},
|
||||
writeMedataNames: []string{"foo", "bar"},
|
||||
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
|
||||
@@ -382,7 +391,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 5,
|
||||
expectRoutes: 6,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -202,6 +202,19 @@ func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.S
|
||||
return outputPaths, nil
|
||||
}
|
||||
|
||||
// FlattenStub merges annotation appearances with page content for each given
|
||||
// PDF, effectively deleting the original annotations.
|
||||
func FlattenStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths []string) error {
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.Flatten(ctx, ctx.Log(), inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertStub transforms a given PDF to the specified formats defined in
|
||||
// [gotenberg.PdfFormats]. If no format, it does nothing and returns the input
|
||||
// paths.
|
||||
@@ -255,8 +268,10 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Bool("flatten", &flatten, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
@@ -278,6 +293,13 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
err = FlattenStub(ctx, engine, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(outputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
@@ -303,8 +325,10 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Bool("flatten", &flatten, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
@@ -325,6 +349,13 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
err = FlattenStub(ctx, engine, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
zeroValuedSplitMode := gotenberg.SplitMode{}
|
||||
zeroValuedPdfFormats := gotenberg.PdfFormats{}
|
||||
if mode != zeroValuedSplitMode && pdfFormats != zeroValuedPdfFormats {
|
||||
@@ -347,6 +378,40 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// flattenRoute returns an [api.Route] which can flatten PDFs.
|
||||
func flattenRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/flatten",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = FlattenStub(ctx, engine, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
||||
// format.
|
||||
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
|
||||
@@ -503,6 +503,45 @@ func TestSplitPdfStub(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenStub(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine gotenberg.PdfEngine
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "flatten error",
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "flatten success",
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := FlattenStub(new(api.Context), tc.engine, []string{"my.pdf", "my2.pdf"})
|
||||
|
||||
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 TestConvertStub(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -647,7 +686,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine merge error",
|
||||
scenario: "error from PDF engine (merge)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
@@ -666,7 +705,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine convert error",
|
||||
scenario: "error from PDF engine (convert)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
@@ -693,7 +732,7 @@ func TestMergeHandler(t *testing.T) {
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine write metadata error",
|
||||
scenario: "error from PDF engine (write metadata)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
@@ -719,6 +758,33 @@ func TestMergeHandler(t *testing.T) {
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine (flatten)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
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 {
|
||||
@@ -754,6 +820,9 @@ func TestMergeHandler(t *testing.T) {
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
@@ -767,6 +836,9 @@ func TestMergeHandler(t *testing.T) {
|
||||
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,
|
||||
@@ -936,6 +1008,38 @@ func TestSplitHandler(t *testing.T) {
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine (flatten)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
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 {
|
||||
@@ -983,6 +1087,9 @@ func TestSplitHandler(t *testing.T) {
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
@@ -996,6 +1103,9 @@ func TestSplitHandler(t *testing.T) {
|
||||
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,
|
||||
@@ -1055,6 +1165,147 @@ func TestSplitHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenHandler(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
engine gotenberg.PdfEngine
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
expectHttpStatus int
|
||||
expectOutputPathsCount int
|
||||
expectOutputPaths []string
|
||||
}{
|
||||
{
|
||||
scenario: "missing at least one mandatory file",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
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{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetCancelled(true)
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success with single file",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
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{
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 2,
|
||||
expectOutputPaths: []string{"/file.pdf", "/file2.pdf"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
c := echo.New().NewContext(nil, nil)
|
||||
c.Set("context", tc.ctx.Context)
|
||||
|
||||
err := flattenRoute(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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertHandler(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package pdftk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -52,6 +55,29 @@ func (engine *PdfTk) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *PdfTk) 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
|
||||
}
|
||||
|
||||
lines := bytes.SplitN(output, []byte("\n"), 2)
|
||||
if len(lines) > 0 {
|
||||
debug["version"] = string(lines[0])
|
||||
} else {
|
||||
debug["version"] = "Unable to determine PDFtk version"
|
||||
}
|
||||
|
||||
return debug
|
||||
}
|
||||
|
||||
// Split splits a given PDF file.
|
||||
func (engine *PdfTk) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
var args []string
|
||||
@@ -99,6 +125,11 @@ func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths [
|
||||
return fmt.Errorf("merge PDFs with PDFtk: %w", err)
|
||||
}
|
||||
|
||||
// Flatten is not available in this implementation.
|
||||
func (engine *PdfTk) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return fmt.Errorf("flatten PDF with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Convert is not available in this implementation.
|
||||
func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
@@ -119,5 +150,6 @@ var (
|
||||
_ gotenberg.Module = (*PdfTk)(nil)
|
||||
_ gotenberg.Provisioner = (*PdfTk)(nil)
|
||||
_ gotenberg.Validator = (*PdfTk)(nil)
|
||||
_ gotenberg.Debuggable = (*PdfTk)(nil)
|
||||
_ gotenberg.PdfEngine = (*PdfTk)(nil)
|
||||
)
|
||||
|
||||
@@ -71,6 +71,50 @@ func TestPdfTk_Validate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfTk_Debug(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *PdfTk
|
||||
expect map[string]interface{}
|
||||
doNotExpect map[string]interface{}
|
||||
}{
|
||||
{
|
||||
scenario: "cannot determine version",
|
||||
engine: &PdfTk{
|
||||
binPath: "foo",
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"version": `exec: "foo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
engine: &PdfTk{
|
||||
binPath: "echo",
|
||||
},
|
||||
doNotExpect: map[string]interface{}{
|
||||
"version": `exec: "echo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
d := tc.engine.Debug()
|
||||
|
||||
if tc.expect != nil {
|
||||
if !reflect.DeepEqual(d, tc.expect) {
|
||||
t.Errorf("expected '%v' but got '%v'", tc.expect, d)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.doNotExpect != nil {
|
||||
if reflect.DeepEqual(d, tc.doNotExpect) {
|
||||
t.Errorf("did not expect '%v'", d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfTk_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -232,6 +276,15 @@ func TestPdfCpu_Split(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfTk_Flatten(t *testing.T) {
|
||||
engine := new(PdfTk)
|
||||
err := engine.Flatten(context.TODO(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfTk_Convert(t *testing.T) {
|
||||
engine := new(PdfTk)
|
||||
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
// 1. The merging of PDF files.
|
||||
// 2. The splitting of PDF files.
|
||||
// 3. Flattening of PDF files
|
||||
//
|
||||
// The path to the QPDF binary must be specified using the QPDK_BIN_PATH
|
||||
// environment variable.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package qpdf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -52,6 +55,29 @@ func (engine *QPdf) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug returns additional debug data.
|
||||
func (engine *QPdf) 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
|
||||
}
|
||||
|
||||
lines := bytes.SplitN(output, []byte("\n"), 2)
|
||||
if len(lines) > 0 {
|
||||
debug["version"] = string(lines[0])
|
||||
} else {
|
||||
debug["version"] = "Unable to determine QPDF version"
|
||||
}
|
||||
|
||||
return debug
|
||||
}
|
||||
|
||||
// Split splits a given PDF file.
|
||||
func (engine *QPdf) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
var args []string
|
||||
@@ -101,6 +127,28 @@ func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
|
||||
return fmt.Errorf("merge PDFs with QPDF: %w", err)
|
||||
}
|
||||
|
||||
// Flatten merges annotation appearances with page content, deleting the
|
||||
// original annotations.
|
||||
func (engine *QPdf) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
var args []string
|
||||
args = append(args, "--generate-appearances")
|
||||
args = append(args, "--flatten-annotations=all")
|
||||
args = append(args, "--replace-input")
|
||||
args = append(args, inputPath)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
_, err = cmd.Exec()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("flatten PDFs with QPDF: %w", err)
|
||||
}
|
||||
|
||||
// Convert is not available in this implementation.
|
||||
func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
@@ -120,5 +168,6 @@ var (
|
||||
_ gotenberg.Module = (*QPdf)(nil)
|
||||
_ gotenberg.Provisioner = (*QPdf)(nil)
|
||||
_ gotenberg.Validator = (*QPdf)(nil)
|
||||
_ gotenberg.Debuggable = (*QPdf)(nil)
|
||||
_ gotenberg.PdfEngine = (*QPdf)(nil)
|
||||
)
|
||||
|
||||
@@ -3,6 +3,8 @@ package qpdf
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -71,6 +73,50 @@ func TestQPdf_Validate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPdf_Debug(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *QPdf
|
||||
expect map[string]interface{}
|
||||
doNotExpect map[string]interface{}
|
||||
}{
|
||||
{
|
||||
scenario: "cannot determine version",
|
||||
engine: &QPdf{
|
||||
binPath: "foo",
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"version": `exec: "foo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
engine: &QPdf{
|
||||
binPath: "echo",
|
||||
},
|
||||
doNotExpect: map[string]interface{}{
|
||||
"version": `exec: "echo": executable file not found in $PATH`,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
d := tc.engine.Debug()
|
||||
|
||||
if tc.expect != nil {
|
||||
if !reflect.DeepEqual(d, tc.expect) {
|
||||
t.Errorf("expected '%v' but got '%v'", tc.expect, d)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.doNotExpect != nil {
|
||||
if reflect.DeepEqual(d, tc.doNotExpect) {
|
||||
t.Errorf("did not expect '%v'", d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPdf_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -232,6 +278,101 @@ func TestQPdf_Split(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPdf_Flatten(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
inputPath string
|
||||
createCopy bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
inputPath: "foo.pdf",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
ctx: context.TODO(),
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample3.pdf",
|
||||
createCopy: true,
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(QPdf)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected 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.Flatten(tc.ctx, zap.NewNop(), 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPdf_Convert(t *testing.T) {
|
||||
engine := new(QPdf)
|
||||
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Args.
|
||||
GOLANG_VERSION="$1"
|
||||
GOTENBERG_VERSION="$2"
|
||||
GOTENBERG_USER_GID="$3"
|
||||
GOTENBERG_USER_UID="$4"
|
||||
NOTO_COLOR_EMOJI_VERSION="$5"
|
||||
PDFTK_VERSION="$6"
|
||||
PDFCPU_VERSION="$7"
|
||||
DOCKER_REGISTRY="$8"
|
||||
DOCKER_REPOSITORY="$9"
|
||||
LINUX_AMD64_RELEASE="${10}"
|
||||
|
||||
# Find out if given version is "semver".
|
||||
GOTENBERG_VERSION="${GOTENBERG_VERSION//v}"
|
||||
IFS='.' read -ra SEMVER <<< "$GOTENBERG_VERSION"
|
||||
VERSION_LENGTH=${#SEMVER[@]}
|
||||
TAGS=()
|
||||
TAGS_CLOUD_RUN=()
|
||||
|
||||
if [ "$VERSION_LENGTH" -eq 3 ]; then
|
||||
MAJOR="${SEMVER[0]}"
|
||||
MINOR="${SEMVER[1]}"
|
||||
PATCH="${SEMVER[2]}"
|
||||
|
||||
TAGS+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest")
|
||||
TAGS+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$MAJOR")
|
||||
TAGS+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$MAJOR.$MINOR")
|
||||
TAGS+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$MAJOR.$MINOR.$PATCH")
|
||||
|
||||
TAGS_CLOUD_RUN+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-cloudrun")
|
||||
TAGS_CLOUD_RUN+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$MAJOR-cloudrun")
|
||||
TAGS_CLOUD_RUN+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$MAJOR.$MINOR-cloudrun")
|
||||
TAGS_CLOUD_RUN+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$MAJOR.$MINOR.$PATCH-cloudrun")
|
||||
else
|
||||
# Normalizes version.
|
||||
GOTENBERG_VERSION="${GOTENBERG_VERSION// /-}"
|
||||
GOTENBERG_VERSION="$(echo "$GOTENBERG_VERSION" | tr -cd '[:alnum:]._\-')"
|
||||
|
||||
if [[ "$GOTENBERG_VERSION" =~ ^[\.\-] ]]; then
|
||||
GOTENBERG_VERSION="_${GOTENBERG_VERSION#?}"
|
||||
fi
|
||||
|
||||
if [ "${#GOTENBERG_VERSION}" -gt 128 ]; then
|
||||
GOTENBERG_VERSION="${GOTENBERG_VERSION:0:128}"
|
||||
fi
|
||||
|
||||
TAGS+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VERSION")
|
||||
TAGS_CLOUD_RUN+=("-t" "$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VERSION-cloudrun")
|
||||
fi
|
||||
|
||||
# Multi-arch build takes a lot of time.
|
||||
if [ "$LINUX_AMD64_RELEASE" = true ]; then
|
||||
PLATFORM_FLAG="--platform linux/amd64"
|
||||
else
|
||||
PLATFORM_FLAG="--platform linux/amd64,linux/arm64,linux/386,linux/arm/v7"
|
||||
fi
|
||||
|
||||
docker buildx 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" \
|
||||
$PLATFORM_FLAG \
|
||||
"${TAGS[@]}" \
|
||||
--push \
|
||||
-f build/Dockerfile .
|
||||
|
||||
# Cloud Run variant.
|
||||
# Only linux/amd64! See https://github.com/gotenberg/gotenberg/issues/505#issuecomment-1264679278.
|
||||
docker buildx build \
|
||||
--build-arg DOCKER_REGISTRY="$DOCKER_REGISTRY" \
|
||||
--build-arg DOCKER_REPOSITORY="$DOCKER_REPOSITORY" \
|
||||
--build-arg GOTENBERG_VERSION="$GOTENBERG_VERSION" \
|
||||
--platform linux/amd64 \
|
||||
"${TAGS_CLOUD_RUN[@]}" \
|
||||
--push \
|
||||
-f build/Dockerfile.cloudrun .
|
||||
@@ -19,6 +19,7 @@ ENV CGO_ENABLED=1
|
||||
COPY --from=golang /usr/local/go /usr/local/go
|
||||
|
||||
RUN apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
apt-get install -y -qq --no-install-recommends \
|
||||
sudo \
|
||||
# gcc for cgo.
|
||||
|
||||
BIN
test/testdata/pdfengines/sample3.pdf
vendored
Normal file
BIN
test/testdata/pdfengines/sample3.pdf
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user