Repository: gotenberg/gotenberg (go)

Issue:
Keep original filenames in zip output
### Discussed in https://github.com/gotenberg/gotenberg/discussions/769

<div type='discussions-op-text'>

<sup>Originally posted by **chrishughes20** January  8, 2024</sup>
Hello!

I am wondering if it's possible to somehow set a specific filename for PDF files that ultimately get zipped? I have cloned the project and am running with the log level set to debug. In the logs, I see the filename I want for each filename but then the output filename is changed to a random filename. Here is part of the log for one of my files:
`{"level":"debug","ts":1704735359.0625625,"logger":"api.formslibreofficeconvert.usrbinunoconv.stderr","msg":"Input file: /tmp/aa2cf6de-5a9c-47c6-8fe2-694c777b5798/visit-report_bart-simpson_2023-12-24.pdf","trace":"a1461428-daa0-47dc-adee-dace4c651afe"}
{"level":"debug","ts":1704735359.1313896,"logger":"api.formslibreofficeconvert.usrbinunoconv.stdout","msg":"Output file: file:///tmp/aa2cf6de-5a9c-47c6-8fe2-694c777b5798/f6d3268d-5a7d-4088-b65f-4eb87338a733.pdf","trace":"a1461428-daa0-47dc-adee-dace4c651afe"}`

The filename I would like to be used as the filename is `visit-report_bart-simpson_2023-12-24.pdf` rather than the output filename `f6d3268d-5a7d-4088-b65f-4eb87338a733.pdf`. 

Also, I'm a total Go newb and have been trying to play around with a cloned version of the project and have added a few `c.Logger().Debug("we are here")` and `fmt.Print("Inside of route")` inside of `pkg/modules/libreoffice/routes.go` but I don't see this output anywhere in the console or Docker logs after rebuilding and restarting the Docker container. Could this output be being saved to a log file or something somewhere or am I (likely) doing something wrong?

Thank you for your help!</div>

Pull request:
feat(libreoffice): preserve filenames for zipped files
Fixes #771.

Patch:
diff --git a/cmd/gotenberg.go b/cmd/gotenberg.go
index 7d46f24..318e36e 100644
--- a/cmd/gotenberg.go
+++ b/cmd/gotenberg.go
@@ -75,7 +75,6 @@ func Run() {
 		go func(app gotenberg.App) {
 			id := app.(gotenberg.Module).Descriptor().ID
 			err = app.Start()
-
 			if err != nil {
 				fmt.Printf("[FATAL] starting %s: %s\n", id, err)
 				os.Exit(1)
diff --git a/go.mod b/go.mod
index 61420bd..9eca517 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.21
 require (
 	github.com/alexliesenfeld/health v0.8.0
 	github.com/andybalholm/brotli v1.1.0 // indirect
-	github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732
+	github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e
 	github.com/chromedp/chromedp v0.9.5
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/uuid v1.6.0
@@ -27,12 +27,12 @@ require (
 	github.com/ulikunitz/xz v0.5.11 // indirect
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.26.0
-	golang.org/x/crypto v0.18.0 // indirect
+	golang.org/x/crypto v0.19.0 // indirect
 	golang.org/x/image v0.15.0 // indirect
-	golang.org/x/net v0.20.0
+	golang.org/x/net v0.21.0
 	golang.org/x/sync v0.6.0
-	golang.org/x/sys v0.16.0 // indirect
-	golang.org/x/term v0.16.0
+	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/term v0.17.0
 	golang.org/x/text v0.14.0
 )
 
@@ -54,9 +54,9 @@ require (
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_model v0.5.0 // indirect
-	github.com/prometheus/common v0.46.0 // indirect
+	github.com/prometheus/common v0.47.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
-	github.com/rivo/uniseg v0.4.6 // 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
diff --git a/go.sum b/go.sum
index 6dafa5b..5f27549 100644
--- a/go.sum
+++ b/go.sum
@@ -9,8 +9,9 @@ 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.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 h1:XYUCaZrW8ckGWlCRJKCSoh/iFwlpX316a8yY9IFEzv8=
 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/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=
@@ -30,8 +31,8 @@ github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
@@ -97,13 +98,13 @@ github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+
 github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
 github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
 github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
-github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
+github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k=
+github.com/prometheus/common v0.47.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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
-github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -131,20 +132,21 @@ 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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
 go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+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/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
 golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+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/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 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+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/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=
diff --git a/pkg/modules/api/context.go b/pkg/modules/api/context.go
index ebb083e..e63c775 100644
--- a/pkg/modules/api/context.go
+++ b/pkg/modules/api/context.go
@@ -164,7 +164,6 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
 	for _, files := range form.File {
 		for _, fh := range files {
 			err = copyToDisk(fh)
-
 			if err != nil {
 				return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
 			}
@@ -191,10 +190,15 @@ func (ctx *Context) FormData() *FormData {
 	}
 }
 
-// GeneratePath generates a path within the context's working directory. It
-// does not create a file.
-func (ctx *Context) GeneratePath(extension string) string {
-	return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension)
+// 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()
+	}
+	return fmt.Sprintf("%s/%s%s", ctx.dirPath, filename, extension)
 }
 
 // AddOutputPaths adds the given paths. Those paths will be used later to build
@@ -246,7 +250,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 {
diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go
index ac4d5d2..97b0169 100644
--- a/pkg/modules/chromium/routes.go
+++ b/pkg/modules/chromium/routes.go
@@ -494,7 +494,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 {
@@ -505,7 +505,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, url, options.Options)
@@ -549,10 +549,9 @@ 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 {
 			if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
 				return api.WrapError(
@@ -581,7 +580,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, url, options.Options)
diff --git a/pkg/modules/libreoffice/routes.go b/pkg/modules/libreoffice/routes.go
index 177308b..a6bee96 100644
--- a/pkg/modules/libreoffice/routes.go
+++ b/pkg/modules/libreoffice/routes.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"path/filepath"
 
 	"github.com/labstack/echo/v4"
 
@@ -54,8 +55,8 @@ 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 {
-				outputPaths[i] = ctx.GeneratePath(".pdf")
-
+				// document.docx -> document.docx.pdf.
+				outputPaths[i] = ctx.GeneratePath(filepath.Base(inputPath), ".pdf")
 				options := libreofficeapi.Options{
 					Landscape:  landscape,
 					PageRanges: nativePageRanges,
@@ -92,7 +93,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
 			// 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 {
@@ -104,7 +105,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 {
@@ -144,7 +145,8 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
 
 				for i, outputPath := range outputPaths {
 					convertInputPath := outputPath
-					convertOutputPaths[i] = ctx.GeneratePath(".pdf")
+					// document.docx -> document.docx.pdf.
+					convertOutputPaths[i] = ctx.GeneratePath(filepath.Base(inputPaths[i]), ".pdf")
 
 					err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
 					if err != nil {
diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go
index 6728e0b..df41116 100644
--- a/pkg/modules/pdfengines/routes.go
+++ b/pkg/modules/pdfengines/routes.go
@@ -43,7 +43,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 {
@@ -56,10 +56,9 @@ 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 {
 					if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
 						return api.WrapError(
@@ -137,10 +136,9 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
 			outputPaths := make([]string, len(inputPaths))
 
 			for i, inputPath := range inputPaths {
-				outputPaths[i] = ctx.GeneratePath(".pdf")
+				outputPaths[i] = ctx.GeneratePath("", ".pdf")
 
 				err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
-
 				if err != nil {
 					if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
 						return api.WrapError(


Test patch:
diff --git a/pkg/modules/api/context_test.go b/pkg/modules/api/context_test.go
index da9bbd9..0b59c5a 100644
--- a/pkg/modules/api/context_test.go
+++ b/pkg/modules/api/context_test.go
@@ -190,11 +190,15 @@ 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_AddOutputPaths(t *testing.T) {
diff --git a/pkg/modules/libreoffice/routes_test.go b/pkg/modules/libreoffice/routes_test.go
index 6470cc6..4fd75de 100644
--- a/pkg/modules/libreoffice/routes_test.go
+++ b/pkg/modules/libreoffice/routes_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"net/http"
+	"reflect"
 	"testing"
 
 	"github.com/labstack/echo/v4"
@@ -22,6 +23,7 @@ func TestConvertRoute(t *testing.T) {
 		engine                 gotenberg.PdfEngine
 		expectOptions          libreofficeapi.Options
 		expectError            bool
+		expectFileNames        []string
 		expectHttpError        bool
 		expectHttpStatus       int
 		expectOutputPathsCount int
@@ -218,6 +220,7 @@ func TestConvertRoute(t *testing.T) {
 			expectError:            false,
 			expectHttpError:        false,
 			expectOutputPathsCount: 1,
+			expectFileNames:        []string{"/document.docx.pdf"},
 		},
 		{
 			scenario: "success (many files)",
@@ -226,6 +229,7 @@ func TestConvertRoute(t *testing.T) {
 				ctx.SetFiles(map[string]string{
 					"document.docx":  "/document.docx",
 					"document2.docx": "/document2.docx",
+					"document2.doc":  "/document2.doc",
 				})
 				return ctx
 			}(),
@@ -234,19 +238,21 @@ func TestConvertRoute(t *testing.T) {
 					return nil
 				},
 				ExtensionsMock: func() []string {
-					return []string{".docx"}
+					return []string{".docx", ".doc"}
 				},
 			},
 			expectError:            false,
 			expectHttpError:        false,
-			expectOutputPathsCount: 2,
+			expectOutputPathsCount: 3,
+			expectFileNames:        []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
 		},
 		{
-			scenario: "success with non-native PDF/A & PDF/UA (single file)",
+			scenario: "success with non-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",
+					"document.docx":  "/document.docx",
+					"document2.docx": "/document2.docx",
 				})
 				ctx.SetValues(map[string][]string{
 					"pdfa": {
@@ -276,14 +282,16 @@ func TestConvertRoute(t *testing.T) {
 			},
 			expectError:            false,
 			expectHttpError:        false,
-			expectOutputPathsCount: 1,
+			expectOutputPathsCount: 2,
+			expectFileNames:        []string{"/document.docx.pdf", "/document2.docx.pdf"},
 		},
 		{
-			scenario: "success with native PDF/A & PDF/UA (single file)",
+			scenario: "success with 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",
+					"document.docx":  "/document.docx",
+					"document2.docx": "/document2.docx",
 				})
 				ctx.SetValues(map[string][]string{
 					"pdfa": {
@@ -310,7 +318,8 @@ func TestConvertRoute(t *testing.T) {
 			},
 			expectError:            false,
 			expectHttpError:        false,
-			expectOutputPathsCount: 1,
+			expectOutputPathsCount: 2,
+			expectFileNames:        []string{"/document.docx.pdf", "/document2.docx.pdf"},
 		},
 		{
 			scenario: "merge error",
@@ -617,6 +626,12 @@ func TestConvertRoute(t *testing.T) {
 			if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
 				t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
 			}
+
+			if len(tc.expectFileNames) != 0 {
+				if !reflect.DeepEqual(tc.ctx.OutputPaths(), tc.expectFileNames) {
+					t.Errorf("expected output paths %v, got %v", tc.expectFileNames, tc.ctx.OutputPaths())
+				}
+			}
 		})
 	}
 }