ref: 08f0b154171793f440928929fbca6e49ac085738
parent: ab3a209ed97ea6d9e085a32bf806fd9395615ade
author: Alex Musolino <alex@musolino.id.au>
date: Thu Nov 23 20:28:04 EST 2023
initial work on an image server
--- /dev/null
+++ b/album.tpl
@@ -1,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>{{.Title}}</title>+<link rel="shortcut icon" href="montage.jpg">
+<style>
+body{+ background-color: black;
+ text-align: center;
+}
+img{+ height: 96px;
+}
+a{+ color: white;
+}
+.disabled{+ color: grey;
+}
+</style>
+</head>
+<body>
+<p>
+{{if .Prev}}+<a href="{{.Prev}}">prev</a>+{{else}}+<span class="disabled">prev</span>
+{{end}}+ | <a href="../index.html">index</a> |
+{{if .Next}}+<a href="{{.Next}}">next</a>+{{else}}+<span class="disabled">next</span>
+{{end}}+</p>
+{{range .Images}}<a href="{{.}}.html"><img src="{{.}}.thumb.JPG"/></a>+{{end}}+<p>
+{{if .Prev}}+<a href="{{.Prev}}">prev</a>+{{else}}+<span class="disabled">prev</span>
+{{end}}+ | <a href="../index.html">index</a> |
+{{if .Next}}+<a href="{{.Next}}">next</a>+{{else}}+<span class="disabled">next</span>
+{{end}}+</p>
+</body>
+</html>
--- /dev/null
+++ b/image.tpl
@@ -1,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+body{+ background-color: black;
+ text-align: center;
+}
+a{+ color: white;
+}
+.disabled{+ color: grey;
+}
+</style>
+</head>
+<body>
+<p>
+{{if .Prev}}+<a href="{{.Prev}}">prev</a>+{{else}}+<span class="disabled">prev</span>
+{{end}}+ | <a href=".">up</a> |
+{{if .Next}}+<a href="{{.Next}}">next</a>+{{else}}+<span class="disabled">next</span>
+{{end}}+</p>
+<p><a href="{{.Image}}.full.JPG"><img src="{{.Image}}.big.JPG"/></a></p>+</body>
+</html>
--- /dev/null
+++ b/imgsrv.go
@@ -1,0 +1,363 @@
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "sort"
+ "strings"
+ "time"
+)
+
+type YearIndexHandler struct {+ Idx *YearIdx
+ Tpl *template.Template
+}
+
+func (h *YearIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {+ log.Printf("YearIndexHandler.ServeHTTP: %s\n", r.URL.Path)+ if r.URL.Path == "montage.jpg" {+ http.ServeFile(w, r, fmt.Sprintf("%s/montage.jpg", h.Idx.Path))+ return
+ }
+
+ type MonthTplData struct {+ Empty bool
+ Number string
+ Name string
+ }
+
+ type TplData struct {+ PrevYear string
+ CurrYear string
+ NextYear string
+ Months [12]MonthTplData
+ }
+ tplData := TplData{+ CurrYear: path.Base(h.Idx.Path),
+ }
+ for i := 0; i < 12; i++ {+ if h.Idx.Months[i] != nil {+ tplData.Months[i].Empty = false
+ tplData.Months[i].Number = fmt.Sprintf("%02d", i+1)+ tplData.Months[i].Name = time.Month(i+1).String()
+ } else {+ tplData.Months[i].Empty = true
+ }
+ }
+ if err := h.Tpl.Execute(w, tplData); err != nil {+ log.Printf("error executing template: %v\n", err)+ }
+}
+
+type AlbumIndexHandler struct {+ Idx *AlbumIdx
+ IndexTpl *template.Template
+ ImageTpl *template.Template
+}
+
+func (h *AlbumIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {+ log.Printf("AlbumIndexHandler.ServeHTTP: %s\n", r.URL.Path)+ switch r.URL.Path {+ case "":
+ fallthrough
+ case "index.html":
+ type TplData struct {+ Title string
+ Prev, Next string
+ Images []string
+ }
+ tplData := TplData{+ Title: path.Base(h.Idx.Path),
+ Images: h.Idx.Images,
+ }
+ if h.Idx.Year != 0 {+ yearStr := fmt.Sprintf("%d", h.Idx.Year)+ if next := h.Idx.DB.nextMonth(yearStr, h.Idx.Month, +1); next != "" {+ tplData.Next = "../../" + next
+ }
+ if prev := h.Idx.DB.nextMonth(yearStr, h.Idx.Month, -1); prev != "" {+ tplData.Prev = "../../" + prev
+ }
+ }
+ if err := h.IndexTpl.Execute(w, tplData); err != nil {+ log.Printf("error executing template: %v\n", err)+ }
+ return
+ }
+ if strings.HasSuffix(r.URL.Path, ".html") {+ type TplData struct {+ Title string
+ Prev, Next string
+ Image string
+ }
+ image, _ := strings.CutSuffix(r.URL.Path, ".html")
+ tplData := TplData{+ Title: path.Base(h.Idx.Path),
+ Next: h.Idx.Next(image, ".html"),
+ Prev: h.Idx.Prev(image, ".html"),
+ Image: image,
+ }
+ if err := h.ImageTpl.Execute(w, tplData); err != nil {+ log.Printf("error executing template: %v\n", err)+ }
+ return
+ }
+ if strings.HasSuffix(strings.ToLower(r.URL.Path), ".jpg") {+ http.ServeFile(w, r, fmt.Sprintf("%s/%s", h.Idx.Path, r.URL.Path))+ return
+ }
+ http.Error(w, "404 page not found", 404)
+}
+
+type MainIndexHandler struct {+ DB *ImgDB
+ Tpl *template.Template
+}
+
+func (h *MainIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {+ log.Printf("IndexHandler.ServeHTTP\n")+ type TplData struct {+ Years sort.StringSlice
+ Albums sort.StringSlice
+ }
+ tplData := TplData{+ Years: make([]string, 0, len(h.DB.Years)),
+ Albums: make([]string, 0, len(h.DB.Albums)),
+ }
+ for year := range h.DB.Years {+ tplData.Years = append(tplData.Years, year)
+ }
+ for album := range h.DB.Albums {+ tplData.Albums = append(tplData.Albums, album)
+ }
+ sort.Sort(sort.Reverse(tplData.Years))
+ sort.Sort(tplData.Albums)
+ if err := h.Tpl.Execute(w, tplData); err != nil {+ log.Printf("error executing template: %v\n", err)+ }
+}
+
+type AlbumIdx struct {+ DB *ImgDB
+ Year int
+ Month int
+ Path string
+ Images []string
+}
+
+func (a *AlbumIdx) indexOf(img string) int {+ for i, x := range a.Images {+ if x == img {+ return i
+ }
+ }
+ return -1
+}
+
+func (a *AlbumIdx) next(img, suffix string, step int) string {+ i := a.indexOf(img)
+ if i >= 0 {+ i += step
+ if 0 <= i && i < len(a.Images) {+ return a.Images[i] + suffix
+ }
+ }
+ return ""
+}
+
+func (a *AlbumIdx) Next(img, suffix string) string {+ return a.next(img, suffix, +1)
+}
+
+func (a *AlbumIdx) Prev(img, suffix string) string {+ return a.next(img, suffix, -1)
+}
+
+type YearIdx struct {+ DB *ImgDB
+ Path string
+ Months [12]*AlbumIdx
+}
+
+type ImgDB struct {+ Path string
+ Years map[string]*YearIdx
+ Albums map[string]*AlbumIdx
+}
+
+func (db *ImgDB) nextMonth(y0 string, m0, step int) string {+ var years []string
+ for y := range db.Years {+ years = append(years, y)
+ }
+ sort.Strings(years)
+ i0 := 0
+ for i0 < len(years) {+ if years[i0] == y0 {+ break
+ }
+ i0++
+ }
+ for i := i0; 0 <= i && i < len(years); i += step {+ log.Printf("y=%s, m0=%d, i=%d\n", years[i], m0+1, i)+ for m := m0 + step; 0 <= m && m < 12; m += step {+ if db.Years[years[i]].Months[m] != nil {+ return fmt.Sprintf("%s/%02d", years[i], m + 1)+ }
+ }
+ if step > 0 {+ m0 = -1
+ } else {+ m0 = 12
+ }
+ }
+ return ""
+}
+
+type Templates struct {+ Main *template.Template
+ Year *template.Template
+ Album *template.Template
+ Image *template.Template
+}
+
+func loadAlbum(db *ImgDB, year, month int, path string) (*AlbumIdx, error) {+ entries, err := os.ReadDir(path)
+ if err != nil {+ return nil, err
+ }
+ albumIdx := &AlbumIdx{+ DB: db,
+ Year: year,
+ Month: month,
+ Path: path,
+ Images: make([]string, 0),
+ }
+ suffix := ".big.JPG"
+ for _, e := range entries {+ if strings.HasSuffix(e.Name(), suffix) {+ name, _ := strings.CutSuffix(e.Name(), suffix)
+ albumIdx.Images = append(albumIdx.Images, name)
+ }
+ }
+ return albumIdx, nil
+}
+
+func loadYear(db *ImgDB, year int, path string) (*YearIdx, error) {+ yearIdx := YearIdx{+ DB: db,
+ Path: path,
+ }
+ for m := 0; m < 12; m++ {+ monthPath := fmt.Sprintf("%s/%02d", path, m + 1)+ if albumIdx, err := loadAlbum(db, year, m, monthPath); err != nil {+ if !os.IsNotExist(err) {+ return nil, fmt.Errorf("error loading album %s: %v\n", monthPath, err)+ }
+ } else {+ yearIdx.Months[m] = albumIdx
+ }
+ }
+ return &yearIdx, nil
+}
+
+func loadDatabase(path string, yearRanges []YearRange, albums []string) (*ImgDB, error) {+ db := &ImgDB{+ Path: path,
+ Years: make(map[string]*YearIdx),
+ Albums: make(map[string]*AlbumIdx),
+ }
+ for _, r := range yearRanges {+ curr := uint(time.Now().Year())
+ for year := r.From; r.To == 0 && year <= curr || year < r.To; year++ {+ subdir := fmt.Sprintf("%s/%d", path, year)+ if _, err := os.Stat(subdir); err != nil && os.IsNotExist(err) {+ continue
+ }
+ if yearIdx, err := loadYear(db, int(year), subdir); err != nil {+ return nil, fmt.Errorf("loadYear: %v\n", err)+ } else {+ db.Years[fmt.Sprintf("%d", year)] = yearIdx+ log.Printf("loaded %s\n", subdir)+ }
+ }
+ }
+ for _, album := range albums {+ subdir := fmt.Sprintf("%s/%s", path, album)+ if albumIdx, err := loadAlbum(db, 0, 0, subdir); err != nil {+ return nil, fmt.Errorf("loadAlbum: %v\n", err)+ } else {+ db.Albums[album] = albumIdx
+ log.Printf("loaded %s\n", subdir)+ }
+ }
+ return db, nil
+}
+
+func loadTemplates(path string) (*Templates, error) {+ mainTpl, err := template.ParseFiles(fmt.Sprintf("%s/main.tpl", path))+ if err != nil {+ return nil, err
+ }
+ yearTpl, err := template.ParseFiles(fmt.Sprintf("%s/year.tpl", path))+ if err != nil {+ return nil, err
+ }
+ albumTpl, err := template.ParseFiles(fmt.Sprintf("%s/album.tpl", path))+ if err != nil {+ return nil, err
+ }
+ imageTpl, err := template.ParseFiles(fmt.Sprintf("%s/image.tpl", path))+ if err != nil {+ return nil, err
+ }
+ return &Templates{+ Main: mainTpl,
+ Year: yearTpl,
+ Album: albumTpl,
+ Image: imageTpl,
+ }, nil
+}
+
+type YearRange struct {+ From uint
+ To uint
+}
+
+func main() {+ yearRanges := []YearRange{+ {From: 2008},+ }
+ albums := []string{+ "misc",
+ }
+ templates, err := loadTemplates(".")+ if err != nil {+ log.Fatalf("could not load templates: %v\n", err)+ }
+ db, err := loadDatabase(".", yearRanges, albums)+ if err != nil {+ log.Fatalf("could not load database: %v\n", err)+ }
+ for y, yIdx := range db.Years {+ for m, mIdx := range yIdx.Months {+ if mIdx != nil {+ prefix := fmt.Sprintf("/%s/%02d/", y, m+1)+ log.Printf("adding handler for %s\n", prefix)+ http.Handle(prefix, http.StripPrefix(prefix, &AlbumIndexHandler{mIdx, templates.Album, templates.Image}))+ }
+ }
+ prefix := fmt.Sprintf("/%s/", y)+ http.Handle(prefix, http.StripPrefix(prefix, &YearIndexHandler{yIdx, templates.Year}))+ }
+ for album, idx := range db.Albums {+ prefix := fmt.Sprintf("/%s/", album)+ http.Handle(prefix, http.StripPrefix(prefix, &AlbumIndexHandler{idx, templates.Album, templates.Image}))+ }
+ http.Handle("/", &MainIndexHandler{db, templates.Main})+ log.Fatal(http.ListenAndServe(":8080", nil))+}
--- /dev/null
+++ b/main.tpl
@@ -1,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title></title>
+<meta http-equiv="content-type" content="text/html; charset=utf-8">
+<style>
+body{+ background-color: black;
+}
+a{+ color: white;
+}
+div{+ float: left;
+ text-align: center;
+ padding: 0.25cm;
+ width: 200px;
+ height: 200px;
+}
+img{+ width: 160px;
+ height: 160px;
+}
+</style>
+</head>
+<body>
+<p> </p>
+{{range .Years}}+<div><a href="{{.}}/index.html"><img src="{{.}}/montage.jpg"/><p>{{.}}</p></a></div>+{{end}}+</body>
+</html>
--- /dev/null
+++ b/year.tpl
@@ -1,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>{{.CurrYear}}</title>+<style>
+body{+ background-color: black;
+ text-align: center;
+}
+div>p, a{+ color: white;
+}
+div{+ float: left;
+ text-align: center;
+ padding: 0.25cm;
+ width: 200px;
+ height: 200px;
+}
+div.bordered{+ float: none;
+ display: inline-block;
+ width: 160px;
+ height: 160px;
+ padding: 0px;
+ border: 1px solid white;
+}
+img{+ width: 160px;
+ height: 160px;
+}
+.disabled{+ color: grey;
+}
+</style>
+</head>
+<body>
+<p>
+{{if .PrevYear}}+<a href="../{{.PrevYear}}/index.html">prev</a>+{{else}}+<span class="disabled">prev</span>
+{{end}}+ | <a href="../index.html">index</a> |
+{{if .NextYear}}+<a href="../{{.NextYear}}/index.html">prev</a>+{{else}}+<span class="disabled">next</span>
+{{end}}+</p>
+{{range .Months}}+ {{if .Empty}}+ <div><div class="bordered"></div><p>{{.Name}}</p></div>+ {{else}}+ <div><a href="{{.Number}}/index.html"><img src="{{.Number}}/montage.jpg"/><p>{{.Name}}</p></a></div>+ {{end}}+{{end}}+</body>
+</html>
--
⑨