package web import ( "io/fs" "net/http" "path" "strings" "khosra/internal/content" ) // serveAsset answers a file that lives inside a bundle's own directory, reporting whether it handled the // request. // // This is how a relative `src` in a body actually resolves: a page at /art/monsoon/ links `10-first.jpg`, // the browser asks for /art/monsoon/10-first.jpg, and the bytes come from that bundle's directory. // // The bundle is looked up *first*, and the file is only then read from the directory that bundle owns — never // from a path assembled out of the request. That ordering is what ADR-0024 requires: every byte inside a // bundle inherits the bundle's publish status, so no route may serve bundle bytes by path alone. When drafts // arrive (queue 19) the filter belongs here, beside the lookup, and nowhere else. func serveAsset(w http.ResponseWriter, req *http.Request, site *content.Site, siteFS fs.FS, res resolution) bool { if siteFS == nil { return false } dir, file := path.Split(res.key) route := strings.TrimSuffix(dir, "/") switch { case route == "" || file == "": return false case strings.Contains(file, "/") || file == "." || file == "..": return false case strings.HasSuffix(strings.ToLower(file), ".md"): // A .md inside a bundle is content or a fragment, never an asset. Serving it raw would publish the // partials an author never addressed (content-model.md). return false } key, live := site.KeyFor(route) if !live { return false } b, _, found := site.Lookup(key, res.lang) if !found { return false } assets, hasAssets := b.Assets() if !hasAssets { return false } name := path.Join(assets, file) if info, err := fs.Stat(siteFS, name); err != nil || info.IsDir() { return false } // ServeFileFS handles the content type, conditional requests and ranges — an image wants all three, and // none of them is this engine's business to reimplement. http.ServeFileFS(w, req, siteFS, name) return true }