summaryrefslogtreecommitdiffstats
path: root/internal/server/fileserver.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/server/fileserver.go')
-rw-r--r--internal/server/fileserver.go86
1 files changed, 86 insertions, 0 deletions
diff --git a/internal/server/fileserver.go b/internal/server/fileserver.go
new file mode 100644
index 0000000..04650dc
--- /dev/null
+++ b/internal/server/fileserver.go
@@ -0,0 +1,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)
+}