huge refactoring

This commit is contained in:
Julien Neuhart
2019-07-23 13:57:18 +02:00
parent f6b357691c
commit 7bfbda4490
105 changed files with 4051 additions and 2667 deletions

View File

@@ -0,0 +1,3 @@
// Package xhttp defines our own implementation
// of echo.Echo.
package xhttp

View File

@@ -0,0 +1,298 @@
package xhttp
import (
"fmt"
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
)
const (
pingEndpoint string = "/ping"
mergeEndpoint string = "/merge"
convertGroupEndpoint string = "/convert"
htmlEndpoint string = "/html"
urlEndpoint string = "/url"
markdownEndpoint string = "/markdown"
officeEndpoint string = "/office"
)
// pingHandler is the handler for healthcheck.
func pingHandler(c echo.Context) error {
const op string = "xhttp.pingHandler"
ctx := context.MustCastFromEchoContext(c)
ctx.XLogger().DebugOp(op, "handling ping request...")
if err := ctx.ProcessesHealthcheck(); err != nil {
return xerror.New(op, err)
}
return nil
}
// mergeHandler is the handler for merging
// PDF files.
func mergeHandler(c echo.Context) error {
const op string = "xhttp.mergeHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling merge request...")
r := ctx.MustResource()
opts, err := mergePrinterOptions(r, ctx.Config())
if err != nil {
return xerror.New(op, err)
}
fpaths, err := r.Fpaths(".pdf")
if err != nil {
return err
}
p := printer.NewMergePrinter(logger, fpaths, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// htmlHandler is the handler for converting
// HTML to PDF.
func htmlHandler(c echo.Context) error {
const op string = "xhttp.htmlHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling HTML request...")
r := ctx.MustResource()
opts, err := chromePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
fpath, err := r.Fpath("index.html")
if err != nil {
return err
}
p := printer.NewHTMLPrinter(logger, fpath, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// urlHandler is the handler for converting
// a URL to PDF.
func urlHandler(c echo.Context) error {
const op string = "xhttp.urlHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling URL request...")
r := ctx.MustResource()
opts, err := chromePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
if !r.HasArg(resource.RemoteURLArgKey) {
return xerror.Invalid(
op,
fmt.Sprintf("'%s' not found or empty", resource.RemoteURLArgKey),
nil,
)
}
remoteURL, err := r.StringArg(resource.RemoteURLArgKey, "")
if err != nil {
return err
}
p := printer.NewURLPrinter(logger, remoteURL, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// markdownHandler is the handler for converting
// Markdown to PDF.
func markdownHandler(c echo.Context) error {
const op string = "xhttp.markdownHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling Markdown request...")
r := ctx.MustResource()
opts, err := chromePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
fpath, err := r.Fpath("index.html")
if err != nil {
return err
}
p, err := printer.NewMarkdownPrinter(logger, fpath, opts)
if err != nil {
return err
}
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// officeHandler is the handler for converting
// Office documents to PDF.
func officeHandler(c echo.Context) error {
const op string = "xhttp.officeHandler"
resolver := func() error {
ctx := context.MustCastFromEchoContext(c)
logger := ctx.XLogger()
logger.DebugOp(op, "handling Office request...")
r := ctx.MustResource()
opts, err := officePrinterOptions(r, ctx.Config())
if err != nil {
return err
}
fpaths, err := r.Fpaths(
".txt",
".rtf",
".fodt",
".doc",
".docx",
".odt",
".xls",
".xlsx",
".ods",
".ppt",
".pptx",
".odp",
)
if err != nil {
return err
}
p := printer.NewOfficePrinter(logger, fpaths, opts)
return convert(ctx, p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func convert(ctx context.Context, p printer.Printer) error {
const op string = "xhttp.convert"
resolver := func() error {
logger := ctx.XLogger()
r := ctx.MustResource()
baseFilename := xrand.Get()
filename := fmt.Sprintf("%s.pdf", baseFilename)
fpath := fmt.Sprintf("%s/%s", r.DirPath(), filename)
// if no webhook URL given, run conversion
// and directly return the resulting PDF file
// or an error.
if !r.HasArg(resource.WebhookURLArgKey) {
logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
return convertSync(ctx, p, filename, fpath)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
return convertAsync(ctx, p, filename, fpath)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func convertSync(ctx context.Context, p printer.Printer, filename, fpath string) error {
const op = "xhttp.convertSync"
resolver := func() error {
logger := ctx.XLogger()
r := ctx.MustResource()
if err := p.Print(fpath); err != nil {
return err
}
if !r.HasArg(resource.ResultFilenameArgKey) {
logger.DebugfOp(
op,
"no '%s' found, using generated filename '%s'",
resource.RemoteURLArgKey,
filename,
)
if err := ctx.Attachment(fpath, filename); err != nil {
return err
}
return nil
}
logger.DebugfOp(
op,
"'%s' found, so not using generated filename",
resource.ResultFilenameArgKey,
)
filename, err := r.StringArg(resource.ResultFilenameArgKey, filename)
if err != nil {
return err
}
if err := ctx.Attachment(fpath, filename); err != nil {
return err
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string) error {
const op = "xhttp.convertAsync"
logger := ctx.XLogger()
r := ctx.MustResource()
go func() {
defer r.Close() // nolint: errcheck
if err := p.Print(fpath); err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
f, err := os.Open(fpath)
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
defer f.Close() // nolint: errcheck
webhookURL, err := r.StringArg(resource.WebhookURLArgKey, "")
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
logger.DebugfOp(
op,
"sending result file '%s' to '%s'",
filename,
webhookURL,
)
// TODO timeout
resp, err := http.Post(webhookURL, "application/pdf", f) /* #nosec */
if err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
defer resp.Body.Close() // nolint: errcheck
}()
return nil
}

View File

@@ -0,0 +1,128 @@
package xhttp
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
)
// contextMiddleware extends the default echo.Context with
// our custom context.Context.
func contextMiddleware(config conf.Config, processes ...pm2.Process) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// generate a unique identifier for the request.
trace := xrand.Get()
// create the logger for this request using
// the previous identifier as trace.
logger := xlog.New(config.LogLevel(), trace)
// extend the current echo context with our custom
// context.
ctx := context.New(c, logger, config, processes...)
// if its an healthcheck request, there
// is no need to create a Resource.
if ctx.Path() == pingEndpoint {
return next(ctx)
}
// if the endpoint is not for healthcheck, create a
// Resource.
if err := ctx.WithResource(trace); err != nil {
// required to have a correct status code.
ctx.Error(err)
return ctx.LogRequestResult(err, false)
}
return next(ctx)
}
}
}
// loggerMiddleware logs the result of a request.
func loggerMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := context.MustCastFromEchoContext(c)
err := next(ctx)
// we do not want to log healthcheck requests if
// log level is not set to DEBUG.
isDebug := ctx.Path() == pingEndpoint
return ctx.LogRequestResult(err, isDebug)
}
}
}
// cleanupMiddleware removes a resource.Resource
// at the end of a request.
func cleanupMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
const op string = "xhttp.cleanupMiddleware"
err := next(c)
ctx := context.MustCastFromEchoContext(c)
if !ctx.HasResource() {
// nothing to remove.
return err
}
r := ctx.MustResource()
// if a webhook URL has been given,
// do not remove the resource.Resource here because
// we don't know if the result file has been
// generated or sent.
if r.HasArg(resource.WebhookURLArgKey) {
return err
}
// a resource.Resource is associated with our custom context.
if resourceErr := r.Close(); resourceErr != nil {
xerr := xerror.New(op, resourceErr)
ctx.XLogger().ErrorOp(xerror.Op(xerr), xerr)
}
return err
}
}
}
// errorMiddleware handles errors (if any).
func errorMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := context.MustCastFromEchoContext(c)
err := next(ctx)
if err == nil {
// so far so good!
return nil
}
// if it's an error from echo
// like 404 not found and so on.
if echoHTTPErr, ok := err.(*echo.HTTPError); ok {
return echoHTTPErr
}
// we log the initial error before returning
// the HTTP error.
errOp := xerror.Op(err)
logger := ctx.XLogger()
logger.ErrorOp(errOp, err)
// handle our custom HTTP error.
var httpErr error
errCode := xerror.Code(err)
errMessage := xerror.Message(err)
switch errCode {
case xerror.InvalidCode:
httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage)
case xerror.TimeoutCode:
// TODO status
httpErr = echo.NewHTTPError(http.StatusBadGateway, errMessage)
default:
httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage)
}
// required to have a correct status code.
ctx.Error(httpErr)
return httpErr
}
}
}

View File

@@ -0,0 +1,93 @@
package xhttp
import (
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
)
func mergePrinterOptions(r resource.Resource, config conf.Config) (printer.MergePrinterOptions, error) {
const op string = "xhttp.mergePrinterOptions"
waitTimeout, err := resource.WaitTimeoutArg(r, config)
if err != nil {
return printer.MergePrinterOptions{}, xerror.New(op, err)
}
return printer.MergePrinterOptions{
WaitTimeout: waitTimeout,
}, nil
}
func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.ChromePrinterOptions, error) {
const op string = "xhttp.chromePrinterOptions"
resolver := func() (printer.ChromePrinterOptions, error) {
waitTimeout, err := resource.WaitTimeoutArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
waitDelay, err := resource.WaitDelayArg(r, config)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
headerHTML, footerHTML,
err := resource.HeaderFooterContents(r)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
paperWidth, paperHeight,
err := resource.PaperSizeArgs(r)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
marginTop, marginBottom, marginLeft, marginRight,
err := resource.MarginArgs(r)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
if err != nil {
return printer.ChromePrinterOptions{}, err
}
return printer.ChromePrinterOptions{
WaitTimeout: waitTimeout,
WaitDelay: waitDelay,
HeaderHTML: headerHTML,
FooterHTML: footerHTML,
PaperWidth: paperWidth,
PaperHeight: paperHeight,
MarginTop: marginTop,
MarginBottom: marginBottom,
MarginLeft: marginLeft,
MarginRight: marginRight,
Landscape: landscape,
}, nil
}
opts, err := resolver()
if err != nil {
return opts, xerror.New(op, err)
}
return opts, nil
}
func officePrinterOptions(r resource.Resource, config conf.Config) (printer.OfficePrinterOptions, error) {
const op string = "xhttp.officePrinterOptions"
resolver := func() (printer.OfficePrinterOptions, error) {
waitTimeout, err := resource.WaitTimeoutArg(r, config)
if err != nil {
return printer.OfficePrinterOptions{}, err
}
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
if err != nil {
return printer.OfficePrinterOptions{}, err
}
return printer.OfficePrinterOptions{
WaitTimeout: waitTimeout,
Landscape: landscape,
}, nil
}
opts, err := resolver()
if err != nil {
return opts, xerror.New(op, err)
}
return opts, nil
}

View File

@@ -0,0 +1,210 @@
package context
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
// Context extends the default echo.Context.
type Context struct {
echo.Context
logger xlog.Logger
config conf.Config
processes []pm2.Process
resource resource.Resource
startTime time.Time
}
// New creates a new Context.
func New(c echo.Context, logger xlog.Logger, config conf.Config, processess ...pm2.Process) Context {
return Context{
c,
logger,
config,
processess,
resource.Resource{},
time.Now(),
}
}
/*
MustCastFromEchoContext cast an echo.Context
to our custom Context.
It panics if casting goes wrong.
*/
func MustCastFromEchoContext(c echo.Context) Context {
const op string = "context.MustCastFromEchoContext"
ctx, ok := c.(Context)
if !ok {
panic(fmt.Sprintf("%s: unable to cast an echo.Context to our custom context.Context", op))
}
return ctx
}
/*
XLogger returns the xlog.Logger associated
with the Context.
This method should be used instead of the
default Logger() method coming from
the echo.Context.
*/
func (ctx Context) XLogger() xlog.Logger {
return ctx.logger
}
// Config returns the conf.Config associated
// with the Context.
func (ctx Context) Config() conf.Config {
return ctx.config
}
// ProcessesHealthcheck returns an error if
// one of the processes is not viable.
func (ctx Context) ProcessesHealthcheck() error {
const op string = "context.Context.ProcessesHealthcheck"
for _, process := range ctx.processes {
if !process.IsViable() {
return xerror.New(
op,
fmt.Errorf("'%s' is not viable", process.Fullname()),
)
}
}
return nil
}
// WithResource creates a resource.Resource and
// adds it to the Context.
func (ctx *Context) WithResource(directoryName string) error {
const op string = "context.Context.WithResource"
resolver := func() (resource.Resource, error) {
r, err := resource.New(ctx.logger, directoryName)
if err != nil {
return r, err
}
// retrieve form values from request.
for _, key := range resource.ArgKeys() {
r.WithArg(key, ctx.FormValue(string(key)))
}
// write form files from request.
form, err := ctx.MultipartForm()
if err != nil {
return r, err
}
for _, files := range form.File {
for _, fh := range files {
in, err := fh.Open()
if err != nil {
return r, err
}
defer in.Close() // nolint: errcheck
if err := r.WithFile(fh.Filename, in); err != nil {
return r, err
}
}
}
return r, nil
}
resource, err := resolver()
ctx.resource = resource
if err != nil {
return xerror.New(op, err)
}
return nil
}
/*
MustResource returns the resource.Resource
associated with the Context.
It panics if no resource.Resource.
*/
func (ctx Context) MustResource() resource.Resource {
const op string = "context.Context.MustResource"
if !ctx.HasResource() {
panic(fmt.Sprintf("%s: unable to retrieve the resource.Resource from our custom context.Context", op))
}
return ctx.resource
}
// HasResource returns true if the Context
// has a resource.Resource.
func (ctx Context) HasResource() bool {
return &ctx.resource != nil
}
/*
LogRequestResult logs the result of a request.
This method should only be used by a middleware!
If an error is given, returns the exact same error.
*/
func (ctx Context) LogRequestResult(err error, isDebug bool) error {
const op string = "context.Context.LogRequestResult"
req := ctx.Request()
resp := ctx.Response()
stopTime := time.Now()
fields := map[string]interface{}{
"remote_ip": ctx.RealIP(),
"host": req.Host,
"uri": req.RequestURI,
"method": req.Method,
"path": path(req),
"referer": req.Referer(),
"user_agent": req.UserAgent(),
"status": resp.Status,
"latency": lantency(ctx.startTime, stopTime),
"latency_human": latencyHuman(ctx.startTime, stopTime),
"bytes_in": bytesIn(req),
"bytes_out": bytesOut(resp),
}
if err != nil {
ctx.logger.WithFields(fields).ErrorfOp(op, "request failed")
return err
}
if isDebug {
ctx.logger.WithFields(fields).DebugfOp(op, "request handled")
return nil
}
ctx.logger.WithFields(fields).InfofOp(op, "request handled")
return nil
}
func path(r *http.Request) string {
path := r.URL.Path
if path == "" {
path = "/"
}
return path
}
func lantency(startTime time.Time, stopTime time.Time) string {
return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10)
}
func latencyHuman(startTime time.Time, stopTime time.Time) string {
return stopTime.Sub(startTime).String()
}
func bytesIn(r *http.Request) string {
bytesIn := r.Header.Get(echo.HeaderContentLength)
if bytesIn == "" {
bytesIn = "0"
}
return bytesIn
}
func bytesOut(r *echo.Response) string {
return strconv.FormatInt(r.Size, 10)
}

View File

@@ -0,0 +1,7 @@
/*
Package context extends the default echo.Context.
All functions return our standard xerror.Error
in case of error.
*/
package context

View File

@@ -0,0 +1,253 @@
package resource
import (
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
)
// ArgKey is a type for
// arguments' keys.
type ArgKey string
const (
// ResultFilenameArgKey is the key
// of the argument "resultFilename".
ResultFilenameArgKey ArgKey = "resultFilename"
// WaitTimeoutArgKey is the key
// of the argument "waitTimeout".
WaitTimeoutArgKey ArgKey = "waitTimeout"
// WebhookURLArgKey is the key
// of the argument "webhookURL".
WebhookURLArgKey ArgKey = "webhookURL"
// WebhookURLTimeoutArgKey is the key
// of the argument "webhookURLTimeout".
WebhookURLTimeoutArgKey ArgKey = "webhookURLTimeout"
// RemoteURLArgKey is the key
// of the argument "remoteURL".
RemoteURLArgKey ArgKey = "remoteURL"
// WaitDelayArgKey is the key
// of the argument "waitDelay".
WaitDelayArgKey ArgKey = "waitDelay"
// PaperWidthArgKey is the key
// of the argument "paperWidth".
PaperWidthArgKey ArgKey = "paperWidth"
// PaperHeightArgKey is the key
// of the argument "paperHeight".
PaperHeightArgKey ArgKey = "paperHeight"
// MarginTopArgKey is the key
// of the argument "marginTop".
MarginTopArgKey ArgKey = "marginTop"
// MarginBottomArgKey is the key
// of the argument "marginBottom".
MarginBottomArgKey ArgKey = "marginBottom"
// MarginLeftArgKey is the key
// of the argument "marginLeft".
MarginLeftArgKey ArgKey = "marginLeft"
// MarginRightArgKey is the key
// of the argument "marginRight".
MarginRightArgKey ArgKey = "marginRight"
// LandscapeArgKey is the key
// of the argument "landscape".
LandscapeArgKey ArgKey = "landscape"
)
/*
ArgKeys returns a slice
containing all available
arguments' keys.
*/
func ArgKeys() []ArgKey {
return []ArgKey{
ResultFilenameArgKey,
WaitTimeoutArgKey,
WebhookURLArgKey,
WebhookURLTimeoutArgKey,
RemoteURLArgKey,
WaitDelayArgKey,
PaperWidthArgKey,
PaperHeightArgKey,
MarginTopArgKey,
MarginBottomArgKey,
MarginLeftArgKey,
MarginRightArgKey,
LandscapeArgKey,
}
}
/*
WaitTimeoutArg is a helper for retrieving
the "waitTimeout" argument as float64.
It also validates it against the application
configuration.
*/
func WaitTimeoutArg(r Resource, config conf.Config) (float64, error) {
const op string = "resource.WaitTimeoutArg"
result, err := r.Float64Arg(
WaitTimeoutArgKey,
config.DefaultWaitTimeout(),
xassert.Float64NotInferiorTo(0),
xassert.Float64NotSuperiorTo(config.MaximumWaitTimeout()),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
WaitDelayArg is a helper for retrieving
the "waitDelay" argument as float64.
It also validates it against the application
configuration.
*/
func WaitDelayArg(r Resource, config conf.Config) (float64, error) {
const (
op string = "resource.WaitDelayArg"
defaultWaitDelay float64 = 0.0
)
result, err := r.Float64Arg(
WaitDelayArgKey,
defaultWaitDelay,
xassert.Float64NotInferiorTo(0.0),
xassert.Float64NotSuperiorTo(config.MaximumWaitDelay()),
)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
PaperSizeArgs is a helper for retrieving
the "paperWidth" and "paperHeight" arguments
as float64.
*/
func PaperSizeArgs(r Resource) (float64, float64, error) {
const (
op string = "resource.PaperSizeArgs"
defaultPaperWidth float64 = 8.27
defaultPaperHeight float64 = 11.7
)
resolver := func() (float64, float64, error) {
paperWidth, err := r.Float64Arg(
PaperWidthArgKey,
defaultPaperWidth,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return defaultPaperWidth,
defaultPaperHeight,
err
}
paperHeight, err := r.Float64Arg(
PaperHeightArgKey,
defaultPaperHeight,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return defaultPaperWidth,
defaultPaperHeight,
err
}
return paperWidth,
paperHeight,
nil
}
paperWidth, paperHeight,
err := resolver()
if err != nil {
return paperWidth,
paperHeight,
xerror.New(op, err)
}
return paperWidth,
paperHeight,
nil
}
/*
MarginArgs is a helper for retrieving
the "marginTop", "marginBottom", "marginLeft"
and "marginRight" arguments as float64.
*/
func MarginArgs(r Resource) (float64, float64, float64, float64, error) {
const (
op string = "resource.MarginArgs"
defaultMarginTop float64 = 1.0
defaultMarginBottom float64 = 1.0
defaultMarginLeft float64 = 1.0
defaultMarginRight float64 = 1.0
)
resolver := func() (float64, float64, float64, float64, error) {
marginTop, err := r.Float64Arg(
MarginTopArgKey,
defaultMarginTop,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return defaultMarginTop,
defaultMarginBottom,
defaultMarginLeft,
defaultMarginRight,
err
}
marginBottom, err := r.Float64Arg(
MarginBottomArgKey,
defaultMarginBottom,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return defaultMarginTop,
defaultMarginBottom,
defaultMarginLeft,
defaultMarginRight,
err
}
marginLeft, err := r.Float64Arg(
MarginLeftArgKey,
defaultMarginLeft,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return defaultMarginTop,
defaultMarginBottom,
defaultMarginLeft,
defaultMarginRight,
err
}
marginRight, err := r.Float64Arg(
MarginRightArgKey,
defaultMarginRight,
xassert.Float64NotInferiorTo(0.0),
)
if err != nil {
return defaultMarginTop,
defaultMarginBottom,
defaultMarginLeft,
defaultMarginRight,
err
}
return marginTop,
marginBottom,
marginLeft,
marginRight,
nil
}
marginTop, marginBottom, marginLeft, marginRight,
err := resolver()
if err != nil {
return marginTop,
marginBottom,
marginLeft,
marginRight,
xerror.New(op, err)
}
return marginTop,
marginBottom,
marginLeft,
marginRight,
nil
}

View File

@@ -0,0 +1,8 @@
/*
Package resource helps managing
arguments and files for a conversion.
All functions return our standard xerror.Error
in case of error.
*/
package resource

View File

@@ -0,0 +1,91 @@
package resource
import (
"io"
"io/ioutil"
"os"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
)
// file represents a file within the resource.
type file struct {
fpath string
}
// write writes given content to the
// resourceFile location.
func (f file) write(in io.Reader) error {
const op string = "resource.file.write"
resolver := func() error {
out, err := os.Create(f.fpath)
if err != nil {
return err
}
defer out.Close() // nolint: errcheck
if err := out.Chmod(0644); err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
return err
}
if _, err := out.Seek(0, 0); err != nil {
return err
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// content returns the string content of
// the file.
func (f file) content() (string, error) {
const op string = "resource.file.content"
b, err := ioutil.ReadFile(f.fpath)
if err != nil {
return "", xerror.New(op, err)
}
return string(b), nil
}
/*
HeaderFooterContents is a helper for retrieving
the content of the files "header.html"
and "footer.html".
*/
func HeaderFooterContents(r Resource) (string, string, error) {
const (
op string = "resource.HeaderFooterContents"
defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
)
resolver := func() (string, string, error) {
headerHTML, err := r.Fcontent("header.html", defaultHeaderFooterHTML)
if err != nil {
return defaultHeaderFooterHTML,
defaultHeaderFooterHTML,
err
}
footerHTML, err := r.Fcontent("footer.html", defaultHeaderFooterHTML)
if err != nil {
return defaultHeaderFooterHTML,
defaultHeaderFooterHTML,
err
}
return headerHTML,
footerHTML,
nil
}
headerHTML, footerHTML,
err := resolver()
if err != nil {
return headerHTML,
footerHTML,
xerror.New(op, err)
}
return headerHTML,
footerHTML,
nil
}

View File

@@ -0,0 +1,227 @@
package resource
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
/*
TemporaryDirectory is the directory
where all the resources directory
are located.
*/
const TemporaryDirectory string = "tmp"
// Resource helps managing
// arguments and files for a conversion.
type Resource struct {
logger xlog.Logger
dirPath string
args map[ArgKey]string
files map[string]file
}
// New creates a Resource where its files will
// be located in the given directory name.
func New(logger xlog.Logger, directoryName string) (Resource, error) {
const op string = "resource.New"
resolver := func() (string, error) {
dirPath := fmt.Sprintf("%s/%s", TemporaryDirectory, directoryName)
if err := os.MkdirAll(dirPath, 0755); err != nil {
return "", err
}
absDirPath, err := filepath.Abs(dirPath)
if err != nil {
return "", err
}
return absDirPath, nil
}
dirPath, err := resolver()
if err != nil {
return Resource{}, xerror.New(op, err)
}
logger.DebugfOp(op, "resource directory '%s' created", directoryName)
return Resource{
logger: logger,
dirPath: dirPath,
args: make(map[ArgKey]string),
files: make(map[string]file),
}, nil
}
// Close removes the working directory of the
// Resource if it exists.
func (r Resource) Close() error {
const op string = "resource.Resource.Close"
if _, err := os.Stat(r.dirPath); os.IsNotExist(err) {
r.logger.DebugfOp(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
return nil
}
if err := os.RemoveAll(r.dirPath); err != nil {
return xerror.New(op, err)
}
r.logger.DebugfOp(op, "resource directory '%s' removed", r.dirPath)
return nil
}
// WithArg add a new argument to the Resource.
func (r *Resource) WithArg(key ArgKey, value string) {
const op string = "resource.Resource.WithArg"
r.args[key] = value
r.logger.DebugfOp(op, "added '%s' with value '%s' to resource args", key, value)
}
// WithFile add a new file to the Resource.
func (r *Resource) WithFile(filename string, in io.Reader) error {
const op string = "resource.Resource.WithFile"
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
file := file{fpath: fpath}
if err := file.write(in); err != nil {
return xerror.New(op, err)
}
r.files[filename] = file
r.logger.DebugfOp(op, "resource file '%s' created", filename)
return nil
}
// DirPath returns the directory path
// of the Resource.
func (r Resource) DirPath() string {
return r.dirPath
}
// HasArg returns true if given key exists
// among the Resource and its value is not empty.
func (r Resource) HasArg(key ArgKey) bool {
if v, ok := r.args[key]; ok {
return v != ""
}
return false
}
/*
StringArg returns the value of the
argument identified by given key.
It works in the same manner as xassert.String.
*/
func (r Resource) StringArg(key ArgKey, defaultValue string, rules ...xassert.RuleString) (string, error) {
const op string = "resource.Resource.StringArg"
result, err := xassert.String(string(key), r.args[key], defaultValue, rules...)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
Int64Arg returns the int64 representation of the
argument identified by given key.
It works in the same manner as xassert.Int64.
*/
func (r Resource) Int64Arg(key ArgKey, defaultValue int64, rules ...xassert.RuleInt64) (int64, error) {
const op string = "resource.Resource.Int64Arg"
result, err := xassert.Int64(string(key), r.args[key], defaultValue, rules...)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
Float64Arg returns the float64 representation of the
argument identified by given key.
It works in the same manner as xassert.Float64.
*/
func (r Resource) Float64Arg(key ArgKey, defaultValue float64, rules ...xassert.RuleFloat64) (float64, error) {
const op string = "resource.Resource.Float64Arg"
result, err := xassert.Float64(string(key), r.args[key], defaultValue, rules...)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
/*
BoolArg returns the boolean representation of the
argument identified by given key.
It works in the same manner as xassert.Bool.
*/
func (r Resource) BoolArg(key ArgKey, defaultValue bool) (bool, error) {
const op string = "resource.Resource.BoolArg"
result, err := xassert.Bool(string(key), r.args[key], defaultValue)
if err != nil {
return result, xerror.New(op, err)
}
return result, nil
}
// Fpath returns the path of the given filename.
// This filename should exist whithin the Resource.
func (r Resource) Fpath(filename string) (string, error) {
const op string = "resource.Resource.Fpath"
file, ok := r.files[filename]
if !ok {
return "", xerror.Invalid(
op,
fmt.Sprintf("resource file '%s' does not exist", filename),
nil,
)
}
return file.fpath, nil
}
/*
Fpaths returns the paths of the files
having one of the given file extensions.
It should found at least one path.
*/
func (r Resource) Fpaths(exts ...string) ([]string, error) {
const op string = "resource.Resource.Fpaths"
var fpaths []string
for filename, file := range r.files {
for _, ext := range exts {
if filepath.Ext(filename) == ext {
fpaths = append(fpaths, file.fpath)
}
}
}
if len(fpaths) == 0 {
return nil, xerror.Invalid(
op,
fmt.Sprintf("no resource file found for extensions '%v'", exts),
nil,
)
}
return fpaths, nil
}
/*
Fcontent returns the string content of the
given filename.
If filename does not exist within the Resource,
returns the default value.
*/
func (r Resource) Fcontent(filename, defaultValue string) (string, error) {
const op string = "resource.Resource.Fcontent"
file, ok := r.files[filename]
if !ok {
return defaultValue, nil
}
content, err := file.content()
if err != nil {
return "", xerror.New(op, err)
}
return content, nil
}

View File

@@ -0,0 +1,33 @@
package xhttp
import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
)
// New returns a custom echo.Echo.
func New(config conf.Config, processes ...pm2.Process) *echo.Echo {
srv := echo.New()
srv.HideBanner = true
srv.HidePort = true
srv.Use(contextMiddleware(config, processes...))
srv.Use(loggerMiddleware())
srv.Use(cleanupMiddleware())
srv.Use(errorMiddleware())
srv.GET(pingEndpoint, pingHandler)
srv.POST(mergeEndpoint, mergeHandler)
if config.DisableGoogleChrome() && config.DisableUnoconv() {
return srv
}
g := srv.Group(convertGroupEndpoint)
if !config.DisableGoogleChrome() {
g.POST(htmlEndpoint, htmlHandler)
g.POST(urlEndpoint, urlHandler)
g.POST(markdownEndpoint, markdownHandler)
}
if !config.DisableUnoconv() {
g.POST(officeEndpoint, officeHandler)
}
return srv
}