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 {--
⑨