func newContext(echoCtx echo.Context, logger *zap.Logger, timeout time.Duration) (*Context, context.CancelFunc, error) {
	processCtx, processCancel := context.WithTimeout(context.Background(), timeout)
	ctx := &Context{
		outputPaths: make([]string, 0),
		cancelled:   false,
		logger:      logger,
		echoCtx:     echoCtx,
		Context:     processCtx,
	}
	cancel := func() context.CancelFunc {
		return func() {
			if ctx.cancelled {
				return
			}
			processCancel()
			if ctx.dirPath == "" {
				return
			}
			err := os.RemoveAll(ctx.dirPath)
			if err != nil {
				ctx.logger.Error(fmt.Sprintf("remove context's working directory: %s", err))
				return
			}
			ctx.logger.Debug(fmt.Sprintf("'%s' removed", ctx.dirPath))
			ctx.cancelled = true
		}
	}()
	form, err := echoCtx.MultipartForm()
	if err != nil {
		if errors.Is(err, http.ErrNotMultipart) {
			return nil, cancel, WrapError(
				fmt.Errorf("get multipart form: %w", err),
				NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: want 'multipart/form-data'"),
			)
		}
		if errors.Is(err, http.ErrMissingBoundary) {
			return nil, cancel, WrapError(
				fmt.Errorf("get multipart form: %w", err),
				NewSentinelHTTPError(http.StatusUnsupportedMediaType, "Invalid 'Content-Type' header value: no boundary"),
			)
		}
		if strings.Contains(err.Error(), io.EOF.Error()) {
			return nil, cancel, WrapError(
				fmt.Errorf("get multipart form: %w", err),
				NewSentinelHTTPError(http.StatusBadRequest, "Malformed body: it does not match the 'Content-Type' header boundaries"),
			)
		}
		return nil, cancel, fmt.Errorf("get multipart form: %w", err)
	}
	dirPath, err := gotenberg.MkdirAll()
	if err != nil {
		return nil, cancel, fmt.Errorf("create working directory: %w", err)
	}
	ctx.dirPath = dirPath
	ctx.values = form.Value
	ctx.files = make(map[string]string)
	copyToDisk := func(fh *multipart.FileHeader) error {
		t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
		filename, _, err := transform.String(t, filepath.Base(fh.Filename))
		if err != nil {
			return fmt.Errorf("transform filename: %w", err)
		}
		in, err := fh.Open()
		if err != nil {
			return fmt.Errorf("open multipart file: %w", err)
		}
		defer func() {
			err := in.Close()
			if err != nil {
				logger.Error(fmt.Sprintf("close file header: %s", err))
			}
		}()
		path := fmt.Sprintf("%s/%s", ctx.dirPath, filename)
		out, err := os.Create(path)
		if err != nil {
			return fmt.Errorf("create local file: %w", err)
		}
		defer func() {
			err := out.Close()
			if err != nil {
				logger.Error(fmt.Sprintf("close local file: %s", err))
			}
		}()
		_, err = io.Copy(out, in)
		if err != nil {
			return fmt.Errorf("copy multipart file to local file: %w", err)
		}
		ctx.files[filename] = path
		return nil
	}
	for _, files := range form.File {
		for _, fh := range files {
			err = copyToDisk(fh)
			if err != nil {
				return ctx, cancel, fmt.Errorf("copy to disk: %w", err)
			}
		}
	}
	ctx.Log().Debug(fmt.Sprintf("form data values: %+v", ctx.values))
	ctx.Log().Debug(fmt.Sprintf("form data files: %+v", ctx.files))
	return ctx, cancel, err
}
newContext returns a Context by parsing a "multipart/form-data" request.
package api
import (
	"compress/flate"
	"context"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
	"unicode"
	"github.com/google/uuid"
	"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
	"github.com/labstack/echo/v4"
	"github.com/mholt/archiver/v3"
	"go.uber.org/zap"
	"golang.org/x/text/runes"
	"golang.org/x/text/transform"
	"golang.org/x/text/unicode/norm"
)
var (
	ErrContextAlreadyClosed = errors.New("context already closed")
	ErrOutOfBoundsOutputPath = errors.New("output path is not within context's working directory")
)
type Context struct {
	dirPath string
	values  map[string][]string
	files   map[string]string
	outputPaths []string
	cancelled bool
	logger    *zap.Logger
	echoCtx   echo.Context
	context.Context
}