diff --git a/.gitignore b/.gitignore index 1b198d3..b1127a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -resource_manager \ No newline at end of file +resource_manager +.idea/ diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..f9afdbb --- /dev/null +++ b/api/api.go @@ -0,0 +1,179 @@ +package api + +import ( + "bytes" + "encoding/base64" + "git.bbr-dev.info/brajkovic/resource_manager/domain/resize" + "git.bbr-dev.info/brajkovic/resource_manager/domain/resource" + "github.com/gin-gonic/gin" + "log" + "net/http" + "strings" + "time" +) + +type UploadRequest struct { + Content string `json:"content"` + Path string `json:"path"` + Properties struct { + Overwrite bool `json:"overwrite"` + MimeType string `json:"mimeType"` + } `json:"properties"` + Resize *struct { + Width int `json:"width"` + Height int `json:"height"` + Type string `json:"type"` + } `json:"resize"` +} + +func NoMethod() gin.HandlerFunc { + return func(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{ + "status": 404, + "created": time.Now(), + "message": "no handler for method '" + c.Request.Method + "'", + }) + } +} + +func NoRoute() gin.HandlerFunc { + return func(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{ + "status": 404, + "created": time.Now(), + "message": "no handler for " + c.Request.Method + " '" + c.Request.URL.RequestURI() + "'", + }) + } +} + +func HandleUpload(resourceManager resource.Manager) gin.HandlerFunc { + return func(c *gin.Context) { + var request UploadRequest + if err := c.BindJSON(&request); err != nil { + c.AbortWithStatusJSON(400, gin.H{"error": "bad request"}) + return + } + content, err := base64.StdEncoding.DecodeString(request.Content) + if err != nil { + c.AbortWithStatusJSON(400, gin.H{"error": "bad request"}) + return + } + if resize.IsResizable(readMimeType(request.Path, request.Properties.MimeType)) && request.Resize != nil { + content, err = resize.ResizeImage(content, resize.Resize{ + Height: request.Resize.Height, + Width: request.Resize.Width, + Type: mapResizeType(request.Resize.Type), + }) + if err != nil { + c.AbortWithStatusJSON(400, gin.H{"error": "bad request"}) + return + } + } + resourceManager.Upload(resource.UploadRequest{ + Buffer: bytes.NewBuffer(content), + Path: request.Path, + MimeType: request.Properties.MimeType, + Overwrite: request.Properties.Overwrite, + }) + // we return this as success + c.Status(http.StatusNoContent) + } +} + +func HandleCopy(resourceManager resource.Manager) gin.HandlerFunc { + return func(c *gin.Context) { + from := c.Query("from") + to := c.Query("to") + overwrite := c.Query("overwrite") == "true" + if err := resourceManager.Copy(from, to, overwrite); err != nil { + log.Println(err) + c.AbortWithStatus(500) + } else { + c.Status(201) + } + + } +} + +func HandlePresign(resourceManager resource.Manager) gin.HandlerFunc { + return func(c *gin.Context) { + path := c.Query("path") + url, err := resourceManager.Presign(c, path) + if err != nil { + c.AbortWithStatus(404) + return + } + if c.Query("redirect") == "true" { + c.Redirect(http.StatusTemporaryRedirect, url) + } else { + c.JSON(200, gin.H{"url": url}) + } + } +} + +func HandleDownload(resourceManager resource.Manager) gin.HandlerFunc { + return func(c *gin.Context) { + path := c.Query("path") + data, err := resourceManager.Download(c, path) + if err == nil { + c.Header("content-disposition", "inline; filename=\""+filename(path)+"\"") + c.Data(200, readMimeType(path, ""), data) + } else { + c.AbortWithStatus(http.StatusNotFound) + } + } +} + +func HandleDelete(resourceManager resource.Manager) gin.HandlerFunc { + return func(c *gin.Context) { + path := c.Query("path") + if err := resourceManager.Delete(path); err != nil { + c.AbortWithError(400, err) + } else { + c.Status(204) + } + } +} + +var mimeTypes = map[string]string{ + "jpg": "image/jpg", + "jpeg": "image/jpeg", + "png": "image/png", + "gif": "image/gif", + "json": "application/json", + "pdf": "application/pdf", + "html": "text/html", + "xml": "application/xml", +} + +func readMimeType(path string, mimeType string) string { + if mimeType != "" { + return mimeType + } + parts := strings.Split(path, ".") + fileType := strings.ToLower(parts[len(parts)-1]) + if value, exists := mimeTypes[fileType]; exists { + return value + } + return "application/octet-stream" +} + +func mapResizeType(resizeType string) resize.ResizeType { + switch resizeType { + case "cover": + return resize.Cover + case "contain": + return resize.Contain + case "exact_height": + return resize.ExactHeight + case "exact_width": + return resize.ExactWidth + default: + return resize.Exact + } +} + +func filename(path string) string { + substrings := strings.Split(path, "/") + return substrings[len(substrings)-1] +} diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 0000000..a7d6de3 --- /dev/null +++ b/api/routes.go @@ -0,0 +1,50 @@ +package api + +import ( + "git.bbr-dev.info/brajkovic/resource_manager/domain/cache" + "git.bbr-dev.info/brajkovic/resource_manager/domain/resource" + "github.com/gin-gonic/gin" + "log" + "os" + "time" +) + +func SetupServer() *gin.Engine { + server := createServer() + RegisterRoutes(server) + return server +} + +func createServer() *gin.Engine { + server := gin.New() + server.NoRoute(NoRoute()) + server.NoMethod(NoMethod()) + server.Use(gin.Recovery()) + return server +} + +func RegisterRoutes(server *gin.Engine) { + cacheManager := cache.NewManager() + expiration := loadExpiration() + + log.Println("Presign | expiration set to " + expiration.String()) + + resourceManager := resource.NewManager(cacheManager, expiration) + + server.POST("/api/v1/upload", HandleUpload(resourceManager)) + server.GET("/api/v1/download", HandleDownload(resourceManager)) + server.GET("/api/v1/presign", HandlePresign(resourceManager)) + server.PUT("/api/v1/copy", HandleCopy(resourceManager)) + server.DELETE("/api/v1/delete", HandleDelete(resourceManager)) +} + +func loadExpiration() time.Duration { + if value := os.Getenv("PRESIGN_DURATION"); value != "" { + duration, err := time.ParseDuration(value) + if err != nil { + return duration + } + } + // default duration + return 1 * time.Hour +} diff --git a/cache/impl.go b/domain/cache/impl.go similarity index 100% rename from cache/impl.go rename to domain/cache/impl.go diff --git a/cache/manager.go b/domain/cache/manager.go similarity index 100% rename from cache/manager.go rename to domain/cache/manager.go diff --git a/domain/resize/resize.go b/domain/resize/resize.go new file mode 100644 index 0000000..840be43 --- /dev/null +++ b/domain/resize/resize.go @@ -0,0 +1,105 @@ +package resize + +import ( + "bytes" + "github.com/anthonynsimon/bild/transform" + "image" + "image/jpeg" + "image/png" + "io" +) + +var mimeTypes = map[string]string{ + "image/jpg": "image/jpg", + "image/jpeg": "image/jpeg", + "image/png": "image/png", +} + +func IsResizable(mimeType string) bool { + _, present := mimeTypes[mimeType] + return present +} + +type ResizeType int + +const ( + // Cover - resize preserving image aspect + // - resizes to the smallest image where both width and height are larger or equal to given size + Cover ResizeType = iota + // Contain - resize preserving image aspect + // - resizes to the largest image where both width and height are smaller or equal to given size + Contain ResizeType = iota + // ExactHeight - resize preserving image aspect + // - resizes to the image with given height + ExactHeight ResizeType = iota + // ExactWidth - resize preserving image aspect + // - resizes to the image with given width + ExactWidth ResizeType = iota + // Exact - resize without preserving image aspect + // - resizes to exact size defined in request + Exact ResizeType = iota +) + +type Resize struct { + Height int + Width int + Type ResizeType +} + +func ResizeImage(imageBytes []byte, resize Resize) ([]byte, error) { + img, format, err := image.Decode(bytes.NewReader(imageBytes)) + if err != nil { + return nil, err + } + var buffer bytes.Buffer + writer := io.Writer(&buffer) + + resizeWidth, resizeHeight := calculateResizedDimensions(img.Bounds(), resize) + + img = transform.Resize(img, resizeWidth, resizeHeight, transform.Gaussian) + switch format { + case "png": + err = png.Encode(writer, img) + case "jpeg": + err = jpeg.Encode(writer, img, nil) + default: + err = jpeg.Encode(writer, img, nil) + } + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func calculateResizedDimensions(bounds image.Rectangle, resize Resize) (width int, height int) { + width = bounds.Dx() + height = bounds.Dy() + + switch resize.Type { + case Cover: + rWidth := resize.Height * width / height + if rWidth >= resize.Width { + return rWidth, resize.Height + } + rHeight := resize.Width * height / width + if rHeight >= resize.Height { + return resize.Width, rHeight + } + case Contain: + rWidth := resize.Height * width / height + if rWidth <= resize.Width { + return rWidth, resize.Height + } + rHeight := resize.Width * height / width + if rHeight <= resize.Height { + return resize.Width, rHeight + } + case ExactHeight: + rWidth := resize.Height * width / height + return rWidth, resize.Height + case ExactWidth: + rHeight := resize.Width * height / width + return resize.Width, rHeight + } + return +} diff --git a/resource/impl.go b/domain/resource/impl.go similarity index 95% rename from resource/impl.go rename to domain/resource/impl.go index 2d43fdf..6b0ffad 100644 --- a/resource/impl.go +++ b/domain/resource/impl.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "errors" - "git.bbr-dev.info/brajkovic/resource_manager/cache" + "git.bbr-dev.info/brajkovic/resource_manager/domain/cache" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" @@ -39,11 +39,20 @@ type fileManager struct { func (f *fileManager) Upload(request UploadRequest) { fullPath := filepath.Join(f.path, request.Path) createFolder(fullPath) + if checkFileExists(fullPath) && !request.Overwrite { + log.Println("Manager | cannot upload file as file on same path already exists") + return + } log.Println("Manager | uploading to (" + request.Path + ")") - if err := ioutil.WriteFile(fullPath, request.Buffer.Bytes(), 0o644); err != nil { + if err := os.WriteFile(fullPath, request.Buffer.Bytes(), 0o644); err != nil { log.Println("Manager | failed uploading (" + request.Path + ") cause: " + err.Error()) } } + +func checkFileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} func (f *fileManager) Download(ctx context.Context, path string) (file []byte, err error) { fullPath := filepath.Join(f.path, path) file, err = ioutil.ReadFile(fullPath) diff --git a/resource/manager.go b/domain/resource/manager.go similarity index 92% rename from resource/manager.go rename to domain/resource/manager.go index e2f8b33..bbd61bb 100644 --- a/resource/manager.go +++ b/domain/resource/manager.go @@ -3,7 +3,7 @@ package resource import ( "bytes" "context" - "git.bbr-dev.info/brajkovic/resource_manager/cache" + "git.bbr-dev.info/brajkovic/resource_manager/domain/cache" "log" "os" "strings" diff --git a/handlers.go b/handlers.go deleted file mode 100644 index b36a025..0000000 --- a/handlers.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "git.bbr-dev.info/brajkovic/resource_manager/resource" - "github.com/gin-gonic/gin" - "log" - "net/http" - "strings" -) - -type LegacySave struct { - Content string `json:"content"` - Path string `json:"path"` - Properties struct { - Height int `json:"height"` - Overwrite bool `json:"overwrite"` - MimeType string `json:"mimeType"` - } `json:"properties"` -} - -func HandleLegacySave(resourceManager resource.Manager) gin.HandlerFunc { - return func(c *gin.Context) { - var legacySave LegacySave - if err := c.ShouldBindJSON(&legacySave); err == nil { - // removing image/(png/jpeg/...); start - if strings.HasPrefix(legacySave.Content, "data:") { - legacySave.Content = strings.Split(legacySave.Content, ";")[1] - } - if imageBytes, err := base64.StdEncoding.DecodeString(legacySave.Content); err == nil { - if legacySave.Properties.Height > 0 { - imageBytes, err = resizeImage(imageBytes, legacySave.Properties.Height) - } - mimeType := readMimeType(legacySave.Path, legacySave.Properties.MimeType) - if err == nil { - // request is sent to uplader service after which it is being uploaded - resourceManager.Upload(resource.UploadRequest{ - Buffer: bytes.NewBuffer(imageBytes), - Path: legacySave.Path, - MimeType: mimeType, - }) - // we return this as success - c.Status(http.StatusNoContent) - } else { - c.AbortWithStatusJSON(400, gin.H{"error": "bad request"}) - } - } else { - c.AbortWithStatusJSON(400, gin.H{"error": "bad request"}) - } - } else { - c.AbortWithStatusJSON(400, gin.H{"error": "bad request"}) - } - } -} - -func HandleCopy(resourceManager resource.Manager) gin.HandlerFunc { - return func(c *gin.Context) { - from := c.Query("from") - to := c.Query("to") - overwrite := c.Query("overwrite") == "true" - if err := resourceManager.Copy(from, to, overwrite); err != nil { - log.Println(err) - c.AbortWithStatus(500) - } else { - c.Status(201) - } - - } -} - -func HandlePresign(resourceManager resource.Manager) gin.HandlerFunc { - return func(c *gin.Context) { - path := c.Query("path") - url, err := resourceManager.Presign(c, path) - if err != nil { - c.AbortWithStatus(404) - return - } - if c.Query("redirect") == "true" { - c.Redirect(http.StatusTemporaryRedirect, url) - } else { - c.JSON(200, url) - } - } -} - -func HandleGet(resourceManager resource.Manager) gin.HandlerFunc { - return func(c *gin.Context) { - path := c.Query("path") - data, err := resourceManager.Download(c, path) - if err == nil { - c.Data(200, readMimeType(path, ""), data) - } else { - c.AbortWithStatus(http.StatusNotFound) - } - } -} - -func HandleDelete(resourceManager resource.Manager) gin.HandlerFunc { - return func(c *gin.Context) { - path := c.Query("path") - if err := resourceManager.Delete(path); err != nil { - c.AbortWithError(400, err) - } else { - c.Status(204) - } - } -} diff --git a/image.go b/image.go deleted file mode 100644 index 09436cf..0000000 --- a/image.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "bytes" - "github.com/anthonynsimon/bild/transform" - "image" - "image/jpeg" - "image/png" - "io" - "strings" -) - -var mimeTypes = map[string]string{ - "jpg": "image/jpg", - "jpeg": "image/jpeg", - "png": "image/png", - "json": "application/json", - "html": "text/html", - "xml": "application/xml", -} - -func readMimeType(path string, mimeType string) string { - if mimeType != "" { - return mimeType - } - parts := strings.Split(path, ".") - fileType := parts[len(parts)-1] - if value, exists := mimeTypes[fileType]; exists { - return value - } - return "application/octet-stream" -} - -func resizeImage(imageBytes []byte, resizeHeight int) ([]byte, error) { - img, format, err := image.Decode(bytes.NewReader(imageBytes)) - if err != nil { - return nil, err - } - var buffer bytes.Buffer - writer := io.Writer(&buffer) - resizeWidth := img.Bounds().Dx() * resizeHeight / img.Bounds().Dy() - img = transform.Resize(img, resizeWidth, resizeHeight, transform.Gaussian) - switch format { - case "png": - err = png.Encode(writer, img) - break - case "jpeg": - err = jpeg.Encode(writer, img, nil) - break - default: - err = jpeg.Encode(writer, img, nil) - } - if err != nil { - return nil, err - } - return buffer.Bytes(), nil -} diff --git a/main.go b/main.go index 3452e37..87e4000 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,10 @@ package main import ( - "git.bbr-dev.info/brajkovic/resource_manager/cache" - "git.bbr-dev.info/brajkovic/resource_manager/resource" - "github.com/gin-gonic/gin" + "git.bbr-dev.info/brajkovic/resource_manager/api" "github.com/joho/godotenv" "log" "net/http" - "os" - "strings" - "time" ) func init() { @@ -18,42 +13,6 @@ func init() { } func main() { - cacheManager := cache.NewManager() - expiration := loadExpiration() - log.Println("Presign | expiration set to " + expiration.String()) - - resourceManager := resource.NewManager(cacheManager, expiration) - - server := gin.Default() - if strings.Contains(os.Getenv("PROFILE"), "legacy") { - setupLegacyEndpoints(server, resourceManager) - } - setupV1Endpoints(server, resourceManager) + server := api.SetupServer() log.Fatalln(http.ListenAndServe(":5201", server)) } - -func loadExpiration() time.Duration { - if value := os.Getenv("PRESIGN_DURATION"); value != "" { - duration, err := time.ParseDuration(value) - if err != nil { - return duration - } - } - // default duration - return 1 * time.Hour -} - -func setupLegacyEndpoints(server *gin.Engine, resourceManager resource.Manager) { - server.POST("/save", HandleLegacySave(resourceManager)) - server.GET("/get", HandleGet(resourceManager)) - server.GET("/presign", HandlePresign(resourceManager)) - server.PUT("/copy", HandleCopy(resourceManager)) -} - -func setupV1Endpoints(server *gin.Engine, resourceManager resource.Manager) { - server.POST("/api/v1/save", HandleLegacySave(resourceManager)) - server.GET("/api/v1/get", HandleGet(resourceManager)) - server.GET("/api/v1/presign", HandlePresign(resourceManager)) - server.PUT("/api/v1/copy", HandleCopy(resourceManager)) - server.DELETE("/api/v1/delete", HandleDelete(resourceManager)) -}