From 7f145d0e6fc731b8671d75f01a0abd7f1a280c09 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Mon, 9 Jul 2018 12:06:44 +0200 Subject: [PATCH 01/12] refac parameters: map[string]interface{} --- application/application.go | 4 ++-- engine/engine.go | 22 +++++++++++----------- middleware/auth.go | 8 ++++---- middleware/parsers.go | 12 ++++++------ signature/signature.go | 4 ++-- util/util.go | 4 ++-- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/application/application.go b/application/application.go index 392ec719..02ed0750 100644 --- a/application/application.go +++ b/application/application.go @@ -253,7 +253,7 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) } var filepath string - parameters := c.MustGet("parameters").(map[string]string) + parameters := c.MustGet("parameters").(map[string]interface{}) var err error u, exists := c.Get("url") @@ -263,7 +263,7 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) // URL provided we use http protocol to retrieve it s := storage.SourceFromContext(c) - filepath = parameters["path"] + filepath = parameters["path"].(string) if !s.Exists(filepath) { return nil, errs.ErrFileNotExists } diff --git a/engine/engine.go b/engine/engine.go index a5387d6f..65b208df 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -12,7 +12,7 @@ import ( "github.com/thoas/picfit/image" ) -var defaultParams = map[string]string{ +var defaultParams = map[string]interface{}{ "upscale": "1", "h": "0", "w": "0", @@ -72,14 +72,14 @@ func (e Engine) String() string { return strings.Join(backendNames, " ") } -func (e Engine) Transform(img *image.ImageFile, operation Operation, qs map[string]string) (*image.ImageFile, error) { +func (e Engine) Transform(img *image.ImageFile, operation Operation, qs map[string]interface{}) (*image.ImageFile, error) { err := mergo.Merge(&qs, defaultParams) if err != nil { return nil, err } - format, ok := qs["fmt"] + format, ok := qs["fmt"].(string) filepath := img.Filepath if ok { @@ -122,7 +122,7 @@ func (e Engine) Transform(img *image.ImageFile, operation Operation, qs map[stri options.Format = formats[format] for i := range e.backends { - file.Processed, err = operate(e.backends[i], img, operation, options) + file.Processed, err = operate(e.backends[i], file, operation, options) if err == nil { break } @@ -153,9 +153,9 @@ func operate(b backend.Backend, img *image.ImageFile, operation Operation, optio } } -func newBackendOptions(e Engine, operation Operation, qs map[string]string) (*backend.Options, error) { +func newBackendOptions(e Engine, operation Operation, qs map[string]interface{}) (*backend.Options, error) { var quality int - q, ok := qs["q"] + q, ok := qs["q"].(string) if ok { quality, err := strconv.Atoi(q) @@ -170,27 +170,27 @@ func newBackendOptions(e Engine, operation Operation, qs map[string]string) (*ba quality = e.DefaultQuality } - position, ok := qs["pos"] + position, ok := qs["pos"].(string) if !ok && operation == Flip { return nil, fmt.Errorf("Parameter \"pos\" not found in query string") } - degree, err := strconv.Atoi(qs["deg"]) + degree, err := strconv.Atoi(qs["deg"].(string)) if err != nil { return nil, err } - upscale, err := strconv.ParseBool(qs["upscale"]) + upscale, err := strconv.ParseBool(qs["upscale"].(string)) if err != nil { return nil, err } - width, err := strconv.Atoi(qs["w"]) + width, err := strconv.Atoi(qs["w"].(string)) if err != nil { return nil, err } - height, err := strconv.Atoi(qs["h"]) + height, err := strconv.Atoi(qs["h"].(string)) if err != nil { return nil, err } diff --git a/middleware/auth.go b/middleware/auth.go index ccbd1a9e..dfc4513b 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -14,7 +14,7 @@ import ( func Security(secretKey string) gin.HandlerFunc { return func(c *gin.Context) { if secretKey != "" { - if !signature.VerifyParameters(secretKey, c.MustGet("parameters").(map[string]string)) { + if !signature.VerifyParameters(secretKey, c.MustGet("parameters").(map[string]interface{})) { c.String(http.StatusUnauthorized, "Invalid signature") c.Abort() return @@ -27,17 +27,17 @@ func Security(secretKey string) gin.HandlerFunc { func RestrictSizes(sizes []config.AllowedSize) gin.HandlerFunc { handler := func(c *gin.Context, sizes []config.AllowedSize) { - params := c.MustGet("parameters").(map[string]string) + params := c.MustGet("parameters").(map[string]interface{}) var w int var h int var err error - if w, err = strconv.Atoi(params["w"]); err != nil { + if w, err = strconv.Atoi(params["w"].(string)); err != nil { return } - if h, err = strconv.Atoi(params["h"]); err != nil { + if h, err = strconv.Atoi(params["h"].(string)); err != nil { return } diff --git a/middleware/parsers.go b/middleware/parsers.go index b5c19233..6ba96c44 100644 --- a/middleware/parsers.go +++ b/middleware/parsers.go @@ -26,7 +26,7 @@ func ParametersParser() gin.HandlerFunc { if result != "" { match := parametersReg.FindStringSubmatch(result) - parameters := make(map[string]string) + parameters := make(map[string]interface{}) for i, name := range parametersReg.SubexpNames() { if i != 0 && match[i] != "" { @@ -50,14 +50,14 @@ func ParametersParser() gin.HandlerFunc { // KeyParser injects an unique key from query parameters func KeyParser() gin.HandlerFunc { return func(c *gin.Context) { - var queryString map[string]string + var queryString map[string]interface{} params, exists := c.Get("parameters") if exists { - queryString = params.(map[string]string) + queryString = params.(map[string]interface{}) } else { - queryString = make(map[string]string) + queryString = make(map[string]interface{}) } for k, v := range c.Request.URL.Query() { @@ -116,9 +116,9 @@ func URLParser(mimetypeDetectorType string) gin.HandlerFunc { // OperationParser extracts the operation and add it to the context func OperationParser() gin.HandlerFunc { return func(c *gin.Context) { - parameters := c.MustGet("parameters").(map[string]string) + parameters := c.MustGet("parameters").(map[string]interface{}) - operation, ok := parameters["op"] + operation, ok := parameters["op"].(string) if !ok { c.String(http.StatusBadRequest, "`op` parameter or query string cannot be empty") diff --git a/signature/signature.go b/signature/signature.go index 4dc91fa6..1898ae99 100644 --- a/signature/signature.go +++ b/signature/signature.go @@ -12,11 +12,11 @@ import ( var signRegex = regexp.MustCompile("&?sig=[^&]*") // VerifyParameters encodes map parameters with a key and returns if parameters match signature -func VerifyParameters(key string, qs map[string]string) bool { +func VerifyParameters(key string, qs map[string]interface{}) bool { params := url.Values{} for k, v := range qs { - params.Set(k, v) + params.Set(k, v.(string)) } return VerifySign(key, params.Encode()) diff --git a/util/util.go b/util/util.go index b746e670..38afee6a 100644 --- a/util/util.go +++ b/util/util.go @@ -15,7 +15,7 @@ func MapInterfaceToMapString(obj map[string]interface{}) map[string]string { return results } -func SortMapString(obj map[string]string) map[string]string { +func SortMapString(obj map[string]interface{}) map[string]interface{} { mk := make([]string, len(obj)) i := 0 @@ -26,7 +26,7 @@ func SortMapString(obj map[string]string) map[string]string { sort.Strings(mk) - results := make(map[string]string) + results := make(map[string]interface{}) for _, index := range mk { results[index] = obj[index] From 36b36c6f26e4e5a6f8c2f9831bebea804b5f384e Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Mon, 9 Jul 2018 12:48:30 +0200 Subject: [PATCH 02/12] feat application parameters --- application/parameters.go | 16 ++++++++++++++++ engine/operations.go | 7 +++++++ 2 files changed, 23 insertions(+) create mode 100644 application/parameters.go diff --git a/application/parameters.go b/application/parameters.go new file mode 100644 index 00000000..3c9d34ef --- /dev/null +++ b/application/parameters.go @@ -0,0 +1,16 @@ +package application + +import ( + "github.com/thoas/picfit/engine" + "github.com/thoas/picfit/image" +) + +type Parameters struct { + Output *image.ImageFile + Operations []engine.EngineOperation +} + +func NewParameters(input *image.ImageFile, qs map[string]interface{}) (*Parameters, error) { + + return &Parameters{}, nil +} diff --git a/engine/operations.go b/engine/operations.go index 4a66b8b9..51358852 100644 --- a/engine/operations.go +++ b/engine/operations.go @@ -1,5 +1,7 @@ package engine +import "github.com/thoas/picfit/engine/backend" + type Operation string func (o Operation) String() string { @@ -23,3 +25,8 @@ var Operations = map[string]Operation{ Fit.String(): Fit, Noop.String(): Noop, } + +type EngineOperation struct { + Options backend.Options + Operation Operation +} From f1ef627ab0390ccb30e70f621ccbdca1ca2fd3bf Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Mon, 9 Jul 2018 15:08:50 +0200 Subject: [PATCH 03/12] feat parameters: multiple operations --- application/application.go | 3 ++- middleware/parsers.go | 34 +++++++++++++++++++++++++--------- signature/signature.go | 13 ++++++++++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/application/application.go b/application/application.go index 02ed0750..c448bc4f 100644 --- a/application/application.go +++ b/application/application.go @@ -274,7 +274,8 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) return nil, err } - op := c.MustGet("op").(engine.Operation) + operations := c.MustGet("op") + op := engine.Operation(operations.(string)) file, err = engine.FromContext(c).Transform(file, op, parameters) if err != nil { return nil, err diff --git a/middleware/parsers.go b/middleware/parsers.go index 6ba96c44..bbb447ee 100644 --- a/middleware/parsers.go +++ b/middleware/parsers.go @@ -61,6 +61,11 @@ func KeyParser() gin.HandlerFunc { } for k, v := range c.Request.URL.Query() { + if k == "op" && len(v) > 1 { + queryString[k] = v + continue + } + queryString[k] = v[0] } @@ -119,23 +124,34 @@ func OperationParser() gin.HandlerFunc { parameters := c.MustGet("parameters").(map[string]interface{}) operation, ok := parameters["op"].(string) - - if !ok { - c.String(http.StatusBadRequest, "`op` parameter or query string cannot be empty") - c.Abort() + if ok && operation != "" { + if _, k := engine.Operations[operation]; !k { + c.String(http.StatusBadRequest, fmt.Sprintf("Invalid method %s or invalid parameters", operation)) + c.Abort() + return + } + c.Set("op", operation) + c.Next() return } - op, ok := engine.Operations[operation] - - if !ok { - c.String(http.StatusBadRequest, fmt.Sprintf("Invalid method %s or invalid parameters", operation)) + operations, ok := parameters["op"].([]string) + if !ok || len(operations) == 0 { + c.String(http.StatusBadRequest, "`op` parameter or query string cannot be empty") c.Abort() return } - c.Set("op", op) + for i := range operations { + _, ok := engine.Operations[operations[i]] + if !ok { + c.String(http.StatusBadRequest, fmt.Sprintf("Invalid method %s or invalid parameters", operations[i])) + c.Abort() + return + } + } + c.Set("op", operations) c.Next() } } diff --git a/signature/signature.go b/signature/signature.go index 1898ae99..ecc21e66 100644 --- a/signature/signature.go +++ b/signature/signature.go @@ -16,7 +16,18 @@ func VerifyParameters(key string, qs map[string]interface{}) bool { params := url.Values{} for k, v := range qs { - params.Set(k, v.(string)) + s, ok := v.(string) + if ok { + params.Set(k, s) + continue + } + + l, ok := v.([]string) + if ok { + for i := range l { + params.Add(k, l[i]) + } + } } return VerifySign(key, params.Encode()) From fd9fd8e76b1796e4f3047ddfbd56c8207c60f665 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Mon, 9 Jul 2018 16:38:32 +0200 Subject: [PATCH 04/12] refac application engine --- application/application.go | 14 ++-- application/parameters.go | 149 ++++++++++++++++++++++++++++++++++++- engine/engine.go | 146 +++++------------------------------- engine/operations.go | 2 +- 4 files changed, 176 insertions(+), 135 deletions(-) diff --git a/application/application.go b/application/application.go index c448bc4f..9cb8e1de 100644 --- a/application/application.go +++ b/application/application.go @@ -253,7 +253,7 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) } var filepath string - parameters := c.MustGet("parameters").(map[string]interface{}) + qs := c.MustGet("parameters").(map[string]interface{}) var err error u, exists := c.Get("url") @@ -263,7 +263,7 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) // URL provided we use http protocol to retrieve it s := storage.SourceFromContext(c) - filepath = parameters["path"].(string) + filepath = qs["path"].(string) if !s.Exists(filepath) { return nil, errs.ErrFileNotExists } @@ -274,9 +274,13 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) return nil, err } - operations := c.MustGet("op") - op := engine.Operation(operations.(string)) - file, err = engine.FromContext(c).Transform(file, op, parameters) + e := engine.FromContext(c) + parameters, err := NewParameters(e, file, qs) + if err != nil { + return nil, err + } + + file, err = e.Transform(parameters.Output, parameters.Operations) if err != nil { return nil, err } diff --git a/application/parameters.go b/application/parameters.go index 3c9d34ef..17d948c6 100644 --- a/application/parameters.go +++ b/application/parameters.go @@ -1,16 +1,161 @@ package application import ( + "fmt" + "strconv" + + "github.com/disintegration/imaging" "github.com/thoas/picfit/engine" + "github.com/thoas/picfit/engine/backend" "github.com/thoas/picfit/image" ) +const ( + defaultUpscale = true + defaultWidth = 0 + defaultHeight = 0 + defaultDegree = 90 +) + +var formats = map[string]imaging.Format{ + "jpeg": imaging.JPEG, + "jpg": imaging.JPEG, + "png": imaging.PNG, + "gif": imaging.GIF, + "bmp": imaging.BMP, +} + type Parameters struct { Output *image.ImageFile Operations []engine.EngineOperation } -func NewParameters(input *image.ImageFile, qs map[string]interface{}) (*Parameters, error) { +func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]interface{}) (*Parameters, error) { + format, ok := qs["fmt"].(string) + filepath := input.Filepath + + if ok { + if _, ok := engine.ContentTypes[format]; !ok { + return nil, fmt.Errorf("Unknown format %s", format) + } + + } + + if format == "" && e.Format != "" { + format = e.Format + } + + if format == "" { + format = input.Format() + } + + if format == "" { + format = e.DefaultFormat + } + + if format != input.Format() { + index := len(filepath) - len(input.Format()) + + filepath = filepath[:index] + format + + if contentType, ok := engine.ContentTypes[format]; ok { + input.Headers["Content-Type"] = contentType + } + } + + output := &image.ImageFile{ + Source: input.Source, + Key: input.Key, + Headers: input.Headers, + Filepath: filepath, + } + + var operations []engine.EngineOperation + + operation, ok := qs["op"].(engine.Operation) + if ok { + opts, err := newBackendOptions(e, operation, qs) + if err != nil { + return nil, err + } + + opts.Format = formats[format] + operations = append(operations, engine.EngineOperation{ + Options: opts, + Operation: operation, + }) + } + + return &Parameters{ + Output: output, + Operations: operations, + }, nil +} + +func newBackendOptions(e *engine.Engine, operation engine.Operation, qs map[string]interface{}) (*backend.Options, error) { + var ( + err error + quality int + upscale = defaultUpscale + height = defaultHeight + width = defaultWidth + degree = defaultDegree + ) + + q, ok := qs["q"].(string) + if ok { + quality, err := strconv.Atoi(q) + + if err != nil { + return nil, err + } + + if quality > 100 { + return nil, fmt.Errorf("Quality should be <= 100") + } + } else { + quality = e.DefaultQuality + } + + position, ok := qs["pos"].(string) + if !ok && operation == engine.Flip { + return nil, fmt.Errorf("Parameter \"pos\" not found in query string") + } + + if deg, ok := qs["deg"].(string); ok { + degree, err = strconv.Atoi(deg) + if err != nil { + return nil, err + } + } + + if up, ok := qs["upscale"].(string); ok { + upscale, err = strconv.ParseBool(up) + if err != nil { + return nil, err + } + } + + if w, ok := qs["w"].(string); ok { + width, err = strconv.Atoi(w) + if err != nil { + return nil, err + } + } + + if h, ok := qs["h"].(string); ok { + height, err = strconv.Atoi(h) + if err != nil { + return nil, err + } + } - return &Parameters{}, nil + return &backend.Options{ + Width: width, + Height: height, + Upscale: upscale, + Position: position, + Quality: quality, + Degree: degree, + }, nil } diff --git a/engine/engine.go b/engine/engine.go index 65b208df..b0c0e0fb 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -2,31 +2,12 @@ package engine import ( "fmt" - "strconv" "strings" - "github.com/disintegration/imaging" - "github.com/imdario/mergo" - "github.com/thoas/picfit/engine/backend" "github.com/thoas/picfit/image" ) -var defaultParams = map[string]interface{}{ - "upscale": "1", - "h": "0", - "w": "0", - "deg": "90", -} - -var formats = map[string]imaging.Format{ - "jpeg": imaging.JPEG, - "jpg": imaging.JPEG, - "png": imaging.PNG, - "gif": imaging.GIF, - "bmp": imaging.BMP, -} - type Engine struct { DefaultFormat string Format string @@ -72,66 +53,29 @@ func (e Engine) String() string { return strings.Join(backendNames, " ") } -func (e Engine) Transform(img *image.ImageFile, operation Operation, qs map[string]interface{}) (*image.ImageFile, error) { - err := mergo.Merge(&qs, defaultParams) - - if err != nil { - return nil, err - } - - format, ok := qs["fmt"].(string) - filepath := img.Filepath - - if ok { - if _, ok := ContentTypes[format]; !ok { - return nil, fmt.Errorf("Unknown format %s", format) +func (e Engine) Transform(output *image.ImageFile, operations []EngineOperation) (*image.ImageFile, error) { + var ( + err error + processed []byte + source = output.Source + ) + for i := range operations { + for j := range e.backends { + processed, err = operate(e.backends[j], output, operations[i].Operation, operations[i].Options) + if err == nil { + output.Source = processed + break + } + if err != backend.MethodNotImplementedError { + return nil, err + } } - - } - - if format == "" && e.Format != "" { - format = e.Format - } - - if format == "" { - format = img.Format() - } - - if format == "" { - format = e.DefaultFormat - } - - if format != img.Format() { - index := len(filepath) - len(img.Format()) - - filepath = filepath[:index] + format - - if contentType, ok := ContentTypes[format]; ok { - img.Headers["Content-Type"] = contentType - } - } - - file := &image.ImageFile{ - Source: img.Source, - Key: img.Key, - Headers: img.Headers, - Filepath: filepath, } - options, err := newBackendOptions(e, operation, qs) - options.Format = formats[format] + output.Source = source + output.Processed = processed - for i := range e.backends { - file.Processed, err = operate(e.backends[i], file, operation, options) - if err == nil { - break - } - if err != backend.MethodNotImplementedError { - return nil, err - } - } - - return file, err + return output, err } func operate(b backend.Backend, img *image.ImageFile, operation Operation, options *backend.Options) ([]byte, error) { @@ -152,55 +96,3 @@ func operate(b backend.Backend, img *image.ImageFile, operation Operation, optio return nil, fmt.Errorf("Operation not found for %s", operation) } } - -func newBackendOptions(e Engine, operation Operation, qs map[string]interface{}) (*backend.Options, error) { - var quality int - q, ok := qs["q"].(string) - if ok { - quality, err := strconv.Atoi(q) - - if err != nil { - return nil, err - } - - if quality > 100 { - return nil, fmt.Errorf("Quality should be <= 100") - } - } else { - quality = e.DefaultQuality - } - - position, ok := qs["pos"].(string) - if !ok && operation == Flip { - return nil, fmt.Errorf("Parameter \"pos\" not found in query string") - } - - degree, err := strconv.Atoi(qs["deg"].(string)) - if err != nil { - return nil, err - } - - upscale, err := strconv.ParseBool(qs["upscale"].(string)) - if err != nil { - return nil, err - } - - width, err := strconv.Atoi(qs["w"].(string)) - if err != nil { - return nil, err - } - - height, err := strconv.Atoi(qs["h"].(string)) - if err != nil { - return nil, err - } - - return &backend.Options{ - Width: width, - Height: height, - Upscale: upscale, - Position: position, - Quality: quality, - Degree: degree, - }, nil -} diff --git a/engine/operations.go b/engine/operations.go index 51358852..8b9a2614 100644 --- a/engine/operations.go +++ b/engine/operations.go @@ -27,6 +27,6 @@ var Operations = map[string]Operation{ } type EngineOperation struct { - Options backend.Options + Options *backend.Options Operation Operation } From 208c240b9c41b56fbcf995af0f27e94dd5ccd329 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Mon, 9 Jul 2018 16:52:33 +0200 Subject: [PATCH 05/12] fix parameters --- application/parameters.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/parameters.go b/application/parameters.go index 17d948c6..379d8de9 100644 --- a/application/parameters.go +++ b/application/parameters.go @@ -72,8 +72,9 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter var operations []engine.EngineOperation - operation, ok := qs["op"].(engine.Operation) + op, ok := qs["op"].(string) if ok { + operation := engine.Operation(op) opts, err := newBackendOptions(e, operation, qs) if err != nil { return nil, err From 589d0eac7f17e460f58fe29fd28284a6115dda9e Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Mon, 9 Jul 2018 18:15:57 +0200 Subject: [PATCH 06/12] middleware parsers: support different urls --- application/parameters.go | 17 +++++++++++++++++ middleware/parsers.go | 17 ++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/application/parameters.go b/application/parameters.go index 379d8de9..c77736ee 100644 --- a/application/parameters.go +++ b/application/parameters.go @@ -87,6 +87,23 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter }) } + ops, ok := qs["op"].([]string) + if ok { + for i := range ops { + operation := engine.Operation(ops[i]) + opts, err := newBackendOptions(e, operation, qs) + if err != nil { + return nil, err + } + + opts.Format = formats[format] + operations = append(operations, engine.EngineOperation{ + Options: opts, + Operation: operation, + }) + } + } + return &Parameters{ Output: output, Operations: operations, diff --git a/middleware/parsers.go b/middleware/parsers.go index bbb447ee..2c08efd7 100644 --- a/middleware/parsers.go +++ b/middleware/parsers.go @@ -61,12 +61,23 @@ func KeyParser() gin.HandlerFunc { } for k, v := range c.Request.URL.Query() { - if k == "op" && len(v) > 1 { - queryString[k] = v + if k != "op" { + queryString[k] = v[0] continue } - queryString[k] = v[0] + var operations []string + op, ok := queryString[k].(string) + if ok { + operations = append(operations, op) + } + operations = append(operations, v...) + + if len(operations) > 1 { + queryString[k] = operations + } else if len(operations) == 1 { + queryString[k] = operations[0] + } } sorted := util.SortMapString(queryString) From 1c7d23e01ccbfacd8df07dce92941ea720a4d108 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Tue, 10 Jul 2018 13:16:17 +0200 Subject: [PATCH 07/12] feat parameters: newEngineOperationFromQuery --- application/parameters.go | 58 +++++++++++++++++++++++++++++++-------- middleware/parsers.go | 22 +++++++++++++-- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/application/parameters.go b/application/parameters.go index c77736ee..5509b265 100644 --- a/application/parameters.go +++ b/application/parameters.go @@ -3,6 +3,7 @@ package application import ( "fmt" "strconv" + "strings" "github.com/disintegration/imaging" "github.com/thoas/picfit/engine" @@ -75,7 +76,7 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter op, ok := qs["op"].(string) if ok { operation := engine.Operation(op) - opts, err := newBackendOptions(e, operation, qs) + opts, err := newBackendOptionsFromParameters(e, operation, qs) if err != nil { return nil, err } @@ -90,17 +91,26 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter ops, ok := qs["op"].([]string) if ok { for i := range ops { - operation := engine.Operation(ops[i]) - opts, err := newBackendOptions(e, operation, qs) - if err != nil { - return nil, err + var err error + engineOperation := &engine.EngineOperation{} + operation, k := engine.Operations[ops[i]] + if k { + engineOperation.Operation = operation + engineOperation.Options, err = newBackendOptionsFromParameters(e, operation, qs) + if err != nil { + return nil, err + } + } else { + engineOperation, err = newEngineOperationFromQuery(e, ops[i]) + if err != nil { + return nil, err + } } - opts.Format = formats[format] - operations = append(operations, engine.EngineOperation{ - Options: opts, - Operation: operation, - }) + if engineOperation != nil { + engineOperation.Options.Format = formats[format] + operations = append(operations, *engineOperation) + } } } @@ -110,7 +120,33 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter }, nil } -func newBackendOptions(e *engine.Engine, operation engine.Operation, qs map[string]interface{}) (*backend.Options, error) { +func newEngineOperationFromQuery(e *engine.Engine, op string) (*engine.EngineOperation, error) { + params := make(map[string]interface{}) + for _, p := range strings.Split(op, " ") { + l := strings.Split(p, ":") + if len(l) > 1 { + params[l[0]] = l[1] + } + } + + op, ok := params["op"].(string) + if !ok { + return nil, nil + } + + operation := engine.Operation(op) + opts, err := newBackendOptionsFromParameters(e, operation, params) + if err != nil { + return nil, err + } + + return &engine.EngineOperation{ + Options: opts, + Operation: operation, + }, nil +} + +func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operation, qs map[string]interface{}) (*backend.Options, error) { var ( err error quality int diff --git a/middleware/parsers.go b/middleware/parsers.go index 2c08efd7..c5b4a681 100644 --- a/middleware/parsers.go +++ b/middleware/parsers.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "regexp" + "strings" "github.com/gin-gonic/gin" @@ -156,9 +157,24 @@ func OperationParser() gin.HandlerFunc { for i := range operations { _, ok := engine.Operations[operations[i]] if !ok { - c.String(http.StatusBadRequest, fmt.Sprintf("Invalid method %s or invalid parameters", operations[i])) - c.Abort() - return + params := make(map[string]string) + for _, p := range strings.Split(operations[i], " ") { + l := strings.Split(p, ":") + if len(l) > 1 { + params[l[0]] = l[1] + } + } + + v, exists := params["op"] + if !exists { + c.String(http.StatusBadRequest, "`op` parameter or query string cannot be empty") + c.Abort() + return + } else if _, ok := engine.Operations[v]; !ok { + c.String(http.StatusBadRequest, fmt.Sprintf("Invalid method %s or invalid parameters", operations[i])) + c.Abort() + return + } } } From 24dd405d81a98a5e0e02c03744bfe9c17e664922 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Wed, 11 Jul 2018 15:03:45 +0200 Subject: [PATCH 08/12] feat TestDummyApplication: add different urls --- application/application_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/application/application_test.go b/application/application_test.go index 11a9f722..47c2590b 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -617,6 +617,20 @@ func TestDummyApplication(t *testing.T) { }, ContentType: "image/jpeg", }, + &TestRequest{ + URL: fmt.Sprintf("http://example.com/display?url=%s&op=op:resize+w:100+h:50&op=op:rotate+deg:90", u.String()), + Dimensions: &Dimension{ + Width: 50, + Height: 100, + }, + }, + &TestRequest{ + URL: fmt.Sprintf("http://example.com/display?url=%s&op=resize&w=100&h=50&op=op:rotate+deg:90", u.String()), + Dimensions: &Dimension{ + Width: 50, + Height: 100, + }, + }, } for _, test := range tests { From 0e53abe5a645546653b308cdca8766c2015627f0 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Wed, 11 Jul 2018 15:55:21 +0200 Subject: [PATCH 09/12] feat parsers: setParamsFromURLValues --- middleware/parsers.go | 52 +++++++++++++++++++------------------- middleware/parsers_test.go | 33 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 middleware/parsers_test.go diff --git a/middleware/parsers.go b/middleware/parsers.go index c5b4a681..09b3900c 100644 --- a/middleware/parsers.go +++ b/middleware/parsers.go @@ -51,38 +51,14 @@ func ParametersParser() gin.HandlerFunc { // KeyParser injects an unique key from query parameters func KeyParser() gin.HandlerFunc { return func(c *gin.Context) { - var queryString map[string]interface{} + queryString := make(map[string]interface{}) params, exists := c.Get("parameters") - if exists { queryString = params.(map[string]interface{}) - } else { - queryString = make(map[string]interface{}) - } - - for k, v := range c.Request.URL.Query() { - if k != "op" { - queryString[k] = v[0] - continue - } - - var operations []string - op, ok := queryString[k].(string) - if ok { - operations = append(operations, op) - } - operations = append(operations, v...) - - if len(operations) > 1 { - queryString[k] = operations - } else if len(operations) == 1 { - queryString[k] = operations[0] - } } - sorted := util.SortMapString(queryString) - + sorted := setParamsFromURLValues(queryString, c.Request.URL.Query()) delete(sorted, sigParamName) serialized := hash.Serialize(sorted) @@ -96,6 +72,30 @@ func KeyParser() gin.HandlerFunc { } } +func setParamsFromURLValues(params map[string]interface{}, values url.Values) map[string]interface{} { + for k, v := range values { + if k != "op" { + params[k] = v[0] + continue + } + + var operations []string + op, ok := params[k].(string) + if ok { + operations = append(operations, op) + } + operations = append(operations, v...) + + if len(operations) > 1 { + params[k] = operations + } else if len(operations) == 1 { + params[k] = operations[0] + } + } + + return util.SortMapString(params) +} + // URLParser extracts the url query string and add a url.URL to the context func URLParser(mimetypeDetectorType string) gin.HandlerFunc { mimetypeDetector := image.GetMimetypeDetector(mimetypeDetectorType) diff --git a/middleware/parsers_test.go b/middleware/parsers_test.go new file mode 100644 index 00000000..ae62a5a7 --- /dev/null +++ b/middleware/parsers_test.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetParamsFromURLValues(t *testing.T) { + v := url.Values{} + v.Set("w", "100") + v.Set("h", "123") + v.Set("op", "resize") + + params := setParamsFromURLValues(make(map[string]interface{}), v) + + assert.Equal(t, params["w"].(string), "100") + assert.Equal(t, params["h"].(string), "123") + assert.Equal(t, params["op"].(string), "resize") + + v.Add("op", "rotate") + v.Add("w", "99") + v.Add("h", "321") + + params = setParamsFromURLValues(make(map[string]interface{}), v) + + assert.Equal(t, params["w"].(string), "100") + assert.Equal(t, params["h"].(string), "123") + assert.Equal(t, len(params["op"].([]string)), 2) + assert.Equal(t, params["op"].([]string)[0], "resize") + assert.Equal(t, params["op"].([]string)[1], "rotate") +} From 20586b7a19f4c486720ae2280891926b871d4ceb Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Wed, 11 Jul 2018 17:21:10 +0200 Subject: [PATCH 10/12] feat application TestEngineOperationFromQuery --- application/parameters.go | 3 +-- application/parameters_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 application/parameters_test.go diff --git a/application/parameters.go b/application/parameters.go index 5509b265..a3c5dc37 100644 --- a/application/parameters.go +++ b/application/parameters.go @@ -158,8 +158,7 @@ func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operatio q, ok := qs["q"].(string) if ok { - quality, err := strconv.Atoi(q) - + quality, err = strconv.Atoi(q) if err != nil { return nil, err } diff --git a/application/parameters_test.go b/application/parameters_test.go new file mode 100644 index 00000000..edbe78eb --- /dev/null +++ b/application/parameters_test.go @@ -0,0 +1,22 @@ +package application + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/thoas/picfit/engine" +) + +func TestEngineOperationFromQuery(t *testing.T) { + op := "op:resize w:123 h:321 upscale:true pos:top q:99" + operation, err := newEngineOperationFromQuery(&engine.Engine{}, op) + assert.Nil(t, err) + + assert.Equal(t, operation.Operation.String(), "resize") + assert.Equal(t, operation.Options.Height, 321) + assert.Equal(t, operation.Options.Width, 123) + assert.Equal(t, operation.Options.Position, "top") + assert.Equal(t, operation.Options.Quality, 99) + assert.True(t, operation.Options.Upscale) +} From 9389511fdefb706ab2d17ed23e9ca2df76b789c0 Mon Sep 17 00:00:00 2001 From: Edouard Paris Date: Wed, 18 Jul 2018 10:31:25 +0200 Subject: [PATCH 11/12] update README: multiple operations --- README.rst | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b128a5eb..f2458d91 100644 --- a/README.rst +++ b/README.rst @@ -511,7 +511,7 @@ storage to your configuration file. "enable_upload": true } } - + To work properly, the input field must be named "data" Test it with the excellent httpie_: @@ -522,6 +522,23 @@ Test it with the excellent httpie_: You will retrieve the uploaded image information in ``JSON`` format. +Multiple operations +=================== + +Multiple operations can be done on the same image following a given order. + +First operation must be described as above then other operation are described in parameters ``op``. +The order of ``op`` parameters is the order used. + +Each options of the operation must be described with subparameters separed by +``:`` with the operation name as argument to ``op``. + +Example of a resize followed by a rotation: + +:: + Date: Wed, 18 Jul 2018 10:34:21 +0200 Subject: [PATCH 12/12] fix README --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f2458d91..765051a1 100644 --- a/README.rst +++ b/README.rst @@ -536,8 +536,8 @@ Each options of the operation must be described with subparameters separed by Example of a resize followed by a rotation: :: -