diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 2a5ef60..af3df85 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -1,4 +1,3 @@
-// Package handlers provides HTTP handler functions for the ISO server.
package handlers
import (
@@ -18,13 +17,11 @@ import (
"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(
@@ -40,101 +37,98 @@ func New(dir string) *Handler {
return h
}
-// -----------------------------------------------------------------------
-// Route: GET / → list all ISO files in the directory
-// -----------------------------------------------------------------------
-
-type isoInfo struct {
- Name string
- Size int64
- ModTime time.Time
- Description string
- HasImage bool
- ImageExt string
+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) {
- if r.URL.Path != "/" {
- http.NotFound(w, r)
+ // The path after / is the directory we are browsing in the library
+ relDir := strings.TrimPrefix(r.URL.Path, "/")
+ fullPath := filepath.Join(h.dir, relDir)
+
+ // Security check
+ if strings.Contains(relDir, "..") {
+ http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
- entries, err := os.ReadDir(h.dir)
+ entries, err := os.ReadDir(fullPath)
if err != nil {
- http.Error(w, "cannot read directory", http.StatusInternalServerError)
+ http.Error(w, "Directory not found", http.StatusNotFound)
return
}
- var isos []isoInfo
+ var items []libraryEntry
for _, e := range entries {
- if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".iso") {
- continue
- }
- info, err := e.Info()
- if err != nil {
- continue
+ 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(),
}
- baseName := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name()))
-
- // Look for Description (.txt)
- desc, _ := os.ReadFile(filepath.Join(h.dir, baseName+".txt"))
-
- // Look for Image (.png, .jpg, .jpeg)
- hasImg := false
- imgExt := ""
- for _, ext := range []string{".png", ".jpg", ".jpeg"} {
- if _, err := os.Stat(filepath.Join(h.dir, baseName+ext)); err == nil {
- hasImg = true
- imgExt = ext
- break
+ if isISO {
+ basePath := filepath.Join(h.dir, relDir, strings.TrimSuffix(name, ".iso"))
+ // Metadata
+ if d, err := os.ReadFile(basePath + ".txt"); err == nil {
+ item.Description = string(d)
+ }
+ // Cover Image
+ for _, ext := range []string{".png", ".jpg", ".jpeg"} {
+ if _, err := os.Stat(basePath + ext); err == nil {
+ item.HasImage = true
+ item.ImageExt = ext
+ break
+ }
}
}
- isos = append(isos, isoInfo{
- Name: e.Name(),
- Size: info.Size(),
- ModTime: info.ModTime(),
- Description: string(desc),
- HasImage: hasImg,
- ImageExt: imgExt,
- })
+ // Only show Directories or ISO files
+ if item.IsDir || item.IsISO {
+ items = append(items, item)
+ }
}
- sort.Slice(isos, func(i, j int) bool {
- return strings.ToLower(isos[i].Name) < strings.ToLower(isos[j].Name)
+ 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)
})
- 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)
- }
+ h.tmpl.ExecuteTemplate(w, "index", map[string]any{
+ "Title": "ISO Library",
+ "Items": items,
+ "CurrentPath": relDir,
+ "Breadcrumbs": buildLibraryBreadcrumbs(relDir),
+ })
}
-// -----------------------------------------------------------------------
-// Route: GET /browse/{iso}[/{path...}] → browse inside an ISO
-// -----------------------------------------------------------------------
-
-type browseData struct {
- Title string
- ISOName string
- InternalPath string
- Entries []iso.Entry
- Breadcrumbs []breadcrumb
-}
+// ... BrowseISO, DownloadFile, and RawFile remain largely same as previous version ...
+// ... Ensure BrowseISO and DownloadFile use the full relative path to open the ISO ...
func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) {
- isoName, internalPath, ok := parsePath(r.URL.Path, "/browse/")
+ isoPath, internalPath, ok := parsePath(r.URL.Path, "/browse/")
if !ok {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
- reader, err := h.openISO(isoName)
+ reader, err := h.openISO(isoPath)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
@@ -143,41 +137,32 @@ func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) {
entries, err := reader.ListDir(internalPath)
if err != nil {
- http.Error(w, fmt.Sprintf("cannot list directory: %v", err), http.StatusNotFound)
+ 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
- }
+ 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)
- }
+ h.tmpl.ExecuteTemplate(w, "browse", map[string]any{
+ "Title": filepath.Base(isoPath),
+ "ISOName": isoPath,
+ "InternalPath": internalPath,
+ "Entries": entries,
+ "Breadcrumbs": buildISOBreadcrumbs(isoPath, internalPath),
+ })
}
-// -----------------------------------------------------------------------
-// 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/")
+ isoPath, internalPath, ok := parsePath(r.URL.Path, "/file/")
if !ok || internalPath == "" {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
- reader, err := h.openISO(isoName)
+ reader, err := h.openISO(isoPath)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
@@ -186,7 +171,7 @@ func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
rc, size, err := reader.FileReader(internalPath)
if err != nil {
- http.Error(w, fmt.Sprintf("cannot open file: %v", err), http.StatusNotFound)
+ http.Error(w, "file not found", http.StatusNotFound)
return
}
defer rc.Close()
@@ -194,14 +179,11 @@ func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(internalPath)
ext := strings.ToLower(filepath.Ext(filename))
ct := mime.TypeByExtension(ext)
- if ct == "" {
- ct = "application/octet-stream"
- }
+ if ct == "" { ct = "application/octet-stream" }
- // Determine if browser can/should view it inline
viewable := false
switch ext {
- case ".pdf", ".txt", ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mp3", ".webp", ".svg":
+ case ".pdf", ".txt", ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mp3", ".webp":
viewable = true
}
@@ -213,124 +195,69 @@ func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
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)
- }
+ io.Copy(w, rc)
}
-// -----------------------------------------------------------------------
-// Route: GET /raw/{path...} → serve actual files from the disk
-// -----------------------------------------------------------------------
-
func (h *Handler) RawFile(w http.ResponseWriter, r *http.Request) {
fileName := strings.TrimPrefix(r.URL.Path, "/raw/")
- if fileName == "" || strings.Contains(fileName, "..") {
- http.Error(w, "invalid path", http.StatusBadRequest)
- return
- }
-
- fullPath := filepath.Join(h.dir, fileName)
- http.ServeFile(w, r, fullPath)
+ http.ServeFile(w, r, filepath.Join(h.dir, fileName))
}
-// -----------------------------------------------------------------------
-// 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))
+func (h *Handler) openISO(relPath string) (*iso.Reader, error) {
+ return iso.Open(filepath.Join(h.dir, relPath))
}
-func parsePath(urlPath, prefix string) (isoName, internalPath string, ok bool) {
+func parsePath(urlPath, prefix string) (isoPath, 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
+ 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
}
-type breadcrumb struct {
- Name string
- URL string
+func buildLibraryBreadcrumbs(relDir string) []breadcrumb {
+ crumbs := []breadcrumb{{Name: "Library", URL: "/"}}
+ if relDir == "" { return crumbs }
+ acc := ""
+ for _, p := range strings.Split(relDir, "/") {
+ if p == "" { continue }
+ acc = filepath.Join(acc, p)
+ crumbs = append(crumbs, breadcrumb{Name: p, URL: "/" + filepath.ToSlash(acc)})
+ }
+ return crumbs
}
-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
+func buildISOBreadcrumbs(isoPath, internalPath string) []breadcrumb {
+ crumbs := buildLibraryBreadcrumbs(filepath.Dir(isoPath))
+ if crumbs[len(crumbs)-1].Name == "." { crumbs = crumbs[:len(crumbs)-1] }
+
+ 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})
}
- 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)
- }
+ 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++
- }
+ 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 "📄"
+ ext := strings.ToLower(filepath.Ext(name))
+ switch ext {
+ case ".iso": return "💿"
+ case ".pdf": return "📄"
+ case ".jpg", ".png": return "🖼️"
+ default: return "📄"
}
}
@@ -338,294 +265,96 @@ const allTemplates = `
{{define "css"}}
{{end}}
-{{define "index"}}
-
-
-
-
- {{.Title}}
- {{template "css" .}}
-
+{{define "index"}}
+
+
+{{.Title}}{{template "css" .}}
- ISO Library {{len .ISOs}} images
- Serving from {{.Dir}}
+
- {{if .ISOs}}
- {{range .ISOs}}
-
- {{if .HasImage}}
-
}}{{.ImageExt}})
+ {{range .Items}}
+ {{if .IsDir}}
+
+ 📁
+ {{.Name}}
+
{{else}}
-
💿
- {{end}}
-
-
{{.Name}}
-
{{if .Description}}{{.Description}}{{else}}No description available.{{end}}
-
- {{humanSize .Size}}
- {{.ModTime.Format "2006-01-02"}}
-
-
+ {{end}}
{{end}}
- {{else}}
-
No .iso files found in the served directory.
- {{end}}
{{end}}
-{{define "browse"}}
-
-
-
-
-
{{.Title}}
- {{template "css" .}}
-
+{{define "browse"}}
+
+
+
{{.Title}}{{template "css" .}}
-
-
-
- {{if .InternalPath}}📁 {{base .InternalPath}}{{else}}💿 {{.ISOName}}{{end}}
- {{len .Entries}} items
-
-
- {{if .Entries}}
-
-
- | Name |
- Size |
- Modified |
- |
-
-
+ | Name | Size | Action |
- {{range .Entries}}
- {{if .IsDir}}
-
- |
- — |
- {{.ModTime.Format "2006-01-02 15:04"}} |
- |
-
- {{else}}
+ {{range .Entries}}
- |
- {{humanSize .Size}} |
- {{.ModTime.Format "2006-01-02 15:04"}} |
- ↓ Download |
+
+ {{if .IsDir}}📁 {{.Name}}
+ {{else}}{{fileIcon .Name}} {{.Name}}{{end}}
+ |
+ {{if .IsDir}}—{{else}}{{humanSize .Size}}{{end}} |
+ {{if not .IsDir}}Download{{end}} |
{{end}}
- {{end}}
- {{else}}
-
- {{end}}
-
diff --git a/main.go b/main.go
index fad61bc..99c2a6a 100644
--- a/main.go
+++ b/main.go
@@ -11,33 +11,23 @@ import (
)
func main() {
- dir := flag.String("dir", ".", "Directory containing ISO files to serve")
- addr := flag.String("addr", ":8080", "Address to listen on")
+ dir := flag.String("dir", ".", "Directory to serve")
+ addr := flag.String("addr", ":8080", "Address")
flag.Parse()
- info, err := os.Stat(*dir)
- if err != nil {
- log.Fatalf("Cannot access directory %q: %v", *dir, err)
- }
- if !info.IsDir() {
- log.Fatalf("%q is not a directory", *dir)
+ if info, err := os.Stat(*dir); err != nil || !info.IsDir() {
+ log.Fatalf("Invalid directory: %s", *dir)
}
mux := http.NewServeMux()
h := handlers.New(*dir)
- // Routes:
- // GET / → list all ISOs in the directory
- // GET /browse/{iso} → list root of ISO
- // GET /browse/{iso}/{path...} → list directory inside ISO
- // GET /file/{iso}/{path...} → stream/view a file from inside ISO
- // GET /raw/{filename} → serve raw disk files (ISOs, covers, descriptions)
- mux.HandleFunc("/", h.ListISOs)
+ // Routes
mux.HandleFunc("/browse/", h.BrowseISO)
mux.HandleFunc("/file/", h.DownloadFile)
mux.HandleFunc("/raw/", h.RawFile)
+ mux.HandleFunc("/", h.ListISOs) // Catch-all for directory navigation
- fmt.Printf("ISOSilo listening on %s\n", *addr)
- fmt.Printf("Serving ISOs from: %s\n", *dir)
+ fmt.Printf("ISOSilo running at %s\n", *addr)
log.Fatal(http.ListenAndServe(*addr, mux))
}