mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-15 11:52:14 +01:00
feat(pdfengines): add split feature
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
type multiPdfEngines struct {
|
||||
mergeEngines []gotenberg.PdfEngine
|
||||
splitEngines []gotenberg.PdfEngine
|
||||
convertEngines []gotenberg.PdfEngine
|
||||
readMedataEngines []gotenberg.PdfEngine
|
||||
writeMedataEngines []gotenberg.PdfEngine
|
||||
@@ -20,12 +21,14 @@ type multiPdfEngines struct {
|
||||
|
||||
func newMultiPdfEngines(
|
||||
mergeEngines,
|
||||
splitEngines,
|
||||
convertEngines,
|
||||
readMetadataEngines,
|
||||
writeMedataEngines []gotenberg.PdfEngine,
|
||||
) *multiPdfEngines {
|
||||
return &multiPdfEngines{
|
||||
mergeEngines: mergeEngines,
|
||||
splitEngines: splitEngines,
|
||||
convertEngines: convertEngines,
|
||||
readMedataEngines: readMetadataEngines,
|
||||
writeMedataEngines: writeMedataEngines,
|
||||
@@ -57,6 +60,44 @@ func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inp
|
||||
return fmt.Errorf("merge PDFs with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
type splitResult struct {
|
||||
outputPaths []string
|
||||
err error
|
||||
}
|
||||
|
||||
// Split tries to split at intervals a given PDF thanks to its children. If the
|
||||
// context is done, it stops and returns an error.
|
||||
func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
resultChan := make(chan splitResult, len(multi.splitEngines))
|
||||
|
||||
for _, engine := range multi.splitEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
outputPaths, err := engine.Split(ctx, logger, mode, inputPath, outputDirPath)
|
||||
resultChan <- splitResult{outputPaths: outputPaths, err: err}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
for range multi.splitEngines {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = multierr.Append(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
return result.outputPaths, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("split PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format. thanks to its
|
||||
// children. If the context is done, it stops and returns an error.
|
||||
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
|
||||
@@ -19,25 +19,22 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -49,17 +46,14 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -71,27 +65,21 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
@@ -115,6 +103,97 @@ func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Split(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
_, err := tc.engine.Split(tc.ctx, zap.NewNop(), gotenberg.SplitMode{}, "", "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -124,25 +203,21 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -154,16 +229,13 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -175,26 +247,21 @@ func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
@@ -227,26 +294,21 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
@@ -258,16 +320,13 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return nil, errors.New("foo")
|
||||
@@ -279,25 +338,21 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
readMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||
return make(map[string]interface{}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
@@ -330,27 +385,21 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -362,16 +411,13 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
@@ -383,24 +429,21 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]gotenberg.PdfEngine{
|
||||
engine: &multiPdfEngines{
|
||||
writeMedataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
@@ -28,6 +28,7 @@ func init() {
|
||||
// enabled.
|
||||
type PdfEngines struct {
|
||||
mergeNames []string
|
||||
splitNames []string
|
||||
convertNames []string
|
||||
readMetadataNames []string
|
||||
writeMedataNames []string
|
||||
@@ -42,6 +43,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
||||
fs.StringSlice("pdfengines-merge-engines", []string{"qpdf", "pdfcpu", "pdftk"}, "Set the PDF engines and their order for the merge feature - empty means all")
|
||||
fs.StringSlice("pdfengines-split-engines", []string{"pdfcpu", "qpdf", "pdftk"}, "Set the PDF engines and their order for the split feature - empty means all")
|
||||
fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all")
|
||||
fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all")
|
||||
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
||||
@@ -64,6 +66,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
flags := ctx.ParsedFlags()
|
||||
mergeNames := flags.MustStringSlice("pdfengines-merge-engines")
|
||||
splitNames := flags.MustStringSlice("pdfengines-split-engines")
|
||||
convertNames := flags.MustStringSlice("pdfengines-convert-engines")
|
||||
readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines")
|
||||
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
|
||||
@@ -98,6 +101,11 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.mergeNames = mergeNames
|
||||
}
|
||||
|
||||
mod.splitNames = defaultNames
|
||||
if len(splitNames) > 0 {
|
||||
mod.splitNames = splitNames
|
||||
}
|
||||
|
||||
mod.convertNames = defaultNames
|
||||
if len(convertNames) > 0 {
|
||||
mod.convertNames = convertNames
|
||||
@@ -161,6 +169,7 @@ func (mod *PdfEngines) Validate() error {
|
||||
}
|
||||
|
||||
findNonExistingEngines(mod.mergeNames)
|
||||
findNonExistingEngines(mod.splitNames)
|
||||
findNonExistingEngines(mod.convertNames)
|
||||
findNonExistingEngines(mod.readMetadataNames)
|
||||
findNonExistingEngines(mod.writeMedataNames)
|
||||
@@ -177,6 +186,7 @@ func (mod *PdfEngines) Validate() error {
|
||||
func (mod *PdfEngines) SystemMessages() []string {
|
||||
return []string{
|
||||
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
||||
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")),
|
||||
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||
fmt.Sprintf("write medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")),
|
||||
@@ -201,6 +211,7 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||
|
||||
return newMultiPdfEngines(
|
||||
engines(mod.mergeNames),
|
||||
engines(mod.splitNames),
|
||||
engines(mod.convertNames),
|
||||
engines(mod.readMetadataNames),
|
||||
engines(mod.writeMedataNames),
|
||||
@@ -222,6 +233,7 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||
|
||||
return []api.Route{
|
||||
mergeRoute(engine),
|
||||
splitRoute(engine),
|
||||
convertRoute(engine),
|
||||
readMetadataRoute(engine),
|
||||
writeMetadataRoute(engine),
|
||||
|
||||
@@ -26,6 +26,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectedMergePdfEngines []string
|
||||
expectedSplitPdfEngines []string
|
||||
expectedConvertPdfEngines []string
|
||||
expectedReadMetadataPdfEngines []string
|
||||
expectedWriteMetadataPdfEngines []string
|
||||
@@ -66,6 +67,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
)
|
||||
}(),
|
||||
expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"},
|
||||
expectedSplitPdfEngines: []string{"pdfcpu", "qpdf", "pdftk"},
|
||||
expectedConvertPdfEngines: []string{"libreoffice-pdfengine"},
|
||||
expectedReadMetadataPdfEngines: []string{"exiftool"},
|
||||
expectedWriteMetadataPdfEngines: []string{"exiftool"},
|
||||
@@ -107,7 +109,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}
|
||||
|
||||
fs := new(PdfEngines).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"})
|
||||
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-split-engines=a", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
@@ -125,6 +127,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}(),
|
||||
|
||||
expectedMergePdfEngines: []string{"b"},
|
||||
expectedSplitPdfEngines: []string{"a"},
|
||||
expectedConvertPdfEngines: []string{"b"},
|
||||
expectedReadMetadataPdfEngines: []string{"a"},
|
||||
expectedWriteMetadataPdfEngines: []string{"a"},
|
||||
@@ -200,6 +203,12 @@ func TestPdfEngines_Provision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.splitNames {
|
||||
if name != tc.expectedSplitPdfEngines[index] {
|
||||
t.Fatalf("expected split name at index %d to be %s, but got: %s", index, name, tc.expectedSplitPdfEngines[index])
|
||||
}
|
||||
}
|
||||
|
||||
for index, name := range mod.convertNames {
|
||||
if name != tc.expectedConvertPdfEngines[index] {
|
||||
t.Fatalf("expected convert name at index %d to be %s, but got: %s", index, name, tc.expectedConvertPdfEngines[index])
|
||||
@@ -303,17 +312,19 @@ func TestPdfEngines_Validate(t *testing.T) {
|
||||
func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
mod.mergeNames = []string{"foo", "bar"}
|
||||
mod.splitNames = []string{"foo", "bar"}
|
||||
mod.convertNames = []string{"foo", "bar"}
|
||||
mod.readMetadataNames = []string{"foo", "bar"}
|
||||
mod.writeMedataNames = []string{"foo", "bar"}
|
||||
|
||||
messages := mod.SystemMessages()
|
||||
if len(messages) != 4 {
|
||||
if len(messages) != 5 {
|
||||
t.Errorf("expected one and only one message, but got %d", len(messages))
|
||||
}
|
||||
|
||||
expect := []string{
|
||||
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
||||
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")),
|
||||
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||
fmt.Sprintf("write medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")),
|
||||
@@ -329,6 +340,7 @@ func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
func TestPdfEngines_PdfEngine(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
mergeNames: []string{"foo", "bar"},
|
||||
splitNames: []string{"foo", "bar"},
|
||||
convertNames: []string{"foo", "bar"},
|
||||
readMetadataNames: []string{"foo", "bar"},
|
||||
writeMedataNames: []string{"foo", "bar"},
|
||||
@@ -370,7 +382,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 4,
|
||||
expectRoutes: 5,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
@@ -13,6 +15,63 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
)
|
||||
|
||||
// FormDataPdfSplitMode creates a [gotenberg.SplitMode] from the form data.
|
||||
func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg.SplitMode {
|
||||
var (
|
||||
mode string
|
||||
span string
|
||||
)
|
||||
|
||||
splitModeFunc := func(value string) error {
|
||||
if value != "" && value != gotenberg.SplitModeIntervals && value != gotenberg.SplitModePages {
|
||||
return fmt.Errorf("wrong value, expected either '%s' or '%s'", gotenberg.SplitModeIntervals, gotenberg.SplitModePages)
|
||||
}
|
||||
mode = value
|
||||
return nil
|
||||
}
|
||||
|
||||
splitSpanFunc := func(value string) error {
|
||||
value = strings.Join(strings.Fields(value), "")
|
||||
|
||||
if mode == gotenberg.SplitModeIntervals {
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if intValue < 1 {
|
||||
return errors.New("value is inferior to 1")
|
||||
}
|
||||
}
|
||||
|
||||
span = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.
|
||||
MandatoryCustom("splitMode", func(value string) error {
|
||||
return splitModeFunc(value)
|
||||
}).
|
||||
MandatoryCustom("splitSpan", func(value string) error {
|
||||
return splitSpanFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.
|
||||
Custom("splitMode", func(value string) error {
|
||||
return splitModeFunc(value)
|
||||
}).
|
||||
Custom("splitSpan", func(value string) error {
|
||||
return splitSpanFunc(value)
|
||||
})
|
||||
}
|
||||
|
||||
return gotenberg.SplitMode{
|
||||
Mode: mode,
|
||||
Span: span,
|
||||
}
|
||||
}
|
||||
|
||||
// FormDataPdfFormats creates [gotenberg.PdfFormats] from the form data.
|
||||
// Fallback to default value if the considered key is not present.
|
||||
func FormDataPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||
@@ -32,9 +91,10 @@ func FormDataPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||
}
|
||||
|
||||
// FormDataPdfMetadata creates metadata object from the form data.
|
||||
func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
|
||||
func FormDataPdfMetadata(form *api.FormData, mandatory bool) map[string]interface{} {
|
||||
var metadata map[string]interface{}
|
||||
form.Custom("metadata", func(value string) error {
|
||||
|
||||
metadataFunc := func(value string) error {
|
||||
if len(value) > 0 {
|
||||
err := json.Unmarshal([]byte(value), &metadata)
|
||||
if err != nil {
|
||||
@@ -42,7 +102,18 @@ func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.MandatoryCustom("metadata", func(value string) error {
|
||||
return metadataFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.Custom("metadata", func(value string) error {
|
||||
return metadataFunc(value)
|
||||
})
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
@@ -66,6 +137,52 @@ func MergeStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths []string
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
// SplitPdfStub splits a list of PDF files based on [gotenberg.SplitMode].
|
||||
// It returns a list of output paths or the list of provided input paths if no
|
||||
// split requested.
|
||||
func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.SplitMode, inputPaths []string) ([]string, error) {
|
||||
zeroValued := gotenberg.SplitMode{}
|
||||
if mode == zeroValued {
|
||||
return inputPaths, nil
|
||||
}
|
||||
|
||||
var outputPaths []string
|
||||
for _, inputPath := range inputPaths {
|
||||
inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))]
|
||||
filenameNoExt := filepath.Base(inputPathNoExt)
|
||||
outputDirPath, err := ctx.CreateSubDirectory(strings.ReplaceAll(filepath.Base(filenameNoExt), ".", "_"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create subdirectory from input path: %w", err)
|
||||
}
|
||||
|
||||
paths, err := engine.Split(ctx, ctx.Log(), mode, inputPath, outputDirPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("split PDF '%s': %w", inputPath, err)
|
||||
}
|
||||
|
||||
if mode.Mode == gotenberg.SplitModePages {
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// Keep the original filename.
|
||||
for i, path := range paths {
|
||||
newPath := fmt.Sprintf(
|
||||
"%s/%s_%d.pdf",
|
||||
outputDirPath, filenameNoExt, i,
|
||||
)
|
||||
|
||||
err = ctx.Rename(path, newPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rename path: %w", err)
|
||||
}
|
||||
|
||||
outputPaths = append(outputPaths, newPath)
|
||||
}
|
||||
}
|
||||
|
||||
return outputPaths, nil
|
||||
}
|
||||
|
||||
// ConvertStub transforms a given PDF to the specified formats defined in
|
||||
// [gotenberg.PdfFormats]. If no format, it does nothing and returns the input
|
||||
// paths.
|
||||
@@ -116,7 +233,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
|
||||
form := ctx.FormData()
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form)
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
@@ -152,6 +269,65 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// splitRoute returns an [api.Route] which can extract pages from a PDF.
|
||||
func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/split",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
mode := FormDataPdfSplitMode(form, true)
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
outputPaths, err := SplitPdfStub(ctx, engine, mode, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("split PDFs: %w", err)
|
||||
}
|
||||
|
||||
convertOutputPaths, err := ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = WriteMetadataStub(ctx, engine, metadata, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
zeroValuedSplitMode := gotenberg.SplitMode{}
|
||||
zeroValuedPdfFormats := gotenberg.PdfFormats{}
|
||||
if mode != zeroValuedSplitMode && pdfFormats != zeroValuedPdfFormats {
|
||||
// Rename the files to keep the split naming.
|
||||
for i, convertOutputPath := range convertOutputPaths {
|
||||
err = ctx.Rename(convertOutputPath, outputPaths[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename output path: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(outputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
||||
// format.
|
||||
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
@@ -258,25 +434,12 @@ func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
var (
|
||||
inputPaths []string
|
||||
metadata map[string]interface{}
|
||||
)
|
||||
form := ctx.FormData()
|
||||
metadata := FormDataPdfMetadata(form, true)
|
||||
|
||||
err := ctx.FormData().
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
MandatoryCustom("metadata", func(value string) error {
|
||||
if len(value) > 0 {
|
||||
err := json.Unmarshal([]byte(value), &metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal metadata: %w", err)
|
||||
}
|
||||
}
|
||||
if len(metadata) == 0 {
|
||||
return errors.New("no metadata")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
|
||||
@@ -3,13 +3,16 @@ package pdfengines
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -17,6 +20,156 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
)
|
||||
|
||||
func TestFormDataPdfSplitMode(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
mandatory bool
|
||||
expectedSplitMode gotenberg.SplitMode
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
mandatory: false,
|
||||
expectedSplitMode: gotenberg.SplitMode{},
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "no custom form fields (mandatory)",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
mandatory: true,
|
||||
expectedSplitMode: gotenberg.SplitMode{},
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid splitMode",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: false,
|
||||
expectedSplitMode: gotenberg.SplitMode{},
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid splitSpan (intervals)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
"intervals",
|
||||
},
|
||||
"splitSpan": {
|
||||
"1-2",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: false,
|
||||
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals},
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "splitSpan inferior to 1 (intervals)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
"intervals",
|
||||
},
|
||||
"splitSpan": {
|
||||
"-1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: false,
|
||||
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals},
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "valid form fields (intervals)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
"intervals",
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: false,
|
||||
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "valid form fields (pages)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
"pages",
|
||||
},
|
||||
"splitSpan": {
|
||||
"1-2",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: false,
|
||||
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"},
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "valid form fields (mandatory)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
"intervals",
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: true,
|
||||
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
expectValidationError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataPdfSplitMode(form, tc.mandatory)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedSplitMode) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedSplitMode, actual)
|
||||
}
|
||||
|
||||
err := form.Validate()
|
||||
|
||||
if tc.expectValidationError && err == nil {
|
||||
t.Fatal("expected validation error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidationError && err != nil {
|
||||
t.Fatalf("expected no validation error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataPdfFormats(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -74,15 +227,24 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
mandatory bool
|
||||
expectedMetadata map[string]interface{}
|
||||
expectValidationError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no metadata form field",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
mandatory: false,
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: false,
|
||||
},
|
||||
{
|
||||
scenario: "no metadata form field (mandatory)",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
mandatory: true,
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid metadata form field",
|
||||
ctx: func() *api.ContextMock {
|
||||
@@ -94,6 +256,7 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: false,
|
||||
expectedMetadata: nil,
|
||||
expectValidationError: true,
|
||||
},
|
||||
@@ -108,6 +271,7 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
mandatory: false,
|
||||
expectedMetadata: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
@@ -117,7 +281,7 @@ func TestFormDataPdfMetadata(t *testing.T) {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
form := tc.ctx.Context.FormData()
|
||||
actual := FormDataPdfMetadata(form)
|
||||
actual := FormDataPdfMetadata(form, tc.mandatory)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
|
||||
@@ -193,6 +357,128 @@ func TestMergeStub(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitPdfStub(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
engine gotenberg.PdfEngine
|
||||
mode gotenberg.SplitMode
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "no split mode",
|
||||
mode: gotenberg.SplitMode{},
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "cannot create subdirectory",
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return errors.New("cannot create subdirectory")
|
||||
}})
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "split error",
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "rename error",
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return errors.New("cannot rename")
|
||||
}})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "success (intervals)",
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"},
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success (pages)",
|
||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"},
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString())
|
||||
tc.ctx.SetDirPath(dirPath)
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
|
||||
_, err := SplitPdfStub(tc.ctx.Context, tc.engine, tc.mode, []string{"my.pdf", "my2.pdf"})
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertStub(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -503,6 +789,287 @@ func TestMergeHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitHandler(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
engine gotenberg.PdfEngine
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
expectHttpStatus int
|
||||
expectOutputPathsCount int
|
||||
expectOutputPaths []string
|
||||
}{
|
||||
{
|
||||
scenario: "missing at least one mandatory file",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "no split mode",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine (split)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine (convert)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine (write metadata)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "cannot add output paths",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
})
|
||||
ctx.SetCancelled(true)
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{inputPath}, nil
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success (intervals)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModeIntervals,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1",
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{"file_split_1.pdf", "file_split_2.pdf"}, 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, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 2,
|
||||
expectOutputPaths: []string{"/file/file_0.pdf", "/file/file_1.pdf"},
|
||||
},
|
||||
{
|
||||
scenario: "success (pages)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"splitMode": {
|
||||
gotenberg.SplitModePages,
|
||||
},
|
||||
"splitSpan": {
|
||||
"1-2",
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
"metadata": {
|
||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return []string{"/file/file.pdf"}, 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, metadata map[string]interface{}, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
expectOutputPaths: []string{"/file/file.pdf"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
tc.ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||
return nil
|
||||
}})
|
||||
tc.ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||
return nil
|
||||
}})
|
||||
c := echo.New().NewContext(nil, nil)
|
||||
c.Set("context", tc.ctx.Context)
|
||||
|
||||
err := splitRoute(tc.engine).Handler(c)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
var httpErr api.HttpError
|
||||
isHttpError := errors.As(err, &httpErr)
|
||||
|
||||
if tc.expectHttpError && !isHttpError {
|
||||
t.Errorf("expected an HTTP error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectHttpError && isHttpError {
|
||||
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||
}
|
||||
|
||||
if err != nil && tc.expectHttpError && isHttpError {
|
||||
status, _ := httpErr.HttpError()
|
||||
if status != tc.expectHttpStatus {
|
||||
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||
}
|
||||
|
||||
for _, path := range tc.expectOutputPaths {
|
||||
if !slices.Contains(tc.ctx.OutputPaths(), path) {
|
||||
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertHandler(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
|
||||
Reference in New Issue
Block a user