mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
feat(pdfengines): add embed feature
This commit is contained in:
@@ -38,11 +38,12 @@ 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
|
||||
outputPaths []string
|
||||
cancelled bool
|
||||
dirPath string
|
||||
values map[string][]string
|
||||
files map[string]string
|
||||
filesByField map[string][]string
|
||||
outputPaths []string
|
||||
cancelled bool
|
||||
|
||||
logger *zap.Logger
|
||||
echoCtx echo.Context
|
||||
@@ -79,6 +80,9 @@ type downloadFrom struct {
|
||||
|
||||
// ExtraHttpHeaders are the HTTP headers to send alongside.
|
||||
ExtraHttpHeaders map[string]string `json:"extraHttpHeaders"`
|
||||
|
||||
// Download as embed file
|
||||
Embedded bool `json:"embedded"`
|
||||
}
|
||||
|
||||
// newContext returns a [Context] by parsing a "multipart/form-data" request.
|
||||
@@ -184,6 +188,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
||||
ctx.dirPath = dirPath
|
||||
ctx.values = form.Value
|
||||
ctx.files = make(map[string]string)
|
||||
ctx.filesByField = make(map[string][]string)
|
||||
|
||||
// First, try to download files listed in the "downloadFrom" form field, if
|
||||
// any.
|
||||
@@ -318,6 +323,9 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
||||
}
|
||||
|
||||
ctx.files[filename] = path
|
||||
if dl.Embedded {
|
||||
ctx.filesByField[EmbedsFormField] = append(ctx.filesByField[EmbedsFormField], path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -373,17 +381,22 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
||||
}
|
||||
|
||||
// Then, copy the form files, if any.
|
||||
for _, files := range form.File {
|
||||
for fieldName, files := range form.File {
|
||||
for _, fh := range files {
|
||||
err = copyToDisk(fh)
|
||||
if err != nil {
|
||||
return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
|
||||
}
|
||||
// Track files by field name
|
||||
filename := norm.NFC.String(filepath.Base(fh.Filename))
|
||||
filePath := ctx.files[filename]
|
||||
ctx.filesByField[fieldName] = append(ctx.filesByField[fieldName], filePath)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Log().Debug(fmt.Sprintf("form fields: %+v", ctx.values))
|
||||
ctx.Log().Debug(fmt.Sprintf("form files: %+v", ctx.files))
|
||||
ctx.Log().Debug(fmt.Sprintf("form files by field: %+v", ctx.filesByField))
|
||||
ctx.Log().Debug(fmt.Sprintf("total bytes: %d", totalBytesRead.Load()))
|
||||
|
||||
return ctx, cancel, err
|
||||
@@ -397,9 +410,10 @@ func (ctx *Context) Request() *http.Request {
|
||||
// FormData return a [FormData].
|
||||
func (ctx *Context) FormData() *FormData {
|
||||
return &FormData{
|
||||
values: ctx.values,
|
||||
files: ctx.files,
|
||||
errors: nil,
|
||||
values: ctx.values,
|
||||
files: ctx.files,
|
||||
filesByField: ctx.filesByField,
|
||||
errors: nil,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -16,14 +17,20 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
// EmbedsFormField represents the form field name for embedding files.
|
||||
const (
|
||||
EmbedsFormField string = "embeds"
|
||||
)
|
||||
|
||||
// FormData is a helper for validating and hydrating values from a
|
||||
// "multipart/form-data" request.
|
||||
//
|
||||
// form := ctx.FormData()
|
||||
type FormData struct {
|
||||
values map[string][]string
|
||||
files map[string]string
|
||||
errors error
|
||||
values map[string][]string
|
||||
files map[string]string
|
||||
filesByField map[string][]string
|
||||
errors error
|
||||
}
|
||||
|
||||
// Validate returns nil or an error related to the [FormData] values, with a
|
||||
@@ -358,6 +365,26 @@ func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
|
||||
return form.paths(extensions, target)
|
||||
}
|
||||
|
||||
// Embeds binds the absolute paths of form data files that should be
|
||||
// embedded in the PDF. Only files uploaded with the "embeds" field name
|
||||
// will be included.
|
||||
//
|
||||
// var embeds []string
|
||||
//
|
||||
// ctx.FormData().Embeds(&embeds)
|
||||
func (form *FormData) Embeds(target *[]string) *FormData {
|
||||
if form.errors != nil {
|
||||
return form
|
||||
}
|
||||
|
||||
// Get files from the "embeds" field
|
||||
if paths, ok := form.filesByField[EmbedsFormField]; ok {
|
||||
*target = append(*target, paths...)
|
||||
}
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
// MandatoryPaths binds the absolute paths of form data files, according to a
|
||||
// list of file extensions, to a string slice variable. It populates an error
|
||||
// if there is no file for given file extensions.
|
||||
@@ -381,8 +408,15 @@ func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *For
|
||||
|
||||
// paths bind the absolute paths of form data files, according to a list of
|
||||
// file extensions, to a string slice variable.
|
||||
// embeds are excluded.
|
||||
func (form *FormData) paths(extensions []string, target *[]string) *FormData {
|
||||
embeds, ok := form.filesByField[EmbedsFormField]
|
||||
|
||||
for filename, path := range form.files {
|
||||
if ok && slices.Contains(embeds, path) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ext := range extensions {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/228.
|
||||
if strings.ToLower(filepath.Ext(filename)) == ext {
|
||||
|
||||
@@ -1612,6 +1612,24 @@ func TestFormData_Paths(t *testing.T) {
|
||||
},
|
||||
expectCount: 2,
|
||||
},
|
||||
{
|
||||
scenario: "files except embeds",
|
||||
form: &FormData{
|
||||
files: map[string]string{
|
||||
"foo.pdf": "/foo.pdf",
|
||||
"embed_1.pdf": "/embed_1.pdf",
|
||||
"embed_2.xml": "/embed_2.xml",
|
||||
},
|
||||
filesByField: map[string][]string{
|
||||
"embeds": {"/embed_1.pdf", "/embed_2.xml"},
|
||||
},
|
||||
},
|
||||
extensions: []string{".pdf"},
|
||||
expect: []string{
|
||||
"/foo.pdf",
|
||||
},
|
||||
expectCount: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
var actual []string
|
||||
@@ -1740,3 +1758,28 @@ func TestFormData_mustAssign(t *testing.T) {
|
||||
var target []string
|
||||
form.mustAssign("foo", "foo", &target)
|
||||
}
|
||||
|
||||
func TestFormData_Embeds(t *testing.T) {
|
||||
expected := []string{"/bar.xml", "/baz.xml"}
|
||||
|
||||
var actual []string
|
||||
form := &FormData{
|
||||
files: map[string]string{
|
||||
"foo.pdf": "/foo.pdf",
|
||||
"bar.xml": "/bar.xml",
|
||||
"baz.xml": "/baz.xml",
|
||||
},
|
||||
filesByField: map[string][]string{
|
||||
"embeds": {"/bar.xml", "/baz.xml"},
|
||||
},
|
||||
}
|
||||
form.Embeds(&actual)
|
||||
|
||||
if len(actual) != len(expected) {
|
||||
t.Errorf("expected %d embeds but got %d", len(expected), len(actual))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("expected %v but got %v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user