Files
gotenberg/pkg/modules/libreoffice/api/api.go
T

928 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
"github.com/alexliesenfeld/health"
flag "github.com/spf13/pflag"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/trace"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
)
func init() {
gotenberg.MustRegisterModule(new(Api))
}
var (
// ErrInvalidPdfFormats happens if LibreOffice cannot handle the PDF
// formats option.
ErrInvalidPdfFormats = errors.New("invalid PDF formats")
// ErrUnoException happens when unoconverter returns exit code 5.
ErrUnoException = errors.New("uno exception")
// ErrRuntimeException happens when unoconverter returns exit code 6.
ErrRuntimeException = errors.New("runtime exception")
// ErrCoreDumped happens randomly; sometimes a conversion will work as
// expected, and some other time the same conversion will fail.
// See https://github.com/gotenberg/gotenberg/issues/639.
ErrCoreDumped = errors.New("core dumped")
)
// Api is a module that provides a [Uno] to interact with LibreOffice.
type Api struct {
autoStart bool
args libreOfficeArguments
logger *slog.Logger
libreOffice libreOffice
supervisor gotenberg.ProcessSupervisor
version string
versionOnce sync.Once
reqsCounter metric.Int64Counter
errsCounter metric.Int64Counter
conversionDurationCounter metric.Float64Histogram
queueWaitDurationCounter metric.Float64Histogram
pdfOutputSizeCounter metric.Int64Histogram
coreDumpedRetriesCounter metric.Int64Counter
}
// Options gathers available options when converting a document to PDF.
// See: https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html.
type Options struct {
// Password specifies the password for opening the source file.
Password string // #nosec
// Landscape allows changing the orientation of the resulting PDF.
Landscape bool
// PageRanges allows selecting the pages to convert.
PageRanges string
// UpdateIndexes specifies whether to update the indexes before conversion,
// keeping in mind that doing so might result in missing links in the final
// PDF.
UpdateIndexes bool
// ExportFormFields specifies whether form fields are exported as widgets
// or only their fixed print representation is exported.
ExportFormFields bool
// AllowDuplicateFieldNames specifies whether multiple form fields exported
// are allowed to have the same field name.
AllowDuplicateFieldNames bool
// ExportBookmarks specifies if bookmarks are exported to PDF.
ExportBookmarks bool
// ExportBookmarksToPdfDestination specifies that the bookmarks contained
// in the source LibreOffice file should be exported to the PDF file as
// Named Destination.
ExportBookmarksToPdfDestination bool
// ExportPlaceholders exports the placeholder fields visual markings only.
// The exported placeholder is ineffective.
ExportPlaceholders bool
// ExportNotes specifies if notes are exported to PDF.
ExportNotes bool
// ExportNotesPages specifies if notes pages are exported to PDF.
// Notes pages are available in Impress documents only.
ExportNotesPages bool
// ExportOnlyNotesPages specifies if the property ExportNotesPages is set
// to true if only notes pages are exported to PDF.
ExportOnlyNotesPages bool
// ExportNotesInMargin specifies if notes in the margin are exported to
// PDF.
ExportNotesInMargin bool
// ConvertOooTargetToPdfTarget specifies that the target documents with
// .od[tpgs] extension will have that extension changed to .pdf when the
// link is exported to PDF. The source document remains untouched.
ConvertOooTargetToPdfTarget bool
// ExportLinksRelativeFsys specifies that the file system related
// hyperlinks (file:// protocol) present in the document will be exported
// as relative to the source document location.
ExportLinksRelativeFsys bool
// ExportHiddenSlides exports, for LibreOffice Impress, slides that are not
// included in slide shows.
ExportHiddenSlides bool
// SkipEmptyPages specifies that automatically inserted empty pages are
// suppressed. This option is active only if storing Writer documents.
SkipEmptyPages bool
// AddOriginalDocumentAsStream specifies that a stream is inserted to the
// PDF file which contains the original document for archiving purposes.
AddOriginalDocumentAsStream bool
// SinglePageSheets ignores each sheets paper size, print ranges and
// shown/hidden status and puts every sheet (even hidden sheets) on exactly
// one page.
SinglePageSheets bool
// InitialView specifies how the PDF document should be displayed when
// opened. 0 = neither outlines nor thumbnails, 1 = outline pane open,
// 2 = thumbnail pane open.
InitialView int
// InitialPage specifies the page on which the PDF document should be
// opened in the viewer.
InitialPage int
// Magnification specifies the action to be performed when the PDF document
// is opened. 0 = default, 1 = fit entire page, 2 = fit page width,
// 3 = fit visible, 4 = use zoom value from Zoom property.
Magnification int
// Zoom specifies the zoom level the PDF document is opened with. Only
// used if Magnification is set to 4.
Zoom int
// PageLayout specifies the page layout when the document is opened.
// 0 = default, 1 = single page, 2 = one column, 3 = two columns.
PageLayout int
// FirstPageOnLeft is used with PageLayout value 3. If true, the first
// page is displayed on the left side.
FirstPageOnLeft bool
// ResizeWindowToInitialPage specifies that the PDF viewer window is
// resized to show the whole initial page.
ResizeWindowToInitialPage bool
// CenterWindow specifies that the PDF viewer window is centered on the
// screen.
CenterWindow bool
// OpenInFullScreenMode specifies that the PDF viewer window is opened
// full screen.
OpenInFullScreenMode bool
// DisplayPDFDocumentTitle specifies that the document title is displayed
// in the PDF viewer title bar.
DisplayPDFDocumentTitle bool
// HideViewerMenubar specifies whether to hide the PDF viewer menubar.
HideViewerMenubar bool
// HideViewerToolbar specifies whether to hide the PDF viewer toolbar.
HideViewerToolbar bool
// HideViewerWindowControls specifies whether to hide the PDF viewer
// window controls.
HideViewerWindowControls bool
// UseTransitionEffects specifies that slide transitions are exported to
// PDF. Only active for Impress documents.
UseTransitionEffects bool
// OpenBookmarkLevels specifies how many bookmark levels should be opened
// in the reader. -1 = all levels, 1-10 = specific level.
OpenBookmarkLevels int
// LosslessImageCompression specifies if images are exported to PDF using
// a lossless compression format like PNG or compressed using the JPEG
// format.
LosslessImageCompression bool
// Quality specifies the quality of the JPG export. A higher value produces
// a higher-quality image and a larger file. Between 1 and 100.
Quality int
// ReduceImageResolution specifies if the resolution of each image is
// reduced to the resolution specified by the property MaxImageResolution.
ReduceImageResolution bool
// MaxImageResolution, if the property ReduceImageResolution is set to
// true, tells if all images will be reduced to the given value in DPI.
// Possible values are: 75, 150, 300, 600 and 1200.
MaxImageResolution int
// NativeWatermarkText specifies the text for a watermark to be drawn on
// every page of the exported PDF file.
// See https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html.
NativeWatermarkText string
// NativeWatermarkColor specifies the color for the watermark text as a
// decimal long value. Default is 8388223 (light green).
NativeWatermarkColor int
// NativeWatermarkFontHeight specifies the font size for the watermark text.
NativeWatermarkFontHeight int
// NativeWatermarkRotateAngle specifies the rotation angle for the watermark
// text in tenths of a degree (e.g., 450 = 45°).
NativeWatermarkRotateAngle int
// NativeWatermarkFontName specifies the font name for the watermark text.
// Default is "Helvetica".
NativeWatermarkFontName string
// NativeTiledWatermarkText specifies the tiled watermark text.
NativeTiledWatermarkText string
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
// PDF/A-3b and PDF/UA.
PdfFormats gotenberg.PdfFormats
}
// DefaultOptions returns the default values for Options.
func DefaultOptions() Options {
return Options{
Password: "",
Landscape: false,
PageRanges: "",
UpdateIndexes: true,
ExportFormFields: true,
AllowDuplicateFieldNames: false,
ExportBookmarks: true,
ExportBookmarksToPdfDestination: false,
ExportPlaceholders: false,
ExportNotes: false,
ExportNotesPages: false,
ExportOnlyNotesPages: false,
ExportNotesInMargin: false,
ConvertOooTargetToPdfTarget: false,
ExportLinksRelativeFsys: false,
ExportHiddenSlides: false,
SkipEmptyPages: false,
AddOriginalDocumentAsStream: false,
SinglePageSheets: false,
InitialView: 0,
InitialPage: 1,
Magnification: 0,
Zoom: 100,
PageLayout: 0,
FirstPageOnLeft: false,
ResizeWindowToInitialPage: false,
CenterWindow: false,
OpenInFullScreenMode: false,
DisplayPDFDocumentTitle: true,
HideViewerMenubar: false,
HideViewerToolbar: false,
HideViewerWindowControls: false,
UseTransitionEffects: true,
OpenBookmarkLevels: -1,
LosslessImageCompression: false,
Quality: 90,
ReduceImageResolution: false,
MaxImageResolution: 300,
NativeWatermarkText: "",
NativeWatermarkColor: 8388223,
NativeWatermarkFontHeight: 0,
NativeWatermarkRotateAngle: 0,
NativeWatermarkFontName: "Helvetica",
NativeTiledWatermarkText: "",
PdfFormats: gotenberg.PdfFormats{
PdfA: "",
PdfUa: false,
},
}
}
// Uno is an abstraction on top of the Universal Network Objects API.
type Uno interface {
Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error
Extensions() []string
}
// Provider is a module interface that exposes a method for creating a
// [Uno] for other modules.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(libreofficeapi.Provider))
// libreOffice, _ := provider.(api.Provider).LibreOffice()
// }
type Provider interface {
LibreOffice() (Uno, error)
}
// Descriptor returns a [Api]'s module descriptor.
func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "libreoffice-api",
FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("api", flag.ExitOnError)
fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature")
fs.Int64("libreoffice-max-queue-size", 0, "Maximum request queue size for LibreOffice. Set to 0 to disable this feature")
fs.Duration("libreoffice-idle-shutdown-timeout", 0, "Shutdown LibreOffice after being idle for the given duration. Set to 0 to disable this feature")
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
fs.StringSlice("libreoffice-allow-list", []string{}, "Set the allowed URLs for LibreOffice outbound fetches (embedded images, linked content) using regular expressions - supports multiple values")
fs.StringSlice("libreoffice-deny-list", []string{}, "Set the denied URLs for LibreOffice outbound fetches using regular expressions - supports multiple values")
fs.Bool("libreoffice-deny-private-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted documents to mitigate SSRF against internal services")
fs.Bool("libreoffice-deny-public-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")
return fs
}(),
New: func() gotenberg.Module { return new(Api) },
}
}
// Provision sets the module properties.
func (a *Api) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags()
a.autoStart = flags.MustBool("libreoffice-auto-start")
libreOfficeBinPath, ok := os.LookupEnv("LIBREOFFICE_BIN_PATH")
if !ok {
return errors.New("LIBREOFFICE_BIN_PATH environment variable is not set")
}
unoBinPath, ok := os.LookupEnv("UNOCONVERTER_BIN_PATH")
if !ok {
return errors.New("UNOCONVERTER_BIN_PATH environment variable is not set")
}
a.args = libreOfficeArguments{
binPath: libreOfficeBinPath,
unoBinPath: unoBinPath,
startTimeout: flags.MustDuration("libreoffice-start-timeout"),
proxyOptions: outboundProxyOptions{
allowList: flags.MustRegexpSlice("libreoffice-allow-list"),
denyList: flags.MustRegexpSlice("libreoffice-deny-list"),
denyPrivateIPs: flags.MustBool("libreoffice-deny-private-ips"),
denyPublicIPs: flags.MustBool("libreoffice-deny-public-ips"),
},
}
// Logger.
a.logger = gotenberg.Logger(a).With(slog.String("logger", "libreoffice"))
// Process.
a.libreOffice = newLibreOfficeProcess(a.args)
a.supervisor = gotenberg.NewProcessSupervisor(a.logger, "libreoffice", a.libreOffice, flags.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"), 1, flags.MustDuration("libreoffice-idle-shutdown-timeout"))
// Metrics.
meter := gotenberg.Meter()
// Observable gauges.
var err error
_, err = meter.Int64ObservableGauge(
"libreoffice.requests.active",
metric.WithDescription("Current number of active LibreOffice requests"),
metric.WithUnit("{request}"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(a.supervisor.ActiveTasksCount())
return nil
}),
)
if err != nil {
return fmt.Errorf("create libreoffice.requests.active gauge: %w", err)
}
_, err = meter.Int64ObservableGauge(
"libreoffice.requests.queue_size",
metric.WithDescription("Current number of LibreOffice conversion requests waiting to be treated"),
metric.WithUnit("{request}"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(a.supervisor.ReqQueueSize())
return nil
}),
)
if err != nil {
return fmt.Errorf("create libreoffice.requests.queue_size gauge: %w", err)
}
_, err = meter.Int64ObservableCounter(
"libreoffice.process.restarts.total",
metric.WithDescription("Current number of LibreOffice restarts"),
metric.WithUnit("{restart}"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(a.supervisor.RestartsCount())
return nil
}),
)
if err != nil {
return fmt.Errorf("create libreoffice.process.restarts.total counter: %w", err)
}
// Counters.
a.reqsCounter, err = meter.Int64Counter(
"libreoffice.requests.total",
metric.WithDescription("Total number of LibreOffice conversion requests"),
metric.WithUnit("{request}"),
)
if err != nil {
return fmt.Errorf("create libreoffice.requests.total counter: %w", err)
}
a.errsCounter, err = meter.Int64Counter(
"libreoffice.errors.total",
metric.WithDescription("Total number of LibreOffice conversion errors"),
metric.WithUnit("{error}"),
)
if err != nil {
return fmt.Errorf("create libreoffice.errors.total counter: %w", err)
}
// Histograms.
durationBuckets := metric.WithExplicitBucketBoundaries(0.5, 1, 2, 5, 10, 30, 60)
a.conversionDurationCounter, err = meter.Float64Histogram(
"libreoffice.conversion.duration",
metric.WithDescription("Duration of LibreOffice conversions"),
metric.WithUnit("s"),
durationBuckets,
)
if err != nil {
return fmt.Errorf("create libreoffice.conversion.duration histogram: %w", err)
}
a.queueWaitDurationCounter, err = meter.Float64Histogram(
"libreoffice.queue.wait.duration",
metric.WithDescription("Duration of waiting in queue for LibreOffice conversions"),
metric.WithUnit("s"),
durationBuckets,
)
if err != nil {
return fmt.Errorf("create libreoffice.queue.wait.duration histogram: %w", err)
}
a.pdfOutputSizeCounter, err = meter.Int64Histogram(
"libreoffice.pdf.output.size",
metric.WithDescription("Size of PDF output from LibreOffice conversions"),
metric.WithUnit("By"),
)
if err != nil {
return fmt.Errorf("create libreoffice.pdf.output.size histogram: %w", err)
}
a.coreDumpedRetriesCounter, err = meter.Int64Counter(
"libreoffice.conversion.retries.total",
metric.WithDescription("Total number of LibreOffice conversion retries after a core dump"),
metric.WithUnit("{retry}"),
)
if err != nil {
return fmt.Errorf("create libreoffice.conversion.retries.total counter: %w", err)
}
return nil
}
// Validate validates the module properties.
func (a *Api) Validate() error {
var err error
_, statErr := os.Stat(a.args.binPath)
if os.IsNotExist(statErr) {
err = errors.Join(err, fmt.Errorf("LibreOffice binary does not exist at %q; check the LIBREOFFICE_BIN_PATH environment variable: %w", a.args.binPath, statErr))
}
_, statErr = os.Stat(a.args.unoBinPath)
if os.IsNotExist(statErr) {
err = errors.Join(err, fmt.Errorf("unoconverter binary does not exist at %q; check the UNOCONVERTER_BIN_PATH environment variable: %w", a.args.unoBinPath, statErr))
}
return err
}
// Start does nothing if auto-start is not enabled. Otherwise, it starts a
// LibreOffice instance.
func (a *Api) Start() error {
if !a.autoStart {
return nil
}
err := a.supervisor.Launch()
if err != nil {
return fmt.Errorf("launch supervisor: %w", err)
}
return nil
}
// StartupMessage returns a custom startup message.
func (a *Api) StartupMessage() string {
if !a.autoStart {
return "LibreOffice ready to start"
}
return "LibreOffice automatically started"
}
// Stop stops the current browser instance.
func (a *Api) Stop(ctx context.Context) error {
// Block until the context is done so that another module may gracefully
// stop before we do a shutdown.
a.logger.DebugContext(ctx, "wait for the end of grace duration")
<-ctx.Done()
err := a.supervisor.Shutdown()
if err == nil {
return nil
}
return fmt.Errorf("stop LibreOffice: %w", err)
}
// Debug returns additional debug data.
func (a *Api) Debug() map[string]any {
return map[string]any{"version": a.detectVersion()}
}
// detectVersion resolves the LibreOffice version once, preferring the value
// captured at image build time so it never spawns LibreOffice at runtime. It
// falls back to running soffice --version for local or non-Docker builds.
func (a *Api) detectVersion() string {
a.versionOnce.Do(func() {
if v, ok := gotenberg.BuildVersion("libreoffice-api"); ok {
a.version = v
return
}
cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
output, err := cmd.Output()
if err != nil {
a.version = err.Error()
return
}
a.version = strings.TrimSpace(string(output))
})
return a.version
}
// spanAttrs returns the client-span attributes for a LibreOffice invocation:
// the server address and the LibreOffice version, plus any extra attributes.
// The version rides on every conversion span so a trace records which
// LibreOffice rendered the document.
func (a *Api) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 0, 2+len(extra))
attrs = append(attrs, semconv.ServerAddress(a.args.binPath))
if v := a.detectVersion(); v != "" {
attrs = append(attrs, attribute.String("gotenberg.libreoffice.version", v))
}
return append(attrs, extra...)
}
// Metrics returns the metrics.
func (a *Api) Metrics() ([]gotenberg.Metric, error) {
return []gotenberg.Metric{
{
Name: "libreoffice_requests_queue_size",
Description: "Current number of LibreOffice conversion requests waiting to be treated.",
Read: func() float64 {
return float64(a.supervisor.ReqQueueSize())
},
},
{
Name: "libreoffice_restarts_count",
Description: "Current number of LibreOffice restarts.",
Read: func() float64 {
return float64(a.supervisor.RestartsCount())
},
},
}, nil
}
// Checks adds a health check that verifies if LibreOffice is healthy.
func (a *Api) Checks() ([]health.CheckerOption, error) {
return []health.CheckerOption{
health.WithCheck(health.Check{
Name: "libreoffice",
Check: func(_ context.Context) error {
if a.supervisor.Healthy() {
return nil
}
return errors.New("LibreOffice is unhealthy")
},
}),
}, nil
}
// Ready returns no error if the module is ready.
func (a *Api) Ready() error {
if !a.autoStart {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), a.args.startTimeout)
defer cancel()
ticker := time.NewTicker(time.Duration(100) * time.Millisecond)
for {
select {
case <-ctx.Done():
ticker.Stop()
return fmt.Errorf("context done while waiting for LibreOffice to be ready: %w", ctx.Err())
case <-ticker.C:
ok := a.libreOffice.Healthy(a.logger)
if ok {
ticker.Stop()
return nil
}
continue
}
}
}
// LibreOffice returns a [Uno] for interacting with LibreOffice.
func (a *Api) LibreOffice() (Uno, error) {
return a, nil
}
// Pdf converts a document to PDF.
func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error {
ctx, span := gotenberg.Tracer().Start(ctx, "libreoffice.Pdf",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(a.spanAttrs()...),
)
defer span.End()
span.SetAttributes(
attribute.Int64("gotenberg.queue.depth_at_arrival", a.supervisor.ReqQueueSize()),
attribute.Int64("gotenberg.conversions_since_last_restart", a.supervisor.ConversionsSinceRestart()),
)
span.SetAttributes(conversionRequestAttributes(inputPath, options)...)
// ErrCoreDumped happens randomly (https://github.com/gotenberg/gotenberg/issues/639);
// retry the conversion, but cap the retries so a permanently failing
// document cannot loop forever. Each attempt records its own metrics.
const maxCoreDumpedRetries = 10
var err error
var reason string
for attempt := 0; ; attempt++ {
start := time.Now()
var conversionStart time.Time
err = a.supervisor.Run(ctx, logger, func() error {
conversionStart = time.Now()
return a.libreOffice.pdf(ctx, logger, inputPath, outputPath, options)
})
// Determine status and error reason.
status := "success"
reason = ""
if err != nil {
status = "error"
if errors.Is(err, context.DeadlineExceeded) {
status = "timeout"
}
reason = libreofficeErrorType(err)
}
// Record metrics for this attempt.
attrs := metric.WithAttributes(attribute.String("status", status))
a.reqsCounter.Add(ctx, 1, attrs)
if reason != "" {
a.errsCounter.Add(ctx, 1, metric.WithAttributes(attribute.String("reason", reason)))
}
if !conversionStart.IsZero() {
queueWait := conversionStart.Sub(start).Seconds()
a.queueWaitDurationCounter.Record(ctx, queueWait, attrs)
conversionDuration := time.Since(conversionStart).Seconds()
a.conversionDurationCounter.Record(ctx, conversionDuration, attrs)
}
if err == nil {
stat, statErr := os.Stat(outputPath)
if statErr == nil {
a.pdfOutputSizeCounter.Record(ctx, stat.Size(), attrs)
span.SetAttributes(attribute.Int64("gotenberg.conversion.output.bytes", stat.Size()))
}
span.SetStatus(codes.Ok, "")
return nil
}
if errors.Is(err, ErrCoreDumped) && attempt < maxCoreDumpedRetries {
logger.DebugContext(ctx, fmt.Sprintf("got a '%s' error, retry conversion (attempt %d)", err, attempt+1))
span.AddEvent("conversion.retry", trace.WithAttributes(
attribute.Int("attempt", attempt+1),
))
a.coreDumpedRetriesCounter.Add(ctx, 1)
continue
}
break
}
gotenberg.SpanErrorType(span, reason)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return fmt.Errorf("supervisor run task: %w", err)
}
// conversionRequestAttributes derives low-cardinality attributes describing the
// requested conversion: the input document size and the requested PDF format
// options.
func conversionRequestAttributes(inputPath string, options Options) []attribute.KeyValue {
attrs := []attribute.KeyValue{
attribute.String("gotenberg.libreoffice.pdf_a", options.PdfFormats.PdfA),
attribute.Bool("gotenberg.libreoffice.pdf_ua", options.PdfFormats.PdfUa),
attribute.Bool("gotenberg.conversion.landscape", options.Landscape),
attribute.Bool("gotenberg.conversion.has_page_ranges", options.PageRanges != ""),
}
if info, err := os.Stat(inputPath); err == nil {
attrs = append(attrs, attribute.Int64("gotenberg.conversion.input.bytes", info.Size()))
}
return attrs
}
// libreofficeErrorType maps a conversion error to LibreOffice's bounded reason
// value, reused as the span error.type. Generic failures fall back to
// [gotenberg.ClassifyError].
func libreofficeErrorType(err error) string {
switch {
case errors.Is(err, ErrInvalidPdfFormats):
return gotenberg.ErrorTypeInvalidInput
case errors.Is(err, ErrUnoException), errors.Is(err, ErrRuntimeException):
return "libreoffice_exception"
case errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded), errors.Is(err, gotenberg.ErrProcessAlreadyRestarting):
return "libreoffice_unavailable"
default:
return gotenberg.ClassifyError(err)
}
}
// Extensions returns the file extensions available for conversions.
// FIXME: don't care, take all on the route level?
func (a *Api) Extensions() []string {
return []string{
".123",
".602",
".abw",
".bib",
".bmp",
".cdr",
".cgm",
".cmx",
".csv",
".cwk",
".dbf",
".dif",
".doc",
".docm",
".docx",
".dot",
".dotm",
".dotx",
".dxf",
".emf",
".eps",
".epub",
".fodg",
".fodp",
".fods",
".fodt",
".fopd",
".gif",
".htm",
".html",
".hwp",
".jpeg",
".jpg",
".key",
".ltx",
".lwp",
".mcw",
".met",
".mml",
".mw",
".numbers",
".odd",
".odg",
".odm",
".odp",
".ods",
".odt",
".otg",
".oth",
".otp",
".ots",
".ott",
".pages",
".pbm",
".pcd",
".pct",
".pcx",
".pdb",
".pdf",
".pgm",
".png",
".pot",
".potm",
".potx",
".ppm",
".pps",
".ppt",
".pptm",
".pptx",
".psd",
".psw",
".pub",
".pwp",
".pxl",
".ras",
".rtf",
".sda",
".sdc",
".sdd",
".sdp",
".sdw",
".sgl",
".slk",
".smf",
".stc",
".std",
".sti",
".stw",
".svg",
".svm",
".swf",
".sxc",
".sxd",
".sxg",
".sxi",
".sxm",
".sxw",
".tga",
".tif",
".tiff",
".txt",
".uof",
".uop",
".uos",
".uot",
".vdx",
".vor",
".vsd",
".vsdm",
".vsdx",
".wb2",
".wk1",
".wks",
".wmf",
".wpd",
".wpg",
".wps",
".xbm",
".xhtml",
".xls",
".xlsb",
".xlsm",
".xlsx",
".xlt",
".xltm",
".xltx",
".xlw",
".xml",
".xpm",
".zabw",
}
}
// Interface guards.
var (
_ gotenberg.Module = (*Api)(nil)
_ gotenberg.Provisioner = (*Api)(nil)
_ gotenberg.Validator = (*Api)(nil)
_ gotenberg.App = (*Api)(nil)
_ gotenberg.Debuggable = (*Api)(nil)
_ gotenberg.MetricsProvider = (*Api)(nil)
_ api.HealthChecker = (*Api)(nil)
_ Uno = (*Api)(nil)
_ Provider = (*Api)(nil)
)