mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 04:12:16 +01:00
huge refactoring
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"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"
|
||||
)
|
||||
|
||||
// New returns an API.
|
||||
func New(config *config.Config) *echo.Echo {
|
||||
api := echo.New()
|
||||
api.HideBanner = true
|
||||
api.HidePort = true
|
||||
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(handler.ConvertGroupEndpoint)
|
||||
if config.EnableChromeEndpoints() {
|
||||
g.POST(handler.HTMLEndpoint, handler.HTML)
|
||||
g.POST(handler.URLEndpoint, handler.URL)
|
||||
g.POST(handler.MarkdownEndpoint, handler.Markdown)
|
||||
}
|
||||
if config.EnableUnoconvEndpoints() {
|
||||
g.POST(handler.OfficeEndpoint, handler.Office)
|
||||
}
|
||||
return api
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/handler"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/middleware"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/config"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
endpoint := handler.PingEndpoint
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// should be OK.
|
||||
req := httptest.NewRequest(http.MethodGet, endpoint, nil)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
||||
endpoint := handler.MergeEndpoint
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// should be OK.
|
||||
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// bad request.
|
||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// timeout.
|
||||
body, contentType = test.PDFTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
// should have no more resources.
|
||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
||||
}
|
||||
|
||||
func TestHTML(t *testing.T) {
|
||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
||||
endpoint := fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.HTMLEndpoint)
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// should be OK.
|
||||
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// bad request.
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.WaitDelayFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.PaperWidthFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.PaperHeightFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginTopFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginBottomFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginLeftFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.MarginRightFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.LandscapeFormField: "not a bool"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// timeout.
|
||||
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
// should have no more resources.
|
||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
||||
}
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
||||
endpoint := fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.MarkdownEndpoint)
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// should be OK.
|
||||
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// bad request.
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitDelayFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.PaperWidthFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.PaperHeightFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginTopFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginBottomFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginLeftFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.MarginRightFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.LandscapeFormField: "not a bool"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// timeout.
|
||||
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
// should have no more resources.
|
||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
||||
}
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
||||
endpoint := fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.URLEndpoint)
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// should be OK.
|
||||
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
// bad request.
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.WaitDelayFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.PaperWidthFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.PaperHeightFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginTopFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginBottomFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginLeftFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.MarginRightFormField: "not a float"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.LandscapeFormField: "not a bool"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||
// timeout.
|
||||
body, contentType = test.URLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "0"})
|
||||
req = httptest.NewRequest(http.MethodPost, endpoint, body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||
// should have no more resources.
|
||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
||||
}
|
||||
|
||||
func TestConcurrent(t *testing.T) {
|
||||
const concurrentRequests int = 4
|
||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
// Merge.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
||||
req := httptest.NewRequest(http.MethodPost, handler.MergeEndpoint, 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
|
||||
},
|
||||
concurrentRequests,
|
||||
)
|
||||
// HTML.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.HTMLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.HTMLEndpoint), 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
|
||||
},
|
||||
concurrentRequests,
|
||||
)
|
||||
// Markdown.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.MarkdownTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.MarkdownEndpoint), 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
|
||||
},
|
||||
concurrentRequests,
|
||||
)
|
||||
// URL.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.URLTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.URLEndpoint), 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
|
||||
},
|
||||
concurrentRequests,
|
||||
)
|
||||
// Office.
|
||||
test.AssertConcurrent(
|
||||
t,
|
||||
func() error {
|
||||
body, contentType := test.OfficeTestMultipartForm(t, map[string]string{resource.WaitTimeoutFormField: "120"})
|
||||
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", handler.ConvertGroupEndpoint, handler.OfficeEndpoint), 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
|
||||
},
|
||||
concurrentRequests,
|
||||
)
|
||||
// should have no more resources.
|
||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
||||
}
|
||||
|
||||
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")
|
||||
}()
|
||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resource.WebhookURLFormField: "http://localhost:3001/foo"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||
err = <-status
|
||||
assert.NoError(t, err)
|
||||
// should have no more resources.
|
||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
||||
}
|
||||
|
||||
func TestResultFilename(t *testing.T) {
|
||||
os.Setenv(middleware.TestingTraceEnvVar, "1")
|
||||
config, err := config.FromEnv()
|
||||
assert.Nil(t, err)
|
||||
srv := New(config)
|
||||
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resource.ResultFilenameFormField: "foo.pdf"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/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"))
|
||||
// should have no more resources.
|
||||
test.AssertDirectoryEmpty(t, middleware.TestsTracePrefix)
|
||||
err = os.RemoveAll(middleware.TestsTracePrefix)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(middleware.TestingTraceEnvVar)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package api helps managing the HTTP server behind Gotenberg.
|
||||
package api
|
||||
@@ -1,134 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 {
|
||||
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 {
|
||||
const op string = "context.MustCastFromEchoContext"
|
||||
ctx, ok := c.(*Context)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("%s: unable to cast an echo.Context to a custom context", op))
|
||||
}
|
||||
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 string = "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 {
|
||||
const op string = "context.LogRequestResult"
|
||||
req := ctx.Request()
|
||||
resp := ctx.Response()
|
||||
stopTime := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"remote_ip": ctx.RealIP(),
|
||||
"host": req.Host,
|
||||
"uri": req.RequestURI,
|
||||
"method": req.Method,
|
||||
"path": path(req),
|
||||
"referer": req.Referer(),
|
||||
"user_agent": req.UserAgent(),
|
||||
"status": resp.Status,
|
||||
"latency": lantency(ctx.startTime, stopTime),
|
||||
"latency_human": latencyHuman(ctx.startTime, stopTime),
|
||||
"bytes_in": bytesIn(req),
|
||||
"bytes_out": bytesOut(resp),
|
||||
}
|
||||
if err != nil {
|
||||
ctx.logger.WithFields(fields).ErrorfOp(op, "request failed")
|
||||
return err
|
||||
}
|
||||
if isDebug {
|
||||
ctx.logger.WithFields(fields).DebugfOp(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
ctx.logger.WithFields(fields).InfofOp(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func path(r *http.Request) string {
|
||||
path := r.URL.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func lantency(startTime time.Time, stopTime time.Time) string {
|
||||
return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10)
|
||||
}
|
||||
|
||||
func latencyHuman(startTime time.Time, stopTime time.Time) string {
|
||||
return stopTime.Sub(startTime).String()
|
||||
}
|
||||
|
||||
func bytesIn(r *http.Request) string {
|
||||
bytesIn := r.Header.Get(echo.HeaderContentLength)
|
||||
if bytesIn == "" {
|
||||
bytesIn = "0"
|
||||
}
|
||||
return bytesIn
|
||||
}
|
||||
|
||||
func bytesOut(r *echo.Response) string {
|
||||
return strconv.FormatInt(r.Size, 10)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package context helps extending
|
||||
// the default echo.Context.
|
||||
package context
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package handler contains all
|
||||
// the endpoint methods of the API.
|
||||
package handler
|
||||
@@ -1,142 +0,0 @@
|
||||
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 string = "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(op, "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(op, "'%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 = "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(
|
||||
op,
|
||||
"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(
|
||||
op,
|
||||
"'%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 = "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(
|
||||
op,
|
||||
"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
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// HTML is the endpoint for converting
|
||||
// HTML to PDF.
|
||||
func HTML(c echo.Context) error {
|
||||
const op string = "handler.HTML"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
ctx.StandardLogger().DebugfOp(op, "html request")
|
||||
r := ctx.Resource()
|
||||
opts, err := r.ChromePrinterOptions()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
p := printer.NewHTML(fpath, opts)
|
||||
if err := convert(ctx, p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// Markdown is the endpoint for converting
|
||||
// Markdown to PDF.
|
||||
func Markdown(c echo.Context) error {
|
||||
const op string = "handler.Markdown"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
ctx.StandardLogger().DebugfOp(op, "markdown request")
|
||||
r := ctx.Resource()
|
||||
opts, err := r.ChromePrinterOptions()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
p, err := printer.NewMarkdown(fpath, opts)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
if err := convert(ctx, p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// Merge is the endpoint for
|
||||
// merging PDF files.
|
||||
func Merge(c echo.Context) error {
|
||||
const op string = "handler.Merge"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
ctx.StandardLogger().DebugfOp(op, "merge request")
|
||||
r := ctx.Resource()
|
||||
opts, err := r.MergePrinterOptions()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
fpaths, err := r.Fpaths(".pdf")
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
p := printer.NewMerge(fpaths, opts)
|
||||
if err := convert(ctx, p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// Office is the endpoint for converting
|
||||
// Office files to PDF.
|
||||
func Office(c echo.Context) error {
|
||||
const op string = "handler.Office"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
ctx.StandardLogger().DebugfOp(op, "office request")
|
||||
r := ctx.Resource()
|
||||
opts, err := r.OfficePrinterOptions()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
fpaths, err := r.Fpaths(
|
||||
".txt",
|
||||
".rtf",
|
||||
".fodt",
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ods",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odp",
|
||||
)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
p := printer.NewOffice(fpaths, opts)
|
||||
if err := convert(ctx, p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Ping is the endpoint for healthcheck.
|
||||
func Ping(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// URL is the endpoint for converting
|
||||
// a URL to PDF.
|
||||
func URL(c echo.Context) error {
|
||||
const op string = "handler.URL"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
ctx.StandardLogger().DebugfOp(op, "url request")
|
||||
r := ctx.Resource()
|
||||
opts, err := r.ChromePrinterOptions()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
remoteURL, err := r.Get(resource.RemoteURLFormField)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
p := printer.NewURL(remoteURL, opts)
|
||||
if err := convert(ctx, p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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 string = "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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
// TestingTraceEnvVar is an environment
|
||||
// variable used in some tests.
|
||||
TestingTraceEnvVar string = "TESTING_TRACE"
|
||||
// TestsTracePrefix helps
|
||||
// creating all resources inside a prefix.
|
||||
// Only used in some tests
|
||||
// to check if the resources
|
||||
// have been removed.
|
||||
TestsTracePrefix string = "tmp"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
var trace string
|
||||
if os.Getenv(TestingTraceEnvVar) == "1" {
|
||||
trace = fmt.Sprintf("%s/%s", TestsTracePrefix, random.Get())
|
||||
} else {
|
||||
// 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.
|
||||
ctx.Error(err)
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package middleware contains the
|
||||
// middleware of the API.
|
||||
package middleware
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
}
|
||||
// if it's an error from echo
|
||||
// like 404 not found and so on.
|
||||
if echoHTTPErr, ok := err.(*echo.HTTPError); ok {
|
||||
return echoHTTPErr
|
||||
}
|
||||
// we log the initial error before returning
|
||||
// the HTTP error.
|
||||
errOp := standarderror.Op(err)
|
||||
logger := ctx.StandardLogger()
|
||||
logger.ErrorOp(errOp, err)
|
||||
// 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.
|
||||
ctx.Error(httpErr)
|
||||
return httpErr
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// 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
|
||||
@@ -1,421 +0,0 @@
|
||||
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 string = "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 op string = "resource.formValues"
|
||||
v := make(map[string]string)
|
||||
fetch := func(formField string) string {
|
||||
value := c.FormValue(formField)
|
||||
if value == "" {
|
||||
logger.DebugfOp(op, "'%s' is empty", formField)
|
||||
return value
|
||||
}
|
||||
logger.DebugfOp(op, "'%s' retrieved, got '%s'", formField, value)
|
||||
return value
|
||||
}
|
||||
v[ResultFilenameFormField] = fetch(ResultFilenameFormField)
|
||||
v[WaitTimeoutFormField] = fetch(WaitTimeoutFormField)
|
||||
v[WebhookURLFormField] = fetch(WebhookURLFormField)
|
||||
v[RemoteURLFormField] = fetch(RemoteURLFormField)
|
||||
v[WaitDelayFormField] = fetch(WaitDelayFormField)
|
||||
v[PaperWidthFormField] = fetch(PaperWidthFormField)
|
||||
v[PaperHeightFormField] = fetch(PaperHeightFormField)
|
||||
v[MarginTopFormField] = fetch(MarginTopFormField)
|
||||
v[MarginBottomFormField] = fetch(MarginBottomFormField)
|
||||
v[MarginLeftFormField] = fetch(MarginLeftFormField)
|
||||
v[MarginRightFormField] = fetch(MarginRightFormField)
|
||||
v[LandscapeFormField] = fetch(LandscapeFormField)
|
||||
return v
|
||||
}
|
||||
|
||||
func formFiles(c echo.Context, logger *logger.Logger, dirPath string) error {
|
||||
const op string = "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(op, "'%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 string = "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 string = "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, "printer options: %+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 string = "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, "printer options: %+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 string = "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, "printer options: %+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 string = "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 string = "resource.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 string = "resource.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, got '%s'", formField, v),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (r *Resource) bool(formField string, defaultValue bool) (bool, error) {
|
||||
const op string = "resource.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, got '%s'", formField, v),
|
||||
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 string = "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 string = "resource.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 string = "resource.Fpaths"
|
||||
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 &standarderror.Error{Op: op, 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
|
||||
}
|
||||
3
internal/app/xhttp/doc.go
Normal file
3
internal/app/xhttp/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package xhttp defines our own implementation
|
||||
// of echo.Echo.
|
||||
package xhttp
|
||||
298
internal/app/xhttp/handler.go
Normal file
298
internal/app/xhttp/handler.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
)
|
||||
|
||||
const (
|
||||
pingEndpoint string = "/ping"
|
||||
mergeEndpoint string = "/merge"
|
||||
convertGroupEndpoint string = "/convert"
|
||||
htmlEndpoint string = "/html"
|
||||
urlEndpoint string = "/url"
|
||||
markdownEndpoint string = "/markdown"
|
||||
officeEndpoint string = "/office"
|
||||
)
|
||||
|
||||
// pingHandler is the handler for healthcheck.
|
||||
func pingHandler(c echo.Context) error {
|
||||
const op string = "xhttp.pingHandler"
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
ctx.XLogger().DebugOp(op, "handling ping request...")
|
||||
if err := ctx.ProcessesHealthcheck(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeHandler is the handler for merging
|
||||
// PDF files.
|
||||
func mergeHandler(c echo.Context) error {
|
||||
const op string = "xhttp.mergeHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling merge request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := mergePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
fpaths, err := r.Fpaths(".pdf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewMergePrinter(logger, fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// htmlHandler is the handler for converting
|
||||
// HTML to PDF.
|
||||
func htmlHandler(c echo.Context) error {
|
||||
const op string = "xhttp.htmlHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling HTML request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewHTMLPrinter(logger, fpath, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// urlHandler is the handler for converting
|
||||
// a URL to PDF.
|
||||
func urlHandler(c echo.Context) error {
|
||||
const op string = "xhttp.urlHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling URL request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !r.HasArg(resource.RemoteURLArgKey) {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' not found or empty", resource.RemoteURLArgKey),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
remoteURL, err := r.StringArg(resource.RemoteURLArgKey, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewURLPrinter(logger, remoteURL, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// markdownHandler is the handler for converting
|
||||
// Markdown to PDF.
|
||||
func markdownHandler(c echo.Context) error {
|
||||
const op string = "xhttp.markdownHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling Markdown request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := chromePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpath, err := r.Fpath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := printer.NewMarkdownPrinter(logger, fpath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// officeHandler is the handler for converting
|
||||
// Office documents to PDF.
|
||||
func officeHandler(c echo.Context) error {
|
||||
const op string = "xhttp.officeHandler"
|
||||
resolver := func() error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
logger := ctx.XLogger()
|
||||
logger.DebugOp(op, "handling Office request...")
|
||||
r := ctx.MustResource()
|
||||
opts, err := officePrinterOptions(r, ctx.Config())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fpaths, err := r.Fpaths(
|
||||
".txt",
|
||||
".rtf",
|
||||
".fodt",
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ods",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odp",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer.NewOfficePrinter(logger, fpaths, opts)
|
||||
return convert(ctx, p)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert(ctx context.Context, p printer.Printer) error {
|
||||
const op string = "xhttp.convert"
|
||||
resolver := func() error {
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
baseFilename := xrand.Get()
|
||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||
fpath := fmt.Sprintf("%s/%s", r.DirPath(), filename)
|
||||
// if no webhook URL given, run conversion
|
||||
// and directly return the resulting PDF file
|
||||
// or an error.
|
||||
if !r.HasArg(resource.WebhookURLArgKey) {
|
||||
logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
|
||||
return convertSync(ctx, p, filename, fpath)
|
||||
}
|
||||
// as a webhook URL has been given, we
|
||||
// run the following lines in a goroutine so that
|
||||
// it doesn't block.
|
||||
logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
|
||||
return convertAsync(ctx, p, filename, fpath)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertSync(ctx context.Context, p printer.Printer, filename, fpath string) error {
|
||||
const op = "xhttp.convertSync"
|
||||
resolver := func() error {
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
|
||||
if err := p.Print(fpath); err != nil {
|
||||
return err
|
||||
}
|
||||
if !r.HasArg(resource.ResultFilenameArgKey) {
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"no '%s' found, using generated filename '%s'",
|
||||
resource.RemoteURLArgKey,
|
||||
filename,
|
||||
)
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"'%s' found, so not using generated filename",
|
||||
resource.ResultFilenameArgKey,
|
||||
)
|
||||
filename, err := r.StringArg(resource.ResultFilenameArgKey, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Attachment(fpath, filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string) error {
|
||||
const op = "xhttp.convertAsync"
|
||||
logger := ctx.XLogger()
|
||||
r := ctx.MustResource()
|
||||
go func() {
|
||||
defer r.Close() // nolint: errcheck
|
||||
if err := p.Print(fpath); err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
defer f.Close() // nolint: errcheck
|
||||
webhookURL, err := r.StringArg(resource.WebhookURLArgKey, "")
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
logger.DebugfOp(
|
||||
op,
|
||||
"sending result file '%s' to '%s'",
|
||||
filename,
|
||||
webhookURL,
|
||||
)
|
||||
// TODO timeout
|
||||
resp, err := http.Post(webhookURL, "application/pdf", f) /* #nosec */
|
||||
if err != nil {
|
||||
xerr := xerror.New(op, err)
|
||||
logger.ErrorOp(xerror.Op(xerr), xerr)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
128
internal/app/xhttp/middleware.go
Normal file
128
internal/app/xhttp/middleware.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
)
|
||||
|
||||
// contextMiddleware extends the default echo.Context with
|
||||
// our custom context.Context.
|
||||
func contextMiddleware(config conf.Config, processes ...pm2.Process) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// generate a unique identifier for the request.
|
||||
trace := xrand.Get()
|
||||
// create the logger for this request using
|
||||
// the previous identifier as trace.
|
||||
logger := xlog.New(config.LogLevel(), trace)
|
||||
// extend the current echo context with our custom
|
||||
// context.
|
||||
ctx := context.New(c, logger, config, processes...)
|
||||
// if its an healthcheck request, there
|
||||
// is no need to create a Resource.
|
||||
if ctx.Path() == pingEndpoint {
|
||||
return next(ctx)
|
||||
}
|
||||
// if the endpoint is not for healthcheck, create a
|
||||
// Resource.
|
||||
if err := ctx.WithResource(trace); err != nil {
|
||||
// required to have a correct status code.
|
||||
ctx.Error(err)
|
||||
return ctx.LogRequestResult(err, false)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loggerMiddleware logs the result of a request.
|
||||
func loggerMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
err := next(ctx)
|
||||
// we do not want to log healthcheck requests if
|
||||
// log level is not set to DEBUG.
|
||||
isDebug := ctx.Path() == pingEndpoint
|
||||
return ctx.LogRequestResult(err, isDebug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupMiddleware removes a resource.Resource
|
||||
// at the end of a request.
|
||||
func cleanupMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
const op string = "xhttp.cleanupMiddleware"
|
||||
err := next(c)
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
if !ctx.HasResource() {
|
||||
// nothing to remove.
|
||||
return err
|
||||
}
|
||||
r := ctx.MustResource()
|
||||
// if a webhook URL has been given,
|
||||
// do not remove the resource.Resource here because
|
||||
// we don't know if the result file has been
|
||||
// generated or sent.
|
||||
if r.HasArg(resource.WebhookURLArgKey) {
|
||||
return err
|
||||
}
|
||||
// a resource.Resource is associated with our custom context.
|
||||
if resourceErr := r.Close(); resourceErr != nil {
|
||||
xerr := xerror.New(op, resourceErr)
|
||||
ctx.XLogger().ErrorOp(xerror.Op(xerr), xerr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errorMiddleware handles errors (if any).
|
||||
func errorMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := context.MustCastFromEchoContext(c)
|
||||
err := next(ctx)
|
||||
if err == nil {
|
||||
// so far so good!
|
||||
return nil
|
||||
}
|
||||
// if it's an error from echo
|
||||
// like 404 not found and so on.
|
||||
if echoHTTPErr, ok := err.(*echo.HTTPError); ok {
|
||||
return echoHTTPErr
|
||||
}
|
||||
// we log the initial error before returning
|
||||
// the HTTP error.
|
||||
errOp := xerror.Op(err)
|
||||
logger := ctx.XLogger()
|
||||
logger.ErrorOp(errOp, err)
|
||||
// handle our custom HTTP error.
|
||||
var httpErr error
|
||||
errCode := xerror.Code(err)
|
||||
errMessage := xerror.Message(err)
|
||||
switch errCode {
|
||||
case xerror.InvalidCode:
|
||||
httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage)
|
||||
case xerror.TimeoutCode:
|
||||
// TODO status
|
||||
httpErr = echo.NewHTTPError(http.StatusBadGateway, errMessage)
|
||||
default:
|
||||
httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage)
|
||||
}
|
||||
// required to have a correct status code.
|
||||
ctx.Error(httpErr)
|
||||
return httpErr
|
||||
}
|
||||
}
|
||||
}
|
||||
93
internal/app/xhttp/option.go
Normal file
93
internal/app/xhttp/option.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
func mergePrinterOptions(r resource.Resource, config conf.Config) (printer.MergePrinterOptions, error) {
|
||||
const op string = "xhttp.mergePrinterOptions"
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.MergePrinterOptions{}, xerror.New(op, err)
|
||||
}
|
||||
return printer.MergePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.ChromePrinterOptions, error) {
|
||||
const op string = "xhttp.chromePrinterOptions"
|
||||
resolver := func() (printer.ChromePrinterOptions, error) {
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
waitDelay, err := resource.WaitDelayArg(r, config)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
headerHTML, footerHTML,
|
||||
err := resource.HeaderFooterContents(r)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
paperWidth, paperHeight,
|
||||
err := resource.PaperSizeArgs(r)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
marginTop, marginBottom, marginLeft, marginRight,
|
||||
err := resource.MarginArgs(r)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
|
||||
if err != nil {
|
||||
return printer.ChromePrinterOptions{}, err
|
||||
}
|
||||
return printer.ChromePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
WaitDelay: waitDelay,
|
||||
HeaderHTML: headerHTML,
|
||||
FooterHTML: footerHTML,
|
||||
PaperWidth: paperWidth,
|
||||
PaperHeight: paperHeight,
|
||||
MarginTop: marginTop,
|
||||
MarginBottom: marginBottom,
|
||||
MarginLeft: marginLeft,
|
||||
MarginRight: marginRight,
|
||||
Landscape: landscape,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
if err != nil {
|
||||
return opts, xerror.New(op, err)
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func officePrinterOptions(r resource.Resource, config conf.Config) (printer.OfficePrinterOptions, error) {
|
||||
const op string = "xhttp.officePrinterOptions"
|
||||
resolver := func() (printer.OfficePrinterOptions, error) {
|
||||
waitTimeout, err := resource.WaitTimeoutArg(r, config)
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
|
||||
if err != nil {
|
||||
return printer.OfficePrinterOptions{}, err
|
||||
}
|
||||
return printer.OfficePrinterOptions{
|
||||
WaitTimeout: waitTimeout,
|
||||
Landscape: landscape,
|
||||
}, nil
|
||||
}
|
||||
opts, err := resolver()
|
||||
if err != nil {
|
||||
return opts, xerror.New(op, err)
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
210
internal/app/xhttp/pkg/context/context.go
Normal file
210
internal/app/xhttp/pkg/context/context.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// Context extends the default echo.Context.
|
||||
type Context struct {
|
||||
echo.Context
|
||||
logger xlog.Logger
|
||||
config conf.Config
|
||||
processes []pm2.Process
|
||||
resource resource.Resource
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// New creates a new Context.
|
||||
func New(c echo.Context, logger xlog.Logger, config conf.Config, processess ...pm2.Process) Context {
|
||||
return Context{
|
||||
c,
|
||||
logger,
|
||||
config,
|
||||
processess,
|
||||
resource.Resource{},
|
||||
time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
MustCastFromEchoContext cast an echo.Context
|
||||
to our custom Context.
|
||||
|
||||
It panics if casting goes wrong.
|
||||
*/
|
||||
func MustCastFromEchoContext(c echo.Context) Context {
|
||||
const op string = "context.MustCastFromEchoContext"
|
||||
ctx, ok := c.(Context)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("%s: unable to cast an echo.Context to our custom context.Context", op))
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
/*
|
||||
XLogger returns the xlog.Logger associated
|
||||
with the Context.
|
||||
|
||||
This method should be used instead of the
|
||||
default Logger() method coming from
|
||||
the echo.Context.
|
||||
*/
|
||||
func (ctx Context) XLogger() xlog.Logger {
|
||||
return ctx.logger
|
||||
}
|
||||
|
||||
// Config returns the conf.Config associated
|
||||
// with the Context.
|
||||
func (ctx Context) Config() conf.Config {
|
||||
return ctx.config
|
||||
}
|
||||
|
||||
// ProcessesHealthcheck returns an error if
|
||||
// one of the processes is not viable.
|
||||
func (ctx Context) ProcessesHealthcheck() error {
|
||||
const op string = "context.Context.ProcessesHealthcheck"
|
||||
for _, process := range ctx.processes {
|
||||
if !process.IsViable() {
|
||||
return xerror.New(
|
||||
op,
|
||||
fmt.Errorf("'%s' is not viable", process.Fullname()),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithResource creates a resource.Resource and
|
||||
// adds it to the Context.
|
||||
func (ctx *Context) WithResource(directoryName string) error {
|
||||
const op string = "context.Context.WithResource"
|
||||
resolver := func() (resource.Resource, error) {
|
||||
r, err := resource.New(ctx.logger, directoryName)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
// retrieve form values from request.
|
||||
for _, key := range resource.ArgKeys() {
|
||||
r.WithArg(key, ctx.FormValue(string(key)))
|
||||
}
|
||||
// write form files from request.
|
||||
form, err := ctx.MultipartForm()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
for _, files := range form.File {
|
||||
for _, fh := range files {
|
||||
in, err := fh.Open()
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
defer in.Close() // nolint: errcheck
|
||||
if err := r.WithFile(fh.Filename, in); err != nil {
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
resource, err := resolver()
|
||||
ctx.resource = resource
|
||||
if err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
MustResource returns the resource.Resource
|
||||
associated with the Context.
|
||||
|
||||
It panics if no resource.Resource.
|
||||
*/
|
||||
func (ctx Context) MustResource() resource.Resource {
|
||||
const op string = "context.Context.MustResource"
|
||||
if !ctx.HasResource() {
|
||||
panic(fmt.Sprintf("%s: unable to retrieve the resource.Resource from our custom context.Context", op))
|
||||
}
|
||||
return ctx.resource
|
||||
}
|
||||
|
||||
// HasResource returns true if the Context
|
||||
// has a resource.Resource.
|
||||
func (ctx Context) HasResource() bool {
|
||||
return &ctx.resource != nil
|
||||
}
|
||||
|
||||
/*
|
||||
LogRequestResult logs the result of a request.
|
||||
This method should only be used by a middleware!
|
||||
|
||||
If an error is given, returns the exact same error.
|
||||
*/
|
||||
func (ctx Context) LogRequestResult(err error, isDebug bool) error {
|
||||
const op string = "context.Context.LogRequestResult"
|
||||
req := ctx.Request()
|
||||
resp := ctx.Response()
|
||||
stopTime := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"remote_ip": ctx.RealIP(),
|
||||
"host": req.Host,
|
||||
"uri": req.RequestURI,
|
||||
"method": req.Method,
|
||||
"path": path(req),
|
||||
"referer": req.Referer(),
|
||||
"user_agent": req.UserAgent(),
|
||||
"status": resp.Status,
|
||||
"latency": lantency(ctx.startTime, stopTime),
|
||||
"latency_human": latencyHuman(ctx.startTime, stopTime),
|
||||
"bytes_in": bytesIn(req),
|
||||
"bytes_out": bytesOut(resp),
|
||||
}
|
||||
if err != nil {
|
||||
ctx.logger.WithFields(fields).ErrorfOp(op, "request failed")
|
||||
return err
|
||||
}
|
||||
if isDebug {
|
||||
ctx.logger.WithFields(fields).DebugfOp(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
ctx.logger.WithFields(fields).InfofOp(op, "request handled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func path(r *http.Request) string {
|
||||
path := r.URL.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func lantency(startTime time.Time, stopTime time.Time) string {
|
||||
return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10)
|
||||
}
|
||||
|
||||
func latencyHuman(startTime time.Time, stopTime time.Time) string {
|
||||
return stopTime.Sub(startTime).String()
|
||||
}
|
||||
|
||||
func bytesIn(r *http.Request) string {
|
||||
bytesIn := r.Header.Get(echo.HeaderContentLength)
|
||||
if bytesIn == "" {
|
||||
bytesIn = "0"
|
||||
}
|
||||
return bytesIn
|
||||
}
|
||||
|
||||
func bytesOut(r *echo.Response) string {
|
||||
return strconv.FormatInt(r.Size, 10)
|
||||
}
|
||||
7
internal/app/xhttp/pkg/context/doc.go
Normal file
7
internal/app/xhttp/pkg/context/doc.go
Normal file
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
Package context extends the default echo.Context.
|
||||
|
||||
All functions return our standard xerror.Error
|
||||
in case of error.
|
||||
*/
|
||||
package context
|
||||
253
internal/app/xhttp/pkg/resource/arg.go
Normal file
253
internal/app/xhttp/pkg/resource/arg.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// ArgKey is a type for
|
||||
// arguments' keys.
|
||||
type ArgKey string
|
||||
|
||||
const (
|
||||
// ResultFilenameArgKey is the key
|
||||
// of the argument "resultFilename".
|
||||
ResultFilenameArgKey ArgKey = "resultFilename"
|
||||
// WaitTimeoutArgKey is the key
|
||||
// of the argument "waitTimeout".
|
||||
WaitTimeoutArgKey ArgKey = "waitTimeout"
|
||||
// WebhookURLArgKey is the key
|
||||
// of the argument "webhookURL".
|
||||
WebhookURLArgKey ArgKey = "webhookURL"
|
||||
// WebhookURLTimeoutArgKey is the key
|
||||
// of the argument "webhookURLTimeout".
|
||||
WebhookURLTimeoutArgKey ArgKey = "webhookURLTimeout"
|
||||
// RemoteURLArgKey is the key
|
||||
// of the argument "remoteURL".
|
||||
RemoteURLArgKey ArgKey = "remoteURL"
|
||||
// WaitDelayArgKey is the key
|
||||
// of the argument "waitDelay".
|
||||
WaitDelayArgKey ArgKey = "waitDelay"
|
||||
// PaperWidthArgKey is the key
|
||||
// of the argument "paperWidth".
|
||||
PaperWidthArgKey ArgKey = "paperWidth"
|
||||
// PaperHeightArgKey is the key
|
||||
// of the argument "paperHeight".
|
||||
PaperHeightArgKey ArgKey = "paperHeight"
|
||||
// MarginTopArgKey is the key
|
||||
// of the argument "marginTop".
|
||||
MarginTopArgKey ArgKey = "marginTop"
|
||||
// MarginBottomArgKey is the key
|
||||
// of the argument "marginBottom".
|
||||
MarginBottomArgKey ArgKey = "marginBottom"
|
||||
// MarginLeftArgKey is the key
|
||||
// of the argument "marginLeft".
|
||||
MarginLeftArgKey ArgKey = "marginLeft"
|
||||
// MarginRightArgKey is the key
|
||||
// of the argument "marginRight".
|
||||
MarginRightArgKey ArgKey = "marginRight"
|
||||
// LandscapeArgKey is the key
|
||||
// of the argument "landscape".
|
||||
LandscapeArgKey ArgKey = "landscape"
|
||||
)
|
||||
|
||||
/*
|
||||
ArgKeys returns a slice
|
||||
containing all available
|
||||
arguments' keys.
|
||||
*/
|
||||
func ArgKeys() []ArgKey {
|
||||
return []ArgKey{
|
||||
ResultFilenameArgKey,
|
||||
WaitTimeoutArgKey,
|
||||
WebhookURLArgKey,
|
||||
WebhookURLTimeoutArgKey,
|
||||
RemoteURLArgKey,
|
||||
WaitDelayArgKey,
|
||||
PaperWidthArgKey,
|
||||
PaperHeightArgKey,
|
||||
MarginTopArgKey,
|
||||
MarginBottomArgKey,
|
||||
MarginLeftArgKey,
|
||||
MarginRightArgKey,
|
||||
LandscapeArgKey,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
WaitTimeoutArg is a helper for retrieving
|
||||
the "waitTimeout" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WaitTimeoutArg(r Resource, config conf.Config) (float64, error) {
|
||||
const op string = "resource.WaitTimeoutArg"
|
||||
result, err := r.Float64Arg(
|
||||
WaitTimeoutArgKey,
|
||||
config.DefaultWaitTimeout(),
|
||||
xassert.Float64NotInferiorTo(0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWaitTimeout()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
WaitDelayArg is a helper for retrieving
|
||||
the "waitDelay" argument as float64.
|
||||
|
||||
It also validates it against the application
|
||||
configuration.
|
||||
*/
|
||||
func WaitDelayArg(r Resource, config conf.Config) (float64, error) {
|
||||
const (
|
||||
op string = "resource.WaitDelayArg"
|
||||
defaultWaitDelay float64 = 0.0
|
||||
)
|
||||
result, err := r.Float64Arg(
|
||||
WaitDelayArgKey,
|
||||
defaultWaitDelay,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
xassert.Float64NotSuperiorTo(config.MaximumWaitDelay()),
|
||||
)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
PaperSizeArgs is a helper for retrieving
|
||||
the "paperWidth" and "paperHeight" arguments
|
||||
as float64.
|
||||
*/
|
||||
func PaperSizeArgs(r Resource) (float64, float64, error) {
|
||||
const (
|
||||
op string = "resource.PaperSizeArgs"
|
||||
defaultPaperWidth float64 = 8.27
|
||||
defaultPaperHeight float64 = 11.7
|
||||
)
|
||||
resolver := func() (float64, float64, error) {
|
||||
paperWidth, err := r.Float64Arg(
|
||||
PaperWidthArgKey,
|
||||
defaultPaperWidth,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return defaultPaperWidth,
|
||||
defaultPaperHeight,
|
||||
err
|
||||
}
|
||||
paperHeight, err := r.Float64Arg(
|
||||
PaperHeightArgKey,
|
||||
defaultPaperHeight,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return defaultPaperWidth,
|
||||
defaultPaperHeight,
|
||||
err
|
||||
}
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
nil
|
||||
}
|
||||
paperWidth, paperHeight,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return paperWidth,
|
||||
paperHeight,
|
||||
nil
|
||||
}
|
||||
|
||||
/*
|
||||
MarginArgs is a helper for retrieving
|
||||
the "marginTop", "marginBottom", "marginLeft"
|
||||
and "marginRight" arguments as float64.
|
||||
*/
|
||||
func MarginArgs(r Resource) (float64, float64, float64, float64, error) {
|
||||
const (
|
||||
op string = "resource.MarginArgs"
|
||||
defaultMarginTop float64 = 1.0
|
||||
defaultMarginBottom float64 = 1.0
|
||||
defaultMarginLeft float64 = 1.0
|
||||
defaultMarginRight float64 = 1.0
|
||||
)
|
||||
resolver := func() (float64, float64, float64, float64, error) {
|
||||
marginTop, err := r.Float64Arg(
|
||||
MarginTopArgKey,
|
||||
defaultMarginTop,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return defaultMarginTop,
|
||||
defaultMarginBottom,
|
||||
defaultMarginLeft,
|
||||
defaultMarginRight,
|
||||
err
|
||||
}
|
||||
marginBottom, err := r.Float64Arg(
|
||||
MarginBottomArgKey,
|
||||
defaultMarginBottom,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return defaultMarginTop,
|
||||
defaultMarginBottom,
|
||||
defaultMarginLeft,
|
||||
defaultMarginRight,
|
||||
err
|
||||
}
|
||||
marginLeft, err := r.Float64Arg(
|
||||
MarginLeftArgKey,
|
||||
defaultMarginLeft,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return defaultMarginTop,
|
||||
defaultMarginBottom,
|
||||
defaultMarginLeft,
|
||||
defaultMarginRight,
|
||||
err
|
||||
}
|
||||
marginRight, err := r.Float64Arg(
|
||||
MarginRightArgKey,
|
||||
defaultMarginRight,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
if err != nil {
|
||||
return defaultMarginTop,
|
||||
defaultMarginBottom,
|
||||
defaultMarginLeft,
|
||||
defaultMarginRight,
|
||||
err
|
||||
}
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
nil
|
||||
}
|
||||
marginTop, marginBottom, marginLeft, marginRight,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return marginTop,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
nil
|
||||
}
|
||||
8
internal/app/xhttp/pkg/resource/doc.go
Normal file
8
internal/app/xhttp/pkg/resource/doc.go
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package resource helps managing
|
||||
arguments and files for a conversion.
|
||||
|
||||
All functions return our standard xerror.Error
|
||||
in case of error.
|
||||
*/
|
||||
package resource
|
||||
91
internal/app/xhttp/pkg/resource/file.go
Normal file
91
internal/app/xhttp/pkg/resource/file.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// file represents a file within the resource.
|
||||
type file struct {
|
||||
fpath string
|
||||
}
|
||||
|
||||
// write writes given content to the
|
||||
// resourceFile location.
|
||||
func (f file) write(in io.Reader) error {
|
||||
const op string = "resource.file.write"
|
||||
resolver := func() error {
|
||||
out, err := os.Create(f.fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close() // nolint: errcheck
|
||||
if err := out.Chmod(0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := out.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// content returns the string content of
|
||||
// the file.
|
||||
func (f file) content() (string, error) {
|
||||
const op string = "resource.file.content"
|
||||
b, err := ioutil.ReadFile(f.fpath)
|
||||
if err != nil {
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
/*
|
||||
HeaderFooterContents is a helper for retrieving
|
||||
the content of the files "header.html"
|
||||
and "footer.html".
|
||||
*/
|
||||
func HeaderFooterContents(r Resource) (string, string, error) {
|
||||
const (
|
||||
op string = "resource.HeaderFooterContents"
|
||||
defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||
)
|
||||
resolver := func() (string, string, error) {
|
||||
headerHTML, err := r.Fcontent("header.html", defaultHeaderFooterHTML)
|
||||
if err != nil {
|
||||
return defaultHeaderFooterHTML,
|
||||
defaultHeaderFooterHTML,
|
||||
err
|
||||
}
|
||||
footerHTML, err := r.Fcontent("footer.html", defaultHeaderFooterHTML)
|
||||
if err != nil {
|
||||
return defaultHeaderFooterHTML,
|
||||
defaultHeaderFooterHTML,
|
||||
err
|
||||
}
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
nil
|
||||
}
|
||||
headerHTML, footerHTML,
|
||||
err := resolver()
|
||||
if err != nil {
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
xerror.New(op, err)
|
||||
}
|
||||
return headerHTML,
|
||||
footerHTML,
|
||||
nil
|
||||
}
|
||||
227
internal/app/xhttp/pkg/resource/resource.go
Normal file
227
internal/app/xhttp/pkg/resource/resource.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
/*
|
||||
TemporaryDirectory is the directory
|
||||
where all the resources directory
|
||||
are located.
|
||||
*/
|
||||
const TemporaryDirectory string = "tmp"
|
||||
|
||||
// Resource helps managing
|
||||
// arguments and files for a conversion.
|
||||
type Resource struct {
|
||||
logger xlog.Logger
|
||||
dirPath string
|
||||
args map[ArgKey]string
|
||||
files map[string]file
|
||||
}
|
||||
|
||||
// New creates a Resource where its files will
|
||||
// be located in the given directory name.
|
||||
func New(logger xlog.Logger, directoryName string) (Resource, error) {
|
||||
const op string = "resource.New"
|
||||
resolver := func() (string, error) {
|
||||
dirPath := fmt.Sprintf("%s/%s", TemporaryDirectory, directoryName)
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
absDirPath, err := filepath.Abs(dirPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return absDirPath, nil
|
||||
}
|
||||
dirPath, err := resolver()
|
||||
if err != nil {
|
||||
return Resource{}, xerror.New(op, err)
|
||||
}
|
||||
logger.DebugfOp(op, "resource directory '%s' created", directoryName)
|
||||
return Resource{
|
||||
logger: logger,
|
||||
dirPath: dirPath,
|
||||
args: make(map[ArgKey]string),
|
||||
files: make(map[string]file),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close removes the working directory of the
|
||||
// Resource if it exists.
|
||||
func (r Resource) Close() error {
|
||||
const op string = "resource.Resource.Close"
|
||||
if _, err := os.Stat(r.dirPath); os.IsNotExist(err) {
|
||||
r.logger.DebugfOp(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath)
|
||||
return nil
|
||||
}
|
||||
if err := os.RemoveAll(r.dirPath); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
r.logger.DebugfOp(op, "resource directory '%s' removed", r.dirPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithArg add a new argument to the Resource.
|
||||
func (r *Resource) WithArg(key ArgKey, value string) {
|
||||
const op string = "resource.Resource.WithArg"
|
||||
r.args[key] = value
|
||||
r.logger.DebugfOp(op, "added '%s' with value '%s' to resource args", key, value)
|
||||
}
|
||||
|
||||
// WithFile add a new file to the Resource.
|
||||
func (r *Resource) WithFile(filename string, in io.Reader) error {
|
||||
const op string = "resource.Resource.WithFile"
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
file := file{fpath: fpath}
|
||||
if err := file.write(in); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
r.files[filename] = file
|
||||
r.logger.DebugfOp(op, "resource file '%s' created", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DirPath returns the directory path
|
||||
// of the Resource.
|
||||
func (r Resource) DirPath() string {
|
||||
return r.dirPath
|
||||
}
|
||||
|
||||
// HasArg returns true if given key exists
|
||||
// among the Resource and its value is not empty.
|
||||
func (r Resource) HasArg(key ArgKey) bool {
|
||||
if v, ok := r.args[key]; ok {
|
||||
return v != ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
StringArg returns the value of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.String.
|
||||
*/
|
||||
func (r Resource) StringArg(key ArgKey, defaultValue string, rules ...xassert.RuleString) (string, error) {
|
||||
const op string = "resource.Resource.StringArg"
|
||||
result, err := xassert.String(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Int64Arg returns the int64 representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Int64.
|
||||
*/
|
||||
func (r Resource) Int64Arg(key ArgKey, defaultValue int64, rules ...xassert.RuleInt64) (int64, error) {
|
||||
const op string = "resource.Resource.Int64Arg"
|
||||
result, err := xassert.Int64(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Float64Arg returns the float64 representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Float64.
|
||||
*/
|
||||
func (r Resource) Float64Arg(key ArgKey, defaultValue float64, rules ...xassert.RuleFloat64) (float64, error) {
|
||||
const op string = "resource.Resource.Float64Arg"
|
||||
result, err := xassert.Float64(string(key), r.args[key], defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
BoolArg returns the boolean representation of the
|
||||
argument identified by given key.
|
||||
|
||||
It works in the same manner as xassert.Bool.
|
||||
*/
|
||||
func (r Resource) BoolArg(key ArgKey, defaultValue bool) (bool, error) {
|
||||
const op string = "resource.Resource.BoolArg"
|
||||
result, err := xassert.Bool(string(key), r.args[key], defaultValue)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Fpath returns the path of the given filename.
|
||||
// This filename should exist whithin the Resource.
|
||||
func (r Resource) Fpath(filename string) (string, error) {
|
||||
const op string = "resource.Resource.Fpath"
|
||||
file, ok := r.files[filename]
|
||||
if !ok {
|
||||
return "", xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("resource file '%s' does not exist", filename),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return file.fpath, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Fpaths returns the paths of the files
|
||||
having one of the given file extensions.
|
||||
|
||||
It should found at least one path.
|
||||
*/
|
||||
func (r Resource) Fpaths(exts ...string) ([]string, error) {
|
||||
const op string = "resource.Resource.Fpaths"
|
||||
var fpaths []string
|
||||
for filename, file := range r.files {
|
||||
for _, ext := range exts {
|
||||
if filepath.Ext(filename) == ext {
|
||||
fpaths = append(fpaths, file.fpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return nil, xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("no resource file found for extensions '%v'", exts),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return fpaths, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Fcontent returns the string content of the
|
||||
given filename.
|
||||
|
||||
If filename does not exist within the Resource,
|
||||
returns the default value.
|
||||
*/
|
||||
func (r Resource) Fcontent(filename, defaultValue string) (string, error) {
|
||||
const op string = "resource.Resource.Fcontent"
|
||||
file, ok := r.files[filename]
|
||||
if !ok {
|
||||
return defaultValue, nil
|
||||
}
|
||||
content, err := file.content()
|
||||
if err != nil {
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
33
internal/app/xhttp/xhttp.go
Normal file
33
internal/app/xhttp/xhttp.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package xhttp
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||
)
|
||||
|
||||
// New returns a custom echo.Echo.
|
||||
func New(config conf.Config, processes ...pm2.Process) *echo.Echo {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
srv.Use(contextMiddleware(config, processes...))
|
||||
srv.Use(loggerMiddleware())
|
||||
srv.Use(cleanupMiddleware())
|
||||
srv.Use(errorMiddleware())
|
||||
srv.GET(pingEndpoint, pingHandler)
|
||||
srv.POST(mergeEndpoint, mergeHandler)
|
||||
if config.DisableGoogleChrome() && config.DisableUnoconv() {
|
||||
return srv
|
||||
}
|
||||
g := srv.Group(convertGroupEndpoint)
|
||||
if !config.DisableGoogleChrome() {
|
||||
g.POST(htmlEndpoint, htmlHandler)
|
||||
g.POST(urlEndpoint, urlHandler)
|
||||
g.POST(markdownEndpoint, markdownHandler)
|
||||
}
|
||||
if !config.DisableUnoconv() {
|
||||
g.POST(officeEndpoint, officeHandler)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
Reference in New Issue
Block a user