func (ctx *MockContext) SetValues(values map[string][]string) {
	ctx.values = values
}
SetValues sets the values.

ctx := &api.MockContext{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"url": {
"foo",
},
})
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
}
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
}
func (ctx Context) Request() *http.Request {
	return ctx.echoCtx.Request()
}
func (ctx Context) FormData() *FormData {
	return &FormData{
		values: ctx.values,
		files:  ctx.files,
		errors: nil,
	}
}
func (ctx Context) GeneratePath(extension string) string {
	return fmt.Sprintf("%s/%s%s", ctx.dirPath, uuid.New(), extension)
}
func (ctx *Context) AddOutputPaths(paths ...string) error {
	if ctx.cancelled {
		return ErrContextAlreadyClosed
	}
	for _, path := range paths {
		if !strings.HasPrefix(path, ctx.dirPath) {
			return ErrOutOfBoundsOutputPath
		}
		ctx.outputPaths = append(ctx.outputPaths, path)
	}
	return nil
}
func (ctx Context) Log() *zap.Logger {
	return ctx.logger
}
func (ctx Context) BuildOutputFile() (string, error) {
	if ctx.cancelled {
		return "", ErrContextAlreadyClosed
	}
	if len(ctx.outputPaths) == 0 {
		return "", errors.New("no output path")
	}
	if len(ctx.outputPaths) == 1 {
		ctx.logger.Debug(fmt.Sprintf("only one output file '%s', skip archive creation", ctx.outputPaths[0]))
		return ctx.outputPaths[0], nil
	}
	z := archiver.Zip{
		CompressionLevel:       flate.DefaultCompression,
		MkdirAll:               true,
		SelectiveCompression:   true,
		ContinueOnError:        false,
		OverwriteExisting:      false,
		ImplicitTopLevelFolder: false,
	}
	archivePath := ctx.GeneratePath(".zip")
	err := z.Archive(ctx.outputPaths, archivePath)
	if err != nil {
		return "", fmt.Errorf("archive output files: %w", err)
	}
	ctx.logger.Debug(fmt.Sprintf("archive '%s' created", archivePath))
	return archivePath, nil
}
func (ctx Context) OutputFilename(outputPath string) string {
	filename := ctx.echoCtx.Request().Header.Get("Gotenberg-Output-Filename")
	if filename == "" {
		return filepath.Base(outputPath)
	}
	return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
}
type MockContext struct {
	*Context
}
func (ctx *MockContext) SetDirPath(path string) {
	ctx.dirPath = path
}