mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 11:22:15 +01:00
wip refactoring: better logging and error systems
This commit is contained in:
@@ -2,32 +2,33 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
conf "github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/middleware"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
||||
)
|
||||
|
||||
const pingEndpoint = "/ping"
|
||||
|
||||
// New returns an API.
|
||||
func New(config *conf.Config) *echo.Echo {
|
||||
func New(config *config.Config) *echo.Echo {
|
||||
api := echo.New()
|
||||
api.HideBanner = true
|
||||
api.HidePort = true
|
||||
api.Use(contextMiddleware(config))
|
||||
api.Use(loggingMiddleware())
|
||||
api.Use(finalizeMiddleware())
|
||||
api.GET(pingEndpoint, func(c echo.Context) error { return nil })
|
||||
api.POST("/merge", merge)
|
||||
api.Use(middleware.Context(config))
|
||||
api.Use(middleware.Logger())
|
||||
api.Use(middleware.Cleanup())
|
||||
api.Use(middleware.Error())
|
||||
api.GET(handler.PingEndpoint, handler.Ping)
|
||||
api.POST(handler.MergeEndpoint, handler.Merge)
|
||||
if !config.EnableChromeEndpoints() && !config.EnableUnoconvEndpoints() {
|
||||
return api
|
||||
}
|
||||
g := api.Group("/convert")
|
||||
g := api.Group(handler.ConvertGroupEndpoint)
|
||||
if config.EnableChromeEndpoints() {
|
||||
g.POST("/html", convertHTML)
|
||||
g.POST("/url", convertURL)
|
||||
g.POST("/markdown", convertMarkdown)
|
||||
g.POST(handler.HTMLEndpoint, handler.HTML)
|
||||
g.POST(handler.URLEndpoint, handler.URL)
|
||||
g.POST(handler.MarkdownEndpoint, handler.Markdown)
|
||||
}
|
||||
if config.EnableUnoconvEndpoints() {
|
||||
g.POST("/office", convertOffice)
|
||||
g.POST(handler.OfficeEndpoint, handler.Office)
|
||||
}
|
||||
return api
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestDefaultWaitTimeout(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
opts.DefaultWaitTimeout = 0
|
||||
srv := New(opts)
|
||||
// testing if timeout.
|
||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
// testing if no timeout.
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "10"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
}
|
||||
|
||||
func TestDisableChromeEndpoints(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
opts.EnableChromeEndpoints = false
|
||||
srv := New(opts)
|
||||
// Ping.
|
||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge.
|
||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML.
|
||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown.
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL.
|
||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office.
|
||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
}
|
||||
|
||||
func TestDisableUnoconvEndpoints(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
opts.EnableUnoconvEndpoints = false
|
||||
srv := New(opts)
|
||||
// Ping.
|
||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge.
|
||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML.
|
||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Markdown.
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// URL.
|
||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Office.
|
||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
}
|
||||
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
opts.EnableChromeEndpoints = false
|
||||
opts.EnableUnoconvEndpoints = false
|
||||
srv := New(opts)
|
||||
// Ping.
|
||||
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Merge.
|
||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// HTML.
|
||||
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Markdown.
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// URL.
|
||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
// Office.
|
||||
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/gommon/random"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
type errBadRequest struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *errBadRequest) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func merge(c echo.Context) error {
|
||||
ctx := c.(*resourceContext)
|
||||
opts, err := ctx.resource.mergePrinterOptions(ctx.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
fpaths, err := ctx.resource.fpaths(".pdf")
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
p := printer.NewMerge(fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
|
||||
func convertHTML(c echo.Context) error {
|
||||
ctx := c.(*resourceContext)
|
||||
opts, err := ctx.resource.chromePrinterOptions(ctx.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
fpath, err := ctx.resource.fpath("index.html")
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
p := printer.NewHTML(fpath, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
|
||||
func convertMarkdown(c echo.Context) error {
|
||||
ctx := c.(*resourceContext)
|
||||
opts, err := ctx.resource.chromePrinterOptions(ctx.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
fpath, err := ctx.resource.fpath("index.html")
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
p, err := printer.NewMarkdown(fpath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convert(ctx, p)
|
||||
}
|
||||
|
||||
func convertURL(c echo.Context) error {
|
||||
ctx := c.(*resourceContext)
|
||||
opts, err := ctx.resource.chromePrinterOptions(ctx.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
remote, err := ctx.resource.get(remoteURL)
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
p := printer.NewURL(remote, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
|
||||
func convertOffice(c echo.Context) error {
|
||||
ctx := c.(*resourceContext)
|
||||
opts, err := ctx.resource.officePrinterOptions(ctx.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
fpaths, err := ctx.resource.fpaths(
|
||||
".txt",
|
||||
".rtf",
|
||||
".fodt",
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ods",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odp",
|
||||
)
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
p := printer.NewOffice(fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
|
||||
func convert(ctx *resourceContext, p printer.Printer) error {
|
||||
baseFilename := random.String(32)
|
||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||
fpath := fmt.Sprintf("%s/%s", ctx.resource.formFilesDirPath, filename)
|
||||
// if no webhook URL given, run conversion
|
||||
// and directly return the resulting PDF file
|
||||
// or an error.
|
||||
if !ctx.resource.has(webhookURL) {
|
||||
if err := p.Print(fpath); err != nil {
|
||||
return err
|
||||
}
|
||||
if !ctx.resource.has(resultFilename) {
|
||||
return ctx.Attachment(fpath, filename)
|
||||
}
|
||||
filename, err := ctx.resource.get(resultFilename)
|
||||
if err != nil {
|
||||
return &errBadRequest{err}
|
||||
}
|
||||
return ctx.Attachment(fpath, filename)
|
||||
}
|
||||
// as a webhook URL has been given, we
|
||||
// run the following lines in a goroutine so that
|
||||
// it doesn't block.
|
||||
go func() {
|
||||
defer ctx.resource.close() // nolint: errcheck
|
||||
if err := p.Print(fpath); err != nil {
|
||||
ctx.logger.Error(err)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
ctx.logger.Error(err)
|
||||
return
|
||||
}
|
||||
defer f.Close() // nolint: errcheck
|
||||
webhook, err := ctx.resource.get(webhookURL)
|
||||
if err != nil {
|
||||
ctx.logger.Error(err)
|
||||
return
|
||||
}
|
||||
resp, err := http.Post(webhook, "application/pdf", f) /* #nosec */
|
||||
if err != nil {
|
||||
ctx.logger.Error(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
srv := New(opts)
|
||||
// OK.
|
||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Bad request.
|
||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// Timeout.
|
||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
}
|
||||
|
||||
func TestHTML(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
srv := New(opts)
|
||||
// OK.
|
||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Bad request.
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// Timeout.
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
}
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
srv := New(opts)
|
||||
// OK.
|
||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Bad request.
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// Timeout.
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
}
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
srv := New(opts)
|
||||
// OK.
|
||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Bad request.
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// Timeout.
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
}
|
||||
|
||||
func TestOffice(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
srv := New(opts)
|
||||
// OK.
|
||||
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// Bad request.
|
||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// Timeout.
|
||||
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
}
|
||||
|
||||
func TestConcurrent(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
opts.DefaultWaitTimeout = 30
|
||||
srv := New(opts)
|
||||
// Merge.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
10,
|
||||
)
|
||||
// HTML.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
10,
|
||||
)
|
||||
// Markdown.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
10,
|
||||
)
|
||||
// URL.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
10,
|
||||
)
|
||||
// Office.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
10,
|
||||
)
|
||||
}
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
status := make(chan error, 2)
|
||||
rcv := echo.New()
|
||||
rcv.POST("/foo", func(c echo.Context) error {
|
||||
if c.Request().Header.Get("Content-type") != "application/pdf" {
|
||||
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
|
||||
return nil
|
||||
}
|
||||
body, err := ioutil.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
status <- err
|
||||
return nil
|
||||
}
|
||||
if body == nil || len(body) == 0 {
|
||||
status <- errors.New("empty body")
|
||||
return nil
|
||||
}
|
||||
status <- nil
|
||||
return nil
|
||||
})
|
||||
go func() {
|
||||
rcv.Start(":3001")
|
||||
}()
|
||||
opts := DefaultOptions()
|
||||
srv := New(opts)
|
||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{webhookURL: "http://localhost:3001/foo"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
err := <-status
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestResultFilename(t *testing.T) {
|
||||
opts := DefaultOptions()
|
||||
srv := New(opts)
|
||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resultFilename: "foo.pdf"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/gommon/random"
|
||||
conf "github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
||||
log "github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
||||
)
|
||||
|
||||
func contextMiddleware(config *conf.Config) echo.MiddlewareFunc {
|
||||
// middleware for extending the default context
|
||||
// with one of our own context.
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// generate a unique identifier for our request.
|
||||
trace := random.String(32)
|
||||
// create the logger for this request using
|
||||
// the previous identifier as trace.
|
||||
logger := log.New(config.LogLevel(), trace)
|
||||
// extend the current echo context with our standard
|
||||
// context.
|
||||
ctx := newStandardContext(c, logger, config)
|
||||
// if the endpoint is not for liveness, make a
|
||||
// context with resource.
|
||||
if ctx.Path() != pingEndpoint {
|
||||
ctx, err := ctx.withResource(trace)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return ctx.logEndOfRequest(err)
|
||||
}
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loggingMiddleware() echo.MiddlewareFunc {
|
||||
// middleware for enabling logging.
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.(*standardContext)
|
||||
err := next(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
}
|
||||
return ctx.logEndOfRequest(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finalizeMiddleware() echo.MiddlewareFunc {
|
||||
// middleware for removing resources at the end of a request
|
||||
// and for improving response in case of error.
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
ctx, ok := c.(*resourceContext)
|
||||
// a resource is associated with the context.
|
||||
if ok {
|
||||
// if a webhookURL has been given,
|
||||
// do not remove the resources here because
|
||||
// we don't know if the result file has been
|
||||
// generated or sent.
|
||||
if !ctx.resource.has(webhookURL) {
|
||||
if resourceErr := ctx.resource.close(); resourceErr != nil {
|
||||
ctx.logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := err.(*echo.HTTPError); ok {
|
||||
return err
|
||||
}
|
||||
if _, ok := err.(*errBadRequest); ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
||||
return echo.NewHTTPError(http.StatusRequestTimeout, err.Error())
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
137
internal/app/api/pkg/context/context.go
Normal file
137
internal/app/api/pkg/context/context.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// Context extends the default echo.Context.
|
||||
type Context struct {
|
||||
echo.Context
|
||||
logger *logger.Logger
|
||||
config *config.Config
|
||||
resource *resource.Resource
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// New creates a new context.
|
||||
func New(c echo.Context, logger *logger.Logger, config *config.Config) *Context {
|
||||
// TODO timeout context?
|
||||
return &Context{
|
||||
c,
|
||||
logger,
|
||||
config,
|
||||
nil,
|
||||
time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// MustCastFromEchoContext cast an echo.Context to our custom
|
||||
// context. If something goes wrong, panic.
|
||||
func MustCastFromEchoContext(c echo.Context) *Context {
|
||||
ctx, ok := c.(*Context)
|
||||
if !ok {
|
||||
panic("unable to cast an echo.Context to a custom context")
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// StandardLogger returns the custom logger.
|
||||
// This method should be used instead of the
|
||||
// default Logger() method coming from
|
||||
// the echo.Context!
|
||||
func (ctx *Context) StandardLogger() *logger.Logger {
|
||||
return ctx.logger
|
||||
}
|
||||
|
||||
// Resource returns the associated resource
|
||||
// to the context.
|
||||
func (ctx *Context) Resource() *resource.Resource {
|
||||
return ctx.resource
|
||||
}
|
||||
|
||||
// WithResource adds a resource to the context.
|
||||
func (ctx *Context) WithResource(resourceDirPath string) error {
|
||||
const op = "context.WithResource"
|
||||
r, err := resource.New(ctx, ctx.logger, ctx.config, resourceDirPath)
|
||||
ctx.resource = r
|
||||
if err != nil {
|
||||
return &standarderror.Error{
|
||||
Op: op,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogRequestResult logs the result of a request.
|
||||
// This method should only be used by a middleware!
|
||||
func (ctx *Context) LogRequestResult(err error, isDebug bool) error {
|
||||
req := ctx.Request()
|
||||
resp := ctx.Response()
|
||||
stopTime := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"time_rfc3339": timeRFC3339(), // FIXME required?
|
||||
"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).Error("request failed")
|
||||
return err
|
||||
}
|
||||
if isDebug {
|
||||
ctx.logger.WithFields(fields).Debug("request handled")
|
||||
return nil
|
||||
}
|
||||
ctx.logger.WithFields(fields).Info("request handled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func timeRFC3339() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
3
internal/app/api/pkg/context/doc.go
Normal file
3
internal/app/api/pkg/context/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package context helps extending
|
||||
// the default echo.Context.
|
||||
package context
|
||||
3
internal/app/api/pkg/handler/doc.go
Normal file
3
internal/app/api/pkg/handler/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package handler contains all
|
||||
// the endpoint methods of the API.
|
||||
package handler
|
||||
151
internal/app/api/pkg/handler/handler.go
Normal file
151
internal/app/api/pkg/handler/handler.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/random"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
const (
|
||||
// PingEndpoint is the route for healthcheck.
|
||||
PingEndpoint = "/ping"
|
||||
// MergeEndpoint is the route for merging PDF files.
|
||||
MergeEndpoint = "/merge"
|
||||
// ConvertGroupEndpoint is the route of the group
|
||||
// in charge of converting files to PDF.
|
||||
ConvertGroupEndpoint = "/convert"
|
||||
// HTMLEndpoint is the route for converting
|
||||
// HTML to PDF.
|
||||
HTMLEndpoint = "/html"
|
||||
// URLEndpoint is the route for converting
|
||||
// a URL to PDF.
|
||||
URLEndpoint = "/url"
|
||||
// MarkdownEndpoint is the route for converting
|
||||
// Markdown to PDF.
|
||||
MarkdownEndpoint = "/markdown"
|
||||
// OfficeEndpoint is the route for converting
|
||||
// Office files to PDF.
|
||||
OfficeEndpoint = "/office"
|
||||
)
|
||||
|
||||
func convert(ctx *context.Context, p printer.Printer) error {
|
||||
const (
|
||||
op = "convert"
|
||||
debugOp = "handler.convert"
|
||||
)
|
||||
r := ctx.Resource()
|
||||
logger := ctx.StandardLogger()
|
||||
baseFilename := random.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.Has(resource.WebhookURLFormField) {
|
||||
logger.DebugfOp(debugOp, "no '%s' found, converting synchronously", resource.WebhookURLFormField)
|
||||
if err := convertSync(filename, fpath, ctx, p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// as a webhook URL has been given, we
|
||||
// run the following lines in a goroutine so that
|
||||
// it doesn't block.
|
||||
logger.DebugfOp(debugOp, "'%s' found, converting asynchronously", resource.WebhookURLFormField)
|
||||
return convertAsync(filename, fpath, ctx, p)
|
||||
}
|
||||
|
||||
func convertSync(filename, fpath string, ctx *context.Context, p printer.Printer) error {
|
||||
const (
|
||||
op = "convertSync"
|
||||
debugOp = "handler.convertSync"
|
||||
)
|
||||
r := ctx.Resource()
|
||||
logger := ctx.StandardLogger()
|
||||
if err := p.Print(fpath); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
if !r.Has(resource.ResultFilenameFormField) {
|
||||
logger.DebugfOp(
|
||||
debugOp,
|
||||
"no '%s' found, using generated filename '%s'",
|
||||
resource.ResultFilenameFormField,
|
||||
filename,
|
||||
)
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
logger.DebugfOp(
|
||||
debugOp,
|
||||
"'%s' found, so not using generated filename",
|
||||
resource.ResultFilenameFormField,
|
||||
)
|
||||
filename, err := r.Get(resource.ResultFilenameFormField)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertAsync(filename, fpath string, ctx *context.Context, p printer.Printer) error {
|
||||
const (
|
||||
op = "convertAsync"
|
||||
debugOp = "handler.convertAsync"
|
||||
)
|
||||
r := ctx.Resource()
|
||||
logger := ctx.StandardLogger()
|
||||
go func() {
|
||||
defer r.Close() // nolint: errcheck
|
||||
if err := p.Print(fpath); err != nil {
|
||||
logger.ErrorOp(
|
||||
op,
|
||||
&standarderror.Error{Op: op, Err: err},
|
||||
)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
logger.ErrorOp(
|
||||
op,
|
||||
&standarderror.Error{Op: op, Err: err},
|
||||
)
|
||||
return
|
||||
}
|
||||
defer f.Close() // nolint: errcheck
|
||||
webhookURL, err := r.Get(resource.WebhookURLFormField)
|
||||
if err != nil {
|
||||
logger.ErrorOp(
|
||||
op,
|
||||
&standarderror.Error{Op: op, Err: err},
|
||||
)
|
||||
return
|
||||
}
|
||||
logger.DebugfOp(
|
||||
debugOp,
|
||||
"sending result file '%s' to '%s'",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
resp, err := http.Post(webhookURL, "application/pdf", f) /* #nosec */
|
||||
if err != nil {
|
||||
logger.ErrorOp(
|
||||
op,
|
||||
&standarderror.Error{Op: op, Err: err},
|
||||
)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
24
internal/app/api/pkg/handler/html.go
Normal file
24
internal/app/api/pkg/handler/html.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
// HTML is the endpoint for converting
|
||||
// HTML to PDF.
|
||||
func HTML(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
r := ctx.Resource()
|
||||
opts, err := r.ChromePrinterOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewHTML(fpath, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
27
internal/app/api/pkg/handler/markdown.go
Normal file
27
internal/app/api/pkg/handler/markdown.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
// Markdown is the endpoint for converting
|
||||
// Markdown to PDF.
|
||||
func Markdown(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
r := ctx.Resource()
|
||||
opts, err := r.ChromePrinterOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := printer.NewMarkdown(fpath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convert(ctx, p)
|
||||
}
|
||||
24
internal/app/api/pkg/handler/merge.go
Normal file
24
internal/app/api/pkg/handler/merge.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
// Merge is the endpoint for
|
||||
// merging PDF files.
|
||||
func Merge(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
r := ctx.Resource()
|
||||
opts, err := r.MergePrinterOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpaths, err := r.Fpaths(".pdf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewMerge(fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
37
internal/app/api/pkg/handler/office.go
Normal file
37
internal/app/api/pkg/handler/office.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
// Office is the endpoint for converting
|
||||
// Office files to PDF.
|
||||
func Office(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
r := ctx.Resource()
|
||||
opts, err := r.OfficePrinterOptions()
|
||||
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.NewOffice(fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
10
internal/app/api/pkg/handler/ping.go
Normal file
10
internal/app/api/pkg/handler/ping.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Ping is the endpoint for healthcheck.
|
||||
func Ping(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
25
internal/app/api/pkg/handler/url.go
Normal file
25
internal/app/api/pkg/handler/url.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
// URL is the endpoint for converting
|
||||
// a URL to PDF.
|
||||
func URL(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
r := ctx.Resource()
|
||||
opts, err := r.ChromePrinterOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remoteURL, err := r.Get(resource.RemoteURLFormField)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewURL(remoteURL, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
38
internal/app/api/pkg/middleware/cleanup.go
Normal file
38
internal/app/api/pkg/middleware/cleanup.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// Cleanup helps removing a resource at the end of a request.
|
||||
func Cleanup() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
const op = "middleware.Cleanup"
|
||||
err := next(c)
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
r := ctx.Resource()
|
||||
if r == nil {
|
||||
return err
|
||||
}
|
||||
// if a webhook URL has been given,
|
||||
// do not remove the resource here because
|
||||
// we don't know if the result file has been
|
||||
// generated or sent.
|
||||
if r.Has(resource.WebhookURLFormField) {
|
||||
return err
|
||||
}
|
||||
// a resource is associated with our custom context.
|
||||
if resourceErr := r.Close(); resourceErr != nil {
|
||||
ctx.StandardLogger().ErrorOp(op, &standarderror.Error{
|
||||
Op: op,
|
||||
Err: resourceErr,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
41
internal/app/api/pkg/middleware/context.go
Normal file
41
internal/app/api/pkg/middleware/context.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/random"
|
||||
)
|
||||
|
||||
// Context helps extending the default echo.Context with
|
||||
// our custom context.
|
||||
func Context(config *config.Config) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// generate a unique identifier for the request.
|
||||
trace := random.Get()
|
||||
// create the logger for this request using
|
||||
// the previous identifier as trace.
|
||||
logger := logger.New(config.LogLevel(), trace)
|
||||
// extend the current echo context with our custom
|
||||
// context.
|
||||
ctx := context.New(c, logger, config)
|
||||
// if its an healthcheck request, there
|
||||
// is no resource associated to it.
|
||||
if ctx.Path() == handler.PingEndpoint {
|
||||
return next(ctx)
|
||||
}
|
||||
// if the endpoint is not for healthcheck, associate a
|
||||
// resource to our custom context.
|
||||
if err := ctx.WithResource(trace); err != nil {
|
||||
// required to have a correct status code
|
||||
// in the logs.
|
||||
ctx.Error(err)
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
internal/app/api/pkg/middleware/doc.go
Normal file
3
internal/app/api/pkg/middleware/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package middleware contains the
|
||||
// middleware of the API.
|
||||
package middleware
|
||||
43
internal/app/api/pkg/middleware/error.go
Normal file
43
internal/app/api/pkg/middleware/error.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// Error helps handling errors (if any).
|
||||
func Error() 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
|
||||
}
|
||||
// we log the initial error before returning
|
||||
// the HTTP error.
|
||||
logger := ctx.StandardLogger()
|
||||
logger.Error(err.Error())
|
||||
// handle our custom HTTP error.
|
||||
var httpErr error
|
||||
errCode := standarderror.Code(err)
|
||||
errMessage := standarderror.Message(err)
|
||||
switch errCode {
|
||||
case standarderror.Invalid:
|
||||
httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage)
|
||||
case standarderror.Timeout:
|
||||
httpErr = echo.NewHTTPError(http.StatusRequestTimeout, errMessage)
|
||||
default:
|
||||
httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage)
|
||||
}
|
||||
// required to have a correct status code
|
||||
// in the logs.
|
||||
ctx.Error(httpErr)
|
||||
return httpErr
|
||||
}
|
||||
}
|
||||
}
|
||||
21
internal/app/api/pkg/middleware/logger.go
Normal file
21
internal/app/api/pkg/middleware/logger.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
||||
)
|
||||
|
||||
// Logger helps logging the result of a request.
|
||||
func Logger() 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() == handler.PingEndpoint
|
||||
return ctx.LogRequestResult(err, isDebug)
|
||||
}
|
||||
}
|
||||
}
|
||||
5
internal/app/api/pkg/resource/doc.go
Normal file
5
internal/app/api/pkg/resource/doc.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// Package resource helps creating a folder
|
||||
// containing all uploaded files and the resulting
|
||||
// PDF file. It also helps centralizing all
|
||||
// the form values.
|
||||
package resource
|
||||
417
internal/app/api/pkg/resource/resource.go
Normal file
417
internal/app/api/pkg/resource/resource.go
Normal file
@@ -0,0 +1,417 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
const (
|
||||
// ResultFilenameFormField contains the name
|
||||
// of a form field.
|
||||
ResultFilenameFormField string = "resultFilename"
|
||||
// WaitTimeoutFormField contains the name
|
||||
// of a form field.
|
||||
WaitTimeoutFormField string = "waitTimeout"
|
||||
// WebhookURLFormField contains the name
|
||||
// of a form field.
|
||||
WebhookURLFormField string = "webhookURL"
|
||||
// RemoteURLFormField contains the name
|
||||
// of a form field.
|
||||
RemoteURLFormField string = "remoteURL"
|
||||
// WaitDelayFormField contains the name
|
||||
// of a form field.
|
||||
WaitDelayFormField string = "waitDelay"
|
||||
// PaperWidthFormField contains the name
|
||||
// of a form field.
|
||||
PaperWidthFormField string = "paperWidth"
|
||||
// PaperHeightFormField contains the name
|
||||
// of a form field.
|
||||
PaperHeightFormField string = "paperHeight"
|
||||
// MarginTopFormField contains the name
|
||||
// of a form field.
|
||||
MarginTopFormField string = "marginTop"
|
||||
// MarginBottomFormField contains the name
|
||||
// of a form field.
|
||||
MarginBottomFormField string = "marginBottom"
|
||||
// MarginLeftFormField contains the name
|
||||
// of a form field.
|
||||
MarginLeftFormField string = "marginLeft"
|
||||
// MarginRightFormField contains the name
|
||||
// of a form field.
|
||||
MarginRightFormField string = "marginRight"
|
||||
// LandscapeFormField contains the name
|
||||
// of a form field.
|
||||
LandscapeFormField string = "landscape"
|
||||
)
|
||||
|
||||
// Resource helps retrieving form values
|
||||
// and form files from a request.
|
||||
type Resource struct {
|
||||
logger *logger.Logger
|
||||
config *config.Config
|
||||
formValues map[string]string
|
||||
formFilesDirPath string
|
||||
}
|
||||
|
||||
// New creates a new resource.
|
||||
func New(c echo.Context, logger *logger.Logger, config *config.Config, dirPath string) (*Resource, error) {
|
||||
const op = "resource.New"
|
||||
r := &Resource{
|
||||
logger: logger,
|
||||
config: config,
|
||||
formValues: formValues(c, logger),
|
||||
formFilesDirPath: dirPath,
|
||||
}
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
r.logger.DebugfOp(op, "directory '%s' created", dirPath)
|
||||
if err := formFiles(c, logger, dirPath); err != nil {
|
||||
return r, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func formValues(c echo.Context, logger *logger.Logger) map[string]string {
|
||||
const debugOp = "resource.formValues"
|
||||
v := make(map[string]string)
|
||||
v[ResultFilenameFormField] = c.FormValue(ResultFilenameFormField)
|
||||
v[WaitTimeoutFormField] = c.FormValue(WaitTimeoutFormField)
|
||||
v[WebhookURLFormField] = c.FormValue(WebhookURLFormField)
|
||||
v[RemoteURLFormField] = c.FormValue(RemoteURLFormField)
|
||||
v[WaitDelayFormField] = c.FormValue(WaitDelayFormField)
|
||||
v[PaperWidthFormField] = c.FormValue(PaperWidthFormField)
|
||||
v[PaperHeightFormField] = c.FormValue(PaperHeightFormField)
|
||||
v[MarginTopFormField] = c.FormValue(MarginTopFormField)
|
||||
v[MarginBottomFormField] = c.FormValue(MarginBottomFormField)
|
||||
v[MarginLeftFormField] = c.FormValue(MarginLeftFormField)
|
||||
v[MarginRightFormField] = c.FormValue(MarginRightFormField)
|
||||
v[LandscapeFormField] = c.FormValue(LandscapeFormField)
|
||||
logger.DebugfOp(debugOp, "%v", v)
|
||||
return v
|
||||
}
|
||||
|
||||
func formFiles(c echo.Context, logger *logger.Logger, dirPath string) error {
|
||||
const (
|
||||
op = "formFiles"
|
||||
debugOp = "resource.formFiles"
|
||||
)
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
for _, files := range form.File {
|
||||
for _, fh := range files {
|
||||
in, err := fh.Open()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
defer in.Close() // nolint: errcheck
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
||||
out, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
defer out.Close() // nolint: errcheck
|
||||
if err := out.Chmod(0644); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
if _, err := out.Seek(0, 0); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
logger.DebugfOp(debugOp, "'%s' created", fh.Filename)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DirPath returns the directory
|
||||
// path where are stored the form
|
||||
// files and the resulting PDF file.
|
||||
func (r *Resource) DirPath() string {
|
||||
return r.formFilesDirPath
|
||||
}
|
||||
|
||||
// Close deletes the working directory of the
|
||||
// resource if it exists.
|
||||
func (r *Resource) Close() error {
|
||||
const op = "resource.Close"
|
||||
if _, err := os.Stat(r.formFilesDirPath); os.IsNotExist(err) {
|
||||
r.logger.DebugfOp(op, "directory '%s' does not exist, nothing to remove", r.formFilesDirPath)
|
||||
return nil
|
||||
}
|
||||
if err := os.RemoveAll(r.formFilesDirPath); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
r.logger.DebugfOp(op, "directory '%s' removed", r.formFilesDirPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||
|
||||
// ChromePrinterOptions returns the Chrome printer options
|
||||
// thanks to the form values and form files from the request
|
||||
// plus the default values from the configuration.
|
||||
func (r *Resource) ChromePrinterOptions() (*printer.ChromeOptions, error) {
|
||||
const op = "resource.ChromePrinterOptions"
|
||||
waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
waitDelay, err := r.float64(WaitDelayFormField, 0.0)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
headerHTML, err := r.content("header.html", defaultHeaderFooterHTML)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
footerHTML, err := r.content("footer.html", defaultHeaderFooterHTML)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
paperWidth, err := r.float64(PaperWidthFormField, 8.27)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
paperHeight, err := r.float64(PaperHeightFormField, 11.7)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
marginTop, err := r.float64(MarginTopFormField, 1)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
marginBottom, err := r.float64(MarginBottomFormField, 1)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
marginLeft, err := r.float64(MarginLeftFormField, 1)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
marginRight, err := r.float64(MarginRightFormField, 1)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
landscape, err := r.bool(LandscapeFormField, false)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
opts := &printer.ChromeOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
}
|
||||
r.logger.DebugfOp(op, "%v", opts)
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// OfficePrinterOptions returns the Office printer options
|
||||
// thanks to the form values from the request
|
||||
// plus the default values from the configuration.
|
||||
func (r *Resource) OfficePrinterOptions() (*printer.OfficeOptions, error) {
|
||||
const op = "resource.OfficePrinterOptions"
|
||||
waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
landscape, err := r.bool(LandscapeFormField, false)
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
opts := &printer.OfficeOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
Landscape: landscape,
|
||||
}
|
||||
r.logger.DebugfOp(op, "%v", opts)
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// MergePrinterOptions returns the merge printer options
|
||||
// thanks to the form values from the request
|
||||
// plus the default values from the configuration.
|
||||
func (r *Resource) MergePrinterOptions() (*printer.MergeOptions, error) {
|
||||
const op = "resource.MergePrinterOptions"
|
||||
waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout())
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
opts := &printer.MergeOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
}
|
||||
r.logger.DebugfOp(op, "%v", opts)
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// Has returns true if the resource
|
||||
// contains the given form field and
|
||||
// its value is not empty.
|
||||
func (r *Resource) Has(formField string) bool {
|
||||
v, ok := r.formValues[formField]
|
||||
if ok {
|
||||
ok = v != ""
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *Resource) hasFile(filename string) bool {
|
||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||
_, err := os.Stat(fpath)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// Get returns the form field value.
|
||||
func (r *Resource) Get(formField string) (string, error) {
|
||||
const op = "resource.Get"
|
||||
v, err := r.value(formField)
|
||||
if err != nil {
|
||||
return "", &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (r *Resource) value(formField string) (string, error) {
|
||||
const op = "value"
|
||||
v, ok := r.formValues[formField]
|
||||
if !ok {
|
||||
return "", &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' does not exist", formField),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (r *Resource) float64(formField string, defaultValue float64) (float64, error) {
|
||||
const op = "float64"
|
||||
if !r.Has(formField) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
v, err := r.value(formField)
|
||||
if err != nil {
|
||||
return 0.0, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return 0.0, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' is not a float", formField),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (r *Resource) bool(formField string, defaultValue bool) (bool, error) {
|
||||
const op = "bool"
|
||||
if !r.Has(formField) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
v, err := r.value(formField)
|
||||
if err != nil {
|
||||
return false, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' is not a boolean", formField),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Fpath returns the path of the given filename.
|
||||
// This filename should be the name of a form file.
|
||||
func (r *Resource) Fpath(filename string) (string, error) {
|
||||
const op = "resource.Fpath"
|
||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||
_, err := os.Stat(fpath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("file '%s' does not exist", filename),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
absPath, err := filepath.Abs(fpath)
|
||||
if err != nil {
|
||||
return "", &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
func (r *Resource) content(filename string, defaultValue string) (string, error) {
|
||||
const op = "content"
|
||||
if !r.hasFile(filename) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
fpath, err := r.Fpath(filename)
|
||||
if err != nil {
|
||||
return "", &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return "", &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Fpaths returns the list of files of the resource
|
||||
// according to given file extensions.
|
||||
func (r *Resource) Fpaths(exts ...string) ([]string, error) {
|
||||
const op = "resource.Fpaths"
|
||||
var fpaths []string
|
||||
err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error {
|
||||
const walkOp = "filepath.Walk"
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fpath, err := r.Fpath(info.Name())
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: walkOp, Err: err}
|
||||
}
|
||||
for _, ext := range exts {
|
||||
if filepath.Ext(fpath) == ext {
|
||||
fpaths = append(fpaths, fpath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return nil, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("no file found for extentions %v", exts),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return fpaths, nil
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
const (
|
||||
resultFilename string = "resultFilename"
|
||||
waitTimeout string = "waitTimeout"
|
||||
webhookURL string = "webhookURL"
|
||||
remoteURL string = "remoteURL"
|
||||
waitDelay string = "waitDelay"
|
||||
paperWidth string = "paperWidth"
|
||||
paperHeight string = "paperHeight"
|
||||
marginTop string = "marginTop"
|
||||
marginBottom string = "marginBottom"
|
||||
marginLeft string = "marginLeft"
|
||||
marginRight string = "marginRight"
|
||||
landscape string = "landscape"
|
||||
)
|
||||
|
||||
type resource struct {
|
||||
formValues map[string]string
|
||||
formFilesDirPath string
|
||||
}
|
||||
|
||||
func newResource(c echo.Context, dirPath string) (*resource, error) {
|
||||
r := &resource{
|
||||
formValues: formValues(c),
|
||||
}
|
||||
r.formFilesDirPath = dirPath
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
||||
}
|
||||
if err := formFiles(c, dirPath); err != nil {
|
||||
return r, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func formValues(c echo.Context) map[string]string {
|
||||
v := make(map[string]string)
|
||||
v[resultFilename] = c.FormValue(resultFilename)
|
||||
v[waitTimeout] = c.FormValue(waitTimeout)
|
||||
v[webhookURL] = c.FormValue(webhookURL)
|
||||
v[remoteURL] = c.FormValue(remoteURL)
|
||||
v[waitDelay] = c.FormValue(waitDelay)
|
||||
v[paperWidth] = c.FormValue(paperWidth)
|
||||
v[paperHeight] = c.FormValue(paperHeight)
|
||||
v[marginTop] = c.FormValue(marginTop)
|
||||
v[marginBottom] = c.FormValue(marginBottom)
|
||||
v[marginLeft] = c.FormValue(marginLeft)
|
||||
v[marginRight] = c.FormValue(marginRight)
|
||||
v[landscape] = c.FormValue(landscape)
|
||||
return v
|
||||
}
|
||||
|
||||
func formFiles(c echo.Context, dirPath string) error {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting multipart form: %v", err)
|
||||
}
|
||||
for _, files := range form.File {
|
||||
for _, fh := range files {
|
||||
in, err := fh.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
||||
}
|
||||
defer in.Close() // nolint: errcheck
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
||||
out, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
||||
}
|
||||
defer out.Close() // nolint: errcheck
|
||||
if err := out.Chmod(0644); err != nil {
|
||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
||||
}
|
||||
if _, err := out.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resource) close() error {
|
||||
if _, err := os.Stat(r.formFilesDirPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return os.RemoveAll(r.formFilesDirPath)
|
||||
}
|
||||
|
||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||
|
||||
func (r *resource) chromePrinterOptions(defaultWaitTimeout float64) (*printer.ChromeOptions, error) {
|
||||
timeout, err := r.float64(waitTimeout, defaultWaitTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delay, err := r.float64(waitDelay, 0.0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header, err := r.content("header.html", defaultHeaderFooterHTML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
footer, err := r.content("footer.html", defaultHeaderFooterHTML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
width, err := r.float64(paperWidth, 8.27)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
height, err := r.float64(paperHeight, 11.7)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
top, err := r.float64(marginTop, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bottom, err := r.float64(marginBottom, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
left, err := r.float64(marginLeft, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := r.float64(marginRight, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
landscape, err := r.bool(landscape, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &printer.ChromeOptions{
|
||||
WaitTimeout: timeout,
|
||||
WaitDelay: delay,
|
||||
HeaderHTML: header,
|
||||
FooterHTML: footer,
|
||||
PaperWidth: width,
|
||||
PaperHeight: height,
|
||||
MarginTop: top,
|
||||
MarginBottom: bottom,
|
||||
MarginLeft: left,
|
||||
MarginRight: right,
|
||||
Landscape: landscape,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *resource) officePrinterOptions(defaultWaitTimeout float64) (*printer.OfficeOptions, error) {
|
||||
timeout, err := r.float64(waitTimeout, defaultWaitTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
landscape, err := r.bool(landscape, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &printer.OfficeOptions{
|
||||
WaitTimeout: timeout,
|
||||
Landscape: landscape,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *resource) mergePrinterOptions(defaultWaitTimeout float64) (*printer.MergeOptions, error) {
|
||||
timeout, err := r.float64(waitTimeout, defaultWaitTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &printer.MergeOptions{
|
||||
WaitTimeout: timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *resource) has(key string) bool {
|
||||
v, ok := r.formValues[key]
|
||||
if ok {
|
||||
ok = v != ""
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *resource) hasFile(filename string) bool {
|
||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||
_, err := os.Stat(fpath)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func (r *resource) get(key string) (string, error) {
|
||||
v, ok := r.formValues[key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("form value %s does not exist", key)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (r *resource) float64(key string, defaultValue float64) (float64, error) {
|
||||
if !r.has(key) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
v, err := r.get(key)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf("form value %s: %v", key, err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (r *resource) bool(key string, defaultValue bool) (bool, error) {
|
||||
if !r.has(key) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
v, err := r.get(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("form value %s: %v", key, err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *resource) fpath(filename string) (string, error) {
|
||||
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||
_, err := os.Stat(fpath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("%s: form file does not exist", filename)
|
||||
}
|
||||
absPath, err := filepath.Abs(fpath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: getting absolute path: %v", fpath, err)
|
||||
}
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
func (r *resource) content(filename string, defaultValue string) (string, error) {
|
||||
if !r.hasFile(filename) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
fpath, err := r.fpath(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: reading form file: %v", fpath, err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (r *resource) fpaths(exts ...string) ([]string, error) {
|
||||
var fpaths []string
|
||||
err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fpath, err := r.fpath(info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ext := range exts {
|
||||
if filepath.Ext(fpath) == ext {
|
||||
fpaths = append(fpaths, fpath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return nil, fmt.Errorf("no form files found for extensions: %v", exts)
|
||||
}
|
||||
return fpaths, nil
|
||||
}
|
||||
Reference in New Issue
Block a user