Skip to content

Commit 0bdf55c

Browse files
raj3kkevinanielsen
andauthored
[Feat] Add Image Metadata endpoint (#86)
* [Feat]: add endpoint for getting image metadata (#73) [Doc]: add openapi spec for image metadata endpoint [Test]: add tests for HandleImageMetadata --------- Co-authored-by: Kevin Nielsen <kevinanielsen@outlook.com>
1 parent 41e4000 commit 0bdf55c

File tree

4 files changed

+263
-0
lines changed

4 files changed

+263
-0
lines changed

handlers/image/handleImageMetadata.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package handlers
2+
3+
import (
4+
"errors"
5+
"github.com/gin-gonic/gin"
6+
"github.com/kevinanielsen/go-fast-cdn/util"
7+
"image"
8+
"log"
9+
"net/http"
10+
"os"
11+
"path/filepath"
12+
)
13+
14+
func HandleImageMetadata(c *gin.Context) {
15+
fileName := c.Param("filename")
16+
if fileName == "" {
17+
c.JSON(http.StatusBadRequest, gin.H{
18+
"error": "Image name is required",
19+
})
20+
return
21+
}
22+
23+
filePath := filepath.Join(util.ExPath, "uploads", "images", fileName)
24+
25+
if fileinfo, err := os.Stat(filePath); err == nil {
26+
if file, err := os.Open(filePath); err != nil {
27+
log.Printf("Failed to open the image %s: %s\n", fileName, err.Error())
28+
c.JSON(http.StatusInternalServerError, gin.H{
29+
"error": "Internal server error",
30+
})
31+
return
32+
} else {
33+
defer file.Close()
34+
35+
img, _, err := image.Decode(file)
36+
if err != nil {
37+
log.Printf("Failed to decode image %s: %s\n", fileName, err.Error())
38+
c.JSON(http.StatusInternalServerError, gin.H{
39+
"error": "Internal server error",
40+
})
41+
return
42+
}
43+
width := img.Bounds().Dx()
44+
height := img.Bounds().Dy()
45+
46+
body := gin.H{
47+
"filename": fileName,
48+
"download_url": c.Request.Host + "/api/cdn/download/images/" + fileName,
49+
"file_size": fileinfo.Size(),
50+
"width": width,
51+
"height": height,
52+
}
53+
54+
c.JSON(http.StatusOK, body)
55+
}
56+
} else if errors.Is(err, os.ErrNotExist) {
57+
c.JSON(http.StatusNotFound, gin.H{
58+
"error": "Image does not exist",
59+
})
60+
return
61+
} else {
62+
log.Printf("Failed to get the image %s: %s\n", fileName, err.Error())
63+
c.JSON(http.StatusInternalServerError, gin.H{
64+
"error": "Internal server error",
65+
})
66+
return
67+
}
68+
}
+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"image"
6+
"image/color"
7+
"image/jpeg"
8+
"io"
9+
"net/http"
10+
"net/http/httptest"
11+
"os"
12+
"path/filepath"
13+
"testing"
14+
15+
"github.com/gin-gonic/gin"
16+
"github.com/kevinanielsen/go-fast-cdn/util"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func TestHandleImageMetadata_NoError(t *testing.T) {
21+
// Arrange
22+
testFileName := "test_image.jpg"
23+
testFileDir := filepath.Join(util.ExPath, "uploads", "images")
24+
defer os.RemoveAll(filepath.Join(util.ExPath, "uploads"))
25+
err := os.MkdirAll(testFileDir, 0766)
26+
require.NoError(t, err)
27+
testFilePath := filepath.Join(testFileDir, testFileName)
28+
_, err = createTempImageFile(testFilePath, 512, 512)
29+
require.NoError(t, err)
30+
w := httptest.NewRecorder()
31+
c, _ := gin.CreateTestContext(w)
32+
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
33+
c.Params = []gin.Param{{
34+
Key: "filename",
35+
Value: testFileName,
36+
}}
37+
38+
// Act
39+
HandleImageMetadata(c)
40+
41+
// Assert
42+
require.Equal(t, http.StatusOK, w.Result().StatusCode)
43+
result := map[string]interface{}{}
44+
err = json.NewDecoder(w.Body).Decode(&result)
45+
require.NoError(t, err)
46+
require.Contains(t, result, "filename")
47+
require.Equal(t, result["filename"], testFileName)
48+
require.Contains(t, result, "download_url")
49+
require.NotEmpty(t, result["download_url"])
50+
require.Contains(t, result, "file_size")
51+
require.Contains(t, result, "width")
52+
require.Contains(t, result, "height")
53+
}
54+
55+
func TestHandleImageMetadata_NameNotProvided(t *testing.T) {
56+
// Arrange
57+
w := httptest.NewRecorder()
58+
c, _ := gin.CreateTestContext(w)
59+
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
60+
61+
// Act
62+
HandleImageMetadata(c)
63+
64+
// Assert
65+
require.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
66+
result := map[string]interface{}{}
67+
err := json.NewDecoder(w.Body).Decode(&result)
68+
require.NoError(t, err)
69+
require.Contains(t, result, "error")
70+
require.Equal(t, result["error"], "Image name is required")
71+
}
72+
73+
func TestHandleImageMetadata_NotFound(t *testing.T) {
74+
// Arrange
75+
testFileName := "test_file.jpg"
76+
w := httptest.NewRecorder()
77+
c, _ := gin.CreateTestContext(w)
78+
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
79+
c.Params = []gin.Param{{
80+
Key: "filename",
81+
Value: testFileName,
82+
}}
83+
84+
// Act
85+
HandleImageMetadata(c)
86+
87+
// Assert
88+
require.Equal(t, http.StatusNotFound, w.Result().StatusCode)
89+
result := map[string]interface{}{}
90+
err := json.NewDecoder(w.Body).Decode(&result)
91+
require.NoError(t, err)
92+
require.Contains(t, result, "error")
93+
require.Equal(t, result["error"], "Image does not exist")
94+
}
95+
96+
// Helper functions
97+
func EncodeImage(w io.Writer, img image.Image) error {
98+
return jpeg.Encode(w, img, &jpeg.Options{Quality: jpeg.DefaultQuality})
99+
}
100+
101+
func createDummyImage(width, height int) (image.Image, error) {
102+
// Create a simple black image
103+
img := image.NewRGBA(image.Rect(0, 0, width, height))
104+
for y := 0; y < height; y++ {
105+
for x := 0; x < width; x++ {
106+
img.Set(x, y, color.RGBA{0, 0, 0, 255})
107+
}
108+
}
109+
110+
return img, nil
111+
}
112+
113+
func createTempImageFile(name string, width, height int) (*os.File, error) {
114+
img, err := createDummyImage(width, height)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
tempFile, err := os.Create(name)
120+
if err != nil {
121+
return nil, err
122+
}
123+
defer tempFile.Close()
124+
125+
err = EncodeImage(tempFile, img)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
return tempFile, nil
131+
}

router/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func AddApiRoutes(r *gin.Engine) {
2424
cdn.GET("/doc/all", dHandlers.HandleAllDocs)
2525
cdn.GET("/doc/:filename", dHandlers.HandleDocMetadata)
2626
cdn.GET("/image/all", iHandlers.HandleAllImages)
27+
cdn.GET("/image/:filename", iHandlers.HandleImageMetadata)
2728
cdn.POST("/drop/database", dbHandlers.HandleDropDB)
2829
cdn.Static("/download/images", util.ExPath+"/uploads/images")
2930
cdn.Static("/download/docs", util.ExPath+"/uploads/docs")

static/openapi.json

+63
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,69 @@
122122
}
123123
}
124124
},
125+
"/api/cdn/image/{filename}": {
126+
"get": {
127+
"summary": "Retrieves metadata about the image",
128+
"responses": {
129+
"200": {
130+
"description": "Metadata about the image",
131+
"content": {
132+
"application/json": {
133+
"schema": {
134+
"type": "object",
135+
"properties": {
136+
"filename": { "type": "string" },
137+
"download_url":{ "type": "string" },
138+
"file_size": { "type": "number" },
139+
"width": { "type": "number" },
140+
"height": { "type": "number" }
141+
}
142+
}
143+
}
144+
}
145+
},
146+
"500": {
147+
"description": "Unknown error while trying to retrieve the image",
148+
"content": {
149+
"application/json": {
150+
"schema": {
151+
"type": "object",
152+
"properties": {
153+
"error": { "type": "string" }
154+
}
155+
}
156+
}
157+
}
158+
},
159+
"404": {
160+
"description": "Image was not found",
161+
"content": {
162+
"application/json": {
163+
"schema": {
164+
"type": "object",
165+
"properties": {
166+
"error": { "type": "string" }
167+
}
168+
}
169+
}
170+
}
171+
},
172+
"400": {
173+
"description": "Image filename was not provided",
174+
"content": {
175+
"application/json": {
176+
"schema": {
177+
"type": "object",
178+
"properties": {
179+
"error": { "type": "string" }
180+
}
181+
}
182+
}
183+
}
184+
}
185+
}
186+
}
187+
},
125188
"/api/cdn/image/all": {
126189
"get": {
127190
"summary": "Get all images",

0 commit comments

Comments
 (0)