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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
package server
import (
"io/fs"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
// AllowedMIMEs maps file extensions (including dot) to Content-Type values.
// A nil map means any extension is allowed; MIME type is detected automatically.
type AllowedMIMEs map[string]string
// FileHandler returns a Gin HandlerFunc that serves files from the given root directory.
// 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
clean = strings.TrimPrefix(clean, "/")
// 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
}
// Determine MIME type
ext := filepath.Ext(fullPath)
var mimeType string
if allowed != nil {
var ok bool
mimeType, ok = allowed[ext]
if !ok {
c.AbortWithStatus(http.StatusNotFound)
return
}
} else {
mimeType = mime.TypeByExtension(ext)
if mimeType == "" {
mimeType = "application/octet-stream"
}
}
// Serve the file with proper MIME type
c.Header("Content-Type", mimeType)
c.File(fullPath)
}
// EmbedHandler serves files from an embedded (or any) fs.FS.
// root is the subdirectory within fsys to serve from.
// The gin wildcard param must be named *filepath.
func EmbedHandler(fsys fs.FS, root string) gin.HandlerFunc {
sub, err := fs.Sub(fsys, root)
if err != nil {
panic("EmbedHandler: " + err.Error())
}
fileServer := http.FileServer(http.FS(sub))
return func(c *gin.Context) {
req := c.Request.Clone(c.Request.Context())
req.URL.Path = c.Param("filepath")
fileServer.ServeHTTP(c.Writer, req)
}
}
|