Compare commits

...

16 Commits

Author SHA1 Message Date
Julien Neuhart
a780373cfc chore(go): update to 1.22 2024-03-15 16:23:27 +01:00
Julien Neuhart
5ad90ccdb8 fix: file rename now correctly works for zip archive 2024-03-15 16:12:18 +01:00
Julien Neuhart
ef97d81ca8 fix(libreoffice): wrong number of file extensions 2024-03-15 11:37:34 +01:00
Julien Neuhart
c9f7b642c5 fix(api): wrong expected message in tests 2024-03-15 11:25:55 +01:00
Julien Neuhart
770962be3f chore(deps): update golangci-lint 2024-03-15 11:22:32 +01:00
Julien Neuhart
d6c857f4a8 chore(libreoffice): disable PDF tagging by default - for consistency with Chromium 2024-03-15 11:11:51 +01:00
Julien Neuhart
7b7ffb427d fix(chromium): disable PDF tagging 2024-03-15 10:56:35 +01:00
Julien Neuhart
b80318d1b6 chore(deps): update Go dependencies 2024-03-15 09:50:19 +01:00
dependabot[bot]
3997d69e49 chore(deps): bump google.golang.org/protobuf from 1.32.0 to 1.33.0
Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-14 07:10:17 +01:00
Julien Neuhart
4af41b33c0 chore: update Go dependencies 2024-03-04 11:33:10 +01:00
Julien Neuhart
0f8bbead66 fix(api): do not catch gotenberg.ErrPdfEngineMethodNotSupported as it may hides other errors 2024-03-04 11:17:36 +01:00
Julien Neuhart
804fcff5b6 fix(chromium): remove tagged PDF by default 2024-03-04 10:28:54 +01:00
Julien Neuhart
d18e6920fc chore(libreoffice): order ext list alphabetically 2024-03-04 10:09:42 +01:00
eMerzh
c95174ff11 feat(libreoffice): add macOS *.key support 2024-02-26 16:47:52 +01:00
Julien Neuhart
c70329c22b chore(api): improve gotenberg.ErrPdfFormatNotSupported HTTP message 2024-02-24 14:55:48 +01:00
Julien Neuhart
34f69263a2 feat(libreoffice): explicity set tagged PDF as default 2024-02-24 13:45:01 +01:00
23 changed files with 400 additions and 169 deletions

View File

@@ -17,14 +17,14 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: '1.22'
cache: false
- name: Checkout source code
uses: actions/checkout@v4
- name: Run linters
uses: golangci/golangci-lint-action@v4
with:
version: v1.55.2
version: v1.56.2
tests:
needs:

View File

@@ -5,14 +5,14 @@ help: ## Show the help
.PHONY: it
it: build build-tests ## Initialize the development environment
GOLANG_VERSION=1.21
GOLANG_VERSION=1.22
DOCKER_REPOSITORY=gotenberg
GOTENBERG_VERSION=snapshot
GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001
NOTO_COLOR_EMOJI_VERSION=v2.042 # See https://github.com/googlefonts/noto-emoji/releases.
PDFTK_VERSION=v3.3.3 # See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
GOLANGCI_LINT_VERSION=v1.55.2 # See https://github.com/golangci/golangci-lint/releases.
GOLANGCI_LINT_VERSION=v1.56.2 # See https://github.com/golangci/golangci-lint/releases.
.PHONY: build
build: ## Build the Gotenberg's Docker image

24
go.mod
View File

@@ -1,11 +1,11 @@
module github.com/gotenberg/gotenberg/v8
go 1.21
go 1.22
require (
github.com/alexliesenfeld/health v0.8.0
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154
github.com/chromedp/chromedp v0.9.5
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0
@@ -19,24 +19,24 @@ require (
github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.26
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pdfcpu/pdfcpu v0.6.0
github.com/pdfcpu/pdfcpu v0.7.0
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/prometheus/client_golang v1.18.0
github.com/prometheus/client_golang v1.19.0
github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/pflag v1.0.5
github.com/ulikunitz/xz v0.5.11 // indirect
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.21.0
golang.org/x/net v0.22.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
)
require github.com/dlclark/regexp2 v1.10.0
require github.com/dlclark/regexp2 v1.11.0
require (
github.com/aymerick/douceur v0.2.0 // indirect
@@ -56,12 +56,12 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/rivo/uniseg v0.4.7 // 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
google.golang.org/protobuf v1.32.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

44
go.sum
View File

@@ -10,16 +10,16 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e h1:kXEolCWQZzuEFcuaTzfqXToX+e29OcvK87BcBiBBJ1c=
github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154 h1:jeAmkzyOAQBPRmZMhX+i/CJv0VViLkHk1nF0qx8s0Mk=
github.com/chromedp/cdproto v0.0.0-20240312231614-1e5096e63154/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -87,8 +87,8 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
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/pdfcpu/pdfcpu v0.6.0 h1:z4kARP5bcWa39TTYMcN/kjBnm7MvhTWjXgeYmkdAGMI=
github.com/pdfcpu/pdfcpu v0.6.0/go.mod h1:kmpD0rk8YnZj0l3qSeGBlAB+XszHUgNv//ORH/E7EYo=
github.com/pdfcpu/pdfcpu v0.7.0 h1:cd7/z7hAyyDuzdciKfNZyQ3TYreJza2DsuPdIHYURcA=
github.com/pdfcpu/pdfcpu v0.7.0/go.mod h1:kmpD0rk8YnZj0l3qSeGBlAB+XszHUgNv//ORH/E7EYo=
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=
@@ -96,14 +96,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -134,26 +134,26 @@ 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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -51,3 +51,11 @@ 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
}

View File

@@ -127,6 +127,15 @@ func (provider *MetricsProviderMock) Metrics() ([]Metric, error) {
return provider.MetricsMock()
}
// PathRenameMock is a mock for the [PathRename] interface.
type PathRenameMock struct {
RenameMock func(oldpath, newpath string) error
}
func (rename *PathRenameMock) Rename(oldpath, newpath string) error {
return rename.RenameMock(oldpath, newpath)
}
// Interface guards.
var (
_ Module = (*ModuleMock)(nil)

View File

@@ -188,3 +188,16 @@ func TestMetricsProviderMock(t *testing.T) {
t.Errorf("expected no error from MetricsProviderMock.Metrics, but got: %v", err)
}
}
func TestPathRenameMock(t *testing.T) {
mock := &PathRenameMock{
RenameMock: func(oldpath, newpath string) error {
return nil
},
}
err := mock.Rename("", "")
if err != nil {
t.Errorf("expected no error from PathRenameMock.Rename, but got: %v", err)
}
}

View File

@@ -34,18 +34,24 @@ var (
// Context is the request context for a "multipart/form-data" requests.
type Context struct {
dirPath string
values map[string][]string
files map[string]string
dirPath string
values map[string][]string
files map[string]string
outputPaths []string
cancelled bool
cancelled bool
logger *zap.Logger
echoCtx echo.Context
logger *zap.Logger
echoCtx echo.Context
pathRename gotenberg.PathRename
context.Context
}
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) (*Context, context.CancelFunc, error) {
processCtx, processCancel := context.WithTimeout(context.Background(), timeout)
@@ -55,6 +61,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
cancelled: false,
logger: logger,
echoCtx: echoCtx,
pathRename: new(osPathRename),
Context: processCtx,
}
@@ -191,14 +198,19 @@ func (ctx *Context) FormData() *FormData {
}
// GeneratePath generates a path within the context's working directory.
// It either generates a new UUID-based filename or uses the provided filename.
// It does not create a file.
func (ctx *Context) GeneratePath(filename, extension string) string {
if filename == "" {
// Generate a new UUID-based filename
filename = uuid.New().String()
// It generates a new UUID-based filename. It does not create a file.
func (ctx *Context) GeneratePath(extension string) string {
return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New().String(), extension)
}
// 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 {
err := ctx.pathRename.Rename(oldpath, newpath)
if err != nil {
return fmt.Errorf("rename path: %w", err)
}
return fmt.Sprintf("%s/%s%s", ctx.dirPath, filename, extension)
return nil
}
// AddOutputPaths adds the given paths. Those paths will be used later to build
@@ -250,7 +262,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
ImplicitTopLevelFolder: false,
}
archivePath := ctx.GeneratePath("", ".zip")
archivePath := ctx.GeneratePath(".zip")
err := z.Archive(ctx.outputPaths, archivePath)
if err != nil {
@@ -273,3 +285,8 @@ func (ctx *Context) OutputFilename(outputPath string) string {
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
}
// Interface guard.
var (
_ gotenberg.PathRename = (*osPathRename)(nil)
)

View File

@@ -3,21 +3,77 @@ package api
import (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"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) {
for _, tc := range []struct {
scenario string
@@ -190,14 +246,44 @@ func TestContext_GeneratePath(t *testing.T) {
dirPath: "/foo",
}
path := ctx.GeneratePath("", ".pdf")
path := ctx.GeneratePath(".pdf")
if !strings.HasPrefix(path, ctx.dirPath) {
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
}
}
path = ctx.GeneratePath("foo.txt", ".pdf")
if !strings.Contains(path, "foo.txt.pdf") {
t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
func TestContext_Rename(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *Context
expectError bool
}{
{
scenario: "failure",
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return errors.New("cannot rename")
}}},
expectError: true,
},
{
scenario: "success",
ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}}},
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
err := tc.ctx.Rename("", "")
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}

View File

@@ -40,12 +40,8 @@ func ParseError(err error) (int, string) {
return http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)
}
if errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
return http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)
}
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return http.StatusBadRequest, "A least one PDF engine does not handle one of the requested PDF format, while other have failed to convert for other reasons"
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"
}
var httpErr HttpError

View File

@@ -38,15 +38,10 @@ func TestParseError(t *testing.T) {
expectStatus: http.StatusTooManyRequests,
expectMessage: http.StatusText(http.StatusTooManyRequests),
},
{
err: gotenberg.ErrPdfEngineMethodNotSupported,
expectStatus: http.StatusNotImplemented,
expectMessage: http.StatusText(http.StatusNotImplemented),
},
{
err: gotenberg.ErrPdfFormatNotSupported,
expectStatus: http.StatusBadRequest,
expectMessage: "A least one PDF engine does not handle one of the requested PDF format, while other have failed to convert for other reasons",
expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues",
},
{
err: WrapError(

View File

@@ -4,6 +4,8 @@ import (
"github.com/alexliesenfeld/health"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
// ContextMock is a helper for tests.
@@ -76,7 +78,7 @@ func (ctx *ContextMock) SetLogger(logger *zap.Logger) {
ctx.logger = logger
}
// SetEchoContext sets the echo.Context.
// SetEchoContext sets the [echo.Context].
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.setEchoContext(c)
@@ -84,6 +86,14 @@ func (ctx *ContextMock) SetEchoContext(c echo.Context) {
ctx.Context.echoCtx = c
}
// SetPathRename sets the [gotenberg.PathRename].
//
// ctx := &api.ContextMock{Context: &api.Context{}}
// ctx.setPathRename(rename)
func (ctx *ContextMock) SetPathRename(rename gotenberg.PathRename) {
ctx.Context.pathRename = rename
}
// RouterMock is a mock for the [Router] interface.
type RouterMock struct {
RoutesMock func() ([]Route, error)

View File

@@ -117,6 +117,19 @@ func TestContextMock_SetEchoContext(t *testing.T) {
}
}
func TestContextMock_SetPathRename(t *testing.T) {
mock := ContextMock{&Context{}}
expect := new(osPathRename)
mock.SetPathRename(expect)
actual := mock.pathRename
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestRouterMock(t *testing.T) {
mock := &RouterMock{
RoutesMock: func() ([]Route, error) {

View File

@@ -90,6 +90,8 @@ func (b *chromiumBrowser) Start(logger *zap.Logger) error {
// https://github.com/puppeteer/puppeteer/issues/2410
chromedp.Flag("font-render-hinting", "none"),
chromedp.UserDataDir(b.userProfileDirPath),
// See https://github.com/gotenberg/gotenberg/issues/831.
chromedp.Flag("disable-pdf-tagging", true),
)
if b.arguments.incognito {
@@ -164,7 +166,7 @@ func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
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 re-deleting it.
// of time before deleting it.
<-time.After(10 * time.Second)
err := os.RemoveAll(userProfileDirPath)

View File

@@ -496,7 +496,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
)
}
inputPath = ctx.GeneratePath("", ".html")
inputPath = ctx.GeneratePath(".html")
err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
if err != nil {
@@ -507,7 +507,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
}
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options PdfOptions) error {
outputPath := ctx.GeneratePath("", ".pdf")
outputPath := ctx.GeneratePath(".pdf")
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
err = handleChromiumError(err, options.Options)
@@ -551,7 +551,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath("", ".pdf")
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil {
@@ -572,7 +572,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
func screenshotUrl(ctx *api.Context, chromium Api, url string, options ScreenshotOptions) error {
ext := fmt.Sprintf(".%s", options.Format)
outputPath := ctx.GeneratePath("", ext)
outputPath := ctx.GeneratePath(ext)
err := chromium.Screenshot(ctx, ctx.Log(), url, outputPath, options)
err = handleChromiumError(err, options.Options)

View File

@@ -47,7 +47,9 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOpti
WithMarginRight(options.MarginRight).
WithPageRanges(pageRanges).
WithPreferCSSPageSize(options.PreferCssPageSize).
WithGenerateTaggedPDF(true)
// Does not seem to work.
// See https://github.com/gotenberg/gotenberg/issues/831.
WithGenerateTaggedPDF(false)
hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
options.FooterTemplate != DefaultPdfOptions().FooterTemplate

View File

@@ -267,85 +267,86 @@ func (a *Api) Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath
func (a *Api) Extensions() []string {
return []string{
".bib",
".doc",
".xml",
".docx",
".fodt",
".html",
".ltx",
".txt",
".odt",
".ott",
".pdb",
".pdf",
".psw",
".rtf",
".sdw",
".stw",
".sxw",
".uot",
".vor",
".wps",
".epub",
".png",
".bmp",
".emf",
".eps",
".fodg",
".gif",
".jpg",
".jpeg",
".met",
".odd",
".otg",
".pbm",
".pct",
".pgm",
".ppm",
".ras",
".std",
".svg",
".svm",
".swf",
".sxd",
".sxw",
".tif",
".tiff",
".xhtml",
".xpm",
".odp",
".fodp",
".pages",
".potm",
".pot",
".pptx",
".pps",
".ppt",
".pwp",
".sda",
".sdd",
".sti",
".sxi",
".uop",
".wmf",
".csv",
".dbf",
".dif",
".doc",
".docx",
".dotx",
".emf",
".eps",
".epub",
".fodg",
".fodp",
".fods",
".fodt",
".gif",
".html",
".jpeg",
".jpg",
".key",
".ltx",
".met",
".odd",
".odg",
".odp",
".ods",
".odt",
".otg",
".ots",
".ott",
".pages",
".pbm",
".pct",
".pdb",
".pdf",
".pgm",
".png",
".pot",
".potm",
".ppm",
".pps",
".ppt",
".pptx",
".psw",
".pwp",
".pxl",
".ras",
".rtf",
".sda",
".sdc",
".sdd",
".sdw",
".slk",
".stc",
".std",
".sti",
".stw",
".svg",
".svm",
".swf",
".sxc",
".sxd",
".sxi",
".sxw",
".sxw",
".tif",
".tiff",
".txt",
".uop",
".uos",
".uot",
".vor",
".wmf",
".wps",
".xhtml",
".xls",
".xlt",
".xlsx",
".odg",
".dotx",
".xlt",
".xltx",
".xml",
".xpm",
}
}

View File

@@ -450,7 +450,7 @@ func TestApi_Extensions(t *testing.T) {
extensions := a.Extensions()
actual := len(extensions)
expect := 80
expect := 81
if actual != expect {
t.Errorf("expected %d extensions, but got %d", expect, actual)

View File

@@ -288,8 +288,14 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
if options.PdfFormats.PdfUa {
args = append(
args,
"--export", "EnableTextAccessForAccessibilityTools=true",
"--export", "UseTaggedPDF=true",
"--export", "EnableTextAccessForAccessibilityTools=true",
)
} else {
args = append(
args,
"--export", "UseTaggedPDF=false",
"--export", "EnableTextAccessForAccessibilityTools=false",
)
}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"net/http"
"path/filepath"
"github.com/labstack/echo/v4"
@@ -55,8 +54,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
// Alright, let's convert each document to PDF.
outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths {
// document.docx -> document.docx.pdf.
outputPaths[i] = ctx.GeneratePath(filepath.Base(inputPath), ".pdf")
outputPaths[i] = ctx.GeneratePath(".pdf")
options := libreofficeapi.Options{
Landscape: landscape,
PageRanges: nativePageRanges,
@@ -91,9 +89,8 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
// So far so good, let's check if we have to merge the PDFs. Quick
// win: if there is only one PDF, skip this step.
if len(outputPaths) > 1 && merge {
outputPath := ctx.GeneratePath("", ".pdf")
outputPath := ctx.GeneratePath(".pdf")
err = engine.Merge(ctx, ctx.Log(), outputPaths, outputPath)
if err != nil {
@@ -105,7 +102,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
zeroValued := gotenberg.PdfFormats{}
if !nativePdfFormats && pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath("", ".pdf")
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil {
@@ -117,8 +114,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
}
// Last but not least, add the output path to the context so that
// the Uno is able to send it as a response to the client.
// the API is able to send it as a response to the client.
err = ctx.AddOutputPaths(outputPath)
if err != nil {
return fmt.Errorf("add output path: %w", err)
@@ -135,23 +131,34 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
for i, outputPath := range outputPaths {
convertInputPath := outputPath
// document.docx -> document.docx.pdf.
convertOutputPaths[i] = ctx.GeneratePath(filepath.Base(inputPaths[i]), ".pdf")
convertOutputPaths[i] = ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
if err != nil {
return fmt.Errorf("convert PDF: %w", err)
}
}
// Important: the output paths are now the converted files.
outputPaths = convertOutputPaths
}
// Last but not least, add the output paths to the context so that
// the Uno is able to send them as a response to the client.
if len(outputPaths) > 1 {
// If .zip archive, document.docx -> document.docx.pdf.
for i, inputPath := range inputPaths {
outputPath := fmt.Sprintf("%s.pdf", inputPath)
err = ctx.Rename(outputPaths[i], outputPath)
if err != nil {
return fmt.Errorf("rename output path: %w", err)
}
outputPaths[i] = outputPath
}
}
// 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)

View File

@@ -185,7 +185,32 @@ func TestConvertRoute(t *testing.T) {
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
expectOutputPaths: []string{"/document.docx.pdf"},
},
{
scenario: "cannot rename many files",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
"document2.docx": "/document2.docx",
"document2.doc": "/document2.doc",
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return errors.New("cannot rename")
}})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
return nil
},
ExtensionsMock: func() []string {
return []string{".docx", ".doc"}
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "success (many files)",
@@ -196,6 +221,9 @@ func TestConvertRoute(t *testing.T) {
"document2.docx": "/document2.docx",
"document2.doc": "/document2.doc",
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
@@ -230,6 +258,9 @@ func TestConvertRoute(t *testing.T) {
"false",
},
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
@@ -266,6 +297,9 @@ func TestConvertRoute(t *testing.T) {
"true",
},
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{

View File

@@ -4,8 +4,6 @@ import (
"errors"
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/labstack/echo/v4"
@@ -44,8 +42,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
}
// Alright, let's merge the PDFs.
outputPath := ctx.GeneratePath("", ".pdf")
outputPath := ctx.GeneratePath(".pdf")
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
if err != nil {
@@ -58,7 +55,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath("", ".pdf")
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil {
@@ -71,7 +68,6 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
// 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)
if err != nil {
return fmt.Errorf("add output path: %w", err)
@@ -124,26 +120,29 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
)
}
// Alright, let's convert the PDFs.s
// Alright, let's convert the PDFs.
outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths {
if len(outputPaths) > 1 {
// If .zip archive, keep the original filenames.
outputPaths[i] = ctx.GeneratePath(strings.TrimSuffix(filepath.Base(inputPath), filepath.Ext(inputPath)), ".pdf")
} else {
outputPaths[i] = ctx.GeneratePath("", ".pdf")
}
outputPaths[i] = ctx.GeneratePath(".pdf")
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.
err = ctx.Rename(outputPaths[i], inputPath)
if err != nil {
return fmt.Errorf("rename output path: %w", err)
}
outputPaths[i] = inputPath
}
}
// 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)

View File

@@ -294,6 +294,36 @@ func TestConvertHandler(t *testing.T) {
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "cannot rename many files",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1b,
},
"pdfua": {
"true",
},
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return errors.New("cannot rename")
}})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "success with PDF/A & PDF/UA form fields (many files)",
ctx: func() *api.ContextMock {
@@ -310,6 +340,9 @@ func TestConvertHandler(t *testing.T) {
"true",
},
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{