shithub: img

Download patch

ref: e596cb983af53d3a18b484302b53ce8e0f8ec58f
parent: 9f244f7e71c394c37863042e3388a380ae493d8c
author: Alex Musolino <alex@musolino.id.au>
date: Thu Dec 7 21:04:36 EST 2023

imgsrv: implement tagging backed by directory structure

--- a/image.tpl
+++ b/image.tpl
@@ -8,6 +8,9 @@
 	background-color: black;
 	text-align: center;
 }
+form{
+	color: white;
+}
 a{
 	color: white;
 }
@@ -37,8 +40,19 @@
 <body>
 <p>{{if .Prev}}<a id="prev" href="{{.Prev}}">prev</a>{{else}}<span class="disabled">prev</span>{{end}} | <a id="up" href=".">up</a> | {{if .Next}}<a id="next" 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>
-{{range .Tags}} <a href="#">#{{.}}</a>{{end}}
+{{range .ImgTags}} <a href="#">#{{.}}</a>{{else}}<br />{{end}}
+<p>
 <form action="/api/tag" method="post">
+<input type="hidden" name="image" value="{{.Image}}" />
+{{range .Tags}}<input type="submit" name="tags" value="#{{.}}" />
+{{end}}
 </form>
+<form action="/api/tag" method="post">
+<input type="hidden" name="image" value="{{.Image}}" />
+<input id="tag-list" type="text" name="tags" />
+<input type="submit" value="Add" />
+<input type="submit" name="delete" value="Delete" />
+</form>
+</p>
 </body>
 </html>
--- a/imgsrv.go
+++ b/imgsrv.go
@@ -1,10 +1,8 @@
 package main
 
 import (
-	"bufio"
 	"fmt"
 	"html/template"
-	"io"
 	"log"
 	"net/http"
 	"os"
@@ -101,6 +99,7 @@
 			Title string
 			Prev, Next string
 			Image string
+			ImgTags []string
 			Tags []string
 		}
 		image, _ := strings.CutSuffix(r.URL.Path, ".html")
@@ -109,8 +108,10 @@
 			Next: h.Idx.Next(image, ".html"),
 			Prev: h.Idx.Prev(image, ".html"),
 			Image: image,
-			Tags: h.Tags.TagsForImage(image),
+			ImgTags: h.Tags.TagsForImage(image),
+			Tags: h.Tags.ShortList(),
 		}
+		log.Printf("%s has tags: %v\n", image, tplData.Tags)
 		if h.Idx.Year != 0 {
 			tplData.Title = fmt.Sprintf("%s %d", time.Month(h.Idx.Month).String()[0:3], h.Idx.Year)
 		}
@@ -246,8 +247,14 @@
 	Path string
 	Years map[string]*YearIdx
 	Albums map[string]*AlbumIdx
+	Images map[string]struct{}
 }
 
+func (db *ImgDB) imageExists(img string) bool {
+	_, exists := db.Images[img]
+	return exists
+}
+
 func (db *ImgDB) nextMonth(y0 string, m0, step int) *AlbumIdx {
 	var years []string
 	for y := range db.Years {
@@ -300,6 +307,7 @@
 		if strings.HasSuffix(e.Name(), suffix) {
 			name, _ := strings.CutSuffix(e.Name(), suffix)
 			albumIdx.Images = append(albumIdx.Images, name)
+			db.Images[name] = struct{}{}
 		}
 	}
 	return albumIdx, nil
@@ -328,6 +336,7 @@
 		Path: path,
 		Years: make(map[string]*YearIdx),
 		Albums: make(map[string]*AlbumIdx),
+		Images: make(map[string]struct{}),
 	}
 	for _, r := range yearRanges {
 		curr := uint(time.Now().Year())
@@ -358,6 +367,12 @@
 
 type StrLUT map[string]map[string]struct{}
 
+func (lut StrLUT) Clr() {
+	for k := range lut {
+		delete(lut, k)
+	}
+}
+
 func (lut StrLUT) Acc(other StrLUT) {
 	for k1, obin := range other {
 		bin := lut[k1]
@@ -400,6 +415,8 @@
 	sync.RWMutex
 	TagLUT StrLUT
 	ImgLUT StrLUT
+	NewBorns StrLUT
+	DeathRow StrLUT
 }
 
 func NewTags() *Tags {
@@ -406,9 +423,54 @@
 	return &Tags{
 		TagLUT: make(StrLUT),
 		ImgLUT: make(StrLUT),
+		NewBorns: make(StrLUT),
+		DeathRow: make(StrLUT),
 	}
 }
 
+func loadTags(tagDir, img string) (*Tags, error) {
+	entries, err := os.ReadDir(fmt.Sprintf("%s/%s", tagDir, img))
+	if err != nil {
+		return nil, err
+	}
+	tags := NewTags()
+	for _, e := range entries {
+		if e.Type().IsRegular() {
+			tags.Tag(img, e.Name())
+		}
+	}
+	return tags, nil
+}
+
+func OpenTags(path string) (*Tags, error) {
+	entries, err := os.ReadDir(path)
+	if err != nil {
+		return nil, err
+	}
+	tags := NewTags()
+	for _, e := range entries {
+		if e.IsDir() {
+			if t, err := loadTags(path, e.Name()); err != nil {
+				log.Printf("could not load tags for %s: %v\n", e.Name(), err)
+			} else {
+				tags.Acc(t)
+			}
+		}
+	}
+	return tags, nil
+}
+
+func (t *Tags) ShortList() []string {
+	t.RLock()
+	defer t.RUnlock()
+	var tags []string
+	for tag := range t.TagLUT {
+		tags = append(tags, tag)
+	}
+	sort.Strings(tags)
+	return tags
+}
+
 func (t *Tags) Acc(u *Tags) {
 	u.RLock()
 	defer u.RUnlock()
@@ -423,6 +485,8 @@
 	defer t.Unlock()
 	t.TagLUT.Add(tag, img)
 	t.ImgLUT.Add(img, tag)
+	t.NewBorns.Add(img, tag)
+	t.DeathRow.Del(img, tag)
 }
 
 func (t *Tags) Untag(img, tag string) {
@@ -430,6 +494,8 @@
 	defer t.Unlock()
 	t.TagLUT.Del(tag, img)
 	t.ImgLUT.Del(img, tag)
+	t.NewBorns.Del(img, tag)
+	t.DeathRow.Add(img, tag)
 }
 
 func (t *Tags) TagsForImage(img string) []string {
@@ -446,48 +512,78 @@
 	return t.TagLUT.Lookup(tag)	
 }
 
-func parseTagList(r io.Reader) (*Tags, error) {
-	t := NewTags()
-	s := bufio.NewScanner(r)
-	for s.Scan() {
-		line := s.Text()
-		if line == "" || line[0] == '#' {
-			continue
+func (t *Tags) Flush(path string) error {
+	t.Lock()
+	defer t.Unlock()
+	for img, tags := range t.NewBorns {
+		for tag := range tags {
+			imgDir := fmt.Sprintf("%s/%s", path, img)
+			if err := os.MkdirAll(imgDir, 0755); err != nil {
+				return err
+			}
+			f, err := os.Create(fmt.Sprintf("%s/%s", imgDir, tag))
+			f.Close()
+			if err != nil {
+				return err
+			}
 		}
-		f := strings.Fields(line)
-		if len(f) < 2 {
-			return nil, fmt.Errorf("bad format: expected at least 2 fields, got %d", len(f))
+	}
+	for img, tags := range t.DeathRow {
+		for tag := range tags {
+			if err := os.Remove(fmt.Sprintf("%s/%s/%s", path, img, tag)); err != nil {
+				return err
+			}
 		}
-		for i := 1; i < len(f); i++ {
-			t.Tag(f[0], f[i])
-		}
 	}
-	if err := s.Err(); err != nil {
-		return nil, err
-	}
-	return t, nil
+	return nil
 }
 
-func loadTags(path string) (*Tags, error) {
-	f, err := os.Open(path)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-	return parseTagList(bufio.NewReader(f))
-}
-
 type TagApiHandler struct {
+	DB *ImgDB
 	Tags *Tags
 }
 
+func (h *TagApiHandler) addTag(img, tag string) {
+
+}
+
 func (h *TagApiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	t, err := parseTagList(r.Body)
-	if err != nil {
+	if err := r.ParseForm(); err != nil {
 		http.Error(w, "bad request", 400)
 		return
 	}
-	h.Tags.Acc(t)
+	img := r.FormValue("image")
+	if !h.DB.imageExists(img) {
+		http.Error(w, "not found", 404)
+		return
+	}
+	_, delete := r.Form["delete"]
+	action := "adding"
+	if delete {
+		action = "deleting"
+	}
+	for _, tag := range r.Form["tags"] {
+		h.addTag(img, tag)
+	}
+	for _, v := range r.Form["tags"] {
+		tags := strings.Split(v, " ")
+		for _, tag := range tags {
+			tag = strings.TrimSpace(strings.TrimPrefix(tag, "#"))
+			if tag == "" {
+				continue
+			}
+			log.Printf("%s %s tag for %s\n", action, tag, img)
+			if delete {
+				h.Tags.Untag(img, tag)
+			} else {
+				h.Tags.Tag(img, tag)
+			}
+			if err := h.Tags.Flush("tags"); err != nil {
+				log.Printf("error updating tags dir: %v\n", err)
+			}
+		}
+	}
+	http.Redirect(w, r, fmt.Sprintf("/%s/%s/%s.html", img[0:4], img[4:6], img), http.StatusSeeOther)
 }
 
 func loadTemplates(path string) (*Templates, error) {
@@ -535,11 +631,11 @@
 	if err != nil {
 		log.Fatalf("could not load database: %v\n", err)
 	}
-	tags, err := loadTags("tags")
+	tags, err := OpenTags("tags")
 	if err != nil {
 		log.Fatalf("could not load tags: %v\n", err)
 	}
-	http.Handle("/api/tag", &TagApiHandler{tags})
+	http.Handle("/api/tag", &TagApiHandler{db, tags})
 	for y, yIdx := range db.Years {
 		for m, mIdx := range yIdx.Months {
 			if mIdx != nil {