package handlers import ( "fmt" "html/template" "io" "mime" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "time" "isosilo/internal/iso" ) type Handler struct { dir string tmpl *template.Template } // breadcrumb represents a link in the navigation chain type breadcrumb struct { Name string URL string } 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 }, "trimExt": func(s string) string { return strings.TrimSuffix(s, filepath.Ext(s)) }, }).Parse(allTemplates), ) return h } type libraryEntry struct { Name string RelativePath string IsDir bool IsISO bool Size int64 ModTime time.Time Description string HasImage bool ImageExt string } func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) { relDir := strings.TrimPrefix(r.URL.Path, "/") fullPath := filepath.Join(h.dir, relDir) if strings.Contains(relDir, "..") { http.Error(w, "Invalid path", http.StatusBadRequest) return } entries, err := os.ReadDir(fullPath) if err != nil { http.Error(w, "Directory not found", http.StatusNotFound) return } var items []libraryEntry for _, e := range entries { name := e.Name() relPath := filepath.ToSlash(filepath.Join(relDir, name)) info, _ := e.Info() isISO := !e.IsDir() && strings.EqualFold(filepath.Ext(name), ".iso") item := libraryEntry{ Name: name, RelativePath: relPath, IsDir: e.IsDir(), IsISO: isISO, ModTime: info.ModTime(), Size: info.Size(), } if isISO { basePath := filepath.Join(h.dir, relDir, strings.TrimSuffix(name, ".iso")) if d, err := os.ReadFile(basePath + ".txt"); err == nil { item.Description = string(d) } for _, ext := range []string{".png", ".jpg", ".jpeg"} { if _, err := os.Stat(basePath + ext); err == nil { item.HasImage = true item.ImageExt = ext break } } } if item.IsDir || item.IsISO { items = append(items, item) } } sort.Slice(items, func(i, j int) bool { if items[i].IsDir != items[j].IsDir { return items[i].IsDir } return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name) }) h.tmpl.ExecuteTemplate(w, "index", map[string]any{ "Title": "ISO Library", "Items": items, "CurrentPath": relDir, "Breadcrumbs": buildLibraryBreadcrumbs(relDir), }) } func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) { isoPath, internalPath, ok := parsePath(r.URL.Path, "/browse/") if !ok { http.Error(w, "bad request", http.StatusBadRequest) return } reader, err := h.openISO(isoPath) 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, "cannot list directory", http.StatusNotFound) return } 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) }) h.tmpl.ExecuteTemplate(w, "browse", map[string]any{ "Title": filepath.Base(isoPath), "ISOName": isoPath, "InternalPath": internalPath, "Entries": entries, "Breadcrumbs": buildISOBreadcrumbs(isoPath, internalPath), }) } func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) { isoPath, internalPath, ok := parsePath(r.URL.Path, "/file/") if !ok || internalPath == "" { http.Error(w, "bad request", http.StatusBadRequest) return } reader, err := h.openISO(isoPath) 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, "file not found", http.StatusNotFound) return } defer rc.Close() filename := filepath.Base(internalPath) ext := strings.ToLower(filepath.Ext(filename)) ct := mime.TypeByExtension(ext) if ct == "" { ct = "application/octet-stream" } viewable := false switch ext { case ".pdf", ".txt", ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mp3", ".webp": viewable = true } if !viewable { w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename)) } else { w.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename=%q`, filename)) } w.Header().Set("Content-Type", ct) w.Header().Set("Content-Length", fmt.Sprintf("%d", size)) io.Copy(w, rc) } func (h *Handler) RawFile(w http.ResponseWriter, r *http.Request) { fileName := strings.TrimPrefix(r.URL.Path, "/raw/") http.ServeFile(w, r, filepath.Join(h.dir, fileName)) } func (h *Handler) openISO(relPath string) (*iso.Reader, error) { return iso.Open(filepath.Join(h.dir, relPath)) } func parsePath(urlPath, prefix string) (isoPath, internalPath string, ok bool) { rest := strings.TrimPrefix(urlPath, prefix) decoded, _ := url.PathUnescape(rest) lower := strings.ToLower(decoded) idx := strings.Index(lower, ".iso") if idx == -1 { return decoded, "", true } return decoded[:idx+4], strings.TrimPrefix(decoded[idx+4:], "/"), true } func buildLibraryBreadcrumbs(relDir string) []breadcrumb { crumbs := []breadcrumb{{Name: "Library", URL: "/"}} if relDir == "" { return crumbs } acc := "" for _, p := range strings.Split(relDir, "/") { if p == "" { continue } if acc == "" { acc = p } else { acc = filepath.Join(acc, p) } crumbs = append(crumbs, breadcrumb{Name: p, URL: "/" + filepath.ToSlash(acc)}) } return crumbs } func buildISOBreadcrumbs(isoPath, internalPath string) []breadcrumb { parentDir := filepath.Dir(isoPath) var crumbs []breadcrumb if parentDir == "." { crumbs = []breadcrumb{{Name: "Library", URL: "/"}} } else { crumbs = buildLibraryBreadcrumbs(parentDir) } crumbs = append(crumbs, breadcrumb{Name: filepath.Base(isoPath), URL: "/browse/" + url.PathEscape(isoPath)}) if internalPath != "" { acc := "" for _, p := range strings.Split(internalPath, "/") { if p == "" { continue } if acc == "" { acc = p } else { acc += "/" + p } crumbs = append(crumbs, breadcrumb{Name: p, URL: "/browse/" + url.PathEscape(isoPath) + "/" + acc}) } } 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 { ext := strings.ToLower(filepath.Ext(name)) switch ext { case ".iso": return "💿" case ".pdf": return "📄" case ".jpg", ".png", ".jpeg": return "🖼️" case ".txt", ".md": return "📝" default: return "📄" } } const allTemplates = ` {{define "css"}} {{end}} {{define "index"}}
| Name | Size | Action |
|---|---|---|
| {{if .IsDir}}📁 {{.Name}} {{else}}{{fileIcon .Name}} {{.Name}}{{end}} | {{if .IsDir}}—{{else}}{{humanSize .Size}}{{end}} | {{if not .IsDir}}Download{{end}} |