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) }