mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-18 13:12:17 +01:00
feat(pdfengines): add read metadata route
This commit is contained in:
@@ -15,9 +15,15 @@ import (
|
|||||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrAsyncProcess happens when a handler or middleware handles a request in an
|
var (
|
||||||
// asynchronous fashion.
|
// ErrAsyncProcess happens when a handler or middleware handles a request
|
||||||
var ErrAsyncProcess = errors.New("async process")
|
// in an asynchronous fashion.
|
||||||
|
ErrAsyncProcess = errors.New("async process")
|
||||||
|
|
||||||
|
// ErrNoOutputFile happens when a handler or middleware handles a request
|
||||||
|
// without sending any output file.
|
||||||
|
ErrNoOutputFile = errors.New("no output file")
|
||||||
|
)
|
||||||
|
|
||||||
// ParseError parses an error and returns the corresponding HTTP status and
|
// ParseError parses an error and returns the corresponding HTTP status and
|
||||||
// HTTP message.
|
// HTTP message.
|
||||||
@@ -246,6 +252,13 @@ func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration) echo.Mid
|
|||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
if errors.Is(err, ErrNoOutputFile) {
|
||||||
|
// A middleware/handler tells us that it's handling the process
|
||||||
|
// in an asynchronous fashion. Therefore, we must not cancel
|
||||||
|
// the context nor send an output file.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,6 +335,15 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
expectStatus: http.StatusNoContent,
|
expectStatus: http.StatusNoContent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
request: buildMultipartFormDataRequest(),
|
||||||
|
next: func() echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return ErrNoOutputFile
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
request: buildMultipartFormDataRequest(),
|
request: buildMultipartFormDataRequest(),
|
||||||
next: func() echo.HandlerFunc {
|
next: func() echo.HandlerFunc {
|
||||||
|
|||||||
@@ -282,14 +282,14 @@ func TestExiftool_WriteMetadata(t *testing.T) {
|
|||||||
t.Fatal("expected error but got none")
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectError {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), destinationPath)
|
metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), destinationPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
|||||||
return []api.Route{
|
return []api.Route{
|
||||||
mergeRoute(engine),
|
mergeRoute(engine),
|
||||||
convertRoute(engine),
|
convertRoute(engine),
|
||||||
|
readMetadataRoute(engine),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 2,
|
expectRoutes: 3,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
@@ -78,8 +79,8 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertRoute returns an [api.Route] which can convert a PDF to a specific
|
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
||||||
// PDF format.
|
// format.
|
||||||
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
@@ -152,3 +153,43 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readMetadataRoute returns an [api.Route] which returns the metadata of PDFs.
|
||||||
|
func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/pdfengines/metadata/read",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
// Let's get the data from the form and validate them.
|
||||||
|
var inputPaths []string
|
||||||
|
|
||||||
|
err := ctx.FormData().
|
||||||
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alright, let's read the metadata.
|
||||||
|
res := make(map[string]map[string]interface{}, len(inputPaths))
|
||||||
|
for _, inputPath := range inputPaths {
|
||||||
|
metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res[filepath.Base(inputPath)] = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.JSON(http.StatusOK, res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("return JSON response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.ErrNoOutputFile
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -401,3 +403,107 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadMetadataHandler(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *api.ContextMock
|
||||||
|
engine gotenberg.PdfEngine
|
||||||
|
expectError bool
|
||||||
|
expectedError error
|
||||||
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
|
expectedJson string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing at least one mandatory file",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: true,
|
||||||
|
expectHttpStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "error from PDF engine",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectedError: api.ErrNoOutputFile,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectedJson: `{"file.pdf":{"bar":"foo","foo":"bar"}}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/forms/pdfengines/metadata/read", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c := echo.New().NewContext(req, rec)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
|
err := readMetadataRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpErr api.HttpError
|
||||||
|
isHttpError := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHttpError && !isHttpError {
|
||||||
|
t.Errorf("expected an HTTP error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHttpError && isHttpError {
|
||||||
|
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHttpError && isHttpError {
|
||||||
|
status, _ := httpErr.HttpError()
|
||||||
|
if status != tc.expectHttpStatus {
|
||||||
|
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedJson != "" && tc.expectedJson != strings.TrimSpace(rec.Body.String()) {
|
||||||
|
t.Errorf("expected '%s' as HTTP response but got '%s'", tc.expectedJson, strings.TrimSpace(rec.Body.String()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user