internal/library/scan.go
151 lines · 3627 bytes
1package library
2
3import (
4 "log"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9
10 "gitbay.org/cmc/gallery/internal/format"
11)
12
13// Slugs that would collide with fixed routes.
14var reserved = map[string]bool{"img": true, "info": true, "timeline": true, "_": true}
15
16// Scan reads every album under root. Unreadable albums and files are logged and skipped.
17func Scan(root string, reg *format.Registry, cache *MetaCache) (*Library, error) {
18 entries, err := os.ReadDir(root)
19 if err != nil {
20 return nil, err
21 }
22 lib := &Library{bySlug: map[string]*Album{}}
23 keep := map[string]bool{}
24 for _, e := range entries {
25 name := e.Name()
26 if !e.IsDir() || strings.HasPrefix(name, ".") {
27 continue
28 }
29 if reserved[name] {
30 log.Printf("skipping album %q: reserved name", name)
31 continue
32 }
33 a, err := scanAlbum(root, name, reg, cache, keep)
34 if err != nil {
35 log.Printf("album %s: %v", name, err)
36 lib.Incomplete = true
37 continue
38 }
39 if len(a.Items) == 0 {
40 continue
41 }
42 lib.Albums = append(lib.Albums, a)
43 lib.bySlug[name] = a
44 }
45 sortAlbums(lib.Albums)
46 lib.Timeline = buildTimeline(lib.Albums)
47 if len(lib.Albums) == 0 {
48 lib.Incomplete = true
49 }
50
51 if src, err := os.ReadFile(filepath.Join(root, "about.org")); err == nil {
52 if _, html, err := parseOrg(src, "about.org"); err != nil {
53 log.Printf("about.org: %v", err)
54 } else {
55 lib.About = html
56 }
57 }
58 if lib.Incomplete {
59 log.Printf("scan incomplete: keeping existing meta cache")
60 } else if err := cache.save(keep); err != nil {
61 log.Printf("meta cache: %v", err)
62 }
63 return lib, nil
64}
65
66func scanAlbum(root, slug string, reg *format.Registry, cache *MetaCache, keep map[string]bool) (*Album, error) {
67 dir := filepath.Join(root, slug)
68 entries, err := os.ReadDir(dir)
69 if err != nil {
70 return nil, err
71 }
72 a := &Album{Slug: slug, Title: slug, byName: map[string]int{}}
73 sortBy, cover := "taken", ""
74
75 if src, err := os.ReadFile(filepath.Join(dir, "album.org")); err == nil {
76 kw, desc, err := parseOrg(src, filepath.Join(slug, "album.org"))
77 if err != nil {
78 log.Printf("%s/album.org: %v", slug, err)
79 } else {
80 if v := kw["title"]; v != "" {
81 a.Title = v
82 }
83 a.Date, a.Description, cover = kw["date"], desc, kw["cover"]
84 if v := kw["order"]; v != "" {
85 if n, err := strconv.Atoi(v); err == nil {
86 a.Order, a.HasOrder = n, true
87 } else {
88 log.Printf("%s/album.org: bad order %q", slug, v)
89 }
90 }
91 switch v := kw["sort"]; v {
92 case "", "taken":
93 case "name":
94 sortBy = v
95 default:
96 log.Printf("%s/album.org: bad sort %q", slug, v)
97 }
98 }
99 }
100
101 for _, e := range entries {
102 name := e.Name()
103 if e.IsDir() || strings.HasPrefix(name, ".") {
104 continue
105 }
106 path := filepath.Join(dir, name)
107 f := reg.Lookup(path)
108 if f == nil {
109 continue
110 }
111 fi, err := e.Info()
112 if err != nil {
113 log.Printf("%s/%s: %v", slug, name, err)
114 continue
115 }
116 rel := slug + "/" + name
117 m, ok := cache.get(rel, fi)
118 if !ok {
119 if m, err = f.Metadata(path); err != nil {
120 log.Printf("%s: %v", rel, err)
121 continue
122 }
123 cache.put(rel, fi, m)
124 }
125 keep[rel] = true
126 taken := m.Taken
127 if taken.IsZero() {
128 taken = fi.ModTime()
129 }
130 a.Items = append(a.Items, &Item{
131 Album: slug, Name: name, Path: path,
132 ModTime: fi.ModTime(), Size: fi.Size(),
133 Taken: taken,
134 Meta: m, Format: f,
135 })
136 }
137
138 sortItems(a.Items, sortBy)
139 for i, it := range a.Items {
140 a.byName[it.Name] = i
141 }
142 if i, ok := a.byName[cover]; ok {
143 a.Cover = a.Items[i]
144 } else if len(a.Items) > 0 {
145 if cover != "" {
146 log.Printf("%s/album.org: cover %q not found", slug, cover)
147 }
148 a.Cover = a.Items[0]
149 }
150 return a, nil
151}