package web import ( "io/fs" "log/slog" "net/http" "path" "strings" "khosra/internal/content" "khosra/internal/render" ) // serveExtras answers a bundle's supporting files: the tree, one entry selected, or an entry's raw bytes. // // The bundle is looked up first, so an unpublished bundle hides its extras exactly as it hides its assets and // its body — one rule, one place (ADR-0024). func serveExtras(w http.ResponseWriter, req *http.Request, site *content.Site, r *render.Renderer, siteFS fs.FS, res resolution) bool { if siteFS == nil { return false } key, live := site.KeyFor(res.key) if !live { return false } b, served, found := site.Lookup(key, res.lang) if !found { return false } assets, hasAssets := b.Assets() if !hasAssets { return false } entries := content.Extras(siteFS, b) if len(entries) == 0 { return false } if res.entry == "" { return renderExtras(w, r, b, served, entries, nil) } entry, ok := find(entries, res.entry) if !ok || entry.IsDir { return false } name := path.Join(assets, content.ExtrasDir, entry.Path) // ?raw is a representation of the same entry rather than a different one, which is why it is a parameter // and not another path (ADR-0047). if req.URL.Query().Has("raw") { http.ServeFileFS(w, req, siteFS, name) return true } selected := &render.Selected{Entry: entry, RawURL: content.ExtrasURL(b.Route, served, entry.Path) + "?raw"} if entry.Kind == "markdown" || entry.Kind == "text" { data, err := fs.ReadFile(siteFS, name) if err != nil { slog.Error("extras entry unreadable", "path", name, "err", err) } else if html, err := r.RenderText(entry.Kind, data); err != nil { slog.Error("extras entry unrenderable", "path", name, "err", err) } else { selected.HTML = html } } return renderExtras(w, r, b, served, entries, selected) } // renderExtras writes the listing, degrading like every other render failure. func renderExtras(w http.ResponseWriter, r *render.Renderer, b content.Bundle, served string, entries []content.Entry, selected *render.Selected) bool { out, err := r.Extras(b, served, entries, selected) if err != nil { slog.Error("extras failed", "key", b.Key, "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return true } write(w, out, b.Key+"/"+content.ExtrasDir) return true } // find locates an entry by its path within the tree. // // Chosen from the enumeration rather than probed on disk: an entry a request names has to be one the listing // showed, so a path that walks out of the tree is not found rather than refused. func find(entries []content.Entry, want string) (content.Entry, bool) { want = strings.TrimSuffix(want, "/") for _, e := range entries { if e.Path == want { return e, true } } return content.Entry{}, false }