shithub: img

Download patch

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>&nbsp;</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>