package main import ( "errors" "flag" "fmt" "log" "mime" "net/http" "os" "path" "path/filepath" "strings" "time" ) // unionFS tries multiple http.FileSystem roots in order. type unionFS struct{ roots []http.FileSystem } func (u unionFS) Open(name string) (http.File, error) { for _, fs := range u.roots { f, err := fs.Open(name) if err == nil { return f, nil } } return nil, os.ErrNotExist } // fileHandler serves files from an http.FileSystem with nice directory listing type fileHandler struct { fs http.FileSystem autoIndex bool cacheMaxAge time.Duration } func (h fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Normalize path upath := r.URL.Path if !strings.HasPrefix(upath, "/") { upath = "/" + upath } upath = path.Clean(upath) // prevents path traversal // Open f, err := h.fs.Open(upath) if err != nil { http.NotFound(w, r) return } defer f.Close() fi, err := f.Stat() if err != nil { http.NotFound(w, r) return } // Directories if fi.IsDir() { // redirect to slash-terminated path (as net/http does) if !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) return } // If index.html exists, serve it indexPath := path.Join(upath, "index.html") if ff, err := h.fs.Open(indexPath); err == nil { defer ff.Close() info, _ := ff.Stat() h.serveFile(w, r, indexPath, ff, info) return } // Otherwise: simple autoindex (for debugging in browser) if h.autoIndex { h.serveDirList(w, r, f) return } http.NotFound(w, r) return } // Files h.serveFile(w, r, upath, f, fi) } func (h fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name string, f http.File, fi os.FileInfo) { // Content-Type by extension (fallback: octet-stream) ctype := mime.TypeByExtension(strings.ToLower(filepath.Ext(name))) if ctype == "" { ctype = "application/octet-stream" } w.Header().Set("Content-Type", ctype) // Conservative cache (APT macht eigene Validierungen über InRelease/Release) if h.cacheMaxAge > 0 { w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(h.cacheMaxAge.Seconds()))) } // ServeContent enables Range requests + sets Last-Modified // Use name without leading slash to avoid special-case in ServeContent http.ServeContent(w, r, strings.TrimPrefix(name, "/"), fi.ModTime(), f) } func (h fileHandler) serveDirList(w http.ResponseWriter, r *http.Request, d http.File) { entries, err := d.Readdir(-1) if err != nil { http.Error(w, "cannot read directory", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, "