summaryrefslogtreecommitdiffstats
path: root/internal/server/fileserver.go
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-07 00:23:24 +0200
committerivar <i@oiee.no>2026-04-07 00:23:24 +0200
commit85920b8c7a2696115d1f77c046f48f6f00d639f1 (patch)
tree14ed2043796eadd6ed5b0a95c55e38e48713d638 /internal/server/fileserver.go
downloadiblog-85920b8c7a2696115d1f77c046f48f6f00d639f1.tar.xz
iblog-85920b8c7a2696115d1f77c046f48f6f00d639f1.zip
Init
Diffstat (limited to 'internal/server/fileserver.go')
-rw-r--r--internal/server/fileserver.go111
1 files changed, 111 insertions, 0 deletions
diff --git a/internal/server/fileserver.go b/internal/server/fileserver.go
new file mode 100644
index 0000000..d231519
--- /dev/null
+++ b/internal/server/fileserver.go
@@ -0,0 +1,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)
+ }
+}