From f6b357691ce09eb8ecea58ef1ef569ceb575de76 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Wed, 10 Jul 2019 11:46:58 +0200 Subject: [PATCH] minor refactoring of const + better timeout handling in pinter package + api package tests --- build/tests/docker-entrypoint.sh | 1 - internal/app/api/api_test.go | 388 ++++++++++++++++++++ internal/app/api/pkg/context/context.go | 6 +- internal/app/api/pkg/handler/handler.go | 2 +- internal/app/api/pkg/handler/html.go | 2 +- internal/app/api/pkg/handler/markdown.go | 2 +- internal/app/api/pkg/handler/merge.go | 2 +- internal/app/api/pkg/handler/office.go | 2 +- internal/app/api/pkg/handler/url.go | 2 +- internal/app/api/pkg/middleware/cleanup.go | 2 +- internal/app/api/pkg/middleware/context.go | 4 +- internal/app/api/pkg/resource/resource.go | 28 +- internal/pkg/config/config.go | 20 +- internal/pkg/pm2/chrome.go | 10 +- internal/pkg/pm2/pm2.go | 8 +- internal/pkg/pm2/unoconv.go | 8 +- internal/pkg/printer/chrome.go | 144 ++++---- internal/pkg/printer/markdown.go | 4 +- internal/pkg/printer/merge.go | 22 +- internal/pkg/printer/office.go | 24 +- internal/pkg/printer/printer.go | 32 -- internal/pkg/printer/printer_test.go | 35 -- internal/pkg/standarderror/standarderror.go | 2 +- internal/pkg/timeout/doc.go | 2 +- internal/pkg/timeout/timeout.go | 30 ++ internal/pkg/timeout/timeout_test.go | 32 +- test/testfunc.go | 3 + 27 files changed, 604 insertions(+), 213 deletions(-) create mode 100644 internal/app/api/api_test.go delete mode 100644 internal/pkg/printer/printer_test.go diff --git a/build/tests/docker-entrypoint.sh b/build/tests/docker-entrypoint.sh index 49aa729b..21a86c41 100755 --- a/build/tests/docker-entrypoint.sh +++ b/build/tests/docker-entrypoint.sh @@ -13,7 +13,6 @@ go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/int go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/random go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/standarderror go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/timeout -go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/printer go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api # Finally testing processes shutdown. diff --git a/internal/app/api/api_test.go b/internal/app/api/api_test.go new file mode 100644 index 00000000..7a4f5dce --- /dev/null +++ b/internal/app/api/api_test.go @@ -0,0 +1,388 @@ +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) +} diff --git a/internal/app/api/pkg/context/context.go b/internal/app/api/pkg/context/context.go index 2b32c949..f8437f95 100644 --- a/internal/app/api/pkg/context/context.go +++ b/internal/app/api/pkg/context/context.go @@ -36,7 +36,7 @@ func New(c echo.Context, logger *logger.Logger, config *config.Config) *Context // MustCastFromEchoContext cast an echo.Context to our custom // context. If something goes wrong, panic. func MustCastFromEchoContext(c echo.Context) *Context { - const op = "context.MustCastFromEchoContext" + 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)) @@ -60,7 +60,7 @@ func (ctx *Context) Resource() *resource.Resource { // WithResource adds a resource to the context. func (ctx *Context) WithResource(resourceDirPath string) error { - const op = "context.WithResource" + const op string = "context.WithResource" r, err := resource.New(ctx, ctx.logger, ctx.config, resourceDirPath) ctx.resource = r if err != nil { @@ -75,7 +75,7 @@ func (ctx *Context) WithResource(resourceDirPath string) error { // 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 = "context.LogRequestResult" + const op string = "context.LogRequestResult" req := ctx.Request() resp := ctx.Response() stopTime := time.Now() diff --git a/internal/app/api/pkg/handler/handler.go b/internal/app/api/pkg/handler/handler.go index b511818c..92d70f45 100644 --- a/internal/app/api/pkg/handler/handler.go +++ b/internal/app/api/pkg/handler/handler.go @@ -35,7 +35,7 @@ const ( ) func convert(ctx *context.Context, p printer.Printer) error { - const op = "handler.convert" + const op string = "handler.convert" r := ctx.Resource() logger := ctx.StandardLogger() baseFilename := random.Get() diff --git a/internal/app/api/pkg/handler/html.go b/internal/app/api/pkg/handler/html.go index eb1581bb..c64f70c7 100644 --- a/internal/app/api/pkg/handler/html.go +++ b/internal/app/api/pkg/handler/html.go @@ -10,7 +10,7 @@ import ( // HTML is the endpoint for converting // HTML to PDF. func HTML(c echo.Context) error { - const op = "handler.HTML" + const op string = "handler.HTML" ctx := context.MustCastFromEchoContext(c) ctx.StandardLogger().DebugfOp(op, "html request") r := ctx.Resource() diff --git a/internal/app/api/pkg/handler/markdown.go b/internal/app/api/pkg/handler/markdown.go index a7f59379..6283d4c3 100644 --- a/internal/app/api/pkg/handler/markdown.go +++ b/internal/app/api/pkg/handler/markdown.go @@ -10,7 +10,7 @@ import ( // Markdown is the endpoint for converting // Markdown to PDF. func Markdown(c echo.Context) error { - const op = "handler.Markdown" + const op string = "handler.Markdown" ctx := context.MustCastFromEchoContext(c) ctx.StandardLogger().DebugfOp(op, "markdown request") r := ctx.Resource() diff --git a/internal/app/api/pkg/handler/merge.go b/internal/app/api/pkg/handler/merge.go index 381e7da2..e3422673 100644 --- a/internal/app/api/pkg/handler/merge.go +++ b/internal/app/api/pkg/handler/merge.go @@ -10,7 +10,7 @@ import ( // Merge is the endpoint for // merging PDF files. func Merge(c echo.Context) error { - const op = "handler.Merge" + const op string = "handler.Merge" ctx := context.MustCastFromEchoContext(c) ctx.StandardLogger().DebugfOp(op, "merge request") r := ctx.Resource() diff --git a/internal/app/api/pkg/handler/office.go b/internal/app/api/pkg/handler/office.go index 60ccca1c..2532e21e 100644 --- a/internal/app/api/pkg/handler/office.go +++ b/internal/app/api/pkg/handler/office.go @@ -10,7 +10,7 @@ import ( // Office is the endpoint for converting // Office files to PDF. func Office(c echo.Context) error { - const op = "handler.Office" + const op string = "handler.Office" ctx := context.MustCastFromEchoContext(c) ctx.StandardLogger().DebugfOp(op, "office request") r := ctx.Resource() diff --git a/internal/app/api/pkg/handler/url.go b/internal/app/api/pkg/handler/url.go index f7383a3c..f4264f00 100644 --- a/internal/app/api/pkg/handler/url.go +++ b/internal/app/api/pkg/handler/url.go @@ -11,7 +11,7 @@ import ( // URL is the endpoint for converting // a URL to PDF. func URL(c echo.Context) error { - const op = "handler.URL" + const op string = "handler.URL" ctx := context.MustCastFromEchoContext(c) ctx.StandardLogger().DebugfOp(op, "url request") r := ctx.Resource() diff --git a/internal/app/api/pkg/middleware/cleanup.go b/internal/app/api/pkg/middleware/cleanup.go index ee21c9fb..6f6c57f7 100644 --- a/internal/app/api/pkg/middleware/cleanup.go +++ b/internal/app/api/pkg/middleware/cleanup.go @@ -11,7 +11,7 @@ import ( func Cleanup() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - const op = "middleware.Cleanup" + const op string = "middleware.Cleanup" err := next(c) ctx := context.MustCastFromEchoContext(c) r := ctx.Resource() diff --git a/internal/app/api/pkg/middleware/context.go b/internal/app/api/pkg/middleware/context.go index d4da5100..d917ba7b 100644 --- a/internal/app/api/pkg/middleware/context.go +++ b/internal/app/api/pkg/middleware/context.go @@ -15,13 +15,13 @@ import ( const ( // TestingTraceEnvVar is an environment // variable used in some tests. - TestingTraceEnvVar = "TESTING_TRACE" + 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 = "tmp" + TestsTracePrefix string = "tmp" ) // Context helps extending the default echo.Context with diff --git a/internal/app/api/pkg/resource/resource.go b/internal/app/api/pkg/resource/resource.go index 69eb487a..c8eba0d9 100644 --- a/internal/app/api/pkg/resource/resource.go +++ b/internal/app/api/pkg/resource/resource.go @@ -65,7 +65,7 @@ type Resource struct { // New creates a new resource. func New(c echo.Context, logger *logger.Logger, config *config.Config, dirPath string) (*Resource, error) { - const op = "resource.New" + const op string = "resource.New" r := &Resource{ logger: logger, config: config, @@ -83,7 +83,7 @@ func New(c echo.Context, logger *logger.Logger, config *config.Config, dirPath s } func formValues(c echo.Context, logger *logger.Logger) map[string]string { - const op = "resource.formValues" + const op string = "resource.formValues" v := make(map[string]string) fetch := func(formField string) string { value := c.FormValue(formField) @@ -110,7 +110,7 @@ func formValues(c echo.Context, logger *logger.Logger) map[string]string { } func formFiles(c echo.Context, logger *logger.Logger, dirPath string) error { - const op = "resource.formFiles" + const op string = "resource.formFiles" form, err := c.MultipartForm() if err != nil { return &standarderror.Error{Op: op, Err: err} @@ -153,7 +153,7 @@ func (r *Resource) DirPath() string { // Close deletes the working directory of the // resource if it exists. func (r *Resource) Close() error { - const op = "resource.Close" + 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 @@ -171,7 +171,7 @@ const defaultHeaderFooterHTML string = "" // thanks to the form values and form files from the request // plus the default values from the configuration. func (r *Resource) ChromePrinterOptions() (*printer.ChromeOptions, error) { - const op = "resource.ChromePrinterOptions" + const op string = "resource.ChromePrinterOptions" waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout()) if err != nil { return nil, &standarderror.Error{Op: op, Err: err} @@ -237,7 +237,7 @@ func (r *Resource) ChromePrinterOptions() (*printer.ChromeOptions, error) { // thanks to the form values from the request // plus the default values from the configuration. func (r *Resource) OfficePrinterOptions() (*printer.OfficeOptions, error) { - const op = "resource.OfficePrinterOptions" + const op string = "resource.OfficePrinterOptions" waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout()) if err != nil { return nil, &standarderror.Error{Op: op, Err: err} @@ -258,7 +258,7 @@ func (r *Resource) OfficePrinterOptions() (*printer.OfficeOptions, error) { // thanks to the form values from the request // plus the default values from the configuration. func (r *Resource) MergePrinterOptions() (*printer.MergeOptions, error) { - const op = "resource.MergePrinterOptions" + const op string = "resource.MergePrinterOptions" waitTimeout, err := r.float64(WaitTimeoutFormField, r.config.DefaultWaitTimeout()) if err != nil { return nil, &standarderror.Error{Op: op, Err: err} @@ -289,7 +289,7 @@ func (r *Resource) hasFile(filename string) bool { // Get returns the form field value. func (r *Resource) Get(formField string) (string, error) { - const op = "resource.Get" + const op string = "resource.Get" v, err := r.value(formField) if err != nil { return "", &standarderror.Error{Op: op, Err: err} @@ -298,7 +298,7 @@ func (r *Resource) Get(formField string) (string, error) { } func (r *Resource) value(formField string) (string, error) { - const op = "resource.value" + const op string = "resource.value" v, ok := r.formValues[formField] if !ok { return "", &standarderror.Error{ @@ -311,7 +311,7 @@ func (r *Resource) value(formField string) (string, error) { } func (r *Resource) float64(formField string, defaultValue float64) (float64, error) { - const op = "resource.float64" + const op string = "resource.float64" if !r.Has(formField) { return defaultValue, nil } @@ -331,7 +331,7 @@ func (r *Resource) float64(formField string, defaultValue float64) (float64, err } func (r *Resource) bool(formField string, defaultValue bool) (bool, error) { - const op = "resource.bool" + const op string = "resource.bool" if !r.Has(formField) { return defaultValue, nil } @@ -353,7 +353,7 @@ func (r *Resource) bool(formField string, defaultValue bool) (bool, error) { // Fpath returns the path of the given filename. // This filename should be the name of a form file. func (r *Resource) Fpath(filename string) (string, error) { - const op = "resource.Fpath" + const op string = "resource.Fpath" fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename) _, err := os.Stat(fpath) if os.IsNotExist(err) { @@ -371,7 +371,7 @@ func (r *Resource) Fpath(filename string) (string, error) { } func (r *Resource) content(filename string, defaultValue string) (string, error) { - const op = "resource.content" + const op string = "resource.content" if !r.hasFile(filename) { return defaultValue, nil } @@ -389,7 +389,7 @@ func (r *Resource) content(filename string, defaultValue string) (string, error) // Fpaths returns the list of files of the resource // according to given file extensions. func (r *Resource) Fpaths(exts ...string) ([]string, error) { - const op = "resource.Fpaths" + const op string = "resource.Fpaths" var fpaths []string err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error { if info.IsDir() { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 6379a18e..41be1df9 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -10,11 +10,11 @@ import ( ) const ( - defaultWaitTimeoutEnvVar = "DEFAULT_WAIT_TIMEOUT" - defaultListenPortEnvVar = "DEFAULT_LISTEN_PORT" - disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME" - disableUnoconvEnvVar = "DISABLE_UNOCONV" - logLevelEnvVar = "LOG_LEVEL" + defaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_TIMEOUT" + defaultListenPortEnvVar string = "DEFAULT_LISTEN_PORT" + disableGoogleChromeEnvVar string = "DISABLE_GOOGLE_CHROME" + disableUnoconvEnvVar string = "DISABLE_UNOCONV" + logLevelEnvVar string = "LOG_LEVEL" ) // Config contains the application @@ -40,7 +40,7 @@ func defaultConfig() *Config { // FromEnv fetches configuration // from environment variables. func FromEnv() (*Config, error) { - const op = "config.FromEnv" + const op string = "config.FromEnv" c := defaultConfig() defaultWaitTimeout, err := defaultWaitTimeoutFromEnv(defaultWaitTimeoutEnvVar, c.DefaultWaitTimeout()) c.defaultWaitTimeout = defaultWaitTimeout @@ -103,7 +103,7 @@ func (c *Config) LogLevel() logrus.Level { } func defaultWaitTimeoutFromEnv(envVar string, defaultValue float64) (float64, error) { - const op = "config.defaultWaitTimeoutFromEnv" + const op string = "config.defaultWaitTimeoutFromEnv" if v, ok := os.LookupEnv(envVar); ok { waitTimeout, err := strconv.ParseFloat(v, 64) if err != nil { @@ -119,7 +119,7 @@ func defaultWaitTimeoutFromEnv(envVar string, defaultValue float64) (float64, er } func defaultListenPortFromEnv(envVar string, defaultValue string) (string, error) { - const op = "config.defaultListenPortFromEnv" + const op string = "config.defaultListenPortFromEnv" if v, ok := os.LookupEnv(envVar); ok { portAsUint, err := strconv.ParseUint(v, 10, 64) if err != nil { @@ -142,7 +142,7 @@ func defaultListenPortFromEnv(envVar string, defaultValue string) (string, error } func boolFromEnv(envVar string, defaultValue bool) (bool, error) { - const op = "config.boolFromEnv" + const op string = "config.boolFromEnv" if v, ok := os.LookupEnv(envVar); ok { if v != "1" && v != "0" { return defaultValue, &standarderror.Error{ @@ -157,7 +157,7 @@ func boolFromEnv(envVar string, defaultValue bool) (bool, error) { } func logLevelFromEnv(envVar string, defaultValue logrus.Level) (logrus.Level, error) { - const op = "config.logLevelFromEnv" + const op string = "config.logLevelFromEnv" if v, ok := os.LookupEnv(envVar); ok { switch v { case "DEBUG": diff --git a/internal/pkg/pm2/chrome.go b/internal/pkg/pm2/chrome.go index 94492ede..52a639c9 100644 --- a/internal/pkg/pm2/chrome.go +++ b/internal/pkg/pm2/chrome.go @@ -9,7 +9,7 @@ import ( "github.com/thecodingmachine/gotenberg/internal/pkg/standarderror" ) -const chromeWarmupTime = 10 * time.Second +const chromeWarmupTime time.Duration = 10 * time.Second type chrome struct { manager *processManager @@ -28,7 +28,7 @@ func (p *chrome) Fullname() string { } func (p *chrome) Start() error { - const op = "pm2.chrome.Start" + const op string = "pm2.chrome.Start" if err := p.manager.start(p); err != nil { return &standarderror.Error{Op: op, Err: err} } @@ -36,7 +36,7 @@ func (p *chrome) Start() error { } func (p *chrome) Shutdown() error { - const op = "pm2.chrome.Shutdown" + const op string = "pm2.chrome.Shutdown" if err := p.manager.shutdown(p); err != nil { return &standarderror.Error{Op: op, Err: err} } @@ -67,7 +67,7 @@ func (p *chrome) name() string { } func (p *chrome) viable() bool { - const op = "pm2.chrome.viable" + const op string = "pm2.chrome.viable" // check if Google Chrome is correctly running. ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -93,7 +93,7 @@ func (p *chrome) viable() bool { } func (p *chrome) warmup() { - const op = "pm2.chrome.warmup" + const op string = "pm2.chrome.warmup" p.manager.logger.DebugfOp( op, "allowing %v to startup", diff --git a/internal/pkg/pm2/pm2.go b/internal/pkg/pm2/pm2.go index 065ff5eb..7f1113f0 100644 --- a/internal/pkg/pm2/pm2.go +++ b/internal/pkg/pm2/pm2.go @@ -12,7 +12,7 @@ import ( ) const ( - stoppedState = iota + stoppedState int32 = iota runningState errorState ) @@ -35,7 +35,7 @@ type processManager struct { } func (m *processManager) start(p Process) error { - const op = "pm2.start" + const op string = "pm2.start" if err := m.pm2(p, "start"); err != nil { return &standarderror.Error{Op: op, Err: err} } @@ -63,7 +63,7 @@ func (m *processManager) start(p Process) error { } func (m *processManager) shutdown(p Process) error { - const op = "pm2.shutdown" + const op string = "pm2.shutdown" if m.heuristicState != runningState { return nil } @@ -76,7 +76,7 @@ func (m *processManager) shutdown(p Process) error { } func (m *processManager) pm2(p Process, cmdName string) error { - const op = "pm2.pm2" + const op string = "pm2.pm2" cmdArgs := []string{ cmdName, p.name(), diff --git a/internal/pkg/pm2/unoconv.go b/internal/pkg/pm2/unoconv.go index ab1f9cde..6c67b3f6 100644 --- a/internal/pkg/pm2/unoconv.go +++ b/internal/pkg/pm2/unoconv.go @@ -7,7 +7,7 @@ import ( "github.com/thecodingmachine/gotenberg/internal/pkg/standarderror" ) -const unoconvWarmupTime = 5 * time.Second +const unoconvWarmupTime time.Duration = 5 * time.Second type unoconv struct { manager *processManager @@ -26,7 +26,7 @@ func (p *unoconv) Fullname() string { } func (p *unoconv) Start() error { - const op = "pm2.unoconv.Start" + const op string = "pm2.unoconv.Start" if err := p.manager.start(p); err != nil { return &standarderror.Error{Op: op, Err: err} } @@ -34,7 +34,7 @@ func (p *unoconv) Start() error { } func (p *unoconv) Shutdown() error { - const op = "pm2.unoconv.Shutdown" + const op string = "pm2.unoconv.Shutdown" if err := p.manager.shutdown(p); err != nil { return &standarderror.Error{Op: op, Err: err} } @@ -60,7 +60,7 @@ func (p *unoconv) viable() bool { } func (p *unoconv) warmup() { - const op = "pm2.unoconv.warmup" + const op string = "pm2.unoconv.warmup" p.manager.logger.DebugfOp( op, "allowing %v to startup", diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index fe1d181a..968030dd 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -39,83 +39,89 @@ type ChromeOptions struct { } func (p *chrome) Print(destination string) error { - const op = "printer.chrome.Print" + const op string = "printer.chrome.Print" ctx, cancel := timeout.Context(p.opts.WaitTimeout + p.opts.WaitDelay) defer cancel() - devt, err := devtool.New("http://localhost:9222").Version(ctx) - if err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) + resolver := func() error { + devt, err := devtool.New("http://localhost:9222").Version(ctx) + if err != nil { + return &standarderror.Error{Op: op, Err: err} + } + // connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol. + devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL) + if err != nil { + return &standarderror.Error{Op: op, Err: err} + } + defer devtConn.Close() // nolint: errcheck + // create a new CDP Client that uses conn. + devtClient := cdp.NewClient(devtConn) + newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx) + if err != nil { + return &standarderror.Error{Op: op, Err: err} + } + // create a new blank target with the new browser context. + createTargetArgs := target. + NewCreateTargetArgs("about:blank"). + SetBrowserContextID(newContextTarget.BrowserContextID) + newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs) + if err != nil { + return &standarderror.Error{Op: op, Err: err} + } + // connect the client to the new target. + newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID) + newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL) + if err != nil { + return &standarderror.Error{Op: op, Err: err} + } + defer newContextConn.Close() // nolint: errcheck + // create a new CDP Client that uses newContextConn. + targetClient := cdp.NewClient(newContextConn) + closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID) + // close the target when done. + defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck + if err := runBatch( + // enable all the domain events that we're interested in. + func() error { return targetClient.DOM.Enable(ctx) }, + func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) }, + func() error { return targetClient.Page.Enable(ctx) }, + func() error { return targetClient.Runtime.Enable(ctx) }, + ); err != nil { + return &standarderror.Error{Op: op, Err: err} + } + if err := p.navigate(ctx, targetClient); err != nil { + return &standarderror.Error{Op: op, Err: err} + } + print, err := targetClient.Page.PrintToPDF( + ctx, + page.NewPrintToPDFArgs(). + SetPaperWidth(p.opts.PaperWidth). + SetPaperHeight(p.opts.PaperHeight). + SetMarginTop(p.opts.MarginTop). + SetMarginBottom(p.opts.MarginBottom). + SetMarginLeft(p.opts.MarginLeft). + SetMarginRight(p.opts.MarginRight). + SetLandscape(p.opts.Landscape). + SetDisplayHeaderFooter(true). + SetHeaderTemplate(p.opts.HeaderHTML). + SetFooterTemplate(p.opts.FooterHTML). + SetPrintBackground(true), + ) + if err != nil { + return &standarderror.Error{Op: op, Err: err} + } + if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil { + return &standarderror.Error{Op: op, Err: err} + } + return nil } - // connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol. - devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL) - if err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) - } - defer devtConn.Close() // nolint: errcheck - // create a new CDP Client that uses conn. - devtClient := cdp.NewClient(devtConn) - newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx) - if err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) - } - // create a new blank target with the new browser context. - createTargetArgs := target. - NewCreateTargetArgs("about:blank"). - SetBrowserContextID(newContextTarget.BrowserContextID) - newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs) - if err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) - } - // connect the client to the new target. - newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID) - newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL) - if err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) - } - defer newContextConn.Close() // nolint: errcheck - // create a new CDP Client that uses newContextConn. - targetClient := cdp.NewClient(newContextConn) - closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID) - // close the target when done. - defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck - if err := runBatch( - // enable all the domain events that we're interested in. - func() error { return targetClient.DOM.Enable(ctx) }, - func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) }, - func() error { return targetClient.Page.Enable(ctx) }, - func() error { return targetClient.Runtime.Enable(ctx) }, - ); err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) - } - if err := p.navigate(ctx, targetClient); err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) - } - print, err := targetClient.Page.PrintToPDF( - ctx, - page.NewPrintToPDFArgs(). - SetPaperWidth(p.opts.PaperWidth). - SetPaperHeight(p.opts.PaperHeight). - SetMarginTop(p.opts.MarginTop). - SetMarginBottom(p.opts.MarginBottom). - SetMarginLeft(p.opts.MarginLeft). - SetMarginRight(p.opts.MarginRight). - SetLandscape(p.opts.Landscape). - SetDisplayHeaderFooter(true). - SetHeaderTemplate(p.opts.HeaderHTML). - SetFooterTemplate(p.opts.FooterHTML). - SetPrintBackground(true), - ) - if err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) - } - if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil { - return &standarderror.Error{Op: op, Err: err} + if err := resolver(); err != nil { + return timeout.Err(ctx, err) } return nil } func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error { - const op = "printer.chrome.navigate" + const op string = "printer.chrome.navigate" // make sure Page events are enabled. if err := client.Page.Enable(ctx); err != nil { return &standarderror.Error{Op: op, Err: err} diff --git a/internal/pkg/printer/markdown.go b/internal/pkg/printer/markdown.go index 07ea3b55..199ebeea 100644 --- a/internal/pkg/printer/markdown.go +++ b/internal/pkg/printer/markdown.go @@ -15,7 +15,7 @@ import ( // NewMarkdown returns a Markdown printer. func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) { - const op = "printer.NewMarkdown" + const op string = "printer.NewMarkdown" tmpl, err := template. New(filepath.Base(fpath)). Funcs(template.FuncMap{"toHTML": markdownToHTML}). @@ -46,7 +46,7 @@ type templateData struct { } func markdownToHTML(dirPath, filename string) (template.HTML, error) { - const op = "printer.markdownToHTML" + const op string = "printer.markdownToHTML" fpath := fmt.Sprintf("%s/%s", dirPath, filename) b, err := ioutil.ReadFile(fpath) if err != nil { diff --git a/internal/pkg/printer/merge.go b/internal/pkg/printer/merge.go index 746a5ee1..4bf5e2e5 100644 --- a/internal/pkg/printer/merge.go +++ b/internal/pkg/printer/merge.go @@ -29,19 +29,25 @@ func NewMerge(fpaths []string, opts *MergeOptions) Printer { } func (p *merge) Print(destination string) error { - const op = "printer.merge.Print" + const op string = "printer.merge.Print" if p.ctx == nil { ctx, cancel := timeout.Context(p.opts.WaitTimeout) defer cancel() p.ctx = ctx } - var cmdArgs []string - cmdArgs = append(cmdArgs, p.fpaths...) - cmdArgs = append(cmdArgs, "cat", "output", destination) - cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...) - _, err := cmd.Output() - if err != nil { - return handleErrContext(p.ctx, &standarderror.Error{Op: op, Err: err}) + resolver := func() error { + var cmdArgs []string + cmdArgs = append(cmdArgs, p.fpaths...) + cmdArgs = append(cmdArgs, "cat", "output", destination) + cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...) + _, err := cmd.Output() + if err != nil { + return &standarderror.Error{Op: op, Err: err} + } + return nil + } + if err := resolver(); err != nil { + return timeout.Err(p.ctx, err) } return nil } diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go index 26db09e9..d7a9520f 100644 --- a/internal/pkg/printer/office.go +++ b/internal/pkg/printer/office.go @@ -34,18 +34,24 @@ func NewOffice(fpaths []string, opts *OfficeOptions) Printer { } func (p *office) Print(destination string) error { - const op = "printer.office.Print" + const op string = "printer.office.Print" ctx, cancel := timeout.Context(p.opts.WaitTimeout) defer cancel() fpaths := make([]string, len(p.fpaths)) - dirPath := filepath.Dir(destination) - for i, fpath := range p.fpaths { - baseFilename := random.String(32) - tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename) - if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil { - return handleErrContext(ctx, &standarderror.Error{Op: op, Err: err}) + resolver := func() error { + dirPath := filepath.Dir(destination) + for i, fpath := range p.fpaths { + baseFilename := random.String(32) + tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename) + if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil { + return &standarderror.Error{Op: op, Err: err} + } + fpaths[i] = tmpDest } - fpaths[i] = tmpDest + return nil + } + if err := resolver(); err != nil { + return timeout.Err(ctx, err) } if len(fpaths) == 1 { if err := os.Rename(fpaths[0], destination); err != nil { @@ -67,7 +73,7 @@ func (p *office) Print(destination string) error { var mu sync.Mutex func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error { - const op = "printer.unoconv" + const op string = "printer.unoconv" mu.Lock() defer mu.Unlock() cmdArgs := []string{ diff --git a/internal/pkg/printer/printer.go b/internal/pkg/printer/printer.go index 5fff84c7..e1e72497 100644 --- a/internal/pkg/printer/printer.go +++ b/internal/pkg/printer/printer.go @@ -1,39 +1,7 @@ package printer -import ( - "context" - "fmt" - "strings" - - "github.com/thecodingmachine/gotenberg/internal/pkg/standarderror" -) - // Printer is a type that can create a PDF file from a source. // The source is defined in the underlying implementation. type Printer interface { Print(destination string) error } - -func handleErrContext(ctx context.Context, previousErr error) error { - const op = "printer.handleErrContext" - if previousErr == nil { - panic(fmt.Sprintf("%s: previous error should not be nil", op)) - } - err := ctx.Err() - if err == nil { - return previousErr - } - if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) { - return &standarderror.Error{ - Code: standarderror.Timeout, - Message: "context has timed out", - Op: op, - Err: previousErr, - } - } - return &standarderror.Error{ - Message: "context finished with an error", - Op: op, - Err: previousErr, - } -} diff --git a/internal/pkg/printer/printer_test.go b/internal/pkg/printer/printer_test.go deleted file mode 100644 index d844ac72..00000000 --- a/internal/pkg/printer/printer_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package printer - -import ( - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/standarderror" - "github.com/thecodingmachine/gotenberg/internal/pkg/timeout" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestHandlerErr(t *testing.T) { - previousErr := errors.New("previous error") - // should be OK. - ctx, cancel := timeout.Context(5) - defer cancel() - assert.NotNil(t, handleErrContext(ctx, previousErr)) - // should timeout. - ctx, cancel = timeout.Context(0.5) - defer cancel() - time.Sleep(timeout.Duration(1)) - err := handleErrContext(ctx, previousErr) - assert.NotNil(t, err) - standardized := test.RequireStandardError(t, err) - assert.Equal(t, standarderror.Timeout, standardized.Code) - // should failed. - ctx, cancel = timeout.Context(5) - cancel() - err = handleErrContext(ctx, previousErr) - assert.NotNil(t, err) - standardized = test.RequireStandardError(t, err) - assert.Equal(t, standarderror.Internal, standarderror.Code(err)) -} diff --git a/internal/pkg/standarderror/standarderror.go b/internal/pkg/standarderror/standarderror.go index f5ad9ec8..d39cc2f0 100644 --- a/internal/pkg/standarderror/standarderror.go +++ b/internal/pkg/standarderror/standarderror.go @@ -64,7 +64,7 @@ func Code(err error) string { return Internal } -const defaultMessage = "an internal error has occurred: please contact technical support" +const defaultMessage string = "an internal error has occurred: please contact technical support" // Message returns the human-readable message of the error, if available. // Otherwise returns a generic error message. diff --git a/internal/pkg/timeout/doc.go b/internal/pkg/timeout/doc.go index 771db613..e8c95004 100644 --- a/internal/pkg/timeout/doc.go +++ b/internal/pkg/timeout/doc.go @@ -1,3 +1,3 @@ -// Package timeout helps creating +// Package timeout helps managing // context with timeout. package timeout diff --git a/internal/pkg/timeout/timeout.go b/internal/pkg/timeout/timeout.go index f29a03d0..37a61325 100644 --- a/internal/pkg/timeout/timeout.go +++ b/internal/pkg/timeout/timeout.go @@ -2,7 +2,11 @@ package timeout import ( "context" + "fmt" + "strings" "time" + + "github.com/thecodingmachine/gotenberg/internal/pkg/standarderror" ) // Context creates a context with timeout for @@ -15,3 +19,29 @@ func Context(seconds float64) (context.Context, context.CancelFunc) { func Duration(seconds float64) time.Duration { return time.Duration(1000*seconds) * time.Millisecond } + +// Err checks if there is an error in the given context +// and wraps the previous error inside a standarderror.Error. +func Err(ctx context.Context, previousErr error) error { + const op string = "timeout.Err" + if previousErr == nil { + panic(fmt.Sprintf("%s: previous error should not be nil", op)) + } + err := ctx.Err() + if err == nil { + return previousErr + } + if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) { + return &standarderror.Error{ + Code: standarderror.Timeout, + Message: "context has timed out", + Op: op, + Err: previousErr, + } + } + return &standarderror.Error{ + Message: "context finished with an error", + Op: op, + Err: previousErr, + } +} diff --git a/internal/pkg/timeout/timeout_test.go b/internal/pkg/timeout/timeout_test.go index 7fa27881..3ff7f724 100644 --- a/internal/pkg/timeout/timeout_test.go +++ b/internal/pkg/timeout/timeout_test.go @@ -1,20 +1,40 @@ package timeout import ( + "errors" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/thecodingmachine/gotenberg/internal/pkg/standarderror" + "github.com/thecodingmachine/gotenberg/test" ) -func TestContext(t *testing.T) { - ctx, cancel := Context(1.5) - assert.NotNil(t, ctx) - assert.NotNil(t, cancel) -} - func TestDuration(t *testing.T) { expected := time.Duration(1500) * time.Millisecond result := Duration(1.5) assert.Equal(t, expected.String(), result.String()) } + +func TestErr(t *testing.T) { + previousErr := errors.New("previous error") + // should be OK. + ctx, cancel := Context(5) + defer cancel() + assert.NotNil(t, Err(ctx, previousErr)) + // should timeout. + ctx, cancel = Context(0.5) + defer cancel() + time.Sleep(Duration(1)) + err := Err(ctx, previousErr) + assert.NotNil(t, err) + standardized := test.RequireStandardError(t, err) + assert.Equal(t, standarderror.Timeout, standardized.Code) + // should failed. + ctx, cancel = Context(5) + cancel() + err = Err(ctx, previousErr) + assert.NotNil(t, err) + standardized = test.RequireStandardError(t, err) + assert.Equal(t, standarderror.Internal, standarderror.Code(err)) +} diff --git a/test/testfunc.go b/test/testfunc.go index c21ba9d5..ba597b25 100644 --- a/test/testfunc.go +++ b/test/testfunc.go @@ -37,6 +37,9 @@ func AssertDirectoryEmpty(t *testing.T, directory string) { assert.Nil(t, err) defer f.Close() // nolint: errcheck _, err = f.Readdir(1) + if err == nil { + return + } assert.Equal(t, io.EOF, err) }