This commit is contained in:
Julien Neuhart
2019-04-14 17:07:45 +02:00
committed by GitHub
parent c4222e0981
commit 194670c3bf
63 changed files with 2114 additions and 1503 deletions

View File

@@ -1,139 +1,48 @@
package api
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
// Start starts the API server on port 3000.
func Start() error {
e := setup()
// start Chrome headless and
// unoconv listener with PM2.
chrome := &pm2.Chrome{}
unoconv := &pm2.Unoconv{}
if err := chrome.Launch(); err != nil {
return err
}
if err := unoconv.Launch(); err != nil {
return err
}
// run our API in a goroutine so that it doesn't block.
go func() {
notify.Println("http server started on port 3000")
if err := e.Start(":3000"); err != nil {
e.Logger.Fatalf("%v", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(quit, os.Interrupt)
// block until we receive our signal.
<-quit
// create a deadline to wait for.
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
// doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
notify.Println("shutting down http server... (Ctrl+C to force)")
return e.Shutdown(ctx)
// Options allows to customize the behaviour
// of the API.
type Options struct {
DefaultWaitTimeout float64
EnableChromeEndpoints bool
EnableUnoconvEndpoints bool
}
func setup() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(middleware.Logger())
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if err := next(c); err != nil {
// TODO should return a better HTTP status code
// than 500 for some cases.
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}
return nil
}
})
e.GET("/ping", func(c echo.Context) error { return nil })
e.POST("/merge", merge)
g := e.Group("/convert")
g.POST("/html", convertHTML)
g.POST("/url", convertURL)
g.POST("/markdown", convertMarkdown)
g.POST("/office", convertOffice)
return e
// DefaultOptions returns default options.
func DefaultOptions() *Options {
return &Options{
DefaultWaitTimeout: 10,
EnableChromeEndpoints: true,
EnableUnoconvEndpoints: true,
}
}
func newContext(r *resource) (context.Context, context.CancelFunc) {
webhookURL := r.webhookURL()
if webhookURL == "" {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
return ctx, cancel
// New returns an API.
func New(opts *Options) *echo.Echo {
api := echo.New()
api.HideBanner = true
api.HidePort = true
api.Use(middleware.Logger())
api.GET("/ping", func(c echo.Context) error { return nil })
g := api.Group("/convert")
g.Use(handleContext(opts))
g.Use(handleError())
g.POST("/merge", merge)
if !opts.EnableChromeEndpoints && !opts.EnableUnoconvEndpoints {
return api
}
return context.Background(), nil
}
func print(c echo.Context, p printer.Printer, r *resource) error {
baseFilename, err := rand.Get()
if err != nil {
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
}
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.webhookURL() == "" {
defer r.removeAll()
if err := p.Print(fpath); err != nil {
return err
}
if r.filename() != "" {
filename = r.filename()
}
return c.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 r.removeAll()
if err := p.Print(fpath); err != nil {
c.Logger().Errorf("%v", err)
return
}
f, err := os.Open(fpath)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer f.Close()
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer resp.Body.Close()
}()
return nil
}
func hijackErr(err error, r *resource) error {
if r != nil {
defer r.removeAll()
}
return err
if opts.EnableChromeEndpoints {
g.POST("/html", convertHTML)
g.POST("/url", convertURL)
g.POST("/markdown", convertMarkdown)
}
if opts.EnableUnoconvEndpoints {
g.POST("/office", convertOffice)
}
return api
}

View File

@@ -6,13 +6,123 @@ import (
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestPing(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
rec := httptest.NewRecorder()
e := echo.New()
_ = e.NewContext(req, rec)
assert.Equal(t, http.StatusOK, rec.Code)
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)
}

View File

@@ -1,9 +1,2 @@
/*
Package api starts a HTTP server on port 3000.
It accepts POST requests with a multipart/form-data Content-Type
for converting HTML, Markdown and Office documents to PDF.
It is also able to merge a list of PDF files.
*/
// Package api helps managing the HTTP server behind Gotenberg.
package api

157
internal/app/api/handler.go Normal file
View File

@@ -0,0 +1,157 @@
package api
import (
"fmt"
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
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()
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()
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()
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()
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()
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, err := rand.Get()
if err != nil {
return err
}
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) {
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
}

View File

@@ -0,0 +1,360 @@
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"))
}

View File

@@ -1,56 +0,0 @@
package api
import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
func convertHTML(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
p := &printer.HTML{Context: ctx}
indexPath, err := r.filePath("index.html")
if err != nil {
return hijackErr(err, r)
}
p.WithLocalURL(indexPath)
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
p.MarginLeft = paperMargins[2]
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
chromeWebFontsTimeout, err := r.webFontsTimeout()
if err != nil {
return hijackErr(err, r)
}
p.WebFontsTimeout = chromeWebFontsTimeout
return print(c, p, r)
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestHTML(t *testing.T) {
body, contentType := test.HTMLTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertHTML(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -1,55 +0,0 @@
package api
import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
func convertMarkdown(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
indexPath, err := r.filePath("index.html")
if err != nil {
return hijackErr(err, r)
}
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
p.MarginLeft = paperMargins[2]
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
chromeWebFontsTimeout, err := r.webFontsTimeout()
if err != nil {
return hijackErr(err, r)
}
p.WebFontsTimeout = chromeWebFontsTimeout
return print(c, p, r)
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestMarkdown(t *testing.T) {
body, contentType := test.MarkdownTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertMarkdown(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -1,68 +0,0 @@
package api
import (
"errors"
"fmt"
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
func merge(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
fpaths, err := r.filePaths([]string{".pdf"})
if err != nil {
return hijackErr(err, r)
}
if len(fpaths) == 0 {
return hijackErr(errors.New("no suitable PDF files to merge"), r)
}
baseFilename, err := rand.Get()
if err != nil {
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
}
filename := fmt.Sprintf("%s.pdf", baseFilename)
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
// if no webhook URL given, run merge
// and directly return the resulting PDF file
// or an error.
if r.webhookURL() == "" {
defer r.removeAll()
if err := printer.Merge(fpaths, fpath); err != nil {
return err
}
if r.filename() != "" {
filename = r.filename()
}
return c.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 r.removeAll()
if err := printer.Merge(fpaths, fpath); err != nil {
c.Logger().Errorf("%v", err)
return
}
f, err := os.Open(fpath)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer f.Close()
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer resp.Body.Close()
}()
return nil
}

View File

@@ -1,24 +0,0 @@
package api
import (
"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) {
body, contentType := test.PDFTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/merge", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, merge(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -0,0 +1,61 @@
package api
import (
"context"
"net/http"
"strings"
"github.com/labstack/echo/v4"
)
func handleContext(opts *Options) echo.MiddlewareFunc {
// middleware for extending default context with our
// custom constext.
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := &resourceContext{c, opts, nil}
r, err := newResource(ctx)
if err != nil {
if resourceErr := r.close(); resourceErr != nil {
c.Logger().Error(resourceErr)
}
return err
}
ctx.resource = r
return next(ctx)
}
}
}
func handleError() echo.MiddlewareFunc {
// middleware for handling errors and removing resources
// once the request has been handled.
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := next(c)
ctx := c.(*resourceContext)
// 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 {
c.Logger().Error(resourceErr)
}
}
if err != 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)
}
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return nil
}
}
}

View File

@@ -1,48 +0,0 @@
package api
import (
"errors"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
var officeExts = []string{
".txt",
".rtf",
".fodt",
".doc",
".docx",
".odt",
".xls",
".xlsx",
".ods",
".ppt",
".pptx",
".odp",
}
func convertOffice(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
fpaths, err := r.filePaths(officeExts)
if err != nil {
return hijackErr(err, r)
}
if len(fpaths) == 0 {
return hijackErr(errors.New("no suitable office documents to convert"), r)
}
p := &printer.Office{Context: ctx, FilePaths: fpaths}
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
return print(c, p, r)
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestOffice(t *testing.T) {
body, contentType := test.OfficeTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertOffice(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -1,106 +1,264 @@
package api
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
const (
remoteURL string = "remoteURL"
webhookURL string = "webhookURL"
paperWidth string = "paperWidth"
paperHeight string = "paperHeight"
marginTop string = "marginTop"
marginBottom string = "marginBottom"
marginLeft string = "marginLeft"
marginRight string = "marginRight"
landscape string = "landscape"
webFontsTimeout string = "webFontsTimeout"
filename string = "filename"
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"
)
// resource facilitates storing and accessing
// data from a multipart/form-data request.
type resource struct {
values map[string]string
dirPath string
formValues map[string]string
formFilesDirPath string
opts *Options
}
func newResource(c echo.Context) (*resource, error) {
type resourceContext struct {
echo.Context
opts *Options
resource *resource
}
func newResource(ctx *resourceContext) (*resource, error) {
r := &resource{
formValues: formValues(ctx),
opts: ctx.opts,
}
dirPath, err := rand.Get()
if err != nil {
return nil, err
return r, err
}
r.formFilesDirPath = dirPath
if err := os.MkdirAll(dirPath, 0755); err != nil {
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
}
r := &resource{values: values(c), dirPath: dirPath}
form, err := c.MultipartForm()
if err := formFiles(ctx, dirPath); err != nil {
return r, err
}
return r, nil
}
func formValues(ctx *resourceContext) map[string]string {
v := make(map[string]string)
v[resultFilename] = ctx.FormValue(resultFilename)
v[waitTimeout] = ctx.FormValue(waitTimeout)
v[webhookURL] = ctx.FormValue(webhookURL)
v[remoteURL] = ctx.FormValue(remoteURL)
v[waitDelay] = ctx.FormValue(waitDelay)
v[paperWidth] = ctx.FormValue(paperWidth)
v[paperHeight] = ctx.FormValue(paperHeight)
v[marginTop] = ctx.FormValue(marginTop)
v[marginBottom] = ctx.FormValue(marginBottom)
v[marginLeft] = ctx.FormValue(marginLeft)
v[marginRight] = ctx.FormValue(marginRight)
v[landscape] = ctx.FormValue(landscape)
return v
}
func formFiles(ctx *resourceContext, dirPath string) error {
form, err := ctx.MultipartForm()
if err != nil {
return r, fmt.Errorf("getting multipart form: %v", err)
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 r, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
return fmt.Errorf("%s: opening file: %v", fh.Filename, err)
}
defer in.Close()
if err := r.writeFile(fh.Filename, in); err != nil {
return r, 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 r, nil
}
func values(c echo.Context) map[string]string {
v := make(map[string]string)
v[remoteURL] = c.FormValue(remoteURL)
v[webhookURL] = c.FormValue(webhookURL)
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)
v[webFontsTimeout] = c.FormValue(webFontsTimeout)
v[filename] = c.FormValue(filename)
return v
}
func (r *resource) writeFile(filename string, in io.Reader) error {
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
out, err := os.Create(fpath)
if err != nil {
return fmt.Errorf("%s: creating new file: %v", fpath, err)
}
defer out.Close()
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) filePath(filename string) (string, error) {
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
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() (*printer.ChromeOptions, error) {
timeout, err := r.float64(waitTimeout, r.opts.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() (*printer.OfficeOptions, error) {
timeout, err := r.float64(waitTimeout, r.opts.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() (*printer.MergeOptions, error) {
timeout, err := r.float64(waitTimeout, r.opts.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: file does not exist", filename)
return "", fmt.Errorf("%s: form file does not exist", filename)
}
absPath, err := filepath.Abs(fpath)
if err != nil {
@@ -109,13 +267,28 @@ func (r *resource) filePath(filename string) (string, error) {
return absPath, nil
}
func (r *resource) filePaths(exts []string) ([]string, error) {
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.dirPath, func(path string, info os.FileInfo, _ error) error {
err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error {
if info.IsDir() {
return nil
}
fpath, err := r.filePath(info.Name())
fpath, err := r.fpath(info.Name())
if err != nil {
return err
}
@@ -130,86 +303,8 @@ func (r *resource) filePaths(exts []string) ([]string, error) {
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
}
func (r *resource) paperSize() ([2]float64, error) {
defaultSize := [2]float64{8.27, 11.7}
widthStr := r.values[paperWidth]
heightStr := r.values[paperHeight]
if widthStr == "" || heightStr == "" {
return defaultSize, nil
}
width, err := strconv.ParseFloat(widthStr, 64)
if err != nil {
return defaultSize, fmt.Errorf("paper width: %v", err)
}
height, err := strconv.ParseFloat(heightStr, 64)
if err != nil {
return defaultSize, fmt.Errorf("paper height: %v", err)
}
return [2]float64{width, height}, nil
}
func (r *resource) paperMargins() ([4]float64, error) {
defaultMargins := [4]float64{1, 1, 1, 1}
topStr := r.values[marginTop]
bottomStr := r.values[marginBottom]
leftStr := r.values[marginLeft]
rightStr := r.values[marginRight]
if topStr == "" || bottomStr == "" || leftStr == "" || rightStr == "" {
return defaultMargins, nil
}
top, err := strconv.ParseFloat(topStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin top: %v", err)
}
bottom, err := strconv.ParseFloat(bottomStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin bottom: %v", err)
}
left, err := strconv.ParseFloat(leftStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin left: %v", err)
}
right, err := strconv.ParseFloat(rightStr, 64)
if err != nil {
return defaultMargins, fmt.Errorf("margin right: %v", err)
}
return [4]float64{top, bottom, left, right}, nil
}
func (r *resource) landscape() (bool, error) {
landscapeStr := r.values[landscape]
if landscapeStr == "" {
return false, nil
}
landscape, err := strconv.ParseBool(landscapeStr)
if err != nil {
return false, fmt.Errorf("landscape: %v", err)
}
return landscape, nil
}
func (r *resource) remoteURL() (string, error) {
if r.values[remoteURL] == "" {
return "", errors.New("no remote URL")
}
return r.values[remoteURL], nil
}
func (r *resource) webFontsTimeout() (int64, error) {
webFontsTimeoutStr := r.values[webFontsTimeout]
if webFontsTimeoutStr == "" {
return 500, nil
}
timeout, err := strconv.ParseInt(webFontsTimeoutStr, 10, 64)
if err != nil {
return 500, fmt.Errorf("web fonts timeout: %v", err)
}
return timeout, nil
}
func (r *resource) webhookURL() string { return r.values[webhookURL] }
func (r *resource) filename() string { return r.values[filename] }
func (r *resource) removeAll() error { return os.RemoveAll(r.dirPath) }

View File

@@ -1,56 +0,0 @@
package api
import (
"github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
func convertURL(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
p := &printer.HTML{Context: ctx}
URL, err := r.remoteURL()
if err != nil {
return hijackErr(err, r)
}
p.URL = URL
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
p.MarginLeft = paperMargins[2]
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
chromeWebFontsTimeout, err := r.webFontsTimeout()
if err != nil {
return hijackErr(err, r)
}
p.WebFontsTimeout = chromeWebFontsTimeout
return print(c, p, r)
}

View File

@@ -1,24 +0,0 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestURL(t *testing.T) {
body, contentType := test.URLTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertURL(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}