add images and metadata

This commit is contained in:
visionmercer 2026-03-23 11:59:52 +01:00
commit d2a360caf1
2 changed files with 114 additions and 31 deletions

View file

@ -34,6 +34,7 @@ func New(dir string) *Handler {
"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
@ -47,6 +48,9 @@ type isoInfo struct {
Name string
Size int64
ModTime time.Time
Description string
HasImage bool
ImageExt string
}
func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
@ -70,7 +74,31 @@ func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
if err != nil {
continue
}
isos = append(isos, isoInfo{Name: e.Name(), Size: info.Size(), ModTime: info.ModTime()})
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
}
}
isos = append(isos, isoInfo{
Name: e.Name(),
Size: info.Size(),
ModTime: info.ModTime(),
Description: string(desc),
HasImage: hasImg,
ImageExt: imgExt,
})
}
sort.Slice(isos, func(i, j int) bool {
@ -119,7 +147,6 @@ func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) {
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
@ -165,12 +192,25 @@ func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
defer rc.Close()
filename := filepath.Base(internalPath)
ct := mime.TypeByExtension(filepath.Ext(filename))
ext := strings.ToLower(filepath.Ext(filename))
ct := mime.TypeByExtension(ext)
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":
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))
@ -179,6 +219,21 @@ func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
}
}
// -----------------------------------------------------------------------
// 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)
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
@ -193,7 +248,6 @@ func (h *Handler) openISO(name string) (*iso.Reader, error) {
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 == "" {
@ -280,7 +334,6 @@ func fileIcon(name string) string {
}
}
// allTemplates contains both the index and browse HTML templates.
const allTemplates = `
{{define "css"}}
<style>
@ -352,7 +405,6 @@ const allTemplates = `
}
.stats { font-size: .82rem; color: var(--muted); margin-bottom: 1rem; }
/* Breadcrumbs */
.bc {
display: flex;
flex-wrap: wrap;
@ -370,30 +422,45 @@ const allTemplates = `
.bc .sep { color: var(--border); }
.bc .cur { color: var(--text); }
/* ISO grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px,1fr));
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(300px,1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.2rem 1.4rem;
padding: 0;
display: flex;
flex-direction: column;
gap: .45rem;
transition: border-color .15s, transform .15s;
color: inherit;
overflow: hidden;
}
.card:hover { border-color: var(--accent); transform: translateY(-2px); text-decoration: none; }
.card-icon { font-size: 1.8rem; }
.card-name { font-family: var(--mono); font-size: .88rem; font-weight: 600; color: var(--accent); word-break: break-all; }
.card-meta { font-size: .78rem; color: var(--muted); display: flex; gap: 1rem; }
.card:hover { border-color: var(--accent); transform: translateY(-2px); }
.card-img {
width: 100%;
height: 180px;
object-fit: cover;
background: #12151f;
border-bottom: 1px solid var(--border);
}
.card-no-img {
height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
background: #12151f;
}
.card-body { padding: 1.2rem; display: flex; flex-direction: column; flex-grow: 1; }
.card-name { font-family: var(--mono); font-size: .95rem; font-weight: 600; color: var(--accent); word-break: break-all; margin-bottom: 0.5rem; }
.card-desc { font-size: .8rem; color: var(--muted); margin-bottom: 1rem; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; flex-grow: 1; }
.card-meta { font-size: .78rem; color: var(--muted); display: flex; justify-content: space-between; align-items: center; }
.card-actions { display: flex; gap: 0.5rem; margin-top: 1rem; }
/* File table */
.tbl {
width: 100%;
border-collapse: collapse;
@ -439,8 +506,11 @@ const allTemplates = `
font-size: .76rem;
font-family: var(--mono);
transition: background .15s, color .15s;
cursor: pointer;
}
.dl:hover { background: var(--accent); color: #fff; text-decoration: none; }
.dl.secondary { color: var(--muted); border-color: var(--border); }
.dl.secondary:hover { background: var(--border); color: var(--text); }
.empty { text-align: center; color: var(--muted); padding: 3rem 0; }
@ -468,14 +538,25 @@ const allTemplates = `
{{if .ISOs}}
<div class="grid">
{{range .ISOs}}
<a class="card" href="/browse/{{urlenc .Name}}">
<span class="card-icon">💿</span>
<span class="card-name">{{.Name}}</span>
<span class="card-meta">
<div class="card">
{{if .HasImage}}
<img src="/raw/{{urlenc (trimExt .Name)}}{{.ImageExt}}" class="card-img" alt="Cover">
{{else}}
<div class="card-no-img">💿</div>
{{end}}
<div class="card-body">
<a href="/browse/{{urlenc .Name}}" class="card-name">{{.Name}}</a>
<p class="card-desc">{{if .Description}}{{.Description}}{{else}}No description available.{{end}}</p>
<div class="card-meta">
<span>{{humanSize .Size}}</span>
<span>{{.ModTime.Format "2006-01-02"}}</span>
</span>
</a>
</div>
<div class="card-actions">
<a href="/browse/{{urlenc .Name}}" class="dl">Browse Files</a>
<a href="/raw/{{urlenc .Name}}" class="dl secondary" download>Download ISO</a>
</div>
</div>
</div>
{{end}}
</div>
{{else}}
@ -532,7 +613,7 @@ const allTemplates = `
</tr>
{{else}}
<tr>
<td><div class="nc"><span>{{fileIcon .Name}}</span><a href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" download>{{.Name}}</a></div></td>
<td><div class="nc"><span>{{fileIcon .Name}}</span><a href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" target="_blank">{{.Name}}</a></div></td>
<td class="sz">{{humanSize .Size}}</td>
<td class="dt">{{.ModTime.Format "2006-01-02 15:04"}}</td>
<td class="ac"><a class="dl" href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" download> Download</a></td>

View file

@ -30,10 +30,12 @@ func main() {
// 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...} → download a file from 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)
mux.HandleFunc("/browse/", h.BrowseISO)
mux.HandleFunc("/file/", h.DownloadFile)
mux.HandleFunc("/raw/", h.RawFile)
fmt.Printf("ISOSilo listening on %s\n", *addr)
fmt.Printf("Serving ISOs from: %s\n", *dir)