feat(exiftool): refactor read write metadata

This commit is contained in:
Julien Neuhart
2024-03-23 11:13:05 +01:00
parent dc613aa2bf
commit 0a8625227f
22 changed files with 580 additions and 1138 deletions

View File

@@ -37,8 +37,8 @@ func (mod *ValidatorMock) Validate() error {
type PdfEngineMock struct { type PdfEngineMock struct {
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath 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, metadata map[string]interface{}) error ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
} }
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
@@ -49,12 +49,12 @@ func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, fo
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath) return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
} }
func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return engine.ReadMetadataMock(ctx, logger, inputPath, metadata) return engine.ReadMetadataMock(ctx, logger, inputPath)
} }
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return engine.WriteMetadataMock(ctx, logger, inputPath, newMetadata) return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
} }
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface. // PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.

View File

@@ -55,10 +55,10 @@ func TestPDFEngineMock(t *testing.T) {
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error { ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
return nil return nil
}, },
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return nil return nil, nil
}, },
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return nil return nil
}, },
} }
@@ -73,12 +73,12 @@ func TestPDFEngineMock(t *testing.T) {
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err) t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
} }
err = mock.ReadMetadataMock(context.Background(), zap.NewNop(), "", map[string]interface{}{}) _, err = mock.ReadMetadataMock(context.Background(), zap.NewNop(), "")
if err != nil { if err != nil {
t.Errorf("expected no error from PdfEngineMock.ReadMetadata, but got: %v", err) t.Errorf("expected no error from PdfEngineMock.ReadMetadata, but got: %v", err)
} }
err = mock.WriteMetadataMock(context.Background(), zap.NewNop(), "", map[string]interface{}{}) err = mock.WriteMetadataMock(context.Background(), zap.NewNop(), map[string]interface{}{}, "")
if err != nil { if err != nil {
t.Errorf("expected no error from PdfEngineMock.WriteMetadata but got: %v", err) t.Errorf("expected no error from PdfEngineMock.WriteMetadata but got: %v", err)
} }

View File

@@ -15,6 +15,10 @@ var (
// ErrPdfFormatNotSupported is returned when the Convert method of the // ErrPdfFormatNotSupported is returned when the Convert method of the
// PdfEngine interface does not support a requested PDF format conversion. // PdfEngine interface does not support a requested PDF format conversion.
ErrPdfFormatNotSupported = errors.New("PDF format not supported") ErrPdfFormatNotSupported = errors.New("PDF format not supported")
// ErrPdfEngineMetadataValueNotSupported is returned when a metadata value
// is not supported.
ErrPdfEngineMetadataValueNotSupported = errors.New("metadata value not supported")
) )
const ( const (
@@ -65,11 +69,11 @@ type PdfEngine interface {
// PdfFormats. If no format, it does nothing. // PdfFormats. If no format, it does nothing.
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
// ReadMetadata extracts the metadata of a given PDF file and load them into the provided metadata object. // ReadMetadata extracts the metadata of a given PDF file.
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
// WriteMetadata writes the metadata into a given PDF file. // WriteMetadata writes the metadata into a given PDF file.
WriteMetadata(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
} }
// PdfEngineProvider offers an interface to instantiate a [PdfEngine]. // PdfEngineProvider offers an interface to instantiate a [PdfEngine].

View File

@@ -9,7 +9,7 @@ import (
// AlphanumericSort implements sort.Interface and helps to sort strings // AlphanumericSort implements sort.Interface and helps to sort strings
// alphanumerically. // alphanumerically.
// //
// See https://github.com/gotenberg/gotenberg/issues/805. // See: https://github.com/gotenberg/gotenberg/issues/805.
type AlphanumericSort []string type AlphanumericSort []string
func (s AlphanumericSort) Len() int { func (s AlphanumericSort) Len() int {

View File

@@ -44,6 +44,10 @@ func ParseError(err error) (int, string) {
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" 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"
} }
if errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
return http.StatusBadRequest, "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues"
}
var httpErr HttpError var httpErr HttpError
if errors.As(err, &httpErr) { if errors.As(err, &httpErr) {
return httpErr.HttpError() return httpErr.HttpError()

View File

@@ -43,6 +43,11 @@ func TestParseError(t *testing.T) {
expectStatus: http.StatusBadRequest, expectStatus: http.StatusBadRequest,
expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues", expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues",
}, },
{
err: gotenberg.ErrPdfEngineMetadataValueNotSupported,
expectStatus: http.StatusBadRequest,
expectMessage: "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues",
},
{ {
err: WrapError( err: WrapError(
errors.New("foo"), errors.New("foo"),

View File

@@ -78,7 +78,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
} }
if value != "screen" && value != "print" { if value != "screen" && value != "print" {
return fmt.Errorf("wrong value, expected either 'screen', 'print' or empty") return errors.New("wrong value, expected either 'screen', 'print' or empty")
} }
emulatedMediaType = value emulatedMediaType = value
@@ -233,16 +233,14 @@ func FormDataChromiumPdfFormats(form *api.FormData) gotenberg.PdfFormats {
} }
} }
// FormDataMetadata creates metadata object from the form data. // FormDataPdfMetadata creates metadata object from the form data.
func FormDataMetadata(form *api.FormData) map[string]interface{} { func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
var metadata map[string]interface{} var metadata map[string]interface{}
form.Custom("metadata", func(value string) error { form.Custom("metadata", func(value string) error {
metadata = map[string]interface{}{}
if len(value) > 0 { if len(value) > 0 {
err := json.Unmarshal([]byte(value), &metadata) err := json.Unmarshal([]byte(value), &metadata)
if err != nil { if err != nil {
return err return fmt.Errorf("unmarshal metadata: %w", err)
} }
} }
return nil return nil
@@ -260,7 +258,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(form) pdfFormats := FormDataChromiumPdfFormats(form)
metadata := FormDataMetadata(form) metadata := FormDataPdfMetadata(form)
var url string var url string
err := form. err := form.
@@ -270,7 +268,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options, metadata) err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
if err != nil { if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err) return fmt.Errorf("convert URL to PDF: %w", err)
} }
@@ -320,7 +318,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(form) pdfFormats := FormDataChromiumPdfFormats(form)
metadata := FormDataMetadata(form) metadata := FormDataPdfMetadata(form)
var inputPath string var inputPath string
err := form. err := form.
@@ -331,7 +329,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
} }
url := fmt.Sprintf("file://%s", inputPath) url := fmt.Sprintf("file://%s", inputPath)
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options, metadata) err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
if err != nil { if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err) return fmt.Errorf("convert HTML to PDF: %w", err)
} }
@@ -382,7 +380,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(form) pdfFormats := FormDataChromiumPdfFormats(form)
metadata := FormDataMetadata(form) metadata := FormDataPdfMetadata(form)
var ( var (
inputPath string inputPath string
@@ -402,7 +400,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("transform markdown file(s) to HTML: %w", err) return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
} }
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options, metadata) err = convertUrl(ctx, chromium, engine, url, options, pdfFormats, metadata)
if err != nil { if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err) return fmt.Errorf("convert markdown to PDF: %w", err)
} }
@@ -526,7 +524,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
return fmt.Sprintf("file://%s", inputPath), nil return fmt.Sprintf("file://%s", inputPath), nil
} }
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options PdfOptions, metadata map[string]interface{}) error { func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, pdfFormats gotenberg.PdfFormats, metadata map[string]interface{}) error {
outputPath := ctx.GeneratePath(".pdf") outputPath := ctx.GeneratePath(".pdf")
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options) err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
@@ -584,7 +582,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
// Writes and potentially overrides metadata entries, if any. // Writes and potentially overrides metadata entries, if any.
if len(metadata) > 0 { if len(metadata) > 0 {
err = engine.WriteMetadata(ctx, ctx.Log(), outputPath, metadata) err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
} }

View File

@@ -399,6 +399,57 @@ func TestFormDataChromiumPdfFormats(t *testing.T) {
} }
} }
func TestFormDataPdfMetadata(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
expectedMetadata map[string]interface{}
}{
{
scenario: "no metadata form field",
ctx: &api.ContextMock{Context: new(api.Context)},
expectedMetadata: nil,
},
{
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,
},
{
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",
},
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
actual := FormDataPdfMetadata(tc.ctx.Context.FormData())
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
}
})
}
}
func TestConvertUrlRoute(t *testing.T) { func TestConvertUrlRoute(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
@@ -1223,8 +1274,8 @@ func TestConvertUrl(t *testing.T) {
ctx *api.ContextMock ctx *api.ContextMock
api Api api Api
engine gotenberg.PdfEngine engine gotenberg.PdfEngine
pdfFormats gotenberg.PdfFormats
options PdfOptions options PdfOptions
pdfFormats gotenberg.PdfFormats
metadata map[string]interface{} metadata map[string]interface{}
expectError bool expectError bool
expectHttpError bool expectHttpError bool
@@ -1331,7 +1382,7 @@ func TestConvertUrl(t *testing.T) {
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{ {
scenario: "error from PDF engine", scenario: "PDF engine convert error",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
return nil return nil
@@ -1339,14 +1390,14 @@ func TestConvertUrl(t *testing.T) {
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo") return errors.New("foo")
}}, }},
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
options: DefaultPdfOptions(), options: DefaultPdfOptions(),
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
expectError: true, expectError: true,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{ {
scenario: "success with pdfa form field", scenario: "success with PDF formats",
ctx: &api.ContextMock{Context: new(api.Context)}, ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
return nil return nil
@@ -1354,12 +1405,29 @@ func TestConvertUrl(t *testing.T) {
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil return nil
}}, }},
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
options: DefaultPdfOptions(), options: DefaultPdfOptions(),
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
}, },
{
scenario: "PDF engine write metadata 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{WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return errors.New("foo")
}},
options: DefaultPdfOptions(),
metadata: map[string]interface{}{
"Creator": "foo",
"Producer": "bar",
},
expectError: true,
expectHttpError: false,
},
{ {
scenario: "cannot add output paths", scenario: "cannot add output paths",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -1381,38 +1449,16 @@ func TestConvertUrl(t *testing.T) {
api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
return nil return nil
}}, }},
options: DefaultPdfOptions(), engine: &gotenberg.PdfEngineMock{
expectError: false, ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
expectHttpError: false, return nil
expectOutputPathsCount: 1, },
}, WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
{ return nil
scenario: "error with metadata write", },
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{WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return fmt.Errorf("error writing metadata to %s: %w", "foo.pdf", errors.New("foo"))
}},
options: DefaultPdfOptions(),
metadata: map[string]interface{}{
"Creator": "foo",
"Producer": "bar",
}, },
expectError: true, options: DefaultPdfOptions(),
expectHttpError: false, pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b},
},
{
scenario: "success with metadata write",
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{WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return nil
}},
options: DefaultPdfOptions(),
metadata: map[string]interface{}{ metadata: map[string]interface{}{
"Creator": "foo", "Creator": "foo",
"Producer": "bar", "Producer": "bar",
@@ -1424,7 +1470,7 @@ func TestConvertUrl(t *testing.T) {
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop()) tc.ctx.SetLogger(zap.NewNop())
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.pdfFormats, tc.options, tc.metadata) err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.options, tc.pdfFormats, tc.metadata)
if tc.expectError && err == nil { if tc.expectError && err == nil {
t.Fatal("expected error but got none", err) t.Fatal("expected error but got none", err)

View File

@@ -4,59 +4,25 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"os" "os"
"github.com/barasher/go-exiftool" "github.com/barasher/go-exiftool"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
) )
func init() { func init() {
gotenberg.MustRegisterModule(new(ExifTool)) gotenberg.MustRegisterModule(new(ExifTool))
} }
// MetadataValueTypeError is constructed when metadata value types cannot be processed. // ExifTool abstracts the CLI tool ExifTool and implements the
// The underlying library used in this implementation supports the writing of a limited // [gotenberg.PdfEngine] interface.
// number of metadata value types.
//
// For example, according to https://exiftool.org/TagNames/PDF.html metadata can be a boolean,
// i.e. "value format... may be string, date, integer, real, boolean or name".
// Furthermore, a native boolean type is also supported by JSON and Go. However, the
// underlying library does not currently support writing native Go boolean (bool) types.
// Therefore, an instance of this struct is created when a boolean metadata entry is supplied.
//
// The struct contains a key/value map corresponding to individual invalid metadata entries supplied by a consumer.
// This allows a helpful error message to be produced for API consumers.
// See API.WriteMetadata for more information on valid metadata value types.
type MetadataValueTypeError struct {
Entries map[string]interface{}
}
// Error returns a helpful error message.
func (e *MetadataValueTypeError) Error() string {
return fmt.Sprintf("invalid metadata value types supplied - identified by Entries: %s", e.Entries)
}
// GetKeys returns an array of keys with corresponding invalid value types,
func (e *MetadataValueTypeError) GetKeys() []string {
keys := make([]string, len(e.Entries))
i := 0
for key := range e.Entries {
keys[i] = key
i++
}
return keys
}
// ExifTool abstracts the CLI tool ExifTool and implements the [gotenberg.PdfEngine] interface .
type ExifTool struct { type ExifTool struct {
binPath string binPath string
} }
// Descriptor returns ExifTool's module descriptor. // Descriptor returns [ExifTool]'s module descriptor.
func (engine *ExifTool) Descriptor() gotenberg.ModuleDescriptor { func (engine *ExifTool) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ return gotenberg.ModuleDescriptor{
ID: "exiftool", ID: "exiftool",
@@ -64,9 +30,7 @@ func (engine *ExifTool) Descriptor() gotenberg.ModuleDescriptor {
} }
} }
// Provision sets the module properties. It returns an error if // Provision sets the module properties.
// - the environment variable EXIFTOOL_BIN_PATH is not set
// - there is an error creating an instance of exiftool.ExifTool
func (engine *ExifTool) Provision(ctx *gotenberg.Context) error { func (engine *ExifTool) Provision(ctx *gotenberg.Context) error {
binPath, ok := os.LookupEnv("EXIFTOOL_BIN_PATH") binPath, ok := os.LookupEnv("EXIFTOOL_BIN_PATH")
if !ok { if !ok {
@@ -90,99 +54,82 @@ func (engine *ExifTool) Validate() error {
// Merge is not available in this implementation. // Merge is not available in this implementation.
func (engine *ExifTool) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { func (engine *ExifTool) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return fmt.Errorf("merge PDFs with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("merge PDFs with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Convert is not available in this implementation. // Convert is not available in this implementation.
func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { 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 PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
} }
// ReadMetadata reads the metadata of the given PDF files. // ReadMetadata extracts the metadata of a given PDF file.
func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
logger.Debug(fmt.Sprintf("reading metadata of file: %s", inputPath)) exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
exifTool, err := exiftool.NewExiftool()
if err != nil { if err != nil {
fmt.Printf("Error intializing ExifTool: %v\n", err) return nil, fmt.Errorf("new ExifTool: %w", err)
return err
} }
fileMetadataInfos := exifTool.ExtractMetadata([]string{inputPath}...) defer func(exifTool *exiftool.Exiftool) {
err := exifTool.Close()
if err != nil {
logger.Error(fmt.Sprintf("close ExifTool: %v", err))
}
}(exifTool)
if fileMetadataInfos[0].Err != nil { fileMetadata := exifTool.ExtractMetadata(inputPath)
return fmt.Errorf("error reading metadata to following file: %+v", fileMetadataInfos[0]) if fileMetadata[0].Err != nil {
return nil, fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
} }
// load into metadata return fileMetadata[0].Fields, nil
for k, v := range fileMetadataInfos[0].Fields {
metadata[k] = v
}
return exifTool.Close()
} }
// WriteMetadata write the metadata to the given PDF files. // WriteMetadata writes the metadata into a given PDF file.
func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
logger.Debug(fmt.Sprintf("writing new metadata %s to %s", newMetadata, inputPath)) exifTool, err := exiftool.NewExiftool(exiftool.SetExiftoolBinaryPath(engine.binPath))
exifTool, err := exiftool.NewExiftool()
if err != nil { if err != nil {
fmt.Printf("Error intializing ExifTool: %v\n", err) return fmt.Errorf("new ExifTool: %w", err)
return err
} }
fileMetadataInfos := exifTool.ExtractMetadata([]string{inputPath}...) defer func(exifTool *exiftool.Exiftool) {
err := exifTool.Close()
if err != nil {
logger.Error(fmt.Sprintf("close ExifTool: %v", err))
}
}(exifTool)
// there is only file metadata info fileMetadata := exifTool.ExtractMetadata(inputPath)
if fileMetadataInfos[0].Err != nil { if fileMetadata[0].Err != nil {
return fmt.Errorf("error reading metadata to following file: %+v", fileMetadataInfos[0]) return fmt.Errorf("read metadata with ExitfTool: %w", fileMetadata[0].Err)
} }
// Metadata values can only be specific value types. for key, value := range metadata {
// An error is returned if metadata with an invalid type is requested.
metadataValueErrors := MetadataValueTypeError{
Entries: make(map[string]interface{}),
}
// transform metadata
for key, value := range newMetadata {
switch val := value.(type) { switch val := value.(type) {
case string: case string:
fileMetadataInfos[0].SetString(key, val) fileMetadata[0].SetString(key, val)
case int: case int:
fileMetadataInfos[0].SetInt(key, int64(val)) fileMetadata[0].SetInt(key, int64(val))
case int64: case int64:
fileMetadataInfos[0].SetInt(key, val) fileMetadata[0].SetInt(key, val)
case float32: case float32:
fileMetadataInfos[0].SetFloat(key, float64(val)) fileMetadata[0].SetFloat(key, float64(val))
case float64: case float64:
fileMetadataInfos[0].SetFloat(key, val) fileMetadata[0].SetFloat(key, val)
case []string: case []string:
fileMetadataInfos[0].SetStrings(key, val) fileMetadata[0].SetStrings(key, val)
// TODO: support more complex cases, e.g. arrays and nested objects (limitations in underlying library) // TODO: support more complex cases, e.g., arrays and nested objects
// (limitations in underlying library).
default: default:
metadataValueErrors.Entries[key] = value return fmt.Errorf("write PDF metadata with ExifTool: %w", gotenberg.ErrPdfEngineMetadataValueNotSupported)
} }
} }
logger.Debug(fmt.Sprintf("writing metadata %s to %s", fileMetadataInfos[0].Fields, fileMetadataInfos[0].File))
if len(metadataValueErrors.Entries) > 0 { exifTool.WriteMetadata(fileMetadata)
return api.WrapError( if fileMetadata[0].Err != nil {
fmt.Errorf("write metadata: %w", err), return fmt.Errorf("write PDF metadata with ExifTool: %w", fileMetadata[0].Err)
api.NewSentinelHttpError(
http.StatusBadRequest,
fmt.Sprintf("Invalid metdata value types supplied by keys '%s'", metadataValueErrors.GetKeys())),
)
} }
exifTool.WriteMetadata(fileMetadataInfos) return nil
if fileMetadataInfos[0].Err != nil {
return fmt.Errorf("error writing metadata to following file: %+v", fileMetadataInfos[0])
}
return exifTool.Close()
} }
// Interface guards. // Interface guards.

View File

@@ -9,62 +9,11 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
func TestMetadataValueTypeError_Error(t *testing.T) {
instance := MetadataValueTypeError{
Entries: map[string]interface{}{
"foo": "foo",
},
}
assert.True(t, len(instance.Error()) > 0)
}
func TestMetadataValueTypeError_GetKeys(t *testing.T) {
for i, tc := range []struct {
instance MetadataValueTypeError
expect []string
}{
{
instance: MetadataValueTypeError{
Entries: map[string]interface{}{},
},
expect: []string{},
},
{
instance: MetadataValueTypeError{
Entries: map[string]interface{}{
"foo": "foo",
},
},
expect: []string{"foo"},
},
{
instance: MetadataValueTypeError{
Entries: map[string]interface{}{
"foo": "foo",
"bar": float64(123),
"baz": 4.56,
"qux": true,
"quux": nil,
},
},
expect: []string{"foo", "bar", "baz", "qux", "quux"},
},
} {
actual := tc.instance.GetKeys()
if !assert.ElementsMatch(t, actual, tc.expect) {
t.Errorf("test %d: expected %+v but got: %+v", i, tc.expect, actual)
}
}
}
func TestExifTool_Descriptor(t *testing.T) { func TestExifTool_Descriptor(t *testing.T) {
descriptor := new(ExifTool).Descriptor() descriptor := new(ExifTool).Descriptor()
@@ -144,59 +93,28 @@ func TestExiftool_Convert(t *testing.T) {
func TestExiftool_ReadMetadata(t *testing.T) { func TestExiftool_ReadMetadata(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
ctx context.Context inputPath string
inputPath string expectMetadata map[string]interface{}
subset map[string]interface{} expectError bool
expectError bool
expectDiff bool
}{ }{
{ {
scenario: "invalid input path", scenario: "invalid input path",
ctx: context.TODO(), inputPath: "foo",
inputPath: "foo", expectMetadata: nil,
expectError: true, expectError: true,
}, },
{ {
scenario: "single file success", scenario: "success",
ctx: context.TODO(),
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
subset: map[string]interface{}{ expectMetadata: map[string]interface{}{
"FileName": "sample1.pdf", "FileName": "sample1.pdf",
"FileTypeExtension": "pdf", "FileTypeExtension": "pdf",
"MIMEType": "application/pdf", "MIMEType": "application/pdf",
"PDFVersion": 1.4, "PDFVersion": 1.4,
"PageCount": float64(3), "PageCount": float64(3),
"CreateDate": "2018:12:06 17:50:06+00:00",
"ModifyDate": "2018:12:06 17:50:06+00:00",
"Directory": "/tests/test/testdata/pdfengines",
"FileType": "PDF",
"Linearized": "No",
"Creator": "Chromium",
"Producer": "Skia/PDF m70",
"SourceFile": "/tests/test/testdata/pdfengines/sample1.pdf",
}, },
}, expectError: false,
{
scenario: "single file incorrect metadata",
ctx: context.TODO(),
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
subset: map[string]interface{}{
"FileName": "sample1.pdf",
"FileTypeExtension": "pdf",
"MIMEType": "application/pdf",
"PDFVersion": 1.4,
"PageCount": float64(3),
"CreateDate": "2018:12:06 17:50:06+00:00",
"ModifyDate": "2018:12:06 17:50:06+00:00",
"Directory": "/tests/test/testdata/pdfengines",
"FileType": "PDF",
"Linearized": "No",
"Creator": "INVALID",
"Producer": "Skia/PDF m70",
"SourceFile": "/tests/test/testdata/pdfengines/sample1.pdf",
},
expectDiff: true,
}, },
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
@@ -206,8 +124,8 @@ func TestExiftool_ReadMetadata(t *testing.T) {
t.Fatalf("expected error but got: %v", err) t.Fatalf("expected error but got: %v", err)
} }
actualMetadata := map[string]interface{}{} metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), tc.inputPath)
err = engine.ReadMetadata(tc.ctx, zap.NewNop(), tc.inputPath, actualMetadata)
if !tc.expectError && err != nil { if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -216,13 +134,11 @@ func TestExiftool_ReadMetadata(t *testing.T) {
t.Fatal("expected error but got none") t.Fatal("expected error but got none")
} }
if tc.subset != nil && err == nil { if tc.expectMetadata != nil && err == nil {
if !tc.expectDiff && !isMapSubset(actualMetadata, tc.subset) { for k, v := range tc.expectMetadata {
t.Errorf("test: %s: expected: %+v to be a subset of: %+v at path: %s", if v2, ok := metadata[k]; !ok || v != v2 {
tc.scenario, tc.subset, actualMetadata, tc.inputPath) t.Errorf("expected entry %s with value %v to exists", k, v)
} else if tc.expectDiff && isMapSubset(actualMetadata, tc.subset) { }
t.Errorf("test: %s: expected: %+v to be not be a subset of: %+v at path: %s",
tc.scenario, tc.subset, actualMetadata, tc.inputPath)
} }
} }
}) })
@@ -231,101 +147,133 @@ func TestExiftool_ReadMetadata(t *testing.T) {
func TestExiftool_WriteMetadata(t *testing.T) { func TestExiftool_WriteMetadata(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string
ctx context.Context createCopy bool
inputPath string inputPath string
newMetadata map[string]interface{} metadata map[string]interface{}
contains map[string]interface{} expectMetadata map[string]interface{}
expectError bool expectError bool
expectDiff bool expectedError error
}{ }{
{ {
scenario: "single file success", scenario: "invalid input path",
ctx: context.TODO(), createCopy: false,
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", inputPath: "foo",
newMetadata: map[string]interface{}{
"Producer": "foo",
},
contains: map[string]interface{}{
"Producer": "foo",
},
expectError: false,
expectDiff: false,
},
{
scenario: "single file not same metadata",
ctx: context.TODO(),
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
newMetadata: map[string]interface{}{
"Producer": "foo",
},
contains: map[string]interface{}{
"Producer": "foobar",
},
expectError: false,
expectDiff: true,
},
{
scenario: "single file unknown type",
ctx: context.TODO(),
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
newMetadata: map[string]interface{}{
"foo": map[string]string{},
},
expectError: true, expectError: true,
expectDiff: false, },
{
scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported",
createCopy: true,
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
metadata: map[string]interface{}{
"Unsupported": map[string]interface{}{},
},
expectError: true,
expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported,
},
{
scenario: "success",
createCopy: true,
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
metadata: map[string]interface{}{
"Author": "Julien Neuhart",
"Copyright": "Julien Neuhart",
"CreationDate": "2006-09-18T16:27:50-04:00",
"Creator": "Gotenberg",
"Keywords": []string{
"first",
"second",
},
"Marked": "true",
"ModDate": "2006-09-18T16:27:50-04:00",
"PDFVersion": 1.7,
"Producer": "Gotenberg",
"Subject": "Sample",
"Title": "Sample",
"Trapped": "Unknown",
// Those are not valid PDF metadata.
"int": 1,
"int64": int64(2),
"float32": float32(2.2),
"float64": 3.3,
},
expectMetadata: map[string]interface{}{
"Author": "Julien Neuhart",
"Copyright": "Julien Neuhart",
"CreationDate": "2006:09:18 16:27:50-04:00",
"Creator": "Gotenberg",
"Keywords": []interface{}{
"first",
"second",
},
"Marked": true,
"ModDate": "2006:09:18 16:27:50-04:00",
"PDFVersion": 1.7,
"Producer": "Gotenberg",
"Subject": "Sample",
"Title": "Sample",
"Trapped": "Unknown",
},
expectError: false,
}, },
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
engine := new(ExifTool) engine := new(ExifTool)
err := engine.Provision(nil) err := engine.Provision(nil)
if err != nil { if err != nil {
t.Fatalf("expected error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
fs := gotenberg.NewFileSystem() var destinationPath string
outputDir, err := fs.MkdirAll() if tc.createCopy {
if err != nil { fs := gotenberg.NewFileSystem()
t.Fatalf("expected error but got: %v", err) outputDir, err := fs.MkdirAll()
}
defer func() {
err = os.RemoveAll(fs.WorkingDirPath())
if err != nil { if err != nil {
t.Fatalf("expected no error while cleaning up but got: %v", err) t.Fatalf("expected error no but got: %v", err)
} }
}()
copyPath := fmt.Sprintf("%s/copy_temp.pdf", outputDir) defer func() {
// open the source file err = os.RemoveAll(fs.WorkingDirPath())
source, err := os.Open(tc.inputPath) if err != nil {
if err != nil { t.Fatalf("expected no error while cleaning up but got: %v", err)
t.Fatalf("error in opening file: %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
} }
// create the destination file err = engine.WriteMetadata(context.Background(), zap.NewNop(), tc.metadata, destinationPath)
destination, err := os.Create(copyPath)
if err != nil {
t.Fatalf("error in creating file: %v", err)
}
// copy the contents of source to destination file
_, err = io.Copy(destination, source)
if err != nil {
t.Fatalf("error in copying file: %v", err)
}
err = source.Close()
if err != nil {
t.Fatalf("error in source file close: %v", err)
}
err = destination.Close()
if err != nil {
t.Fatalf("error in destination file close: %v", err)
}
// write metadata to new copy files
err = engine.WriteMetadata(tc.ctx, zap.NewNop(), copyPath, tc.newMetadata)
if !tc.expectError && err != nil { if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
} }
@@ -334,85 +282,41 @@ func TestExiftool_WriteMetadata(t *testing.T) {
t.Fatal("expected error but got none") t.Fatal("expected error but got none")
} }
if err == nil { if tc.expectError {
readMetadata := map[string]interface{}{} return
readErr := engine.ReadMetadata(tc.ctx, zap.NewNop(), copyPath, readMetadata) }
if tc.contains != nil && readErr == nil {
// match metadata if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
if !tc.expectDiff && !isMapSubset(readMetadata, tc.contains) { t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
t.Errorf("test: %s: expected: %+v to be a subset of: %+v at path: %s", }
tc.scenario, tc.contains, readMetadata, copyPath)
} else if tc.expectDiff && isMapSubset(readMetadata, tc.contains) { metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), destinationPath)
t.Errorf("test: %s: expected: %+v to be not be a subset of: %+v at path: %s", if err != nil {
tc.scenario, tc.contains, readMetadata, copyPath) t.Fatalf("expected no error but got: %v", err)
}
if tc.expectMetadata != nil && err == nil {
for k, v := range tc.expectMetadata {
v2, ok := metadata[k]
if !ok {
t.Errorf("expected entry %s with value %v to exists, but got none", k, v)
continue
}
switch v2.(type) {
case []interface{}:
for i, entry := range v.([]interface{}) {
if entry != v2.([]interface{})[i] {
t.Errorf("expected entry %s to contain value %v, but got %v", k, entry, v2.([]interface{})[i])
}
}
default:
if v != v2 {
t.Errorf("expected entry %s with value %v to exists, but got %v", k, v, v2)
}
} }
} }
} }
}) })
} }
} }
func isMapSubset(mapSet interface{}, mapSubset interface{}) bool {
mapSetValue := reflect.ValueOf(mapSet)
mapSubsetValue := reflect.ValueOf(mapSubset)
if mapSetValue.Kind() != reflect.Map || mapSubsetValue.Kind() != reflect.Map {
return false
}
if reflect.TypeOf(mapSetValue) != reflect.TypeOf(mapSubsetValue) {
return false
}
if len(mapSubsetValue.MapKeys()) == 0 {
return true
}
iterMapSubset := mapSubsetValue.MapRange()
for iterMapSubset.Next() {
k := iterMapSubset.Key()
v := iterMapSubset.Value()
v2 := mapSetValue.MapIndex(k)
if !v2.IsValid() {
return false
}
if isValueKind(v, reflect.Slice) && isValueKind(v2, reflect.Slice) {
vSlice := convertSlice(v)
v2Slice := convertSlice(v2)
if !equal(vSlice, v2Slice) {
return false
}
} else if v.Interface() != v2.Interface() {
return false
}
}
return true
}
func isValueKind(value reflect.Value, kind reflect.Kind) bool {
return reflect.TypeOf(value.Interface()).Kind() == kind
}
func convertSlice(value reflect.Value) []interface{} {
slice := make([]interface{}, reflect.ValueOf(value.Interface()).Len())
for i := range slice {
slice = append(slice, reflect.ValueOf(value.Interface()).Index(i).Interface())
}
return slice
}
// equal tells whether a and b contain the same elements.
// A nil argument is equivalent to an empty slice.
func equal(a, b []interface{}) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}

View File

@@ -72,12 +72,12 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *LibreOfficePdfEngine) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, metadata map[string]interface{}) error { func (engine *LibreOfficePdfEngine) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return fmt.Errorf("read PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported) return nil, fmt.Errorf("read PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, newMetadata map[string]interface{}) error { func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }

View File

@@ -169,7 +169,7 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
engine := new(LibreOfficePdfEngine) engine := new(LibreOfficePdfEngine)
err := engine.ReadMetadata(context.Background(), zap.NewNop(), "", nil) _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
@@ -178,7 +178,7 @@ func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
engine := new(LibreOfficePdfEngine) engine := new(LibreOfficePdfEngine)
err := engine.WriteMetadata(context.Background(), zap.NewNop(), "", nil) err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)

View File

@@ -46,11 +46,10 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
Bool("nativePdfFormats", &nativePdfFormats, true). Bool("nativePdfFormats", &nativePdfFormats, true).
Bool("merge", &merge, false). Bool("merge", &merge, false).
Custom("metadata", func(value string) error { Custom("metadata", func(value string) error {
metadata = map[string]interface{}{}
if len(value) > 0 { if len(value) > 0 {
err := json.Unmarshal([]byte(value), &metadata) err := json.Unmarshal([]byte(value), &metadata)
if err != nil { if err != nil {
return err return fmt.Errorf("unmarshal metadata: %w", err)
} }
} }
return nil return nil
@@ -102,8 +101,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
} }
} }
// So far so good, let's check if we have to merge the PDFs. Quick // So far so good, let's check if we have to merge the PDFs.
// win: if there is only one PDF, skip this step.
if len(outputPaths) > 1 && merge { if len(outputPaths) > 1 && merge {
outputPath := ctx.GeneratePath(".pdf") outputPath := ctx.GeneratePath(".pdf")
@@ -112,42 +110,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
return fmt.Errorf("merge PDFs: %w", err) return fmt.Errorf("merge PDFs: %w", err)
} }
// Now, let's check if the client want to convert this // Only one output path.
// resulting PDF to specific PDF formats. outputPaths = []string{outputPath}
zeroValued := gotenberg.PdfFormats{}
if !nativePdfFormats && 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(), outputPath, metadata)
if err != nil {
return fmt.Errorf("write metadata failure: %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)
if err != nil {
return fmt.Errorf("add output path: %w", err)
}
return nil
} }
// Ok, we don't have to merge the PDFs. Let's check if the client // Let's check if the client want to convert each PDF to a specific
// want to convert each PDF to a specific PDF format. // PDF format.
zeroValued := gotenberg.PdfFormats{} zeroValued := gotenberg.PdfFormats{}
if !nativePdfFormats && pdfFormats != zeroValued { if !nativePdfFormats && pdfFormats != zeroValued {
convertOutputPaths := make([]string, len(outputPaths)) convertOutputPaths := make([]string, len(outputPaths))
@@ -166,6 +134,16 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
outputPaths = convertOutputPaths 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)
}
}
}
if len(outputPaths) > 1 { if len(outputPaths) > 1 {
// If .zip archive, document.docx -> document.docx.pdf. // If .zip archive, document.docx -> document.docx.pdf.
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
@@ -180,16 +158,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
} }
} }
// Writes and potentially overrides metadata entries, if any.
if len(metadata) > 0 {
for _, outputPath := range outputPaths {
err = engine.WriteMetadata(ctx, ctx.Log(), outputPath, metadata)
if err != nil {
return fmt.Errorf("write metadata: %w", err)
}
}
}
// Last but not least, add the output paths to the context so that // 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. // the API is able to send them as a response to the client.
err = ctx.AddOutputPaths(outputPaths...) err = ctx.AddOutputPaths(outputPaths...)

View File

@@ -39,6 +39,28 @@ func TestConvertRoute(t *testing.T) {
expectHttpStatus: http.StatusBadRequest, expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "invalid metadata form field",
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{
"metadata": {
"foo",
},
})
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string {
return []string{".docx"}
}},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{ {
scenario: "ErrPdfFormatNotSupported (nativePdfFormats)", scenario: "ErrPdfFormatNotSupported (nativePdfFormats)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -110,7 +132,39 @@ func TestConvertRoute(t *testing.T) {
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{ {
scenario: "PDF engine convert error (single file)", scenario: "PDF engine merge 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",
})
ctx.SetValues(map[string][]string{
"merge": {
"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{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "PDF engine convert error",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
@@ -144,13 +198,17 @@ func TestConvertRoute(t *testing.T) {
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{ {
scenario: "cannot add output paths (single file)", scenario: "PDF engine write metadata error",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"document.docx": "/document.docx", "document.docx": "/document.docx",
}) })
ctx.SetCancelled(true) ctx.SetValues(map[string][]string{
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
return ctx return ctx
}(), }(),
libreOffice: &libreofficeapi.ApiMock{ libreOffice: &libreofficeapi.ApiMock{
@@ -161,31 +219,15 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"} return []string{".docx"}
}, },
}, },
engine: &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return errors.New("foo")
},
},
expectError: true, expectError: true,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "success (single file)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
})
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"}
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{ {
scenario: "cannot rename many files", scenario: "cannot rename many files",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -212,6 +254,78 @@ func TestConvertRoute(t *testing.T) {
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 0, expectOutputPathsCount: 0,
}, },
{
scenario: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
})
ctx.SetCancelled(true)
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
return nil
},
ExtensionsMock: func() []string {
return []string{".docx"}
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "success (single file)",
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",
},
"pdfa": {
gotenberg.PdfA1b,
},
"pdfua": {
"true",
},
"nativePdfFormats": {
"false",
},
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
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{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{ {
scenario: "success (many files)", scenario: "success (many files)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
@@ -221,6 +335,20 @@ func TestConvertRoute(t *testing.T) {
"document2.docx": "/document2.docx", "document2.docx": "/document2.docx",
"document2.doc": "/document2.doc", "document2.doc": "/document2.doc",
}) })
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1b,
},
"pdfua": {
"true",
},
"nativePdfFormats": {
"false",
},
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil return nil
}}) }})
@@ -234,328 +362,35 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx", ".doc"} return []string{".docx", ".doc"}
}, },
}, },
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return nil
},
},
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 3, expectOutputPathsCount: 3,
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"}, expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"},
}, },
{ {
scenario: "success with non-native PDF/A & PDF/UA (many files)", scenario: "success with native PDF/A & PDF/UA",
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{
"pdfa": {
gotenberg.PdfA1b,
},
"pdfua": {
"true",
},
"nativePdfFormats": {
"false",
},
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}})
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{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 2,
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf"},
},
{
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",
"document2.docx": "/document2.docx",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1b,
},
"pdfua": {
"true",
},
})
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
return nil
}})
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{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 2,
expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf"},
},
{
scenario: "merge 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",
})
ctx.SetValues(map[string][]string{
"merge": {
"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{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "PDF engine convert error (merge)",
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",
},
"pdfa": {
gotenberg.PdfA1b,
},
"nativePdfFormats": {
"false",
},
})
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
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "cannot add output paths (merge)",
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",
},
})
ctx.SetCancelled(true)
return ctx
}(),
libreOffice: &libreofficeapi.ApiMock{
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
return nil
},
ExtensionsMock: func() []string {
return []string{".docx"}
},
},
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "success (merge)",
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",
},
})
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
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "success with non-native PDF/A & PDF/UA (merge)",
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",
},
"pdfa": {
gotenberg.PdfA1b,
},
"pdfua": {
"true",
},
"nativePdfFormats": {
"false",
},
})
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
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "success with non-native PDF/A & PDF/UA (merge)",
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",
},
"pdfa": {
gotenberg.PdfA1b,
},
"pdfua": {
"true",
},
"nativePdfFormats": {
"false",
},
})
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
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "success with metadata (single file)",
ctx: func() *api.ContextMock { ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)} ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{ ctx.SetFiles(map[string]string{
"document.docx": "/document.docx", "document.docx": "/document.docx",
}) })
ctx.SetValues(map[string][]string{ ctx.SetValues(map[string][]string{
"metadata": { "pdfa": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }", gotenberg.PdfA1b,
},
"pdfua": {
"true",
}, },
}) })
return ctx return ctx
@@ -568,293 +403,10 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"} return []string{".docx"}
}, },
}, },
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return nil
},
},
expectError: false, expectError: false,
expectHttpError: false, expectHttpError: false,
expectOutputPathsCount: 1, expectOutputPathsCount: 1,
}, },
{
scenario: "success with metadata (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{
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
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{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 2,
},
{
scenario: "error with metadata (single file)",
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{
"metadata": {
// invalid json
"{\"Creator\"",
},
})
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"}
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "error with metadata (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{
"metadata": {
// invalid json
"{\"Creator\"",
},
})
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"}
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "error with metadata write failure (single file)",
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{
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
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{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "error with metadata write failure (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{
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
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{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "success merge with metadata",
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",
},
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
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
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "error merge with metadata",
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",
},
"metadata": {
// invalid json
"{\"Creator",
},
})
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"}
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "error merge with metadata write failure",
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",
},
"metadata": {
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
},
})
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
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop()) tc.ctx.SetLogger(zap.NewNop())

View File

@@ -55,12 +55,12 @@ func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats g
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, metadata map[string]interface{}) error { func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return fmt.Errorf("read PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) return nil, fmt.Errorf("read PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, newMetadata map[string]interface{}) error { func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return fmt.Errorf("write PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with PDFcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }

View File

@@ -105,7 +105,7 @@ func TestPdfCpu_Convert(t *testing.T) {
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
engine := new(PdfCpu) engine := new(PdfCpu)
err := engine.ReadMetadata(context.Background(), zap.NewNop(), "", nil) _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
@@ -114,7 +114,7 @@ func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
engine := new(PdfCpu) engine := new(PdfCpu)
err := engine.WriteMetadata(context.Background(), zap.NewNop(), "", nil) err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)

View File

@@ -3,6 +3,7 @@ package pdfengines
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"go.uber.org/multierr" "go.uber.org/multierr"
"go.uber.org/zap" "go.uber.org/zap"
@@ -70,36 +71,49 @@ func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, f
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err) return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
} }
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, metadata map[string]interface{}) error { type readMetadataResult struct {
metadata map[string]interface{}
err error
}
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
var err error var err error
errChan := make(chan error, 1) var mu sync.Mutex // to safely append errors.
resultChan := make(chan readMetadataResult, len(multi.engines))
for _, engine := range multi.engines { for _, engine := range multi.engines {
go func(engine gotenberg.PdfEngine) { go func(engine gotenberg.PdfEngine) {
errChan <- engine.ReadMetadata(ctx, logger, inputPaths, metadata) metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
resultChan <- readMetadataResult{metadata: metadata, err: err}
}(engine) }(engine)
}
for range multi.engines {
select { select {
case readMetadataErr := <-errChan: case result := <-resultChan:
errored := multierr.AppendInto(&err, readMetadataErr) if result.err != nil {
if !errored { mu.Lock()
return nil err = multierr.Append(err, result.err)
mu.Unlock()
} else {
return result.metadata, nil
} }
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return nil, ctx.Err()
} }
} }
return fmt.Errorf("read PDF metadata with multi PDF engines: %w", err) return nil, fmt.Errorf("read PDF metadata with multi PDF engines: %w", err)
} }
func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, newMetadata map[string]interface{}) error { func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
var err error var err error
errChan := make(chan error, 1) errChan := make(chan error, 1)
for _, engine := range multi.engines { for _, engine := range multi.engines {
go func(engine gotenberg.PdfEngine) { go func(engine gotenberg.PdfEngine) {
errChan <- engine.WriteMetadata(ctx, logger, inputPaths, newMetadata) errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
}(engine) }(engine)
select { select {

View File

@@ -189,8 +189,8 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
scenario: "nominal behavior", scenario: "nominal behavior",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return nil return make(map[string]interface{}), nil
}, },
}, },
), ),
@@ -200,13 +200,13 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
scenario: "at least one engine does not return an error", scenario: "at least one engine does not return an error",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return errors.New("foo") return nil, errors.New("foo")
}, },
}, },
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return nil return make(map[string]interface{}), nil
}, },
}, },
), ),
@@ -216,13 +216,13 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
scenario: "all engines return an error", scenario: "all engines return an error",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return errors.New("foo") return nil, errors.New("foo")
}, },
}, },
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return errors.New("foo") return nil, errors.New("foo")
}, },
}, },
), ),
@@ -233,8 +233,8 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
scenario: "context expired", scenario: "context expired",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, metadata map[string]interface{}) error { ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return nil return make(map[string]interface{}), nil
}, },
}, },
), ),
@@ -248,7 +248,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
}, },
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
err := tc.engine.ReadMetadata(tc.ctx, zap.NewNop(), "", nil) _, err := tc.engine.ReadMetadata(tc.ctx, zap.NewNop(), "")
if !tc.expectError && err != nil { if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)
@@ -272,7 +272,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
scenario: "nominal behavior", scenario: "nominal behavior",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return nil return nil
}, },
}, },
@@ -283,12 +283,12 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
scenario: "at least one engine does not return an error", scenario: "at least one engine does not return an error",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
}, },
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return nil return nil
}, },
}, },
@@ -299,12 +299,12 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
scenario: "all engines return an error", scenario: "all engines return an error",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
}, },
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return errors.New("foo") return errors.New("foo")
}, },
}, },
@@ -316,7 +316,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
scenario: "context expired", scenario: "context expired",
engine: newMultiPdfEngines( engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{ &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string, newMetadata map[string]interface{}) error { WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return nil return nil
}, },
}, },
@@ -331,7 +331,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
}, },
} { } {
t.Run(tc.scenario, func(t *testing.T) { t.Run(tc.scenario, func(t *testing.T) {
err := tc.engine.WriteMetadata(tc.ctx, zap.NewNop(), "", nil) err := tc.engine.WriteMetadata(tc.ctx, zap.NewNop(), nil, "")
if !tc.expectError && err != nil { if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err) t.Fatalf("expected no error but got: %v", err)

View File

@@ -76,12 +76,12 @@ func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats go
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, metadata map[string]interface{}) error { func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) return nil, fmt.Errorf("read PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, newMetadata map[string]interface{}) error { func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }

View File

@@ -151,7 +151,7 @@ func TestPdfTk_Convert(t *testing.T) {
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
engine := new(PdfTk) engine := new(PdfTk)
err := engine.ReadMetadata(context.Background(), zap.NewNop(), "", nil) _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
@@ -160,7 +160,7 @@ func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
engine := new(PdfTk) engine := new(PdfTk)
err := engine.WriteMetadata(context.Background(), zap.NewNop(), "", nil) err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)

View File

@@ -78,12 +78,12 @@ func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats got
} }
// ReadMetadata is not available in this implementation. // ReadMetadata is not available in this implementation.
func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, metadata map[string]interface{}) error { func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
return fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) return nil, fmt.Errorf("read PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// WriteMetadata is not available in this implementation. // WriteMetadata is not available in this implementation.
func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, inputPaths string, newMetadata map[string]interface{}) error { func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }

View File

@@ -151,7 +151,7 @@ func TestQPdf_Convert(t *testing.T) {
func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
engine := new(QPdf) engine := new(QPdf)
err := engine.ReadMetadata(context.Background(), zap.NewNop(), "", nil) _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
@@ -160,7 +160,7 @@ func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) {
func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) {
engine := new(QPdf) engine := new(QPdf)
err := engine.WriteMetadata(context.Background(), zap.NewNop(), "", nil) err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "")
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)