moved html and css out to seperate files.

This commit is contained in:
visionmercer 2026-04-15 09:08:14 +02:00
commit f9cfd3fc3b
5 changed files with 152 additions and 126 deletions

View file

@ -17,8 +17,9 @@ import (
) )
type Handler struct { type Handler struct {
dir string dir string
tmpl *template.Template tmpl *template.Template
staticFS http.FileSystem
} }
// breadcrumb represents a link in the navigation chain // breadcrumb represents a link in the navigation chain
@ -29,6 +30,7 @@ type breadcrumb struct {
func New(dir string) *Handler { func New(dir string) *Handler {
h := &Handler{dir: dir} h := &Handler{dir: dir}
h.staticFS = http.FS(os.DirFS("static"))
h.tmpl = template.Must( h.tmpl = template.Must(
template.New("").Funcs(template.FuncMap{ template.New("").Funcs(template.FuncMap{
"humanSize": humanSize, "humanSize": humanSize,
@ -37,7 +39,7 @@ func New(dir string) *Handler {
"base": filepath.Base, "base": filepath.Base,
"add1": func(i int) int { return i + 1 }, "add1": func(i int) int { return i + 1 },
"trimExt": func(s string) string { return strings.TrimSuffix(s, filepath.Ext(s)) }, "trimExt": func(s string) string { return strings.TrimSuffix(s, filepath.Ext(s)) },
}).Parse(allTemplates), }).ParseGlob("templates/*.html"),
) )
return h return h
} }
@ -74,9 +76,9 @@ func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
name := e.Name() name := e.Name()
relPath := filepath.ToSlash(filepath.Join(relDir, name)) relPath := filepath.ToSlash(filepath.Join(relDir, name))
info, _ := e.Info() info, _ := e.Info()
isISO := !e.IsDir() && strings.EqualFold(filepath.Ext(name), ".iso") isISO := !e.IsDir() && strings.EqualFold(filepath.Ext(name), ".iso")
item := libraryEntry{ item := libraryEntry{
Name: name, Name: name,
RelativePath: relPath, RelativePath: relPath,
@ -106,11 +108,13 @@ func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
} }
sort.Slice(items, func(i, j int) bool { 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) 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", "Title": "ISO Library",
"Items": items, "Items": items,
"CurrentPath": relDir, "CurrentPath": relDir,
@ -139,11 +143,13 @@ func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) {
} }
sort.Slice(entries, func(i, j int) bool { 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) 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), "Title": filepath.Base(isoPath),
"ISOName": isoPath, "ISOName": isoPath,
"InternalPath": internalPath, "InternalPath": internalPath,
@ -176,7 +182,9 @@ func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(internalPath) filename := filepath.Base(internalPath)
ext := strings.ToLower(filepath.Ext(filename)) ext := strings.ToLower(filepath.Ext(filename))
ct := mime.TypeByExtension(ext) ct := mime.TypeByExtension(ext)
if ct == "" { ct = "application/octet-stream" } if ct == "" {
ct = "application/octet-stream"
}
viewable := false viewable := false
switch ext { 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)) 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) { func (h *Handler) openISO(relPath string) (*iso.Reader, error) {
return iso.Open(filepath.Join(h.dir, relPath)) 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) decoded, _ := url.PathUnescape(rest)
lower := strings.ToLower(decoded) lower := strings.ToLower(decoded)
idx := strings.Index(lower, ".iso") 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 return decoded[:idx+4], strings.TrimPrefix(decoded[idx+4:], "/"), true
} }
func buildLibraryBreadcrumbs(relDir string) []breadcrumb { func buildLibraryBreadcrumbs(relDir string) []breadcrumb {
crumbs := []breadcrumb{{Name: "Library", URL: "/"}} crumbs := []breadcrumb{{Name: "Library", URL: "/"}}
if relDir == "" { return crumbs } if relDir == "" {
return crumbs
}
acc := "" acc := ""
for _, p := range strings.Split(relDir, "/") { for _, p := range strings.Split(relDir, "/") {
if p == "" { continue } if p == "" {
if acc == "" { acc = p } else { acc = filepath.Join(acc, p) } continue
}
if acc == "" {
acc = p
} else {
acc = filepath.Join(acc, p)
}
crumbs = append(crumbs, breadcrumb{Name: p, URL: "/" + filepath.ToSlash(acc)}) crumbs = append(crumbs, breadcrumb{Name: p, URL: "/" + filepath.ToSlash(acc)})
} }
return crumbs return crumbs
@ -233,13 +255,19 @@ func buildISOBreadcrumbs(isoPath, internalPath string) []breadcrumb {
} else { } else {
crumbs = buildLibraryBreadcrumbs(parentDir) crumbs = buildLibraryBreadcrumbs(parentDir)
} }
crumbs = append(crumbs, breadcrumb{Name: filepath.Base(isoPath), URL: "/browse/" + url.PathEscape(isoPath)}) crumbs = append(crumbs, breadcrumb{Name: filepath.Base(isoPath), URL: "/browse/" + url.PathEscape(isoPath)})
if internalPath != "" { if internalPath != "" {
acc := "" acc := ""
for _, p := range strings.Split(internalPath, "/") { for _, p := range strings.Split(internalPath, "/") {
if p == "" { continue } if p == "" {
if acc == "" { acc = p } else { acc += "/" + p } continue
}
if acc == "" {
acc = p
} else {
acc += "/" + p
}
crumbs = append(crumbs, breadcrumb{Name: p, URL: "/browse/" + url.PathEscape(isoPath) + "/" + acc}) 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 { 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 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]) return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp])
} }
func fileIcon(name string) string { func fileIcon(name string) string {
ext := strings.ToLower(filepath.Ext(name)) ext := strings.ToLower(filepath.Ext(name))
switch ext { switch ext {
case ".iso": return "💿" case ".iso":
case ".pdf": return "📄" return "💿"
case ".jpg", ".png", ".jpeg": return "🖼️" case ".pdf":
case ".txt", ".md": return "📝" return "📄"
default: return "📄" case ".jpg", ".png", ".jpeg":
return "🖼️"
case ".txt", ".md":
return "📝"
default:
return "📄"
} }
} }
const allTemplates = `
{{define "css"}}
<style>
: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; }
</style>
{{end}}
{{define "index"}}
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title>{{template "css" .}}</head>
<body>
<header><a class="logo" href="/">💿 ISOSilo</a></header>
<main>
<nav class="bc">
{{range $i, $c := .Breadcrumbs}}<a href="{{$c.URL}}">{{$c.Name}}</a> {{if lt (add1 $i) (len $.Breadcrumbs)}}/{{end}} {{end}}
</nav>
<div class="grid">
{{range .Items}}
{{if .IsDir}}
<a href="/{{urlenc .RelativePath}}" class="card">
<div class="folder-icon">📁</div>
<div class="card-body"><span class="card-name">{{.Name}}</span></div>
</a>
{{else}}
<div class="card">
<a href="/browse/{{urlenc .RelativePath}}">
{{if .HasImage}}<img src="/raw/{{urlenc (trimExt .RelativePath)}}{{.ImageExt}}" class="card-img">
{{else}}<div class="iso-icon">💿</div>{{end}}
</a>
<div class="card-body">
<a href="/browse/{{urlenc .RelativePath}}" class="card-name">{{.Name}}</a>
<p class="card-desc">{{if .Description}}{{.Description}}{{else}}ISO Disk Image{{end}}</p>
<div style="margin-top:1rem; display:flex; gap:0.5rem;">
<a href="/browse/{{urlenc .RelativePath}}" class="dl-btn">Browse</a>
<a href="/raw/{{urlenc .RelativePath}}" class="dl-btn" download>ISO</a>
</div>
</div>
</div>
{{end}}
{{end}}
</div>
</main>
</body>
</html>
{{end}}
{{define "browse"}}
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title>{{template "css" .}}</head>
<body>
<header><a class="logo" href="/">💿 ISOSilo</a></header>
<main>
<nav class="bc">
{{range $i, $c := .Breadcrumbs}}<a href="{{$c.URL}}">{{$c.Name}}</a> {{if lt (add1 $i) (len $.Breadcrumbs)}}/{{end}} {{end}}
</nav>
<table class="tbl">
<thead><tr><th>Name</th><th>Size</th><th>Action</th></tr></thead>
<tbody>
{{range .Entries}}
<tr>
<td>
{{if .IsDir}}📁 <a href="/browse/{{urlenc $.ISOName}}/{{urlenc .Path}}" style="color:var(--text); text-decoration:none;">{{.Name}}</a>
{{else}}{{fileIcon .Name}} <a href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" target="_blank" style="color:var(--text); text-decoration:none;">{{.Name}}</a>{{end}}
</td>
<td>{{if .IsDir}}{{else}}{{humanSize .Size}}{{end}}</td>
<td>{{if not .IsDir}}<a href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" class="dl-btn" download>Download</a>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</main>
</body>
</html>
{{end}}
`

View file

@ -25,6 +25,7 @@ func main() {
mux.HandleFunc("/browse/", h.BrowseISO) mux.HandleFunc("/browse/", h.BrowseISO)
mux.HandleFunc("/file/", h.DownloadFile) mux.HandleFunc("/file/", h.DownloadFile)
mux.HandleFunc("/raw/", h.RawFile) mux.HandleFunc("/raw/", h.RawFile)
mux.HandleFunc("/static/", h.ServeStatic)
mux.HandleFunc("/", h.ListISOs) mux.HandleFunc("/", h.ListISOs)
fmt.Printf("ISOSilo running at %s\n", *addr) fmt.Printf("ISOSilo running at %s\n", *addr)

24
static/css/style.css Normal file
View file

@ -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; }

27
templates/browse.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title><link rel="stylesheet" href="/static/css/style.css"></head>
<body>
<header><a class="logo" href="/">💿 ISOSilo</a></header>
<main>
<nav class="bc">
{{range $i, $c := .Breadcrumbs}}<a href="{{$c.URL}}">{{$c.Name}}</a> {{if lt (add1 $i) (len $.Breadcrumbs)}}/{{end}} {{end}}
</nav>
<table class="tbl">
<thead><tr><th>Name</th><th>Size</th><th>Action</th></tr></thead>
<tbody>
{{range .Entries}}
<tr>
<td>
{{if .IsDir}}📁 <a href="/browse/{{urlenc $.ISOName}}/{{urlenc .Path}}" style="color:var(--text); text-decoration:none;">{{.Name}}</a>
{{else}}{{fileIcon .Name}} <a href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" target="_blank" style="color:var(--text); text-decoration:none;">{{.Name}}</a>{{end}}
</td>
<td>{{if .IsDir}}—{{else}}{{humanSize .Size}}{{end}}</td>
<td>{{if not .IsDir}}<a href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" class="dl-btn" download>Download</a>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</main>
</body>
</html>

38
templates/index.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title><link rel="stylesheet" href="/static/css/style.css"></head>
<body>
<header><a class="logo" href="/">💿 ISOSilo</a></header>
<main>
<nav class="bc">
{{range $i, $c := .Breadcrumbs}}<a href="{{$c.URL}}">{{$c.Name}}</a> {{if lt (add1 $i) (len $.Breadcrumbs)}}/{{end}} {{end}}
</nav>
<div class="grid">
{{range .Items}}
{{if .IsDir}}
<a href="/{{urlenc .RelativePath}}" class="card">
<div class="folder-icon">📁</div>
<div class="card-body"><span class="card-name">{{.Name}}</span></div>
</a>
{{else}}
<div class="card">
<a href="/browse/{{urlenc .RelativePath}}">
{{if .HasImage}}<img src="/raw/{{urlenc (trimExt .RelativePath)}}{{.ImageExt}}" class="card-img">
{{else}}<div class="iso-icon">💿</div>{{end}}
</a>
<div class="card-body">
<a href="/browse/{{urlenc .RelativePath}}" class="card-name">{{.Name}}</a>
<p class="card-desc">{{if .Description}}{{.Description}}{{else}}ISO Disk Image{{end}}</p>
<div style="margin-top:1rem; display:flex; gap:0.5rem;">
<a href="/browse/{{urlenc .RelativePath}}" class="dl-btn">Browse</a>
<a href="/raw/{{urlenc .RelativePath}}" class="dl-btn" download>ISO</a>
</div>
</div>
</div>
{{end}}
{{end}}
</div>
</main>
</body>
</html>