Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add Image Metadata endpoint #86

Merged
merged 7 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions handlers/image/handleImageMetadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package handlers

import (
"errors"
"github.com/gin-gonic/gin"
"github.com/kevinanielsen/go-fast-cdn/util"
"image"
"log"
"net/http"
"os"
"path/filepath"
)

func HandleImageMetadata(c *gin.Context) {
fileName := c.Param("filename")
if fileName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Image name is required",
})
return
}

filePath := filepath.Join(util.ExPath, "uploads", "images", fileName)

if fileinfo, err := os.Stat(filePath); err == nil {
if file, err := os.Open(filePath); err != nil {
log.Printf("Failed to open the image %s: %s\n", fileName, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
})
return
} else {
defer file.Close()

img, _, err := image.Decode(file)
if err != nil {
log.Printf("Failed to decode image %s: %s\n", fileName, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
})
return
}
width := img.Bounds().Dx()
height := img.Bounds().Dy()

body := gin.H{
"filename": fileName,
"download_url": c.Request.Host + "/api/cdn/download/images/" + fileName,
"file_size": fileinfo.Size(),
"width": width,
"height": height,
}

c.JSON(http.StatusOK, body)
}
} else if errors.Is(err, os.ErrNotExist) {
c.JSON(http.StatusNotFound, gin.H{
"error": "Image does not exist",
})
return
} else {
log.Printf("Failed to get the image %s: %s\n", fileName, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
})
return
}
}
131 changes: 131 additions & 0 deletions handlers/image/handleImageMetadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package handlers

import (
"encoding/json"
"image"
"image/color"
"image/jpeg"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/gin-gonic/gin"
"github.com/kevinanielsen/go-fast-cdn/util"
"github.com/stretchr/testify/require"
)

func TestHandleImageMetadata_NoError(t *testing.T) {
// Arrange
testFileName := "test_image.jpg"
testFileDir := filepath.Join(util.ExPath, "uploads", "images")
defer os.RemoveAll(filepath.Join(util.ExPath, "uploads"))
err := os.MkdirAll(testFileDir, 0766)
require.NoError(t, err)
testFilePath := filepath.Join(testFileDir, testFileName)
_, err = createTempImageFile(testFilePath, 512, 512)
require.NoError(t, err)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.Params = []gin.Param{{
Key: "filename",
Value: testFileName,
}}

// Act
HandleImageMetadata(c)

// Assert
require.Equal(t, http.StatusOK, w.Result().StatusCode)
result := map[string]interface{}{}
err = json.NewDecoder(w.Body).Decode(&result)
require.NoError(t, err)
require.Contains(t, result, "filename")
require.Equal(t, result["filename"], testFileName)
require.Contains(t, result, "download_url")
require.NotEmpty(t, result["download_url"])
require.Contains(t, result, "file_size")
require.Contains(t, result, "width")
require.Contains(t, result, "height")
}

func TestHandleImageMetadata_NameNotProvided(t *testing.T) {
// Arrange
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)

// Act
HandleImageMetadata(c)

// Assert
require.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
result := map[string]interface{}{}
err := json.NewDecoder(w.Body).Decode(&result)
require.NoError(t, err)
require.Contains(t, result, "error")
require.Equal(t, result["error"], "Image name is required")
}

func TestHandleImageMetadata_NotFound(t *testing.T) {
// Arrange
testFileName := "test_file.jpg"
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.Params = []gin.Param{{
Key: "filename",
Value: testFileName,
}}

// Act
HandleImageMetadata(c)

// Assert
require.Equal(t, http.StatusNotFound, w.Result().StatusCode)
result := map[string]interface{}{}
err := json.NewDecoder(w.Body).Decode(&result)
require.NoError(t, err)
require.Contains(t, result, "error")
require.Equal(t, result["error"], "Image does not exist")
}

// Helper functions
func EncodeImage(w io.Writer, img image.Image) error {
return jpeg.Encode(w, img, &jpeg.Options{Quality: jpeg.DefaultQuality})
}

func createDummyImage(width, height int) (image.Image, error) {
// Create a simple black image
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.RGBA{0, 0, 0, 255})
}
}

return img, nil
}

func createTempImageFile(name string, width, height int) (*os.File, error) {
img, err := createDummyImage(width, height)
if err != nil {
return nil, err
}

tempFile, err := os.Create(name)
if err != nil {
return nil, err
}
defer tempFile.Close()

err = EncodeImage(tempFile, img)
if err != nil {
return nil, err
}

return tempFile, nil
}
1 change: 1 addition & 0 deletions router/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func AddApiRoutes(r *gin.Engine) {
cdn.GET("/doc/all", dHandlers.HandleAllDocs)
cdn.GET("/doc/:filename", dHandlers.HandleDocMetadata)
cdn.GET("/image/all", iHandlers.HandleAllImages)
cdn.GET("/image/:filename", iHandlers.HandleImageMetadata)
cdn.POST("/drop/database", dbHandlers.HandleDropDB)
cdn.Static("/download/images", util.ExPath+"/uploads/images")
cdn.Static("/download/docs", util.ExPath+"/uploads/docs")
Expand Down
63 changes: 63 additions & 0 deletions static/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,69 @@
}
}
},
"/api/cdn/image/{filename}": {
"get": {
"summary": "Retrieves metadata about the image",
"responses": {
"200": {
"description": "Metadata about the image",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"filename": { "type": "string" },
"download_url":{ "type": "string" },
"file_size": { "type": "number" },
"width": { "type": "number" },
"height": { "type": "number" }
}
}
}
}
},
"500": {
"description": "Unknown error while trying to retrieve the image",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": { "type": "string" }
}
}
}
}
},
"404": {
"description": "Image was not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": { "type": "string" }
}
}
}
}
},
"400": {
"description": "Image filename was not provided",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": { "type": "string" }
}
}
}
}
}
}
}
},
"/api/cdn/image/all": {
"get": {
"summary": "Get all images",
Expand Down