diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 301802a..6cd7ff8 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -17,8 +17,9 @@ import (
)
type Handler struct {
- dir string
- tmpl *template.Template
+ dir string
+ tmpl *template.Template
+ staticFS http.FileSystem
}
// breadcrumb represents a link in the navigation chain
@@ -29,6 +30,7 @@ type breadcrumb struct {
func New(dir string) *Handler {
h := &Handler{dir: dir}
+ h.staticFS = http.FS(os.DirFS("static"))
h.tmpl = template.Must(
template.New("").Funcs(template.FuncMap{
"humanSize": humanSize,
@@ -37,7 +39,7 @@ func New(dir string) *Handler {
"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),
+ }).ParseGlob("templates/*.html"),
)
return h
}
@@ -74,9 +76,9 @@ func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
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,
@@ -106,11 +108,13 @@ func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
}
sort.Slice(items, func(i, j int) bool {
- if items[i].IsDir != items[j].IsDir { return items[i].IsDir }
+ 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{
+ h.tmpl.ExecuteTemplate(w, "index.html", map[string]any{
"Title": "ISO Library",
"Items": items,
"CurrentPath": relDir,
@@ -139,11 +143,13 @@ func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) {
}
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)
})
- h.tmpl.ExecuteTemplate(w, "browse", map[string]any{
+ h.tmpl.ExecuteTemplate(w, "browse.html", map[string]any{
"Title": filepath.Base(isoPath),
"ISOName": isoPath,
"InternalPath": internalPath,
@@ -176,7 +182,9 @@ 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"
+ }
viewable := false
switch ext {
@@ -200,6 +208,10 @@ func (h *Handler) RawFile(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filepath.Join(h.dir, fileName))
}
+func (h *Handler) ServeStatic(w http.ResponseWriter, r *http.Request) {
+ http.StripPrefix("/static/", http.FileServer(h.staticFS)).ServeHTTP(w, r)
+}
+
func (h *Handler) openISO(relPath string) (*iso.Reader, error) {
return iso.Open(filepath.Join(h.dir, relPath))
}
@@ -209,17 +221,27 @@ func parsePath(urlPath, prefix string) (isoPath, internalPath string, ok bool) {
decoded, _ := url.PathUnescape(rest)
lower := strings.ToLower(decoded)
idx := strings.Index(lower, ".iso")
- if idx == -1 { return decoded, "", true }
+ 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 }
+ if relDir == "" {
+ return crumbs
+ }
acc := ""
for _, p := range strings.Split(relDir, "/") {
- if p == "" { continue }
- if acc == "" { acc = p } else { acc = filepath.Join(acc, p) }
+ 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
@@ -233,13 +255,19 @@ func buildISOBreadcrumbs(isoPath, internalPath string) []breadcrumb {
} 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 }
+ if p == "" {
+ continue
+ }
+ if acc == "" {
+ acc = p
+ } else {
+ acc += "/" + p
+ }
crumbs = append(crumbs, breadcrumb{Name: p, URL: "/browse/" + url.PathEscape(isoPath) + "/" + acc})
}
}
@@ -247,121 +275,29 @@ func buildISOBreadcrumbs(isoPath, internalPath string) []breadcrumb {
}
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 {
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 "📄"
+ case ".iso":
+ return "💿"
+ case ".pdf":
+ return "📄"
+ case ".jpg", ".png", ".jpeg":
+ return "🖼️"
+ case ".txt", ".md":
+ return "📝"
+ default:
+ return "📄"
}
}
-
-const allTemplates = `
-{{define "css"}}
-
-{{end}}
-
-{{define "index"}}
-
-
-
{{.Title}}{{template "css" .}}
-
-
-
-
-
-
- {{range .Items}}
- {{if .IsDir}}
-
- 📁
- {{.Name}}
-
- {{else}}
-
- {{end}}
- {{end}}
-
-
-
-
-{{end}}
-
-{{define "browse"}}
-
-
-{{.Title}}{{template "css" .}}
-
-
-
-
-
- | Name | Size | Action |
-
- {{range .Entries}}
-
- |
- {{if .IsDir}}📁 {{.Name}}
- {{else}}{{fileIcon .Name}} {{.Name}}{{end}}
- |
- {{if .IsDir}}—{{else}}{{humanSize .Size}}{{end}} |
- {{if not .IsDir}}Download{{end}} |
-
- {{end}}
-
-
-
-
-
-{{end}}
-`
diff --git a/main.go b/main.go
index 0f37527..b5ed82f 100644
--- a/main.go
+++ b/main.go
@@ -25,6 +25,7 @@ func main() {
mux.HandleFunc("/browse/", h.BrowseISO)
mux.HandleFunc("/file/", h.DownloadFile)
mux.HandleFunc("/raw/", h.RawFile)
+ mux.HandleFunc("/static/", h.ServeStatic)
mux.HandleFunc("/", h.ListISOs)
fmt.Printf("ISOSilo running at %s\n", *addr)
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..9027afd
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,24 @@
+:root {
+ --bg: #0f1117; --surface: #1a1d27; --border: #252836;
+ --accent: #4f8ef7; --text: #e2e4ef; --muted: #6b7090; --radius: 8px;
+}
+body { background: var(--bg); color: var(--text); font-family: system-ui; margin: 0; }
+header { background: #12151f; border-bottom: 1px solid var(--border); padding: 0 2rem; height: 56px; display: flex; align-items: center; }
+.logo { color: var(--accent); font-weight: bold; text-decoration: none; font-family: monospace; }
+main { max-width: 1100px; margin: 0 auto; padding: 2rem; }
+.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }
+.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; text-decoration: none; color: inherit; transition: transform 0.1s; position: relative;}
+.card:hover { transform: translateY(-3px); border-color: var(--accent); }
+.card-img { width: 100%; height: 150px; object-fit: cover; background: #000; }
+.folder-icon { height: 150px; display: flex; align-items: center; justify-content: center; font-size: 4rem; background: #1c202d; color: #f7c948; }
+.iso-icon { height: 150px; display: flex; align-items: center; justify-content: center; font-size: 4rem; background: #12151f; }
+.card-body { padding: 1rem; }
+.card-name { font-weight: bold; color: var(--accent); margin-bottom: 0.5rem; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.card-desc { font-size: 0.8rem; color: var(--muted); height: 3em; overflow: hidden; }
+.bc { background: var(--surface); padding: 0.6rem 1rem; border-radius: 4px; margin-bottom: 1.5rem; font-size: 0.85rem; }
+.bc a { color: var(--accent); text-decoration: none; }
+.tbl { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: var(--radius); overflow: hidden; }
+.tbl th { text-align: left; padding: 1rem; background: #1e2132; color: var(--muted); font-size: 0.75rem; }
+.tbl td { padding: 1rem; border-bottom: 1px solid var(--border); }
+.dl-btn { border: 1px solid var(--accent); color: var(--accent); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.7rem; text-decoration: none; }
+.dl-btn:hover { background: var(--accent); color: #fff; }
diff --git a/templates/browse.html b/templates/browse.html
new file mode 100644
index 0000000..297df03
--- /dev/null
+++ b/templates/browse.html
@@ -0,0 +1,27 @@
+
+
+{{.Title}}
+
+
+
+
+
+ | Name | Size | Action |
+
+ {{range .Entries}}
+
+ |
+ {{if .IsDir}}📁 {{.Name}}
+ {{else}}{{fileIcon .Name}} {{.Name}}{{end}}
+ |
+ {{if .IsDir}}—{{else}}{{humanSize .Size}}{{end}} |
+ {{if not .IsDir}}Download{{end}} |
+
+ {{end}}
+
+
+
+
+
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..391d1a8
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,38 @@
+
+
+{{.Title}}
+
+
+
+
+
+
+ {{range .Items}}
+ {{if .IsDir}}
+
+ 📁
+ {{.Name}}
+
+ {{else}}
+
+ {{end}}
+ {{end}}
+
+
+
+