Skip to content

Commit c031982

Browse files
authored
feat: add endpoint for getting data about a given document (#81)
* First pass at #75 * doc: add openapi spec for doc metadata endpoint * test: add tests for HandleDocMetadata
1 parent 66c7970 commit c031982

File tree

5 files changed

+203
-1
lines changed

5 files changed

+203
-1
lines changed

go.mod

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ require (
77
github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2
88
github.com/gin-gonic/gin v1.9.1
99
github.com/glebarez/sqlite v1.10.0
10+
github.com/google/uuid v1.5.0
1011
github.com/joho/godotenv v1.5.1
12+
github.com/stretchr/testify v1.8.4
1113
gorm.io/gorm v1.25.5
1214
)
1315

1416
require (
1517
github.com/bytedance/sonic v1.10.2 // indirect
1618
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
1719
github.com/chenzhuoyu/iasm v0.9.1 // indirect
20+
github.com/davecgh/go-spew v1.1.1 // indirect
1821
github.com/dustin/go-humanize v1.0.1 // indirect
1922
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
2023
github.com/gin-contrib/sse v0.1.0 // indirect
@@ -24,7 +27,6 @@ require (
2427
github.com/go-playground/validator/v10 v10.16.0 // indirect
2528
github.com/goccy/go-json v0.10.2 // indirect
2629
github.com/google/go-cmp v0.5.9 // indirect
27-
github.com/google/uuid v1.5.0 // indirect
2830
github.com/jinzhu/inflection v1.0.0 // indirect
2931
github.com/jinzhu/now v1.1.5 // indirect
3032
github.com/json-iterator/go v1.1.12 // indirect
@@ -35,6 +37,7 @@ require (
3537
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3638
github.com/modern-go/reflect2 v1.0.2 // indirect
3739
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
40+
github.com/pmezard/go-difflib v1.0.0 // indirect
3841
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
3942
github.com/rogpeppe/go-internal v1.11.0 // indirect
4043
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

handlers/docs/handleDocMetadata.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package handlers
2+
3+
import (
4+
"errors"
5+
"log"
6+
"net/http"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/kevinanielsen/go-fast-cdn/util"
11+
12+
"github.com/gin-gonic/gin"
13+
)
14+
15+
func HandleDocMetadata(c *gin.Context) {
16+
fileName := c.Param("filename")
17+
if fileName == "" {
18+
c.JSON(http.StatusBadRequest, gin.H{
19+
"error": "Doc name is required",
20+
})
21+
return
22+
}
23+
24+
filePath := filepath.Join(util.ExPath, "uploads", "docs", fileName)
25+
stat, err := os.Stat(filePath)
26+
if err != nil {
27+
if errors.Is(err, os.ErrNotExist) {
28+
c.JSON(http.StatusNotFound, gin.H{
29+
"error": "Doc does not exist",
30+
})
31+
} else {
32+
log.Printf("Failed to get document %s: %s\n", fileName, err.Error())
33+
c.JSON(http.StatusInternalServerError, gin.H{
34+
"error": "Internal error",
35+
})
36+
}
37+
return
38+
}
39+
40+
c.JSON(http.StatusOK, gin.H{
41+
"filename": fileName,
42+
"download_url": c.Request.Host + "/api/cdn/download/docs/" + fileName,
43+
"file_size": stat.Size(),
44+
})
45+
}
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/kevinanielsen/go-fast-cdn/util"
12+
13+
"github.com/gin-gonic/gin"
14+
"github.com/google/uuid"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestHandleDocMetadata_NoError(t *testing.T) {
19+
// Arrange
20+
testFileName := uuid.NewString()
21+
testFileDir := filepath.Join(util.ExPath, "uploads", "docs")
22+
defer os.RemoveAll(filepath.Join(util.ExPath, "uploads"))
23+
err := os.MkdirAll(testFileDir, 0766)
24+
require.NoError(t, err)
25+
testFilePath := filepath.Join(testFileDir, testFileName)
26+
testFileContents := uuid.NewString()
27+
err = os.WriteFile(testFilePath, []byte(testFileContents), 0666)
28+
require.NoError(t, err)
29+
w := httptest.NewRecorder()
30+
c, _ := gin.CreateTestContext(w)
31+
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
32+
c.Params = []gin.Param{{
33+
Key: "filename",
34+
Value: testFileName,
35+
}}
36+
37+
// Act
38+
HandleDocMetadata(c)
39+
40+
// Assert
41+
require.Equal(t, http.StatusOK, w.Result().StatusCode)
42+
result := map[string]interface{}{}
43+
err = json.NewDecoder(w.Body).Decode(&result)
44+
require.NoError(t, err)
45+
require.Contains(t, result, "filename")
46+
require.Equal(t, result["filename"], testFileName)
47+
require.Contains(t, result, "download_url")
48+
require.NotEmpty(t, result["download_url"])
49+
require.Contains(t, result, "file_size")
50+
require.Equal(t, float64(len(testFileContents)), result["file_size"])
51+
}
52+
53+
func TestHandleDocMetadata_NotFound(t *testing.T) {
54+
// Arrange
55+
testFileName := uuid.NewString()
56+
w := httptest.NewRecorder()
57+
c, _ := gin.CreateTestContext(w)
58+
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
59+
c.Params = []gin.Param{{
60+
Key: "filename",
61+
Value: testFileName,
62+
}}
63+
64+
// Act
65+
HandleDocMetadata(c)
66+
67+
// Assert
68+
require.Equal(t, http.StatusNotFound, w.Result().StatusCode)
69+
result := map[string]interface{}{}
70+
err := json.NewDecoder(w.Body).Decode(&result)
71+
require.NoError(t, err)
72+
require.Contains(t, result, "error")
73+
require.Equal(t, result["error"], "Doc does not exist")
74+
}
75+
76+
func TestHandleDocMetadata_NameNotProvided(t *testing.T) {
77+
// Arrange
78+
w := httptest.NewRecorder()
79+
c, _ := gin.CreateTestContext(w)
80+
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
81+
82+
// Act
83+
HandleDocMetadata(c)
84+
85+
// Assert
86+
require.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
87+
result := map[string]interface{}{}
88+
err := json.NewDecoder(w.Body).Decode(&result)
89+
require.NoError(t, err)
90+
require.Contains(t, result, "error")
91+
require.Equal(t, result["error"], "Doc name is required")
92+
}

router/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func AddApiRoutes(r *gin.Engine) {
2222
{
2323
cdn.GET("/size", handlers.GetSizeHandler)
2424
cdn.GET("/doc/all", dHandlers.HandleAllDocs)
25+
cdn.GET("/doc/:filename", dHandlers.HandleDocMetadata)
2526
cdn.GET("/image/all", iHandlers.HandleAllImages)
2627
cdn.POST("/drop/database", dbHandlers.HandleDropDB)
2728
cdn.Static("/download/images", util.ExPath+"/uploads/images")

static/openapi.json

+61
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,67 @@
6161
}
6262
}
6363
},
64+
"/api/cdn/doc/{fileName}": {
65+
"get": {
66+
"description": "Retrieves metadata about the document",
67+
"responses": {
68+
"400": {
69+
"description": "Document filename was not provided",
70+
"content": {
71+
"application/json": {
72+
"schema": {
73+
"type": "object",
74+
"properties": {
75+
"error": { "type": "string" }
76+
}
77+
}
78+
}
79+
}
80+
},
81+
"404": {
82+
"description": "Document was not found",
83+
"content": {
84+
"application/json": {
85+
"schema": {
86+
"type": "object",
87+
"properties": {
88+
"error": { "type": "string" }
89+
}
90+
}
91+
}
92+
}
93+
},
94+
"500": {
95+
"description": "Unknown error while trying to retrieve the document",
96+
"content": {
97+
"application/json": {
98+
"schema": {
99+
"type": "object",
100+
"properties": {
101+
"error": { "type": "string" }
102+
}
103+
}
104+
}
105+
}
106+
},
107+
"200": {
108+
"description": "Metadata about the document",
109+
"content": {
110+
"application/json": {
111+
"schema": {
112+
"type": "object",
113+
"properties": {
114+
"filename": { "type": "string" },
115+
"download_url":{ "type": "string" },
116+
"file_size": { "type": "number" }
117+
}
118+
}
119+
}
120+
}
121+
}
122+
}
123+
}
124+
},
64125
"/api/cdn/image/all": {
65126
"get": {
66127
"summary": "Get all images",

0 commit comments

Comments
 (0)