mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
16807bd57f | ||
|
|
8bc29ad92d | ||
|
|
51a913a5e4 | ||
|
|
c30da805b3 | ||
|
|
42ee593708 | ||
|
|
1b49b35365 | ||
|
|
333ab7066c |
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='.'
|
||||
51
.github/workflows/__build-push.yml
vendored
Normal file
51
.github/workflows/__build-push.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
runs_on:
|
||||
type: string
|
||||
required: true
|
||||
platform:
|
||||
type: string
|
||||
required: true
|
||||
gotenberg_version:
|
||||
type: string
|
||||
required: true
|
||||
alternate_repository:
|
||||
type: string
|
||||
default: ''
|
||||
outputs:
|
||||
tags:
|
||||
value: ${{ jobs.build_push.outputs.tags }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build_push:
|
||||
name: Build and push Docker images
|
||||
runs-on: ${{ inputs.runs_on }}
|
||||
outputs:
|
||||
tags: ${{ steps.build_push.outputs.tags }}
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push ${{ inputs.platform }}
|
||||
id: build_push
|
||||
uses: gotenberg/gotenberg-release-action/actions/build-push@main
|
||||
with:
|
||||
version: ${{ inputs.gotenberg_version }}
|
||||
platform: ${{ inputs.platform }}
|
||||
alternate_repository: ${{ inputs.alternate_repository }}
|
||||
28
.github/workflows/__delete.yml
vendored
Normal file
28
.github/workflows/__delete.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
gotenberg_version:
|
||||
type: string
|
||||
required: true
|
||||
alternate_repository:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
name: Delete Docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Delete
|
||||
uses: gotenberg/gotenberg-release-action/actions/delete@main
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
version: ${{ inputs.gotenberg_version }}
|
||||
alternate_repository: ${{ inputs.alternate_repository }}
|
||||
40
.github/workflows/__merge-clean.yml
vendored
Normal file
40
.github/workflows/__merge-clean.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tags:
|
||||
type: string
|
||||
required: true
|
||||
alternate_registry:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
merge_clean:
|
||||
name: Merge and clean Docker images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Merge and clean
|
||||
uses: gotenberg/gotenberg-release-action/actions/merge-clean@main
|
||||
with:
|
||||
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
tags: ${{ inputs.tags }}
|
||||
alternate_registry: ${{ inputs.alternate_registry }}
|
||||
58
.github/workflows/continuous-delivery.yml
vendored
Normal file
58
.github/workflows/continuous-delivery.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Continuous Delivery
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release_amd64:
|
||||
name: Release linux/amd64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
|
||||
release_386:
|
||||
name: Release linux/386
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/386
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
|
||||
release_arm64:
|
||||
name: Release linux/arm64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
|
||||
release_arm_v7:
|
||||
name: Release linux/arm/v7
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm/v7
|
||||
gotenberg_version: ${{ github.event.release.tag_name }}
|
||||
|
||||
merge_clean_release_tags:
|
||||
needs:
|
||||
- release_amd64
|
||||
- release_386
|
||||
- release_arm64
|
||||
- release_arm_v7
|
||||
name: Merge and clean release tags
|
||||
uses: ./.github/workflows/__merge-clean.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}"
|
||||
alternate_registry: thecodingmachine
|
||||
188
.github/workflows/continuous-integration.yml
vendored
Normal file
188
.github/workflows/continuous-integration.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
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@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
verbose: true
|
||||
|
||||
snapshot_amd64:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
name: Snapshot linux/amd64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
|
||||
snapshot_386:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
name: Snapshot linux/386
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/386
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
|
||||
snapshot_arm64:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
name: Snapshot linux/arm64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
|
||||
snapshot_arm_v7:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs:
|
||||
- tests
|
||||
name: Snapshot linux/arm/v7
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm/v7
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
|
||||
merge_clean_snapshot_tags:
|
||||
needs:
|
||||
- snapshot_amd64
|
||||
- snapshot_386
|
||||
- snapshot_arm64
|
||||
- snapshot_arm_v7
|
||||
name: Merge and clean snapshot tags
|
||||
uses: ./.github/workflows/__merge-clean.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}"
|
||||
|
||||
edge_amd64:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
name: Edge linux/amd64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
gotenberg_version: edge
|
||||
|
||||
edge_386:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
name: Edge linux/386
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
platform: linux/386
|
||||
gotenberg_version: edge
|
||||
|
||||
edge_arm64:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
name: Edge linux/arm64
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
gotenberg_version: edge
|
||||
|
||||
edge_arm_v7:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- tests
|
||||
name: Edge linux/arm/v7
|
||||
uses: ./.github/workflows/__build-push.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
runs_on: ubuntu-24.04-arm
|
||||
platform: linux/arm/v7
|
||||
gotenberg_version: edge
|
||||
|
||||
merge_clean_edge_tags:
|
||||
needs:
|
||||
- edge_amd64
|
||||
- edge_386
|
||||
- edge_arm64
|
||||
- edge_arm_v7
|
||||
name: Merge and clean edge tags
|
||||
uses: ./.github/workflows/__merge-clean.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}"
|
||||
alternate_registry: thecodingmachine
|
||||
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
|
||||
18
.github/workflows/pull-request-cleanup.yml
vendored
Normal file
18
.github/workflows/pull-request-cleanup.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Pull Request Cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
uses: ./.github/workflows/__delete.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gotenberg_version: pr-${{ github.event.pull_request.number }}
|
||||
alternate_repository: snapshot
|
||||
@@ -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,7 @@ 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
|
||||
CHROMIUM_RESTART_AFTER=10
|
||||
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||
CHROMIUM_AUTO_START=false
|
||||
CHROMIUM_START_TIMEOUT=20s
|
||||
@@ -73,6 +64,8 @@ LOG_FORMAT=auto
|
||||
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
|
||||
@@ -141,6 +134,8 @@ run: ## Start a Gotenberg container
|
||||
--log-fields-prefix=$(LOG_FIELDS_PREFIX) \
|
||||
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
||||
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
||||
--pdfengines-split-engines=$(PDFENGINES_SPLIT_ENGINES) \
|
||||
--pdfengines-convert-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) \
|
||||
@@ -197,20 +192,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 &&\
|
||||
|
||||
45
go.mod
45
go.mod
@@ -6,36 +6,39 @@ 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-20241110205750-a72e6703cd9b
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/chromedp/cdproto v0.0.0-20250126231910-1730200a0f74
|
||||
github.com/chromedp/chromedp v0.12.1
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
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.21 // 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.29.0 // indirect
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/sync v0.9.0
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/text v0.20.0
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
|
||||
require github.com/dlclark/regexp2 v1.11.4
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4
|
||||
github.com/shirou/gopsutil/v4 v4.24.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
@@ -43,21 +46,27 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/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/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // 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.14 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.60.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
github.com/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.35.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
)
|
||||
|
||||
92
go.sum
92
go.sum
@@ -11,10 +11,10 @@ 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/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-20241110205750-a72e6703cd9b h1:md1Gk5jkNE91SZxFDCMHmKqX0/GsEr1/VTejht0sCbY=
|
||||
github.com/chromedp/cdproto v0.0.0-20241110205750-a72e6703cd9b/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-20250126231910-1730200a0f74 h1:oul0R+ZyorVxVNr7bALbPolKvbXlpIefB4lDv5sjt00=
|
||||
github.com/chromedp/cdproto v0.0.0-20250126231910-1730200a0f74/go.mod h1:RTGuBeCeabAJGi3OZf71a6cGa7oYBfBP75VJZFLv6SU=
|
||||
github.com/chromedp/chromedp v0.12.1 h1:kBMblXk7xH5/6j3K9uk8d7/c+fzXWiUsCsPte0VMwOA=
|
||||
github.com/chromedp/chromedp v0.12.1/go.mod h1:F6+wdq9LKFDMoyxhq46ZLz4VLXrsrCAR3sFqJz4Nqc0=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -25,16 +25,19 @@ github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
|
||||
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/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/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-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
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=
|
||||
@@ -63,17 +66,18 @@ github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
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.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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/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.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=
|
||||
@@ -88,28 +92,36 @@ github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWk
|
||||
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.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/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.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
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/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/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
|
||||
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
github.com/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=
|
||||
@@ -122,30 +134,34 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
|
||||
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/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -3,22 +3,56 @@ package gotenberg
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MkdirAll defines the method signature for create a directory. Implement this
|
||||
// interface if you don't want to rely on [os.MkdirAll], notably for testing
|
||||
// purpose.
|
||||
type MkdirAll interface {
|
||||
// MkdirAll uses the same signature as [os.MkdirAll].
|
||||
MkdirAll(path string, perm os.FileMode) error
|
||||
}
|
||||
|
||||
// OsMkdirAll implements the [MkdirAll] interface with [os.MkdirAll].
|
||||
type OsMkdirAll struct{}
|
||||
|
||||
// MkdirAll is a wrapper around [os.MkdirAll].
|
||||
func (o *OsMkdirAll) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) }
|
||||
|
||||
// PathRename defines the method signature for renaming files. Implement this
|
||||
// interface if you don't want to rely on [os.Rename], notably for testing
|
||||
// purpose.
|
||||
type PathRename interface {
|
||||
// Rename uses the same signature as [os.Rename].
|
||||
Rename(oldpath, newpath string) error
|
||||
}
|
||||
|
||||
// OsPathRename implements the [PathRename] interface with [os.Rename].
|
||||
type OsPathRename struct{}
|
||||
|
||||
// Rename is a wrapper around [os.Rename].
|
||||
func (o *OsPathRename) Rename(oldpath, newpath string) error {
|
||||
return os.Rename(oldpath, newpath)
|
||||
}
|
||||
|
||||
// FileSystem provides utilities for managing temporary directories. It creates
|
||||
// unique directory names based on UUIDs to ensure isolation of temporary files
|
||||
// for different modules.
|
||||
type FileSystem struct {
|
||||
workingDir string
|
||||
mkdirAll MkdirAll
|
||||
}
|
||||
|
||||
// NewFileSystem initializes a new [FileSystem] instance with a unique working
|
||||
// directory.
|
||||
func NewFileSystem() *FileSystem {
|
||||
func NewFileSystem(mkdirAll MkdirAll) *FileSystem {
|
||||
return &FileSystem{
|
||||
workingDir: uuid.NewString(),
|
||||
mkdirAll: mkdirAll,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +78,7 @@ func (fs *FileSystem) NewDirPath() string {
|
||||
func (fs *FileSystem) MkdirAll() (string, error) {
|
||||
path := fs.NewDirPath()
|
||||
|
||||
err := os.MkdirAll(path, 0o755)
|
||||
err := fs.mkdirAll.MkdirAll(path, 0o755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create directory %s: %w", path, err)
|
||||
}
|
||||
@@ -52,10 +86,27 @@ func (fs *FileSystem) MkdirAll() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// PathRename defines the method signature for renaming files. Implement this
|
||||
// interface if you don't want to rely on [os.Rename], notably for testing
|
||||
// purpose.
|
||||
type PathRename interface {
|
||||
// Rename uses the same signature as [os.Rename].
|
||||
Rename(oldpath, newpath string) error
|
||||
// WalkDir walks through the root level of a directory and returns a list of
|
||||
// files paths that match the specified file extension.
|
||||
func WalkDir(dir, ext string) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(filepath.Ext(info.Name()), ext) {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ MkdirAll = (*OsMkdirAll)(nil)
|
||||
_ PathRename = (*OsPathRename)(nil)
|
||||
)
|
||||
|
||||
@@ -1,14 +1,84 @@
|
||||
package gotenberg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestOsMkdirAll_MkdirAll(t *testing.T) {
|
||||
dirPath, err := NewFileSystem(new(OsMkdirAll)).MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("create working directory: %v", err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("remove working directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOsPathRename_Rename(t *testing.T) {
|
||||
dirPath, err := NewFileSystem(new(OsMkdirAll)).MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("create working directory: %v", err)
|
||||
}
|
||||
|
||||
path := "/tests/test/testdata/api/sample1.txt"
|
||||
copyPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open file: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := in.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := os.Create(copyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create new file: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := out.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close new file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
t.Fatalf("copy file to new file: %v", err)
|
||||
}
|
||||
|
||||
rename := new(OsPathRename)
|
||||
newPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||
|
||||
err = rename.Rename(copyPath, newPath)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("remove working directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystem_WorkingDir(t *testing.T) {
|
||||
fs := NewFileSystem()
|
||||
fs := NewFileSystem(new(MkdirAllMock))
|
||||
dirName := fs.WorkingDir()
|
||||
|
||||
if dirName == "" {
|
||||
@@ -17,7 +87,7 @@ func TestFileSystem_WorkingDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFileSystem_WorkingDirPath(t *testing.T) {
|
||||
fs := NewFileSystem()
|
||||
fs := NewFileSystem(new(MkdirAllMock))
|
||||
expectedPath := fmt.Sprintf("%s/%s", os.TempDir(), fs.WorkingDir())
|
||||
|
||||
if fs.WorkingDirPath() != expectedPath {
|
||||
@@ -26,7 +96,7 @@ func TestFileSystem_WorkingDirPath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFileSystem_NewDirPath(t *testing.T) {
|
||||
fs := NewFileSystem()
|
||||
fs := NewFileSystem(new(MkdirAllMock))
|
||||
newDir := fs.NewDirPath()
|
||||
expectedPrefix := fs.WorkingDirPath()
|
||||
|
||||
@@ -36,20 +106,117 @@ func TestFileSystem_NewDirPath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFileSystem_MkdirAll(t *testing.T) {
|
||||
fs := NewFileSystem()
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
mkdirAll MkdirAll
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "error",
|
||||
mkdirAll: &MkdirAllMock{
|
||||
MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
mkdirAll: &MkdirAllMock{
|
||||
MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
fs := NewFileSystem(tc.mkdirAll)
|
||||
|
||||
newPath, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
_, err := fs.MkdirAll()
|
||||
|
||||
_, err = os.Stat(newPath)
|
||||
if os.IsNotExist(err) {
|
||||
t.Errorf("expected directory '%s' to exist but it doesn't", newPath)
|
||||
}
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDir(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
dir string
|
||||
ext string
|
||||
expectError bool
|
||||
expectFiles []string
|
||||
}{
|
||||
{
|
||||
scenario: "directory does not exist",
|
||||
dir: uuid.NewString(),
|
||||
ext: ".pdf",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "find PDF files",
|
||||
dir: func() string {
|
||||
path := fmt.Sprintf("%s/a_directory", os.TempDir())
|
||||
|
||||
err := os.MkdirAll(path, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_foo_file.pdf", path), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_bar_file.PDF", path), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/a_baz_file.txt", path), []byte{1}, 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return path
|
||||
}(),
|
||||
ext: ".pdf",
|
||||
expectError: false,
|
||||
expectFiles: []string{"/tmp/a_directory/a_bar_file.PDF", "/tmp/a_directory/a_foo_file.pdf"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
defer func() {
|
||||
err := os.RemoveAll(tc.dir)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
files, err := WalkDir(tc.dir, tc.ext)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectError && err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(files, tc.expectFiles) {
|
||||
t.Errorf("expected files %+v, but got %+v", tc.expectFiles, files)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@ package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -34,8 +35,12 @@ func (mod *ValidatorMock) Validate() error {
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -45,6 +50,14 @@ func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inpu
|
||||
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) Split(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
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)
|
||||
}
|
||||
@@ -137,6 +150,15 @@ func (provider *MetricsProviderMock) Metrics() ([]Metric, error) {
|
||||
return provider.MetricsMock()
|
||||
}
|
||||
|
||||
// MkdirAllMock is a mock for the [MkdirAll] interface.
|
||||
type MkdirAllMock struct {
|
||||
MkdirAllMock func(path string, perm os.FileMode) error
|
||||
}
|
||||
|
||||
func (mkdirAll *MkdirAllMock) MkdirAll(path string, perm os.FileMode) error {
|
||||
return mkdirAll.MkdirAllMock(path, perm)
|
||||
}
|
||||
|
||||
// PathRenameMock is a mock for the [PathRename] interface.
|
||||
type PathRenameMock struct {
|
||||
RenameMock func(oldpath, newpath string) error
|
||||
@@ -156,4 +178,6 @@ var (
|
||||
_ ProcessSupervisor = (*ProcessSupervisorMock)(nil)
|
||||
_ LoggerProvider = (*LoggerProviderMock)(nil)
|
||||
_ MetricsProvider = (*MetricsProviderMock)(nil)
|
||||
_ MkdirAll = (*MkdirAllMock)(nil)
|
||||
_ PathRename = (*PathRenameMock)(nil)
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package gotenberg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -52,6 +53,12 @@ func TestPDFEngineMock(t *testing.T) {
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -68,6 +75,16 @@ func TestPDFEngineMock(t *testing.T) {
|
||||
t.Errorf("expected no error from PdfEngineMock.Merge, but got: %v", err)
|
||||
}
|
||||
|
||||
_, err = mock.Split(context.Background(), zap.NewNop(), SplitMode{}, "", "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.Split, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Flatten(context.Background(), zap.NewNop(), "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||
@@ -205,6 +222,19 @@ func TestMetricsProviderMock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkdirAllMock(t *testing.T) {
|
||||
mock := &MkdirAllMock{
|
||||
MkdirAllMock: func(dir string, perm os.FileMode) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.MkdirAll("/foo", 0o755)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from MkdirAllMock.MkdirAll, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathRenameMock(t *testing.T) {
|
||||
mock := &PathRenameMock{
|
||||
RenameMock: func(oldpath, newpath string) error {
|
||||
|
||||
@@ -12,6 +12,10 @@ var (
|
||||
// PdfEngine interface is not supported by its current implementation.
|
||||
ErrPdfEngineMethodNotSupported = errors.New("method not supported")
|
||||
|
||||
// ErrPdfSplitModeNotSupported is returned when the Split method of the
|
||||
// PdfEngine interface does not sumport a requested PDF split mode.
|
||||
ErrPdfSplitModeNotSupported = errors.New("split mode not supported")
|
||||
|
||||
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
||||
// PdfEngine interface does not support a requested PDF format conversion.
|
||||
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
|
||||
@@ -21,6 +25,30 @@ var (
|
||||
ErrPdfEngineMetadataValueNotSupported = errors.New("metadata value not supported")
|
||||
)
|
||||
|
||||
const (
|
||||
// SplitModeIntervals represents a mode where a PDF is split at specific
|
||||
// intervals.
|
||||
SplitModeIntervals string = "intervals"
|
||||
|
||||
// SplitModePages represents a mode where a PDF is split at specific page
|
||||
// ranges.
|
||||
SplitModePages string = "pages"
|
||||
)
|
||||
|
||||
// SplitMode gathers the data required to split a PDF into multiple parts.
|
||||
type SplitMode struct {
|
||||
// Mode is either "intervals" or "pages".
|
||||
Mode string
|
||||
|
||||
// Span is either the intervals or the page ranges to extract, depending on
|
||||
// the selected mode.
|
||||
Span string
|
||||
|
||||
// Unify specifies whether to put extracted pages into a single file or as
|
||||
// many files as there are page ranges. Only works with "pages" mode.
|
||||
Unify bool
|
||||
}
|
||||
|
||||
const (
|
||||
// PdfA1a represents the PDF/A-1a format.
|
||||
PdfA1a string = "PDF/A-1a"
|
||||
@@ -60,11 +88,22 @@ 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.
|
||||
Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||
|
||||
// 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
|
||||
|
||||
@@ -318,7 +318,7 @@ func (a *Api) Provision(ctx *gotenberg.Context) error {
|
||||
a.logger = logger
|
||||
|
||||
// File system.
|
||||
a.fs = gotenberg.NewFileSystem()
|
||||
a.fs = gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -456,14 +456,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 +491,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...)
|
||||
@@ -509,6 +535,7 @@ func (a *Api) Start() error {
|
||||
func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, gotenberg.Version)
|
||||
},
|
||||
securityMiddleware,
|
||||
)
|
||||
|
||||
// Wait for all modules to be ready.
|
||||
|
||||
@@ -850,7 +850,7 @@ func TestApi_Start(t *testing.T) {
|
||||
},
|
||||
}
|
||||
mod.readyFn = tc.readyFn
|
||||
mod.fs = gotenberg.NewFileSystem()
|
||||
mod.fs = gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
mod.logger = zap.NewNop()
|
||||
|
||||
err := mod.Start()
|
||||
@@ -866,15 +866,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,7 +900,9 @@ 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)
|
||||
|
||||
@@ -47,6 +47,7 @@ type Context struct {
|
||||
|
||||
logger *zap.Logger
|
||||
echoCtx echo.Context
|
||||
mkdirAll gotenberg.MkdirAll
|
||||
pathRename gotenberg.PathRename
|
||||
context.Context
|
||||
}
|
||||
@@ -81,12 +82,6 @@ type downloadFrom struct {
|
||||
ExtraHttpHeaders map[string]string `json:"extraHttpHeaders"`
|
||||
}
|
||||
|
||||
type osPathRename struct{}
|
||||
|
||||
func (o *osPathRename) Rename(oldpath, newpath string) error {
|
||||
return os.Rename(oldpath, newpath)
|
||||
}
|
||||
|
||||
// newContext returns a [Context] by parsing a "multipart/form-data" request.
|
||||
func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSystem, timeout time.Duration, bodyLimit int64, downloadFromCfg downloadFromConfig, traceHeader, trace string) (*Context, context.CancelFunc, error) {
|
||||
processCtx, processCancel := context.WithTimeout(context.Background(), timeout)
|
||||
@@ -112,7 +107,8 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
||||
cancelled: false,
|
||||
logger: logger,
|
||||
echoCtx: echoCtx,
|
||||
pathRename: new(osPathRename),
|
||||
mkdirAll: new(gotenberg.OsMkdirAll),
|
||||
pathRename: new(gotenberg.OsPathRename),
|
||||
Context: processCtx,
|
||||
}
|
||||
|
||||
@@ -414,9 +410,21 @@ func (ctx *Context) GeneratePath(extension string) string {
|
||||
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New().String(), extension)
|
||||
}
|
||||
|
||||
// CreateSubDirectory creates a subdirectory within the context's working
|
||||
// directory.
|
||||
func (ctx *Context) CreateSubDirectory(dirName string) (string, error) {
|
||||
path := fmt.Sprintf("%s/%s", ctx.dirPath, dirName)
|
||||
err := ctx.mkdirAll.MkdirAll(path, 0o755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create sub-directory %s: %w", path, err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Rename is just a wrapper around [os.Rename], as we need to mock this
|
||||
// behavior in our tests.
|
||||
func (ctx *Context) Rename(oldpath, newpath string) error {
|
||||
ctx.Log().Debug(fmt.Sprintf("rename %s to %s", oldpath, newpath))
|
||||
err := ctx.pathRename.Rename(oldpath, newpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename path: %w", err)
|
||||
@@ -496,8 +504,3 @@ func (ctx *Context) OutputFilename(outputPath string) string {
|
||||
|
||||
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
|
||||
}
|
||||
|
||||
// Interface guard.
|
||||
var (
|
||||
_ gotenberg.PathRename = (*osPathRename)(nil)
|
||||
)
|
||||
|
||||
@@ -4,78 +4,22 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestOsPathRename_Rename(t *testing.T) {
|
||||
dirPath, err := gotenberg.NewFileSystem().MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("create working directory: %v", err)
|
||||
}
|
||||
|
||||
path := "/tests/test/testdata/api/sample1.txt"
|
||||
copyPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open file: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := in.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := os.Create(copyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create new file: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := out.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("close new file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
t.Fatalf("copy file to new file: %v", err)
|
||||
}
|
||||
|
||||
rename := new(osPathRename)
|
||||
newPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString()))
|
||||
|
||||
err = rename.Rename(copyPath, newPath)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("remove working directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
defaultAllowList, err := regexp2.Compile("", 0)
|
||||
if err != nil {
|
||||
@@ -548,7 +492,7 @@ func TestNewContext(t *testing.T) {
|
||||
}
|
||||
|
||||
handler := func(c echo.Context) error {
|
||||
ctx, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(), time.Duration(10)*time.Second, tc.bodyLimit, tc.downloadFromCfg, "Gotenberg-Trace", "123")
|
||||
ctx, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, tc.bodyLimit, tc.downloadFromCfg, "Gotenberg-Trace", "123")
|
||||
defer cancel()
|
||||
// Context already cancelled.
|
||||
defer cancel()
|
||||
@@ -647,6 +591,42 @@ func TestContext_FormData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_CreateSubDirectory(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "failure",
|
||||
ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return errors.New("cannot rename")
|
||||
}}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}}},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.logger = zap.NewNop()
|
||||
_, err := tc.ctx.CreateSubDirectory("foo")
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_GeneratePath(t *testing.T) {
|
||||
ctx := &Context{
|
||||
dirPath: "/foo",
|
||||
@@ -680,6 +660,7 @@ func TestContext_Rename(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.logger = zap.NewNop()
|
||||
err := tc.ctx.Rename("", "")
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
@@ -788,7 +769,7 @@ func TestContext_BuildOutputFile(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
dirPath, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no erro but got: %v", err)
|
||||
|
||||
@@ -48,6 +48,10 @@ func ParseError(err error) (int, string) {
|
||||
return http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
if errors.Is(err, gotenberg.ErrPdfSplitModeNotSupported) {
|
||||
return http.StatusBadRequest, "At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues"
|
||||
}
|
||||
|
||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||
return http.StatusBadRequest, "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues"
|
||||
}
|
||||
@@ -157,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, ""),
|
||||
"/",
|
||||
@@ -173,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)
|
||||
|
||||
@@ -208,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
|
||||
|
||||
@@ -38,6 +38,11 @@ func TestParseError(t *testing.T) {
|
||||
expectStatus: http.StatusTooManyRequests,
|
||||
expectMessage: http.StatusText(http.StatusTooManyRequests),
|
||||
},
|
||||
{
|
||||
err: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectMessage: "At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues",
|
||||
},
|
||||
{
|
||||
err: gotenberg.ErrPdfFormatNotSupported,
|
||||
expectStatus: http.StatusBadRequest,
|
||||
@@ -462,7 +467,7 @@ func TestContextMiddleware(t *testing.T) {
|
||||
c.Set("trace", "foo")
|
||||
c.Set("startTime", time.Now())
|
||||
|
||||
err := contextMiddleware(gotenberg.NewFileSystem(), time.Duration(10)*time.Second, 0, downloadFromConfig{})(tc.next)(c)
|
||||
err := contextMiddleware(gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, 0, downloadFromConfig{})(tc.next)(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
|
||||
@@ -86,6 +86,14 @@ func (ctx *ContextMock) SetEchoContext(c echo.Context) {
|
||||
ctx.Context.echoCtx = c
|
||||
}
|
||||
|
||||
// SetMkdirAll sets the [gotenberg.MkdirAll].
|
||||
//
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
// ctx.SetMkdirAll(mkdirAll)
|
||||
func (ctx *ContextMock) SetMkdirAll(mkdirAll gotenberg.MkdirAll) {
|
||||
ctx.Context.mkdirAll = mkdirAll
|
||||
}
|
||||
|
||||
// SetPathRename sets the [gotenberg.PathRename].
|
||||
//
|
||||
// ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/alexliesenfeld/health"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestContextMock_SetDirPath(t *testing.T) {
|
||||
@@ -117,10 +119,23 @@ func TestContextMock_SetEchoContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetMkdirAll(t *testing.T) {
|
||||
mock := ContextMock{&Context{}}
|
||||
|
||||
expect := new(gotenberg.OsMkdirAll)
|
||||
mock.SetMkdirAll(expect)
|
||||
|
||||
actual := mock.mkdirAll
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected %v but got %v", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMock_SetPathRename(t *testing.T) {
|
||||
mock := ContextMock{&Context{}}
|
||||
|
||||
expect := new(osPathRename)
|
||||
expect := new(gotenberg.OsPathRename)
|
||||
mock.SetPathRename(expect)
|
||||
|
||||
actual := mock.pathRename
|
||||
|
||||
@@ -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"
|
||||
@@ -62,7 +63,7 @@ func newChromiumBrowser(arguments browserArguments) browser {
|
||||
b := &chromiumBrowser{
|
||||
initialCtx: context.Background(),
|
||||
arguments: arguments,
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
}
|
||||
b.isStarted.Store(false)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -263,7 +263,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
b.isStarted.Store(false)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -275,7 +275,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
b.isStarted.Store(true)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: true,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -291,7 +291,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
b.isStarted.Store(true)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -308,7 +308,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
b.isStarted.Store(true)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -325,7 +325,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -357,7 +357,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -389,7 +389,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -424,7 +424,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -457,7 +457,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -495,7 +495,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -528,7 +528,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -554,7 +554,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -588,7 +588,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -621,7 +621,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -654,7 +654,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -688,7 +688,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -723,7 +723,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -758,7 +758,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -812,7 +812,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -845,7 +845,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -881,7 +881,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -914,7 +914,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -949,7 +949,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -984,7 +984,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1019,7 +1019,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1065,7 +1065,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1100,7 +1100,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1146,7 +1146,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1181,7 +1181,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1217,7 +1217,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1255,7 +1255,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1288,7 +1288,7 @@ func TestChromiumBrowser_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1421,7 +1421,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
b.isStarted.Store(false)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -1437,7 +1437,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
b.isStarted.Store(true)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: true,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -1453,7 +1453,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
b.isStarted.Store(true)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -1470,7 +1470,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
b.isStarted.Store(true)
|
||||
return b
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
noDeadline: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -1487,7 +1487,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1519,7 +1519,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1551,7 +1551,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1586,7 +1586,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1619,7 +1619,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1657,7 +1657,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1690,7 +1690,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1716,7 +1716,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1750,7 +1750,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1783,7 +1783,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1816,7 +1816,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1850,7 +1850,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1885,7 +1885,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1920,7 +1920,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -1974,7 +1974,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2009,7 +2009,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2042,7 +2042,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2077,7 +2077,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2112,7 +2112,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2147,7 +2147,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2193,7 +2193,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2228,7 +2228,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2274,7 +2274,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2317,7 +2317,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -2365,7 +2365,7 @@ func TestChromiumBrowser_screenshot(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
|
||||
@@ -352,7 +352,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")
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
|
||||
)
|
||||
|
||||
// FormDataChromiumOptions creates [Options] from the form data. Fallback to
|
||||
@@ -316,39 +317,6 @@ func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, Screens
|
||||
return form, screenshotOptions
|
||||
}
|
||||
|
||||
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
||||
// data. Fallback to default value if the considered key is not present.
|
||||
func FormDataChromiumPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||
var (
|
||||
pdfa string
|
||||
pdfua bool
|
||||
)
|
||||
|
||||
form.
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false)
|
||||
|
||||
return gotenberg.PdfFormats{
|
||||
PdfA: pdfa,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
}
|
||||
|
||||
// FormDataPdfMetadata creates metadata object from the form data.
|
||||
func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
|
||||
var metadata map[string]interface{}
|
||||
form.Custom("metadata", func(value string) error {
|
||||
if len(value) > 0 {
|
||||
err := json.Unmarshal([]byte(value), &metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return metadata
|
||||
}
|
||||
|
||||
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
|
||||
func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
@@ -358,8 +326,9 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form)
|
||||
mode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
|
||||
var url string
|
||||
err := form.
|
||||
@@ -369,7 +338,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||
}
|
||||
@@ -418,8 +387,9 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form)
|
||||
mode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
|
||||
var inputPath string
|
||||
err := form.
|
||||
@@ -430,7 +400,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("file://%s", inputPath)
|
||||
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||
}
|
||||
@@ -480,8 +450,9 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form)
|
||||
mode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
|
||||
var (
|
||||
inputPath string
|
||||
@@ -501,7 +472,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
||||
}
|
||||
|
||||
err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||
}
|
||||
@@ -625,7 +596,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
|
||||
return fmt.Sprintf("file://%s", inputPath), nil
|
||||
}
|
||||
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}) error {
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}) error {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
||||
@@ -664,34 +635,39 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
||||
return fmt.Errorf("convert to PDF: %w", err)
|
||||
}
|
||||
|
||||
// So far so good, the URL has been converted to PDF.
|
||||
// Now, let's check if the client want to convert the resulting PDF
|
||||
// to specific formats.
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if pdfFormats != zeroValued {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
}
|
||||
|
||||
// Important: the output path is now the converted file.
|
||||
outputPath = convertOutputPath
|
||||
}
|
||||
|
||||
// Writes and potentially overrides metadata entries, if any.
|
||||
if len(metadata) > 0 {
|
||||
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(outputPath)
|
||||
outputPaths, err := pdfengines.SplitPdfStub(ctx, engine, mode, []string{outputPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output path: %w", err)
|
||||
return fmt.Errorf("split PDF: %w", err)
|
||||
}
|
||||
|
||||
convertOutputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF(s): %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.WriteMetadataStub(ctx, engine, metadata, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
zeroValuedSplitMode := gotenberg.SplitMode{}
|
||||
zeroValuedPdfFormats := gotenberg.PdfFormats{}
|
||||
if mode != zeroValuedSplitMode && pdfFormats != zeroValuedPdfFormats {
|
||||
// The PDF has been split and split parts have been converted to a
|
||||
// specific format. We want to keep the split naming.
|
||||
for i, convertOutputPath := range convertOutputPaths {
|
||||
err = ctx.Rename(convertOutputPath, outputPaths[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename output path: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outputPaths = convertOutputPaths
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(outputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -603,125 +603,6 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataChromiumPdfFormats(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedPdfFormats gotenberg.PdfFormats
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{},
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "pdfa and pdfua form fields",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
"foo",
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataChromiumPdfFormats(form)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataPdfMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedMetadata map[string]interface{}
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no metadata form field",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid metadata form field",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"metadata": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid metadata form field",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"metadata": {
|
||||
"{\"foo\":\"bar\"}",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedMetadata: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataPdfMetadata(form)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertUrlRoute(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -1547,6 +1428,7 @@ func TestConvertUrl(t *testing.T) {
|
||||
api Api
|
||||
engine gotenberg.PdfEngine
|
||||
options PdfOptions
|
||||
splitMode gotenberg.SplitMode
|
||||
pdfFormats gotenberg.PdfFormats
|
||||
metadata map[string]interface{}
|
||||
expectError bool
|
||||
@@ -1689,6 +1571,36 @@ func TestConvertUrl(t *testing.T) {
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine split error",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return nil
|
||||
}},
|
||||
engine: &gotenberg.PdfEngineMock{SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
}},
|
||||
options: DefaultPdfOptions(),
|
||||
splitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success with split mode",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return nil
|
||||
}},
|
||||
engine: &gotenberg.PdfEngineMock{SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
}},
|
||||
options: DefaultPdfOptions(),
|
||||
splitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1"},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine convert error",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
@@ -1719,6 +1631,27 @@ func TestConvertUrl(t *testing.T) {
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success with split mode and PDF formats",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||
return nil
|
||||
}},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
options: DefaultPdfOptions(),
|
||||
splitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1"},
|
||||
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine write metadata error",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
@@ -1778,7 +1711,13 @@ func TestConvertUrl(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.options, tc.pdfFormats, tc.metadata)
|
||||
tc.ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
tc.ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}})
|
||||
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.options, tc.splitMode, tc.pdfFormats, tc.metadata)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/barasher/go-exiftool"
|
||||
"go.uber.org/zap"
|
||||
@@ -57,6 +58,16 @@ func (engine *ExifTool) Merge(ctx context.Context, logger *zap.Logger, inputPath
|
||||
return fmt.Errorf("merge PDFs with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Split is not available in this implementation.
|
||||
func (engine *ExifTool) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
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)
|
||||
@@ -109,6 +120,17 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
fileMetadata[0].SetString(key, val)
|
||||
case []string:
|
||||
fileMetadata[0].SetStrings(key, val)
|
||||
case []interface{}:
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1048.
|
||||
strings := make([]string, len(val))
|
||||
for i, entry := range val {
|
||||
if str, ok := entry.(string); ok {
|
||||
strings[i] = str
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
}
|
||||
fileMetadata[0].SetStrings(key, strings)
|
||||
case bool:
|
||||
fileMetadata[0].SetString(key, fmt.Sprintf("%t", val))
|
||||
case int:
|
||||
@@ -122,7 +144,7 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
// TODO: support more complex cases, e.g., arrays and nested objects
|
||||
// (limitations in underlying library).
|
||||
default:
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %w", gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
return fmt.Errorf("write PDF metadata with ExifTool: %s %+v %s %w", key, val, reflect.TypeOf(val), gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,24 @@ func TestExiftool_Merge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Split(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
_, err := engine.Split(context.Background(), zap.NewNop(), gotenberg.SplitMode{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Flatten(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Flatten(context.Background(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExiftool_Convert(t *testing.T) {
|
||||
engine := new(ExifTool)
|
||||
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
@@ -162,7 +180,20 @@ func TestExiftool_WriteMetadata(t *testing.T) {
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported",
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (not string array)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Unsupported": []interface{}{
|
||||
"foo",
|
||||
1,
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (default)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
@@ -171,6 +202,24 @@ func TestExiftool_WriteMetadata(t *testing.T) {
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "success (interface array to string array)",
|
||||
createCopy: true,
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
metadata: map[string]interface{}{
|
||||
"Keywords": []interface{}{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
},
|
||||
expectMetadata: map[string]interface{}{
|
||||
"Keywords": []interface{}{
|
||||
"first",
|
||||
"second",
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success",
|
||||
createCopy: true,
|
||||
@@ -226,7 +275,7 @@ func TestExiftool_WriteMetadata(t *testing.T) {
|
||||
|
||||
var destinationPath string
|
||||
if tc.createCopy {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error no but got: %v", err)
|
||||
|
||||
@@ -44,7 +44,7 @@ type libreOfficeProcess struct {
|
||||
func newLibreOfficeProcess(arguments libreOfficeArguments) libreOffice {
|
||||
p := &libreOfficeProcess{
|
||||
arguments: arguments,
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
}
|
||||
p.isStarted.Store(false)
|
||||
|
||||
@@ -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()
|
||||
@@ -365,13 +366,6 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
return ErrRuntimeException
|
||||
}
|
||||
|
||||
// Possible errors:
|
||||
// 1. LibreOffice failed for some reason.
|
||||
// 2. Context done.
|
||||
//
|
||||
// On the second scenario, LibreOffice might not have time to remove some
|
||||
// of its temporary files, as it has been killed without warning. The
|
||||
// garbage collector will delete them for us (if the module is loaded).
|
||||
return fmt.Errorf("convert to PDF: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
p.isStarted.Store(false)
|
||||
return p
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
cancelledCtx: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -243,7 +243,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
p.isStarted.Store(true)
|
||||
return p
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)),
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: "foo"}},
|
||||
cancelledCtx: false,
|
||||
start: false,
|
||||
@@ -261,7 +261,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
),
|
||||
options: Options{PageRanges: "foo"},
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -291,7 +291,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
),
|
||||
options: Options{Password: "foo"},
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -344,7 +344,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -372,7 +372,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -400,7 +400,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -452,7 +452,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -481,7 +481,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -510,7 +510,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -539,7 +539,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -625,7 +625,7 @@ func TestNonBasicLatinCharactersGuard(t *testing.T) {
|
||||
{
|
||||
scenario: "basic latin characters",
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
@@ -646,7 +646,7 @@ func TestNonBasicLatinCharactersGuard(t *testing.T) {
|
||||
{
|
||||
scenario: "non-basic latin characters",
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
|
||||
@@ -51,6 +51,16 @@ func (engine *LibreOfficePdfEngine) Merge(ctx context.Context, logger *zap.Logge
|
||||
return fmt.Errorf("merge PDFs with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Split is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
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]
|
||||
|
||||
@@ -118,11 +118,30 @@ func TestLibreOfficePdfEngine_Merge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Split(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
_, err := engine.Split(context.Background(), zap.NewNop(), gotenberg.SplitMode{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Flatten(t *testing.T) {
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
err := engine.Flatten(context.Background(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
api api.Uno
|
||||
expectError bool
|
||||
scenario string
|
||||
api api.Uno
|
||||
expectError bool
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
scenario: "convert success",
|
||||
@@ -134,13 +153,14 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "invalid PDF format",
|
||||
scenario: "ErrInvalidPdfFormats",
|
||||
api: &api.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error {
|
||||
return api.ErrInvalidPdfFormats
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfFormatNotSupported,
|
||||
},
|
||||
{
|
||||
scenario: "convert fail",
|
||||
@@ -163,6 +183,10 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
|
||||
)
|
||||
|
||||
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
||||
@@ -26,7 +27,13 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
defaultOptions := libreofficeapi.DefaultOptions()
|
||||
|
||||
// Let's get the data from the form and validate them.
|
||||
form := ctx.FormData()
|
||||
splitMode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
|
||||
zeroValuedSplitMode := gotenberg.SplitMode{}
|
||||
|
||||
var (
|
||||
inputPaths []string
|
||||
password string
|
||||
@@ -51,14 +58,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
quality int
|
||||
reduceImageResolution bool
|
||||
maxImageResolution int
|
||||
pdfa string
|
||||
pdfua bool
|
||||
nativePdfFormats bool
|
||||
merge bool
|
||||
metadata map[string]interface{}
|
||||
flatten bool
|
||||
)
|
||||
|
||||
err := ctx.FormData().
|
||||
err := form.
|
||||
MandatoryPaths(libreOffice.Extensions(), &inputPaths).
|
||||
String("password", &password, defaultOptions.Password).
|
||||
Bool("landscape", &landscape, defaultOptions.Landscape).
|
||||
@@ -120,8 +125,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
maxImageResolution = intValue
|
||||
return nil
|
||||
}).
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false).
|
||||
Bool("nativePdfFormats", &nativePdfFormats, true).
|
||||
Bool("merge", &merge, false).
|
||||
Custom("metadata", func(value string) error {
|
||||
@@ -133,17 +136,12 @@ 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)
|
||||
}
|
||||
|
||||
pdfFormats := gotenberg.PdfFormats{
|
||||
PdfA: pdfa,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
|
||||
// Alright, let's convert each document to PDF.
|
||||
outputPaths := make([]string, len(inputPaths))
|
||||
for i, inputPath := range inputPaths {
|
||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
@@ -172,7 +170,9 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
MaxImageResolution: maxImageResolution,
|
||||
}
|
||||
|
||||
if nativePdfFormats {
|
||||
if nativePdfFormats && splitMode == zeroValuedSplitMode {
|
||||
// Only apply natively given PDF formats if we're not
|
||||
// splitting the PDF later.
|
||||
options.PdfFormats = pdfFormats
|
||||
}
|
||||
|
||||
@@ -206,11 +206,8 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
}
|
||||
}
|
||||
|
||||
// So far so good, let's check if we have to merge the PDFs.
|
||||
if len(outputPaths) > 1 && merge {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Merge(ctx, ctx.Log(), outputPaths, outputPath)
|
||||
if merge {
|
||||
outputPath, err := pdfengines.MergeStub(ctx, engine, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge PDFs: %w", err)
|
||||
}
|
||||
@@ -219,37 +216,61 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
outputPaths = []string{outputPath}
|
||||
}
|
||||
|
||||
// Let's check if the client want to convert each PDF to a specific
|
||||
// PDF format.
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if !nativePdfFormats && pdfFormats != zeroValued {
|
||||
convertOutputPaths := make([]string, len(outputPaths))
|
||||
if splitMode != zeroValuedSplitMode {
|
||||
if !merge {
|
||||
// document.docx -> document.docx.pdf, so that split naming
|
||||
// document.docx_0.pdf, etc.
|
||||
for i, inputPath := range inputPaths {
|
||||
outputPath := fmt.Sprintf("%s.pdf", inputPath)
|
||||
|
||||
for i, outputPath := range outputPaths {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
err = ctx.Rename(outputPaths[i], outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename output path: %w", err)
|
||||
}
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
outputPaths[i] = outputPath
|
||||
}
|
||||
}
|
||||
|
||||
// Important: the output paths are now the converted files.
|
||||
outputPaths = convertOutputPaths
|
||||
}
|
||||
|
||||
// Writes and potentially overrides metadata entries, if any.
|
||||
if len(metadata) > 0 {
|
||||
for _, outputPath := range outputPaths {
|
||||
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
outputPaths, err = pdfengines.SplitPdfStub(ctx, engine, splitMode, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("split PDFs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(outputPaths) > 1 {
|
||||
if !nativePdfFormats || (nativePdfFormats && splitMode != zeroValuedSplitMode) {
|
||||
convertOutputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDFs: %w", err)
|
||||
}
|
||||
|
||||
if splitMode != zeroValuedSplitMode {
|
||||
// The PDF has been split and split parts have been converted to
|
||||
// specific formats. We want to keep the split naming.
|
||||
for i, convertOutputPath := range convertOutputPaths {
|
||||
err = ctx.Rename(convertOutputPath, outputPaths[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename output path: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outputPaths = convertOutputPaths
|
||||
}
|
||||
}
|
||||
|
||||
err = pdfengines.WriteMetadataStub(ctx, engine, metadata, outputPaths)
|
||||
if err != nil {
|
||||
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 {
|
||||
outputPath := fmt.Sprintf("%s.pdf", inputPath)
|
||||
@@ -263,8 +284,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
}
|
||||
}
|
||||
|
||||
// Last but not least, add the output paths to the context so that
|
||||
// the API is able to send them as a response to the client.
|
||||
err = ctx.AddOutputPaths(outputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
|
||||
@@ -3,7 +3,10 @@ package libreoffice
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
@@ -300,6 +303,40 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine split error",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "PDF engine convert error",
|
||||
ctx: func() *api.ContextMock {
|
||||
@@ -366,17 +403,18 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "cannot rename many files",
|
||||
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",
|
||||
"document2.docx": "/document2.docx",
|
||||
"document2.doc": "/document2.doc",
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return errors.New("cannot rename")
|
||||
}})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
ctx.SetCancelled(true)
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
@@ -384,7 +422,12 @@ func TestConvertRoute(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx", ".doc"}
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
@@ -443,6 +486,9 @@ func TestConvertRoute(t *testing.T) {
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
"flatten": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
@@ -464,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,
|
||||
@@ -491,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
|
||||
@@ -515,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,
|
||||
@@ -550,9 +605,173 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success with split (many files)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
return []string{
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 0,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 1,
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 4,
|
||||
expectOutputPaths: []string{"/document_docx/document.docx_0.pdf", "/document_docx/document.docx_1.pdf", "/document2_docx/document2.docx_0.pdf", "/document2_docx/document2.docx_1.pdf"},
|
||||
},
|
||||
{
|
||||
scenario: "success with merge and split",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"merge": {
|
||||
"true",
|
||||
},
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
return []string{
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 0,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 1,
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 2,
|
||||
},
|
||||
{
|
||||
scenario: "success with split and native PDF/A & PDF/UA (many files)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
"document2.docx": "/document2.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1b,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
libreOffice: &libreofficeapi.ApiMock{
|
||||
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||
return nil
|
||||
},
|
||||
ExtensionsMock: func() []string {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
return []string{
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 0,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, 1,
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 4,
|
||||
expectOutputPaths: []string{"/document_docx/document.docx_0.pdf", "/document_docx/document.docx_1.pdf", "/document2_docx/document2.docx_0.pdf", "/document2_docx/document2.docx_1.pdf"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
tc.ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
tc.ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}})
|
||||
c := echo.New().NewContext(nil, nil)
|
||||
c.Set("context", tc.ctx.Context)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// interface using the pdfcpu command-line tool. This package allows for:
|
||||
//
|
||||
// 1. The merging of PDF files.
|
||||
// 2. The splitting of PDF files.
|
||||
//
|
||||
// See: https://github.com/pdfcpu/pdfcpu.
|
||||
package pdfcpu
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -70,6 +71,47 @@ func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths
|
||||
return fmt.Errorf("merge PDFs with pdfcpu: %w", err)
|
||||
}
|
||||
|
||||
// Split splits a given PDF file.
|
||||
func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
var args []string
|
||||
|
||||
switch mode.Mode {
|
||||
case gotenberg.SplitModeIntervals:
|
||||
args = append(args, "split", "-mode", "span", inputPath, outputDirPath, mode.Span)
|
||||
case gotenberg.SplitModePages:
|
||||
if mode.Unify {
|
||||
outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath))
|
||||
args = append(args, "trim", "-pages", mode.Span, inputPath, outputPath)
|
||||
break
|
||||
}
|
||||
args = append(args, "extract", "-mode", "page", "-pages", mode.Span, inputPath, outputDirPath)
|
||||
default:
|
||||
return nil, fmt.Errorf("split PDFs using mode '%s' with pdfcpu: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||
}
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
_, err = cmd.Exec()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("split PDFs with pdfcpu: %w", err)
|
||||
}
|
||||
|
||||
outputPaths, err := gotenberg.WalkDir(outputDirPath, ".pdf")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("walk directory to find resulting PDFs from split with pdfcpu: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -116,7 +116,7 @@ func TestPdfCpu_Merge(t *testing.T) {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
@@ -142,6 +142,112 @@ func TestPdfCpu_Merge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Split(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
mode gotenberg.SplitMode
|
||||
inputPath string
|
||||
expectError bool
|
||||
expectedError error
|
||||
expectOutputPathsCount int
|
||||
}{
|
||||
{
|
||||
scenario: "ErrPdfSplitModeNotSupported",
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
inputPath: "",
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success (intervals)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 3,
|
||||
},
|
||||
{
|
||||
scenario: "success (pages)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1"},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success (pages & unify)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
outputPaths, err := engine.Split(tc.ctx, zap.NewNop(), tc.mode, tc.inputPath, outputDir)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(outputPaths) {
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(outputPaths))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Flatten(t *testing.T) {
|
||||
mod := new(PdfCpu)
|
||||
err := mod.Flatten(context.TODO(), zap.NewNop(), "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Convert(t *testing.T) {
|
||||
mod := new(PdfCpu)
|
||||
err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
@@ -12,23 +12,29 @@ import (
|
||||
)
|
||||
|
||||
type multiPdfEngines struct {
|
||||
mergeEngines []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,
|
||||
convertEngines: convertEngines,
|
||||
readMedataEngines: readMetadataEngines,
|
||||
writeMedataEngines: writeMedataEngines,
|
||||
mergeEngines: mergeEngines,
|
||||
splitEngines: splitEngines,
|
||||
flattenEngines: flattenEngines,
|
||||
convertEngines: convertEngines,
|
||||
readMetadataEngines: readMetadataEngines,
|
||||
writeMetadataEngines: writeMetadataEngines,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +63,69 @@ func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inp
|
||||
return fmt.Errorf("merge PDFs with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
type splitResult struct {
|
||||
outputPaths []string
|
||||
err error
|
||||
}
|
||||
|
||||
// Split tries to split at intervals a given PDF thanks to its children. If the
|
||||
// context is done, it stops and returns an error.
|
||||
func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
resultChan := make(chan splitResult, len(multi.splitEngines))
|
||||
|
||||
for _, engine := range multi.splitEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
outputPaths, err := engine.Split(ctx, logger, mode, inputPath, outputDirPath)
|
||||
resultChan <- splitResult{outputPaths: outputPaths, err: err}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
for range multi.splitEngines {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = multierr.Append(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
return result.outputPaths, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -91,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 {
|
||||
@@ -122,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)
|
||||
|
||||
@@ -19,25 +19,22 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -49,17 +46,14 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -71,27 +65,21 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
@@ -115,6 +103,188 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Split(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, 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.Split(tc.ctx, zap.NewNop(), gotenberg.SplitMode{}, "", "")
|
||||
|
||||
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_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
|
||||
@@ -124,25 +294,21 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -154,16 +320,13 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -175,26 +338,21 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
@@ -227,26 +385,21 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
@@ -258,16 +411,13 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
@@ -279,25 +429,21 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
@@ -330,27 +476,21 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -362,16 +502,13 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -383,24 +520,21 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
@@ -27,12 +27,14 @@ func init() {
|
||||
// the [api.Router] interface to expose relevant PDF processing routes if
|
||||
// enabled.
|
||||
type PdfEngines struct {
|
||||
mergeNames []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.
|
||||
@@ -42,6 +44,8 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
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")
|
||||
@@ -64,6 +68,8 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
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")
|
||||
@@ -98,6 +104,16 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.mergeNames = mergeNames
|
||||
}
|
||||
|
||||
mod.splitNames = defaultNames
|
||||
if len(splitNames) > 0 {
|
||||
mod.splitNames = splitNames
|
||||
}
|
||||
|
||||
mod.flattenNames = defaultNames
|
||||
if len(flattenNames) > 0 {
|
||||
mod.flattenNames = flattenNames
|
||||
}
|
||||
|
||||
mod.convertNames = defaultNames
|
||||
if len(convertNames) > 0 {
|
||||
mod.convertNames = convertNames
|
||||
@@ -108,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
|
||||
@@ -161,9 +177,11 @@ 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
|
||||
@@ -177,9 +195,11 @@ func (mod *PdfEngines) Validate() error {
|
||||
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[:], " ")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,9 +221,11 @@ 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
|
||||
}
|
||||
|
||||
@@ -222,6 +244,8 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||
|
||||
return []api.Route{
|
||||
mergeRoute(engine),
|
||||
splitRoute(engine),
|
||||
flattenRoute(engine),
|
||||
convertRoute(engine),
|
||||
readMetadataRoute(engine),
|
||||
writeMetadataRoute(engine),
|
||||
|
||||
@@ -26,6 +26,8 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectedMergePdfEngines []string
|
||||
expectedSplitPdfEngines []string
|
||||
expectedFlattenPdfEngines []string
|
||||
expectedConvertPdfEngines []string
|
||||
expectedReadMetadataPdfEngines []string
|
||||
expectedWriteMetadataPdfEngines []string
|
||||
@@ -66,6 +68,8 @@ 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"},
|
||||
@@ -107,7 +111,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}
|
||||
|
||||
fs := new(PdfEngines).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--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)
|
||||
}
|
||||
@@ -125,6 +129,8 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}(),
|
||||
|
||||
expectedMergePdfEngines: []string{"b"},
|
||||
expectedSplitPdfEngines: []string{"a"},
|
||||
expectedFlattenPdfEngines: []string{"c"},
|
||||
expectedConvertPdfEngines: []string{"b"},
|
||||
expectedReadMetadataPdfEngines: []string{"a"},
|
||||
expectedWriteMetadataPdfEngines: []string{"a"},
|
||||
@@ -182,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))
|
||||
}
|
||||
@@ -190,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 {
|
||||
@@ -200,6 +210,12 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.splitNames {
|
||||
if name != tc.expectedSplitPdfEngines[index] {
|
||||
t.Fatalf("expected split name at index %d to be %s, but got: %s", index, name, tc.expectedSplitPdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.convertNames {
|
||||
if name != tc.expectedConvertPdfEngines[index] {
|
||||
t.Fatalf("expected convert name at index %d to be %s, but got: %s", index, name, tc.expectedConvertPdfEngines[index])
|
||||
@@ -212,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])
|
||||
}
|
||||
@@ -280,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()
|
||||
@@ -303,20 +319,24 @@ func TestPdfEngines_Validate(t *testing.T) {
|
||||
func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
mod.mergeNames = []string{"foo", "bar"}
|
||||
mod.splitNames = []string{"foo", "bar"}
|
||||
mod.convertNames = []string{"foo", "bar"}
|
||||
mod.readMetadataNames = []string{"foo", "bar"}
|
||||
mod.writeMedataNames = []string{"foo", "bar"}
|
||||
mod.writeMetadataNames = []string{"foo", "bar"}
|
||||
|
||||
expectedMessages := 6
|
||||
messages := mod.SystemMessages()
|
||||
if len(messages) != 4 {
|
||||
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 {
|
||||
@@ -328,10 +348,11 @@ func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
|
||||
func TestPdfEngines_PdfEngine(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
mergeNames: []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
|
||||
@@ -370,7 +391,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 4,
|
||||
expectRoutes: 6,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
@@ -13,6 +15,245 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
)
|
||||
|
||||
// FormDataPdfSplitMode creates a [gotenberg.SplitMode] from the form data.
|
||||
func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg.SplitMode {
|
||||
var (
|
||||
mode string
|
||||
span string
|
||||
unify bool
|
||||
)
|
||||
|
||||
splitModeFunc := func(value string) error {
|
||||
if value != "" && value != gotenberg.SplitModeIntervals && value != gotenberg.SplitModePages {
|
||||
return fmt.Errorf("wrong value, expected either '%s' or '%s'", gotenberg.SplitModeIntervals, gotenberg.SplitModePages)
|
||||
}
|
||||
mode = value
|
||||
return nil
|
||||
}
|
||||
|
||||
splitSpanFunc := func(value string) error {
|
||||
value = strings.Join(strings.Fields(value), "")
|
||||
|
||||
if mode == gotenberg.SplitModeIntervals {
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if intValue < 1 {
|
||||
return errors.New("value is inferior to 1")
|
||||
}
|
||||
}
|
||||
|
||||
span = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.
|
||||
MandatoryCustom("splitMode", func(value string) error {
|
||||
return splitModeFunc(value)
|
||||
}).
|
||||
MandatoryCustom("splitSpan", func(value string) error {
|
||||
return splitSpanFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.
|
||||
Custom("splitMode", func(value string) error {
|
||||
return splitModeFunc(value)
|
||||
}).
|
||||
Custom("splitSpan", func(value string) error {
|
||||
return splitSpanFunc(value)
|
||||
})
|
||||
}
|
||||
|
||||
form.
|
||||
Bool("splitUnify", &unify, false).
|
||||
Custom("splitUnify", func(value string) error {
|
||||
if value != "" && unify && mode != gotenberg.SplitModePages {
|
||||
return fmt.Errorf("unify is not available for split mode '%s'", mode)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return gotenberg.SplitMode{
|
||||
Mode: mode,
|
||||
Span: span,
|
||||
Unify: unify,
|
||||
}
|
||||
}
|
||||
|
||||
// FormDataPdfFormats creates [gotenberg.PdfFormats] from the form data.
|
||||
// Fallback to default value if the considered key is not present.
|
||||
func FormDataPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||
var (
|
||||
pdfa string
|
||||
pdfua bool
|
||||
)
|
||||
|
||||
form.
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false)
|
||||
|
||||
return gotenberg.PdfFormats{
|
||||
PdfA: pdfa,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
}
|
||||
|
||||
// FormDataPdfMetadata creates metadata object from the form data.
|
||||
func FormDataPdfMetadata(form *api.FormData, mandatory bool) map[string]interface{} {
|
||||
var metadata map[string]interface{}
|
||||
|
||||
metadataFunc := func(value string) error {
|
||||
if len(value) > 0 {
|
||||
err := json.Unmarshal([]byte(value), &metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.MandatoryCustom("metadata", func(value string) error {
|
||||
return metadataFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.Custom("metadata", func(value string) error {
|
||||
return metadataFunc(value)
|
||||
})
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
// MergeStub merges given PDFs. If only one input PDF, it does nothing and
|
||||
// returns the corresponding input path.
|
||||
func MergeStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths []string) (string, error) {
|
||||
if len(inputPaths) == 0 {
|
||||
return "", errors.New("no input paths")
|
||||
}
|
||||
|
||||
if len(inputPaths) == 1 {
|
||||
return inputPaths[0], nil
|
||||
}
|
||||
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
err := engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("merge %d PDFs: %w", len(inputPaths), err)
|
||||
}
|
||||
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
// SplitPdfStub splits a list of PDF files based on [gotenberg.SplitMode].
|
||||
// It returns a list of output paths or the list of provided input paths if no
|
||||
// split requested.
|
||||
func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.SplitMode, inputPaths []string) ([]string, error) {
|
||||
zeroValued := gotenberg.SplitMode{}
|
||||
if mode == zeroValued {
|
||||
return inputPaths, nil
|
||||
}
|
||||
|
||||
var outputPaths []string
|
||||
for _, inputPath := range inputPaths {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
outputDirPath, err := ctx.CreateSubDirectory(strings.ReplaceAll(filepath.Base(filenameNoExt), ".", "_"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create subdirectory from input path: %w", err)
|
||||
}
|
||||
|
||||
paths, err := engine.Split(ctx, ctx.Log(), mode, inputPath, outputDirPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("split PDF '%s': %w", inputPath, err)
|
||||
}
|
||||
|
||||
// Keep the original filename.
|
||||
for i, path := range paths {
|
||||
var newPath string
|
||||
if mode.Unify && mode.Mode == gotenberg.SplitModePages {
|
||||
newPath = fmt.Sprintf(
|
||||
"%s/%s.pdf",
|
||||
outputDirPath, filenameNoExt,
|
||||
)
|
||||
} else {
|
||||
newPath = fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, i,
|
||||
)
|
||||
}
|
||||
|
||||
err = ctx.Rename(path, newPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rename path: %w", err)
|
||||
}
|
||||
|
||||
outputPaths = append(outputPaths, newPath)
|
||||
|
||||
if mode.Unify && mode.Mode == gotenberg.SplitModePages {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
func ConvertStub(ctx *api.Context, engine gotenberg.PdfEngine, formats gotenberg.PdfFormats, inputPaths []string) ([]string, error) {
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if formats == zeroValued {
|
||||
return inputPaths, nil
|
||||
}
|
||||
|
||||
outputPaths := make([]string, len(inputPaths))
|
||||
for i, inputPath := range inputPaths {
|
||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
|
||||
err := engine.Convert(ctx, ctx.Log(), formats, inputPath, outputPaths[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return outputPaths, nil
|
||||
}
|
||||
|
||||
// WriteMetadataStub writes the metadata into PDF files. If no metadata, it
|
||||
// does nothing.
|
||||
func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata map[string]interface{}, inputPaths []string) error {
|
||||
if len(metadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.WriteMetadata(ctx, ctx.Log(), metadata, inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata into '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeRoute returns an [api.Route] which can merge PDFs.
|
||||
func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
@@ -22,75 +263,148 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
// Let's get the data from the form and validate them.
|
||||
var (
|
||||
inputPaths []string
|
||||
pdfa string
|
||||
pdfua bool
|
||||
metadata map[string]interface{}
|
||||
)
|
||||
form := ctx.FormData()
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
|
||||
err := ctx.FormData().
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false).
|
||||
Custom("metadata", func(value string) error {
|
||||
if len(value) > 0 {
|
||||
err := json.Unmarshal([]byte(value), &metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Bool("flatten", &flatten, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
pdfFormats := gotenberg.PdfFormats{
|
||||
PdfA: pdfa,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
|
||||
// Alright, let's merge the PDFs.
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge PDFs: %w", err)
|
||||
}
|
||||
|
||||
// So far so good, the PDFs are merged into one unique PDF.
|
||||
// Now, let's check if the client want to convert this result PDF
|
||||
// to specific PDF formats.
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if pdfFormats != zeroValued {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
}
|
||||
|
||||
// Important: the output path is now the converted file.
|
||||
outputPath = convertOutputPath
|
||||
}
|
||||
|
||||
// Writes and potentially overrides metadata entries, if any.
|
||||
if len(metadata) > 0 {
|
||||
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Last but not least, add the output path to the context so that
|
||||
// the API is able to send it as a response to the client.
|
||||
err = ctx.AddOutputPaths(outputPath)
|
||||
outputPaths, err := ConvertStub(ctx, engine, pdfFormats, []string{outputPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output path: %w", err)
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
}
|
||||
|
||||
err = WriteMetadataStub(ctx, engine, metadata, outputPaths)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// splitRoute returns an [api.Route] which can extract pages from a PDF.
|
||||
func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/split",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
mode := FormDataPdfSplitMode(form, true)
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
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)
|
||||
}
|
||||
|
||||
outputPaths, err := SplitPdfStub(ctx, engine, mode, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("split PDFs: %w", err)
|
||||
}
|
||||
|
||||
convertOutputPaths, err := ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = WriteMetadataStub(ctx, engine, metadata, convertOutputPaths)
|
||||
if err != nil {
|
||||
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 {
|
||||
// Rename the files to keep the split naming.
|
||||
for i, convertOutputPath := range convertOutputPaths {
|
||||
err = ctx.Rename(convertOutputPath, outputPaths[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename output path: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(outputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -108,27 +422,17 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
// Let's get the data from the form and validate them.
|
||||
var (
|
||||
inputPaths []string
|
||||
pdfa string
|
||||
pdfua bool
|
||||
)
|
||||
form := ctx.FormData()
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
|
||||
err := ctx.FormData().
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
pdfFormats := gotenberg.PdfFormats{
|
||||
PdfA: pdfa,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if pdfFormats == zeroValued {
|
||||
return api.WrapError(
|
||||
@@ -140,18 +444,14 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
)
|
||||
}
|
||||
|
||||
// Alright, let's convert the PDFs.
|
||||
outputPaths := make([]string, len(inputPaths))
|
||||
for i, inputPath := range inputPaths {
|
||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
outputPaths, err := ConvertStub(ctx, engine, pdfFormats, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
}
|
||||
|
||||
if len(outputPaths) > 1 {
|
||||
// If .zip archive, keep the original filename.
|
||||
if len(outputPaths) > 1 {
|
||||
// If .zip archive, keep the original filename.
|
||||
for i, inputPath := range inputPaths {
|
||||
err = ctx.Rename(outputPaths[i], inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename output path: %w", err)
|
||||
@@ -161,8 +461,6 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// Last but not least, add the output paths to the context so that
|
||||
// the API is able to send them as a response to the client.
|
||||
err = ctx.AddOutputPaths(outputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
@@ -182,9 +480,7 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
// Let's get the data from the form and validate them.
|
||||
var inputPaths []string
|
||||
|
||||
err := ctx.FormData().
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
@@ -192,7 +488,6 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
// Alright, let's read the metadata.
|
||||
res := make(map[string]map[string]interface{}, len(inputPaths))
|
||||
for _, inputPath := range inputPaths {
|
||||
metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath)
|
||||
@@ -223,41 +518,22 @@ func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
// Let's get the data from the form and validate them.
|
||||
var (
|
||||
inputPaths []string
|
||||
metadata map[string]interface{}
|
||||
)
|
||||
form := ctx.FormData()
|
||||
metadata := FormDataPdfMetadata(form, true)
|
||||
|
||||
err := ctx.FormData().
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
MandatoryCustom("metadata", func(value string) error {
|
||||
if len(value) > 0 {
|
||||
err := json.Unmarshal([]byte(value), &metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||
}
|
||||
}
|
||||
if len(metadata) == 0 {
|
||||
return errors.New("no metadata")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
// Alright, let's convert the PDFs.
|
||||
for _, inputPath := range inputPaths {
|
||||
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
err = WriteMetadataStub(ctx, engine, metadata, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
// Last but not least, add the output paths to the context so that
|
||||
// the API is able to send them as a response to the client.
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
// interface using the PDFtk command-line tool. This package allows for:
|
||||
//
|
||||
// 1. The merging of PDF files.
|
||||
// 2. The splitting of PDF files.
|
||||
//
|
||||
// The path to the PDFtk binary must be specified using the PDFTK_BIN_PATH
|
||||
// environment variable.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -51,6 +52,34 @@ func (engine *PdfTk) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath))
|
||||
|
||||
switch mode.Mode {
|
||||
case gotenberg.SplitModePages:
|
||||
if !mode.Unify {
|
||||
return nil, fmt.Errorf("split PDFs using mode '%s' without unify with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||
}
|
||||
args = append(args, inputPath, "cat", mode.Span, "output", outputPath)
|
||||
default:
|
||||
return nil, fmt.Errorf("split PDFs using mode '%s' with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||
}
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
_, err = cmd.Exec()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("split PDFs with PDFtk: %w", err)
|
||||
}
|
||||
|
||||
return []string{outputPath}, nil
|
||||
}
|
||||
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var args []string
|
||||
@@ -70,6 +99,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)
|
||||
|
||||
@@ -116,7 +116,7 @@ func TestPdfTk_Merge(t *testing.T) {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
@@ -142,6 +142,105 @@ func TestPdfTk_Merge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPdfCpu_Split(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
mode gotenberg.SplitMode
|
||||
inputPath string
|
||||
expectError bool
|
||||
expectedError error
|
||||
expectOutputPathsCount int
|
||||
expectOutputPaths []string
|
||||
}{
|
||||
{
|
||||
scenario: "ErrPdfSplitModeNotSupported",
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrPdfSplitModeNotSupported (no unify with pages)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1", Unify: false},
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
inputPath: "",
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success (pages & unify)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfTk)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
outputPaths, err := engine.Split(tc.ctx, zap.NewNop(), tc.mode, tc.inputPath, outputDir)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(outputPaths) {
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(outputPaths))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func 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{}, "", "")
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// interface using the QPDF command-line tool. This package allows for:
|
||||
//
|
||||
// 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.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -45,12 +46,40 @@ func (engine *QPdf) Provision(ctx *gotenberg.Context) error {
|
||||
func (engine *QPdf) Validate() error {
|
||||
_, err := os.Stat(engine.binPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("QPdf binary path does not exist: %w", err)
|
||||
return fmt.Errorf("QPDF binary path does not exist: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath))
|
||||
|
||||
switch mode.Mode {
|
||||
case gotenberg.SplitModePages:
|
||||
if !mode.Unify {
|
||||
return nil, fmt.Errorf("split PDFs using mode '%s' without unify with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||
}
|
||||
args = append(args, inputPath, "--pages", ".", mode.Span, "--", outputPath)
|
||||
default:
|
||||
return nil, fmt.Errorf("split PDFs using mode '%s' with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||
}
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
_, err = cmd.Exec()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("split PDFs with QPDF: %w", err)
|
||||
}
|
||||
|
||||
return []string{outputPath}, nil
|
||||
}
|
||||
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var args []string
|
||||
@@ -72,6 +101,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)
|
||||
|
||||
@@ -3,6 +3,8 @@ package qpdf
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -116,7 +118,7 @@ func TestQPdf_Merge(t *testing.T) {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem()
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
@@ -142,6 +144,191 @@ func TestQPdf_Merge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPdf_Split(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
mode gotenberg.SplitMode
|
||||
inputPath string
|
||||
expectError bool
|
||||
expectedError error
|
||||
expectOutputPathsCount int
|
||||
expectOutputPaths []string
|
||||
}{
|
||||
{
|
||||
scenario: "ErrPdfSplitModeNotSupported",
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrPdfSplitModeNotSupported (no unify with pages)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1", Unify: false},
|
||||
expectError: true,
|
||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
inputPath: "",
|
||||
expectError: true,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success (pages & unify)",
|
||||
ctx: context.TODO(),
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
expectError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(QPdf)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
outputPaths, err := engine.Split(tc.ctx, zap.NewNop(), tc.mode, tc.inputPath, outputDir)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(outputPaths) {
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(outputPaths))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func 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