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, "Index of %s

Index of %s

") } func main() { var ( addr = flag.String("addr", ":8080", "listen address") // Point these to the *ubuntu* directories inside your mirror volume archiveRoot = flag.String("archive", "/data/mirror/archive.ubuntu.com/ubuntu", "archive ubuntu root") securityRoot = flag.String("security", "/data/mirror/security.ubuntu.com/ubuntu", "security ubuntu root") oldReleases = flag.String("old", "/data/mirror/old-releases.ubuntu.com/ubuntu", "old-releases ubuntu root") autoIndex = flag.Bool("autoindex", true, "enable directory listings") cacheSeconds = flag.Int("cache", 600, "Cache-Control max-age seconds (0 to disable)") ) flag.Parse() roots := []http.FileSystem{} for _, p := range []string{*archiveRoot, *securityRoot, *oldReleases} { if p == "" { continue } if st, err := os.Stat(p); err == nil && st.IsDir() { roots = append(roots, http.Dir(p)) log.Printf("added root: %s", p) } else { if errors.Is(err, os.ErrNotExist) { log.Printf("warn: root does not exist (skipping): %s", p) } else if err != nil { log.Printf("warn: cannot stat %s: %v", p, err) } } } if len(roots) == 0 { log.Fatal("no valid roots found") } union := unionFS{roots: roots} handler := fileHandler{ fs: union, autoIndex: *autoIndex, cacheMaxAge: time.Duration(*cacheSeconds) * time.Second, } mux := http.NewServeMux() // Serve under /ubuntu/... to match client sources.list mux.Handle("/ubuntu/", http.StripPrefix("/ubuntu", handler)) // health endpoint mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) srv := &http.Server{ Addr: *addr, Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 10 * time.Minute, // big files IdleTimeout: 120 * time.Second, ReadHeaderTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } log.Printf("listening on %s", *addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }