mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 08:27:41 +08:00
refactor(pdfengines): route fallback ops through a generic runWithFallback helper
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package pdfengines
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func newFallbackRecorder(t *testing.T) *tracetest.SpanRecorder {
|
||||
t.Helper()
|
||||
recorder := tracetest.NewSpanRecorder()
|
||||
provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
|
||||
previous := otel.GetTracerProvider()
|
||||
otel.SetTracerProvider(provider)
|
||||
t.Cleanup(func() { otel.SetTracerProvider(previous) })
|
||||
return recorder
|
||||
}
|
||||
|
||||
func findFallbackSpan(recorder *tracetest.SpanRecorder, name string) sdktrace.ReadOnlySpan {
|
||||
for _, s := range recorder.Ended() {
|
||||
if s.Name() == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapTest(err error) error { return fmt.Errorf("test op with multi PDF engines: %w", err) }
|
||||
|
||||
func TestRunWithFallback_FirstSucceeds(t *testing.T) {
|
||||
recorder := newFallbackRecorder(t)
|
||||
engines := []gotenberg.PdfEngine{&gotenberg.PdfEngineMock{}, &gotenberg.PdfEngineMock{}}
|
||||
|
||||
calls := 0
|
||||
got, err := runWithFallback(context.Background(), "pdfengines.Test", engines,
|
||||
func(_ context.Context, _ gotenberg.PdfEngine) (string, error) {
|
||||
calls++
|
||||
return "ok", nil
|
||||
}, wrapTest)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != "ok" {
|
||||
t.Errorf("got %q, want ok", got)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Errorf("expected 1 engine call, got %d", calls)
|
||||
}
|
||||
|
||||
span := findFallbackSpan(recorder, "pdfengines.Test")
|
||||
if span.Status().Code != codes.Ok {
|
||||
t.Errorf("status = %v, want Ok", span.Status().Code)
|
||||
}
|
||||
attrs := map[string]string{}
|
||||
for _, kv := range span.Attributes() {
|
||||
attrs[string(kv.Key)] = kv.Value.Emit()
|
||||
}
|
||||
if attrs["gotenberg.pdf_engine.attempts"] != "1" {
|
||||
t.Errorf("attempts = %q, want 1", attrs["gotenberg.pdf_engine.attempts"])
|
||||
}
|
||||
if attrs["gotenberg.pdf_engine.selected"] == "" {
|
||||
t.Error("expected a selected engine attribute")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithFallback_SecondSucceeds(t *testing.T) {
|
||||
recorder := newFallbackRecorder(t)
|
||||
engines := []gotenberg.PdfEngine{&gotenberg.PdfEngineMock{}, &gotenberg.PdfEngineMock{}}
|
||||
|
||||
calls := 0
|
||||
got, err := runWithFallback(context.Background(), "pdfengines.Test", engines,
|
||||
func(_ context.Context, _ gotenberg.PdfEngine) (string, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return "", errors.New("first engine failed")
|
||||
}
|
||||
return "ok", nil
|
||||
}, wrapTest)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != "ok" || calls != 2 {
|
||||
t.Errorf("got %q after %d calls, want ok after 2", got, calls)
|
||||
}
|
||||
|
||||
span := findFallbackSpan(recorder, "pdfengines.Test")
|
||||
var failedEvents int
|
||||
for _, e := range span.Events() {
|
||||
if e.Name == "pdf_engine.attempt_failed" {
|
||||
failedEvents++
|
||||
}
|
||||
}
|
||||
if failedEvents != 1 {
|
||||
t.Errorf("expected 1 attempt_failed event, got %d", failedEvents)
|
||||
}
|
||||
for _, kv := range span.Attributes() {
|
||||
if string(kv.Key) == "gotenberg.pdf_engine.attempts" && kv.Value.Emit() != "2" {
|
||||
t.Errorf("attempts = %q, want 2", kv.Value.Emit())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithFallback_AllFail(t *testing.T) {
|
||||
recorder := newFallbackRecorder(t)
|
||||
engines := []gotenberg.PdfEngine{&gotenberg.PdfEngineMock{}, &gotenberg.PdfEngineMock{}}
|
||||
|
||||
sentinel := errors.New("engine failed")
|
||||
_, err := runWithFallback(context.Background(), "pdfengines.Test", engines,
|
||||
func(_ context.Context, _ gotenberg.PdfEngine) (string, error) {
|
||||
return "", sentinel
|
||||
}, wrapTest)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when all engines fail")
|
||||
}
|
||||
if !errors.Is(err, sentinel) {
|
||||
t.Errorf("expected the joined engine error to be wrapped, got %v", err)
|
||||
}
|
||||
|
||||
span := findFallbackSpan(recorder, "pdfengines.Test")
|
||||
if span.Status().Code != codes.Error {
|
||||
t.Errorf("status = %v, want Error", span.Status().Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithFallback_ZeroEngines(t *testing.T) {
|
||||
newFallbackRecorder(t)
|
||||
_, err := runWithFallback(context.Background(), "pdfengines.Test", nil,
|
||||
func(_ context.Context, _ gotenberg.PdfEngine) (string, error) {
|
||||
return "ok", nil
|
||||
}, wrapTest)
|
||||
if err == nil {
|
||||
t.Error("expected an error with no engines")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithFallback_ContextDone(t *testing.T) {
|
||||
recorder := newFallbackRecorder(t)
|
||||
engines := []gotenberg.PdfEngine{&gotenberg.PdfEngineMock{}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
release := make(chan struct{})
|
||||
t.Cleanup(func() { close(release) })
|
||||
|
||||
_, err := runWithFallback(ctx, "pdfengines.Test", engines,
|
||||
func(_ context.Context, _ gotenberg.PdfEngine) (string, error) {
|
||||
<-release // never returns during the call, forcing the ctx.Done branch
|
||||
return "", nil
|
||||
}, wrapTest)
|
||||
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("expected context.Canceled, got %v", err)
|
||||
}
|
||||
|
||||
span := findFallbackSpan(recorder, "pdfengines.Test")
|
||||
if span.Status().Code != codes.Error {
|
||||
t.Errorf("status = %v, want Error (the cancellation must mark the span)", span.Status().Code)
|
||||
}
|
||||
}
|
||||
+170
-493
@@ -5,8 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
@@ -64,583 +64,260 @@ func newMultiPdfEngines(
|
||||
}
|
||||
}
|
||||
|
||||
// Merge combines multiple PDF files into a single document using the first
|
||||
// available engine that supports PDF merging.
|
||||
//
|
||||
//nolint:dupl
|
||||
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Merge", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
// engineName returns the module ID of a PDF engine for telemetry, falling back
|
||||
// to its type name when it does not expose a descriptor.
|
||||
func engineName(engine gotenberg.PdfEngine) string {
|
||||
if module, ok := engine.(gotenberg.Module); ok {
|
||||
return module.Descriptor().ID
|
||||
}
|
||||
return fmt.Sprintf("%T", engine)
|
||||
}
|
||||
|
||||
// runWithFallback runs op against each engine in order and returns the first
|
||||
// success. It wraps the attempts in a pdfengines span, records the winning
|
||||
// engine and attempt count, emits a pdf_engine.attempt_failed event for each
|
||||
// failed engine, and joins all engine errors on total failure. A context
|
||||
// cancellation marks the span as errored too. wrap applies the op-specific
|
||||
// final error message.
|
||||
func runWithFallback[T any](
|
||||
ctx context.Context,
|
||||
spanName string,
|
||||
engines []gotenberg.PdfEngine,
|
||||
op func(ctx context.Context, engine gotenberg.PdfEngine) (T, error),
|
||||
wrap func(err error) error,
|
||||
) (T, error) {
|
||||
var zero T
|
||||
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
type attemptResult struct {
|
||||
value T
|
||||
err error
|
||||
}
|
||||
|
||||
var joined error
|
||||
for attempt, engine := range engines {
|
||||
resultChan := make(chan attemptResult, 1)
|
||||
|
||||
for _, engine := range multi.mergeEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
|
||||
value, err := op(ctx, engine)
|
||||
resultChan <- attemptResult{value: value, err: err}
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case mergeErr := <-errChan:
|
||||
if mergeErr != nil {
|
||||
err = errors.Join(err, mergeErr)
|
||||
} else {
|
||||
case result := <-resultChan:
|
||||
if result.err == nil {
|
||||
span.SetAttributes(
|
||||
attribute.String("gotenberg.pdf_engine.selected", engineName(engine)),
|
||||
attribute.Int("gotenberg.pdf_engine.attempts", attempt+1),
|
||||
)
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
return result.value, nil
|
||||
}
|
||||
|
||||
joined = errors.Join(joined, result.err)
|
||||
span.AddEvent("pdf_engine.attempt_failed", trace.WithAttributes(
|
||||
attribute.String("engine", engineName(engine)),
|
||||
))
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
err := ctx.Err()
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return zero, err
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("merge PDFs with multi PDF engines: %w", err)
|
||||
err := wrap(joined)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return zero, err
|
||||
}
|
||||
|
||||
// runWithFallbackVoid adapts [runWithFallback] for operations that return no
|
||||
// value beyond an error.
|
||||
func runWithFallbackVoid(
|
||||
ctx context.Context,
|
||||
spanName string,
|
||||
engines []gotenberg.PdfEngine,
|
||||
op func(ctx context.Context, engine gotenberg.PdfEngine) error,
|
||||
wrap func(err error) error,
|
||||
) error {
|
||||
_, err := runWithFallback(ctx, spanName, engines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) (struct{}, error) {
|
||||
return struct{}{}, op(ctx, engine)
|
||||
},
|
||||
wrap,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
type splitResult struct {
|
||||
outputPaths []string
|
||||
err error
|
||||
// Merge combines multiple PDF files into a single document using the first
|
||||
// available engine that supports PDF merging.
|
||||
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Merge", multi.mergeEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Merge(ctx, logger, inputPaths, outputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("merge PDFs with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Split divides the PDF into separate pages using the first available engine
|
||||
// that supports PDF splitting.
|
||||
func (multi *multiPdfEngines) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Split", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
for _, engine := range multi.splitEngines {
|
||||
resultChan := make(chan splitResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
outputPaths, err := engine.Split(ctx, logger, mode, inputPath, outputDirPath)
|
||||
resultChan <- splitResult{outputPaths: outputPaths, err: err}
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = errors.Join(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return result.outputPaths, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("split PDF with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return nil, err
|
||||
return runWithFallback(ctx, "pdfengines.Split", multi.splitEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) ([]string, error) {
|
||||
return engine.Split(ctx, logger, mode, inputPath, outputDirPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("split PDF with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Flatten merges existing annotation appearances with page content using the
|
||||
// first available engine that supports flattening.
|
||||
func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Flatten", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.flattenEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Flatten(ctx, logger, inputPath)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case mergeErr := <-errChan:
|
||||
if mergeErr != nil {
|
||||
err = errors.Join(err, mergeErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("flatten PDF with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Flatten", multi.flattenEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Flatten(ctx, logger, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("flatten PDF with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Convert transforms the given PDF to a specific PDF format using the first
|
||||
// available engine that supports PDF conversion.
|
||||
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Convert", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.convertEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case mergeErr := <-errChan:
|
||||
if mergeErr != nil {
|
||||
err = errors.Join(err, mergeErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type readMetadataResult struct {
|
||||
metadata map[string]any
|
||||
err error
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Convert", multi.convertEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Convert(ctx, logger, formats, inputPath, outputPath)
|
||||
},
|
||||
func(err error) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ReadMetadata extracts metadata from a PDF file using the first available
|
||||
// engine that supports metadata reading.
|
||||
//
|
||||
//nolint:dupl
|
||||
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.ReadMetadata", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
for _, engine := range multi.readMetadataEngines {
|
||||
resultChan := make(chan readMetadataResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
|
||||
resultChan <- readMetadataResult{metadata: metadata, err: err}
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = errors.Join(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return result.metadata, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("read PDF metadata with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return nil, err
|
||||
return runWithFallback(ctx, "pdfengines.ReadMetadata", multi.readMetadataEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) (map[string]any, error) {
|
||||
return engine.ReadMetadata(ctx, logger, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("read PDF metadata with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// WriteMetadata embeds metadata into a PDF file using the first available
|
||||
// engine that supports metadata writing.
|
||||
func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.WriteMetadata", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.writeMetadataEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case writeMetadataErr := <-errChan:
|
||||
if writeMetadataErr != nil {
|
||||
err = errors.Join(err, writeMetadataErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type pageCountResult struct {
|
||||
pageCount int
|
||||
err error
|
||||
return runWithFallbackVoid(ctx, "pdfengines.WriteMetadata", multi.writeMetadataEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.WriteMetadata(ctx, logger, metadata, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// PageCount returns the number of pages in a PDF file using the first available
|
||||
// engine that supports metadata reading.
|
||||
func (multi *multiPdfEngines) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.PageCount", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
for _, engine := range multi.readMetadataEngines {
|
||||
resultChan := make(chan pageCountResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
pageCount, err := engine.PageCount(ctx, logger, inputPath)
|
||||
resultChan <- pageCountResult{pageCount: pageCount, err: err}
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = errors.Join(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return result.pageCount, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("page count with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
type readBookmarksResult struct {
|
||||
bookmarks []gotenberg.Bookmark
|
||||
err error
|
||||
return runWithFallback(ctx, "pdfengines.PageCount", multi.readMetadataEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) (int, error) {
|
||||
return engine.PageCount(ctx, logger, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("page count with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// ReadBookmarks reads bookmarks from a PDF file using the first available
|
||||
// engine that supports bookmarks reading.
|
||||
//
|
||||
//nolint:dupl
|
||||
func (multi *multiPdfEngines) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.ReadBookmarks", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
for _, engine := range multi.readBookmarksEngines {
|
||||
resultChan := make(chan readBookmarksResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
bookmarks, err := engine.ReadBookmarks(ctx, logger, inputPath)
|
||||
resultChan <- readBookmarksResult{bookmarks: bookmarks, err: err}
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = errors.Join(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return result.bookmarks, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("read PDF bookmarks with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return nil, err
|
||||
return runWithFallback(ctx, "pdfengines.ReadBookmarks", multi.readBookmarksEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) ([]gotenberg.Bookmark, error) {
|
||||
return engine.ReadBookmarks(ctx, logger, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("read PDF bookmarks with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// WriteBookmarks adds a document outline (bookmarks) to a PDF file using the
|
||||
// first available engine that supports bookmarks writing.
|
||||
func (multi *multiPdfEngines) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.WriteBookmarks", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.writeBookmarksEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.WriteBookmarks(ctx, logger, inputPath, bookmarks)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case writeBookmarksErr := <-errChan:
|
||||
if writeBookmarksErr != nil {
|
||||
err = errors.Join(err, writeBookmarksErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("write PDF bookmarks with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.WriteBookmarks", multi.writeBookmarksEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.WriteBookmarks(ctx, logger, inputPath, bookmarks)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("write PDF bookmarks with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Encrypt adds password protection to a PDF file using the first available
|
||||
// engine that supports password protection.
|
||||
func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Encrypt", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.passwordEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Encrypt(ctx, logger, inputPath, userPassword, ownerPassword)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case protectErr := <-errChan:
|
||||
if protectErr != nil {
|
||||
err = errors.Join(err, protectErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("encrypt PDF using multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Encrypt", multi.passwordEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Encrypt(ctx, logger, inputPath, userPassword, ownerPassword)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("encrypt PDF using multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// EmbedFiles embeds files into a PDF using the first available
|
||||
// engine that supports file embedding.
|
||||
//
|
||||
//nolint:dupl
|
||||
// EmbedFiles embeds files into a PDF using the first available engine that
|
||||
// supports file embedding.
|
||||
func (multi *multiPdfEngines) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.EmbedFiles", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.embedEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.EmbedFiles(ctx, logger, filePaths, inputPath)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case embedErr := <-errChan:
|
||||
if embedErr != nil {
|
||||
err = errors.Join(err, embedErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("embed files into PDF using multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.EmbedFiles", multi.embedEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.EmbedFiles(ctx, logger, filePaths, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("embed files into PDF using multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Watermark applies a watermark (behind page content) to a PDF file using the
|
||||
// first available engine that supports watermarking.
|
||||
//
|
||||
//nolint:dupl
|
||||
func (multi *multiPdfEngines) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Watermark", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.watermarkEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Watermark(ctx, logger, inputPath, stamp)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case watermarkErr := <-errChan:
|
||||
if watermarkErr != nil {
|
||||
err = errors.Join(err, watermarkErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("watermark PDF with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Watermark", multi.watermarkEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Watermark(ctx, logger, inputPath, stamp)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("watermark PDF with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Stamp applies a stamp (on top of page content) to a PDF file using the
|
||||
// first available engine that supports stamping.
|
||||
//
|
||||
//nolint:dupl
|
||||
// Stamp applies a stamp (on top of page content) to a PDF file using the first
|
||||
// available engine that supports stamping.
|
||||
func (multi *multiPdfEngines) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Stamp", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.stampEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Stamp(ctx, logger, inputPath, stamp)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case stampErr := <-errChan:
|
||||
if stampErr != nil {
|
||||
err = errors.Join(err, stampErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("stamp PDF with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Stamp", multi.stampEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Stamp(ctx, logger, inputPath, stamp)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("stamp PDF with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Rotate rotates pages of a PDF file using the first available engine that
|
||||
// supports rotation.
|
||||
func (multi *multiPdfEngines) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.Rotate", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.rotateEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Rotate(ctx, logger, inputPath, angle, pages)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case rotateErr := <-errChan:
|
||||
if rotateErr != nil {
|
||||
err = errors.Join(err, rotateErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("rotate PDF with multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Rotate", multi.rotateEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Rotate(ctx, logger, inputPath, angle, pages)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("rotate PDF with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// EmbedFilesMetadata sets metadata on embedded files using the first available
|
||||
// engine that supports it.
|
||||
//
|
||||
//nolint:dupl
|
||||
func (multi *multiPdfEngines) EmbedFilesMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]map[string]string, inputPath string) error {
|
||||
tracer := gotenberg.Tracer()
|
||||
ctx, span := tracer.Start(ctx, "pdfengines.EmbedFilesMetadata", trace.WithSpanKind(trace.SpanKindInternal))
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.embedMetadataEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.EmbedFilesMetadata(ctx, logger, metadata, inputPath)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case setErr := <-errChan:
|
||||
if setErr != nil {
|
||||
err = errors.Join(err, setErr)
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("set embeds metadata using multi PDF engines: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
return err
|
||||
return runWithFallbackVoid(ctx, "pdfengines.EmbedFilesMetadata", multi.embedMetadataEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.EmbedFilesMetadata(ctx, logger, metadata, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("set embeds metadata using multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
|
||||
Reference in New Issue
Block a user