ISO Library {{len .ISOs}} images
Serving from {{.Dir}}
No .iso files found in the served directory.
// Package handlers provides HTTP handler functions for the ISO server. package handlers import ( "fmt" "html/template" "io" "log" "mime" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "time" "isosilo/internal/iso" ) // Handler holds shared state for all HTTP handlers. type Handler struct { dir string tmpl *template.Template } // New creates a Handler that serves ISOs from the given directory. func New(dir string) *Handler { h := &Handler{dir: dir} h.tmpl = template.Must( template.New("").Funcs(template.FuncMap{ "humanSize": humanSize, "fileIcon": fileIcon, "urlenc": url.PathEscape, "base": filepath.Base, "add1": func(i int) int { return i + 1 }, }).Parse(allTemplates), ) return h } // ----------------------------------------------------------------------- // Route: GET / → list all ISO files in the directory // ----------------------------------------------------------------------- type isoInfo struct { Name string Size int64 ModTime time.Time } func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } entries, err := os.ReadDir(h.dir) if err != nil { http.Error(w, "cannot read directory", http.StatusInternalServerError) return } var isos []isoInfo for _, e := range entries { if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".iso") { continue } info, err := e.Info() if err != nil { continue } isos = append(isos, isoInfo{Name: e.Name(), Size: info.Size(), ModTime: info.ModTime()}) } sort.Slice(isos, func(i, j int) bool { return strings.ToLower(isos[i].Name) < strings.ToLower(isos[j].Name) }) w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := h.tmpl.ExecuteTemplate(w, "index", map[string]any{ "Title": "ISO Library", "ISOs": isos, "Dir": h.dir, }); err != nil { log.Printf("template/index error: %v", err) } } // ----------------------------------------------------------------------- // Route: GET /browse/{iso}[/{path...}] → browse inside an ISO // ----------------------------------------------------------------------- type browseData struct { Title string ISOName string InternalPath string Entries []iso.Entry Breadcrumbs []breadcrumb } func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) { isoName, internalPath, ok := parsePath(r.URL.Path, "/browse/") if !ok { http.Error(w, "bad request", http.StatusBadRequest) return } reader, err := h.openISO(isoName) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } defer reader.Close() entries, err := reader.ListDir(internalPath) if err != nil { http.Error(w, fmt.Sprintf("cannot list directory: %v", err), http.StatusNotFound) return } // Dirs first, then files; both alphabetically. sort.Slice(entries, func(i, j int) bool { if entries[i].IsDir != entries[j].IsDir { return entries[i].IsDir } return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name) }) w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := h.tmpl.ExecuteTemplate(w, "browse", browseData{ Title: fmt.Sprintf("%s — /%s", isoName, internalPath), ISOName: isoName, InternalPath: internalPath, Entries: entries, Breadcrumbs: buildBreadcrumbs(isoName, internalPath), }); err != nil { log.Printf("template/browse error: %v", err) } } // ----------------------------------------------------------------------- // Route: GET /file/{iso}/{path...} → stream a file from inside an ISO // ----------------------------------------------------------------------- func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) { isoName, internalPath, ok := parsePath(r.URL.Path, "/file/") if !ok || internalPath == "" { http.Error(w, "bad request", http.StatusBadRequest) return } reader, err := h.openISO(isoName) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } defer reader.Close() rc, size, err := reader.FileReader(internalPath) if err != nil { http.Error(w, fmt.Sprintf("cannot open file: %v", err), http.StatusNotFound) return } defer rc.Close() filename := filepath.Base(internalPath) ct := mime.TypeByExtension(filepath.Ext(filename)) if ct == "" { ct = "application/octet-stream" } w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename)) w.Header().Set("Content-Type", ct) w.Header().Set("Content-Length", fmt.Sprintf("%d", size)) if _, err := io.Copy(w, rc); err != nil { log.Printf("stream error %q from %q: %v", internalPath, isoName, err) } } // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- func (h *Handler) openISO(name string) (*iso.Reader, error) { if strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") { return nil, fmt.Errorf("invalid ISO name") } if !strings.EqualFold(filepath.Ext(name), ".iso") { return nil, fmt.Errorf("not an ISO file") } return iso.Open(filepath.Join(h.dir, name)) } // parsePath splits "/browse/myfile.iso/some/dir" → ("myfile.iso", "some/dir", true). func parsePath(urlPath, prefix string) (isoName, internalPath string, ok bool) { rest := strings.TrimPrefix(urlPath, prefix) if rest == "" { return "", "", false } decoded, err := url.PathUnescape(rest) if err != nil { return "", "", false } idx := strings.Index(decoded, "/") if idx == -1 { return decoded, "", true } return decoded[:idx], strings.TrimPrefix(decoded[idx:], "/"), true } type breadcrumb struct { Name string URL string } func buildBreadcrumbs(isoName, internalPath string) []breadcrumb { crumbs := []breadcrumb{ {Name: "Library", URL: "/"}, {Name: isoName, URL: "/browse/" + url.PathEscape(isoName)}, } if internalPath == "" { return crumbs } accumulated := "" for _, part := range strings.Split(internalPath, "/") { if part == "" { continue } if accumulated == "" { accumulated = part } else { accumulated += "/" + part } crumbs = append(crumbs, breadcrumb{ Name: part, URL: "/browse/" + url.PathEscape(isoName) + "/" + accumulated, }) } return crumbs } func humanSize(n int64) string { if n < 1024 { return fmt.Sprintf("%d B", n) } div, exp := int64(1024), 0 for v := n / 1024; v >= 1024; v /= 1024 { div *= 1024 exp++ } return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp]) } func fileIcon(name string) string { switch strings.ToLower(filepath.Ext(name)) { case ".iso", ".img", ".dmg": return "💿" case ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar": return "📦" case ".pdf": return "📄" case ".doc", ".docx", ".odt", ".txt", ".md": return "📝" case ".xls", ".xlsx", ".csv": return "📊" case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp": return "🖼️" case ".mp4", ".mkv", ".avi", ".mov", ".wmv": return "🎬" case ".mp3", ".flac", ".wav", ".ogg", ".aac": return "🎵" case ".exe", ".msi", ".deb", ".rpm", ".apk": return "⚙️" case ".sh", ".bash", ".py", ".rb", ".js", ".go", ".c", ".cpp": return "📜" default: return "📄" } } // allTemplates contains both the index and browse HTML templates. const allTemplates = ` {{define "css"}} {{end}} {{define "index"}}
Serving from {{.Dir}}
No .iso files found in the served directory.
| Name | Size | Modified | |
|---|---|---|---|
| — | {{.ModTime.Format "2006-01-02 15:04"}} | ||
{{fileIcon .Name}}{{.Name}} |
{{humanSize .Size}} | {{.ModTime.Format "2006-01-02 15:04"}} | ↓ Download |
This directory is empty.