refactor(pdfengines): embeds => attachments

This commit is contained in:
Julien Neuhart
2026-03-08 15:49:09 +01:00
parent 7af3cd1ff5
commit cf9fd7eedc
92 changed files with 5763 additions and 3082 deletions

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"mime"
"mime/multipart"
"net/http"
@@ -19,11 +20,14 @@ import (
"github.com/hashicorp/go-retryablehttp"
"github.com/labstack/echo/v4"
"github.com/mholt/archives"
"go.uber.org/zap"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"golang.org/x/text/unicode/norm"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
semconvutil "github.com/gotenberg/gotenberg/v8/pkg/gotenberg/semconv"
)
var (
@@ -36,7 +40,7 @@ var (
ErrOutOfBoundsOutputPath = errors.New("output path is not within context's working directory")
)
// Context is the request context for a "multipart/form-data" requests.
// Context is the request context for a "multipart/form-data" request.
type Context struct {
dirPath string
values map[string][]string
@@ -45,7 +49,7 @@ type Context struct {
outputPaths []string
cancelled bool
logger *zap.Logger
logger *slog.Logger
echoCtx echo.Context
mkdirAll gotenberg.MkdirAll
pathRename gotenberg.PathRename
@@ -81,12 +85,12 @@ type downloadFrom struct {
// ExtraHttpHeaders are the HTTP headers to send alongside.
ExtraHttpHeaders map[string]string `json:"extraHttpHeaders"`
// Download as embed file
Embedded bool `json:"embedded"`
// Download as an attachment file.
Attachment bool `json:"attachment"`
}
// newContext returns a [Context] by parsing a "multipart/form-data" request.
func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSystem, timeout time.Duration, bodyLimit int64, downloadFromCfg downloadFromConfig, traceHeader, trace string) (*Context, context.CancelFunc, error) {
func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSystem, timeout time.Duration, bodyLimit int64, downloadFromCfg downloadFromConfig) (*Context, context.CancelFunc, error) {
processCtx, processCancel := context.WithTimeout(echoCtx.Request().Context(), timeout)
// We want to make sure the multipart/form-data does not exceed a given
@@ -131,12 +135,12 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
err := os.RemoveAll(ctx.dirPath)
if err != nil {
ctx.logger.Error(fmt.Sprintf("remove context's working directory: %s", err))
ctx.logger.ErrorContext(ctx, fmt.Sprintf("remove context's working directory: %s", err))
return
}
ctx.logger.Debug(fmt.Sprintf("'%s' context's working directory removed", ctx.dirPath))
ctx.logger.DebugContext(ctx, fmt.Sprintf("'%s' context's working directory removed", ctx.dirPath))
ctx.cancelled = true
}
}()
@@ -224,9 +228,9 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
return fmt.Errorf("filter URL: %w", err)
}
logger.Debug(fmt.Sprintf("download file from '%s'", dl.Url))
logger.DebugContext(ctx, fmt.Sprintf("download file from '%s'", dl.Url))
req, err := retryablehttp.NewRequest(http.MethodGet, dl.Url, nil)
req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, dl.Url, nil)
if err != nil {
return fmt.Errorf("create request to '%s': %w", dl.Url, err)
}
@@ -235,7 +239,20 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
for key, value := range dl.ExtraHttpHeaders {
req.Header.Set(key, value)
}
req.Header.Set(traceHeader, trace)
req.Header.Set(echoCtx.Get("correlationIdHeader").(string), echoCtx.Get("correlationId").(string))
// OpenTelemetry.
meter := gotenberg.Meter()
semconvClient := semconvutil.NewHTTPClient(meter)
tracer := gotenberg.Tracer()
traceCtx, span := tracer.Start(ctx, fmt.Sprintf("%s Download From", req.Method),
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(semconvClient.RequestTraceAttrs(req.Request)...),
)
defer span.End()
otel.GetTextMapPropagator().Inject(traceCtx, propagation.HeaderCarrier(req.Header))
client := &retryablehttp.Client{
HTTPClient: &http.Client{
@@ -244,22 +261,27 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
RetryMax: downloadFromCfg.maxRetry,
RetryWaitMin: time.Duration(1) * time.Second,
RetryWaitMax: time.Until(deadline),
Logger: gotenberg.NewLeveledLogger(logger),
Logger: gotenberg.NewLeveledLogger(logger).WithContext(ctx),
CheckRetry: retryablehttp.DefaultRetryPolicy,
Backoff: retryablehttp.DefaultBackoff,
}
resp, err := client.Do(req)
if err != nil {
span.RecordError(err)
span.SetStatus(semconvClient.Status(0))
return WrapError(
fmt.Errorf("download file from to '%s': %w", dl.Url, err),
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Unable to download file from '%s': %s", dl.Url, err)),
)
}
span.SetAttributes(semconvClient.ResponseTraceAttrs(resp)...)
span.SetStatus(semconvClient.Status(resp.StatusCode))
defer func() {
err := resp.Body.Close()
if err != nil {
logger.Error(fmt.Sprintf("close response body from '%s': %s", dl.Url, err))
logger.ErrorContext(ctx, fmt.Sprintf("close response body from '%s': %s", dl.Url, err))
}
}()
@@ -301,7 +323,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
// normalized.
// See: https://github.com/gotenberg/gotenberg/issues/662.
filename = norm.NFC.String(filepath.Base(filename))
path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
path := fmt.Sprintf("%s/%s", dirPath, filename)
out, err := os.Create(path)
if err != nil {
@@ -310,7 +332,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
defer func() {
err := out.Close()
if err != nil {
logger.Error(fmt.Sprintf("close local file: %s", err))
logger.ErrorContext(ctx, fmt.Sprintf("close local file: %s", err))
}
}()
@@ -323,8 +345,8 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
}
ctx.files[filename] = path
if dl.Embedded {
ctx.filesByField[EmbedsFormField] = append(ctx.filesByField[EmbedsFormField], path)
if dl.Attachment {
ctx.filesByField[AttachmentsFormField] = append(ctx.filesByField[AttachmentsFormField], path)
}
return nil
@@ -346,7 +368,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
defer func() {
err := in.Close()
if err != nil {
logger.Error(fmt.Sprintf("close file header: %s", err))
logger.ErrorContext(ctx, fmt.Sprintf("close file header: %s", err))
}
}()
@@ -366,7 +388,7 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
defer func() {
err := out.Close()
if err != nil {
logger.Error(fmt.Sprintf("close local file: %s", err))
logger.ErrorContext(ctx, fmt.Sprintf("close local file: %s", err))
}
}()
@@ -394,10 +416,10 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
}
}
ctx.Log().Debug(fmt.Sprintf("form fields: %+v", ctx.values))
ctx.Log().Debug(fmt.Sprintf("form files: %+v", ctx.files))
ctx.Log().Debug(fmt.Sprintf("form files by field: %+v", ctx.filesByField))
ctx.Log().Debug(fmt.Sprintf("total bytes: %d", totalBytesRead.Load()))
ctx.Log().DebugContext(ctx, fmt.Sprintf("form fields: %+v", ctx.values))
ctx.Log().DebugContext(ctx, fmt.Sprintf("form files: %+v", ctx.files))
ctx.Log().DebugContext(ctx, fmt.Sprintf("form files by field: %+v", ctx.filesByField))
ctx.Log().DebugContext(ctx, fmt.Sprintf("total bytes: %d", totalBytesRead.Load()))
return ctx, cancel, err
}
@@ -444,7 +466,7 @@ func (ctx *Context) CreateSubDirectory(dirName string) (string, error) {
// Rename is just a wrapper around [os.Rename], as we need to mock this
// behavior in our tests.
func (ctx *Context) Rename(oldpath, newpath string) error {
ctx.Log().Debug(fmt.Sprintf("rename %s to %s", oldpath, newpath))
ctx.Log().DebugContext(ctx, fmt.Sprintf("rename %s to %s", oldpath, newpath))
err := ctx.pathRename.Rename(oldpath, newpath)
if err != nil {
return fmt.Errorf("rename path: %w", err)
@@ -470,8 +492,8 @@ func (ctx *Context) AddOutputPaths(paths ...string) error {
return nil
}
// Log returns the context [zap.Logger].
func (ctx *Context) Log() *zap.Logger {
// Log returns the context [slog.Logger].
func (ctx *Context) Log() *slog.Logger {
return ctx.logger
}
@@ -487,7 +509,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
}
if len(ctx.outputPaths) == 1 {
ctx.logger.Debug(fmt.Sprintf("only one output file '%s', skip archive creation", ctx.outputPaths[0]))
ctx.logger.DebugContext(ctx, fmt.Sprintf("only one output file '%s', skip archive creation", ctx.outputPaths[0]))
return ctx.outputPaths[0], nil
}
@@ -510,7 +532,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
defer func(out *os.File) {
err := out.Close()
if err != nil {
ctx.logger.Error(fmt.Sprintf("close zip file: %s", err))
ctx.logger.ErrorContext(ctx, fmt.Sprintf("close zip file: %s", err))
}
}(out)
@@ -519,7 +541,7 @@ func (ctx *Context) BuildOutputFile() (string, error) {
return "", fmt.Errorf("archive output files: %w", err)
}
ctx.logger.Debug(fmt.Sprintf("archive '%s' created", archivePath))
ctx.logger.DebugContext(ctx, fmt.Sprintf("archive '%s' created", archivePath))
return archivePath, nil
}