From a6355e7a6530af3335c4cd8af05f1e9c8b978169 Mon Sep 17 00:00:00 2001 From: ivar Date: Sat, 4 Apr 2026 16:34:46 +0200 Subject: . --- internal/server/fileserver.go | 86 +++++++++++++++++++++++++++++++++++++++++++ internal/server/frontpage.go | 86 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 internal/server/fileserver.go create mode 100644 internal/server/frontpage.go (limited to 'internal/server') 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) +} diff --git a/internal/server/frontpage.go b/internal/server/frontpage.go new file mode 100644 index 0000000..47a766a --- /dev/null +++ b/internal/server/frontpage.go @@ -0,0 +1,86 @@ +package server + +import ( + "html/template" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "nebbet.no/internal/db" +) + +type FrontpageHandler struct { + DB *db.MetaDB + SearchDB *db.SearchDB + Templates *template.Template +} + +type frontpageData struct { + Posts []db.PostRecord + Query string + ActiveTag string +} + +func NewFrontpageHandler(metaDB *db.MetaDB, searchDB *db.SearchDB, tmpl *template.Template) *FrontpageHandler { + return &FrontpageHandler{DB: metaDB, SearchDB: searchDB, Templates: tmpl} +} + +func (h *FrontpageHandler) Serve(c *gin.Context) { + query := strings.TrimSpace(c.Query("q")) + tag := strings.TrimSpace(c.Query("tag")) + + data := frontpageData{ + Query: query, + ActiveTag: tag, + } + + if query != "" { + // Full-text search + results, err := h.SearchDB.Search(query) + if err == nil { + data.Posts = searchResultsToPosts(results, h.DB) + } + } else { + posts, err := h.DB.ListPosts(false) // exclude drafts + if err == nil { + if tag != "" { + posts = filterByTag(posts, tag) + } + data.Posts = posts + } + } + + c.Header("Content-Type", "text/html; charset=utf-8") + if err := h.Templates.ExecuteTemplate(c.Writer, "index.html", data); err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + } +} + +func filterByTag(posts []db.PostRecord, tag string) []db.PostRecord { + var filtered []db.PostRecord + for _, p := range posts { + for _, t := range p.Tags { + if strings.EqualFold(t, tag) { + filtered = append(filtered, p) + break + } + } + } + return filtered +} + +func searchResultsToPosts(results []db.SearchResult, metaDB *db.MetaDB) []db.PostRecord { + var posts []db.PostRecord + for _, r := range results { + slug := strings.TrimPrefix(r.Path, "/") + if slug == r.Path { + continue // not a post + } + post, err := metaDB.GetPost(slug) + if err != nil || post.Draft { + continue + } + posts = append(posts, *post) + } + return posts +} -- cgit v1.3