add images and metadata
This commit is contained in:
parent
54f573c93b
commit
d2a360caf1
2 changed files with 114 additions and 31 deletions
|
|
@ -34,6 +34,7 @@ func New(dir string) *Handler {
|
||||||
"urlenc": url.PathEscape,
|
"urlenc": url.PathEscape,
|
||||||
"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)) },
|
||||||
}).Parse(allTemplates),
|
}).Parse(allTemplates),
|
||||||
)
|
)
|
||||||
return h
|
return h
|
||||||
|
|
@ -44,9 +45,12 @@ func New(dir string) *Handler {
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
type isoInfo struct {
|
type isoInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Size int64
|
Size int64
|
||||||
ModTime time.Time
|
ModTime time.Time
|
||||||
|
Description string
|
||||||
|
HasImage bool
|
||||||
|
ImageExt string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
|
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 {
|
if err != nil {
|
||||||
continue
|
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 {
|
sort.Slice(isos, func(i, j int) bool {
|
||||||
|
|
@ -119,7 +147,6 @@ func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dirs first, then files; both alphabetically.
|
|
||||||
sort.Slice(entries, func(i, j int) bool {
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
if entries[i].IsDir != entries[j].IsDir {
|
if entries[i].IsDir != entries[j].IsDir {
|
||||||
return entries[i].IsDir
|
return entries[i].IsDir
|
||||||
|
|
@ -165,12 +192,25 @@ func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
|
||||||
defer rc.Close()
|
defer rc.Close()
|
||||||
|
|
||||||
filename := filepath.Base(internalPath)
|
filename := filepath.Base(internalPath)
|
||||||
ct := mime.TypeByExtension(filepath.Ext(filename))
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
ct := mime.TypeByExtension(ext)
|
||||||
if ct == "" {
|
if ct == "" {
|
||||||
ct = "application/octet-stream"
|
ct = "application/octet-stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename))
|
// 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-Type", ct)
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
|
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
|
// Helpers
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
@ -193,7 +248,6 @@ func (h *Handler) openISO(name string) (*iso.Reader, error) {
|
||||||
return iso.Open(filepath.Join(h.dir, name))
|
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) {
|
func parsePath(urlPath, prefix string) (isoName, internalPath string, ok bool) {
|
||||||
rest := strings.TrimPrefix(urlPath, prefix)
|
rest := strings.TrimPrefix(urlPath, prefix)
|
||||||
if rest == "" {
|
if rest == "" {
|
||||||
|
|
@ -280,7 +334,6 @@ func fileIcon(name string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// allTemplates contains both the index and browse HTML templates.
|
|
||||||
const allTemplates = `
|
const allTemplates = `
|
||||||
{{define "css"}}
|
{{define "css"}}
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -352,7 +405,6 @@ const allTemplates = `
|
||||||
}
|
}
|
||||||
.stats { font-size: .82rem; color: var(--muted); margin-bottom: 1rem; }
|
.stats { font-size: .82rem; color: var(--muted); margin-bottom: 1rem; }
|
||||||
|
|
||||||
/* Breadcrumbs */
|
|
||||||
.bc {
|
.bc {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -370,30 +422,45 @@ const allTemplates = `
|
||||||
.bc .sep { color: var(--border); }
|
.bc .sep { color: var(--border); }
|
||||||
.bc .cur { color: var(--text); }
|
.bc .cur { color: var(--text); }
|
||||||
|
|
||||||
/* ISO grid */
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px,1fr));
|
grid-template-columns: repeat(auto-fill, minmax(300px,1fr));
|
||||||
gap: 1rem;
|
gap: 1.5rem;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 1.2rem 1.4rem;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .45rem;
|
|
||||||
transition: border-color .15s, transform .15s;
|
transition: border-color .15s, transform .15s;
|
||||||
color: inherit;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.card:hover { border-color: var(--accent); transform: translateY(-2px); text-decoration: none; }
|
.card:hover { border-color: var(--accent); transform: translateY(-2px); }
|
||||||
.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-img {
|
||||||
.card-meta { font-size: .78rem; color: var(--muted); display: flex; gap: 1rem; }
|
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 {
|
.tbl {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
@ -439,8 +506,11 @@ const allTemplates = `
|
||||||
font-size: .76rem;
|
font-size: .76rem;
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
transition: background .15s, color .15s;
|
transition: background .15s, color .15s;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.dl:hover { background: var(--accent); color: #fff; text-decoration: none; }
|
.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; }
|
.empty { text-align: center; color: var(--muted); padding: 3rem 0; }
|
||||||
|
|
||||||
|
|
@ -468,14 +538,25 @@ const allTemplates = `
|
||||||
{{if .ISOs}}
|
{{if .ISOs}}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{{range .ISOs}}
|
{{range .ISOs}}
|
||||||
<a class="card" href="/browse/{{urlenc .Name}}">
|
<div class="card">
|
||||||
<span class="card-icon">💿</span>
|
{{if .HasImage}}
|
||||||
<span class="card-name">{{.Name}}</span>
|
<img src="/raw/{{urlenc (trimExt .Name)}}{{.ImageExt}}" class="card-img" alt="Cover">
|
||||||
<span class="card-meta">
|
{{else}}
|
||||||
<span>{{humanSize .Size}}</span>
|
<div class="card-no-img">💿</div>
|
||||||
<span>{{.ModTime.Format "2006-01-02"}}</span>
|
{{end}}
|
||||||
</span>
|
<div class="card-body">
|
||||||
</a>
|
<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>
|
||||||
|
</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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
@ -532,7 +613,7 @@ const allTemplates = `
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr>
|
<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="sz">{{humanSize .Size}}</td>
|
||||||
<td class="dt">{{.ModTime.Format "2006-01-02 15:04"}}</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>
|
<td class="ac"><a class="dl" href="/file/{{urlenc $.ISOName}}/{{urlenc .Path}}" download>↓ Download</a></td>
|
||||||
|
|
|
||||||
4
main.go
4
main.go
|
|
@ -30,10 +30,12 @@ func main() {
|
||||||
// GET / → list all ISOs in the directory
|
// GET / → list all ISOs in the directory
|
||||||
// GET /browse/{iso} → list root of ISO
|
// GET /browse/{iso} → list root of ISO
|
||||||
// GET /browse/{iso}/{path...} → list directory inside 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("/", h.ListISOs)
|
||||||
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)
|
||||||
|
|
||||||
fmt.Printf("ISOSilo listening on %s\n", *addr)
|
fmt.Printf("ISOSilo listening on %s\n", *addr)
|
||||||
fmt.Printf("Serving ISOs from: %s\n", *dir)
|
fmt.Printf("Serving ISOs from: %s\n", *dir)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue