1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
package server
import (
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
// AllowedMIMEs maps file extensions (including dot) to Content-Type values.
type AllowedMIMEs map[string]string
// FileHandler returns a Gin HandlerFunc that serves files from the given root directory.
// It validates file extensions against the allowed map and rejects directories.
// The wildcard param name is *filepath.
func FileHandler(root string, allowed AllowedMIMEs) gin.HandlerFunc {
return func(c *gin.Context) {
filepath := c.Param("filepath")
serveFile(c, root, filepath, allowed)
}
}
// PublicFileHandler returns a Gin HandlerFunc that serves files using the request URL path.
// Used for NoRoute fallback to serve public static files.
func PublicFileHandler(root string, allowed AllowedMIMEs) gin.HandlerFunc {
return func(c *gin.Context) {
filepath := c.Request.URL.Path
serveFile(c, root, filepath, allowed)
}
}
func serveFile(c *gin.Context, root, requestPath string, allowed AllowedMIMEs) {
// Clean the path to prevent directory traversal
clean := path.Clean(requestPath)
if strings.Contains(clean, "..") {
c.AbortWithStatus(http.StatusNotFound)
return
}
// Remove leading slash for filepath.Join
if strings.HasPrefix(clean, "/") {
clean = clean[1:]
}
// Map empty path or directory to index.html (clean URLs)
if clean == "" || clean == "." {
clean = "index.html"
}
fullPath := filepath.Join(root, clean)
// Check file existence; if directory, try index.html inside it
info, err := os.Stat(fullPath)
if err == nil && info.IsDir() {
fullPath = filepath.Join(fullPath, "index.html")
info, err = os.Stat(fullPath)
}
if err != nil {
// Try appending .html for clean URLs (e.g. /about -> /about.html)
htmlPath := fullPath + ".html"
if info2, err2 := os.Stat(htmlPath); err2 == nil && !info2.IsDir() {
fullPath = htmlPath
info = info2
err = nil
}
}
if err != nil || info.IsDir() {
c.AbortWithStatus(http.StatusNotFound)
return
}
// Check file extension is allowed
ext := filepath.Ext(fullPath)
mimeType, ok := allowed[ext]
if !ok {
c.AbortWithStatus(http.StatusNotFound)
return
}
// Serve the file with proper MIME type
c.Header("Content-Type", mimeType)
c.File(fullPath)
}
|