Neale Pickett
·
2021-10-25
mothballs.go
1package main
2
3import (
4 "archive/zip"
5 "bufio"
6 "fmt"
7 "io"
8 "log"
9 "sort"
10 "strconv"
11 "strings"
12 "sync"
13 "time"
14
15 "github.com/spf13/afero"
16 "github.com/spf13/afero/zipfs"
17)
18
19type zipCategory struct {
20 afero.Fs
21 io.Closer
22 mtime time.Time
23}
24
25// Mothballs provides a collection of active mothball files (puzzle categories)
26type Mothballs struct {
27 afero.Fs
28 categories map[string]zipCategory
29 categoryLock *sync.RWMutex
30}
31
32// NewMothballs returns a new Mothballs structure backed by the provided directory
33func NewMothballs(fs afero.Fs) *Mothballs {
34 return &Mothballs{
35 Fs: fs,
36 categories: make(map[string]zipCategory),
37 categoryLock: new(sync.RWMutex),
38 }
39}
40
41func (m *Mothballs) getCat(cat string) (zipCategory, bool) {
42 m.categoryLock.RLock()
43 defer m.categoryLock.RUnlock()
44 ret, ok := m.categories[cat]
45 return ret, ok
46}
47
48// Open returns a ReadSeekCloser corresponding to the filename in a puzzle's category and points
49func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
50 zc, ok := m.getCat(cat)
51 if !ok {
52 return nil, time.Time{}, fmt.Errorf("no such category: %s", cat)
53 }
54
55 f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename))
56 if err != nil {
57 return nil, time.Time{}, err
58 }
59
60 fInfo, err := f.Stat()
61 return f, fInfo.ModTime(), err
62}
63
64// Inventory returns the list of current categories
65func (m *Mothballs) Inventory() []Category {
66 m.categoryLock.RLock()
67 defer m.categoryLock.RUnlock()
68 categories := make([]Category, 0, 20)
69 for cat, zfs := range m.categories {
70 pointsList := make([]int, 0, 20)
71 pf, err := zfs.Open("puzzles.txt")
72 if err != nil {
73 // No puzzles = no category
74 continue
75 }
76 scanner := bufio.NewScanner(pf)
77 for scanner.Scan() {
78 line := scanner.Text()
79 if pointval, err := strconv.Atoi(line); err != nil {
80 log.Printf("Reading points for %s: %s", cat, err.Error())
81 } else {
82 pointsList = append(pointsList, pointval)
83 }
84 }
85 sort.Ints(pointsList)
86 categories = append(categories, Category{cat, pointsList})
87 }
88 return categories
89}
90
91// CheckAnswer returns an error if the provided answer is in any way incorrect for the given category and points
92func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) {
93 zfs, ok := m.getCat(cat)
94 if !ok {
95 return false, fmt.Errorf("no such category: %s", cat)
96 }
97
98 af, err := zfs.Open("answers.txt")
99 if err != nil {
100 return false, fmt.Errorf("no answers.txt file")
101 }
102 defer af.Close()
103
104 needle := fmt.Sprintf("%d %s", points, answer)
105 scanner := bufio.NewScanner(af)
106 for scanner.Scan() {
107 if scanner.Text() == needle {
108 return true, nil
109 }
110 }
111
112 return false, nil
113}
114
115// refresh refreshes internal state.
116// It looks for changes to the directory listing, and caches any new mothballs.
117func (m *Mothballs) refresh() {
118 m.categoryLock.Lock()
119 defer m.categoryLock.Unlock()
120
121 // Any new categories?
122 files, err := afero.ReadDir(m.Fs, "/")
123 if err != nil {
124 log.Println("Error listing mothballs:", err)
125 return
126 }
127 found := make(map[string]bool)
128 for _, f := range files {
129 filename := f.Name()
130 if !strings.HasSuffix(filename, ".mb") {
131 continue
132 }
133 categoryName := strings.TrimSuffix(filename, ".mb")
134 found[categoryName] = true
135
136 reopen := false
137 if existingMothball, ok := m.categories[categoryName]; !ok {
138 reopen = true
139 } else if si, err := m.Fs.Stat(filename); err != nil {
140 log.Println(err)
141 } else if si.ModTime().After(existingMothball.mtime) {
142 existingMothball.Close()
143 delete(m.categories, categoryName)
144 reopen = true
145 }
146
147 if reopen {
148 f, err := m.Fs.Open(filename)
149 if err != nil {
150 log.Println(err)
151 continue
152 }
153
154 fi, err := f.Stat()
155 if err != nil {
156 f.Close()
157 log.Println(err)
158 continue
159 }
160
161 zrc, err := zip.NewReader(f, fi.Size())
162 if err != nil {
163 f.Close()
164 log.Println(err)
165 continue
166 }
167
168 m.categories[categoryName] = zipCategory{
169 Fs: zipfs.New(zrc),
170 Closer: f,
171 mtime: fi.ModTime(),
172 }
173
174 log.Println("Adding category:", categoryName)
175 }
176 }
177
178 // Delete anything in the list that wasn't found
179 for categoryName, zc := range m.categories {
180 if !found[categoryName] {
181 zc.Close()
182 delete(m.categories, categoryName)
183 log.Println("Removing category:", categoryName)
184 }
185 }
186}
187
188// Mothball just returns an error
189func (m *Mothballs) Mothball(cat string, w io.Writer) error {
190 return fmt.Errorf("refusing to repackage a compiled mothball")
191}
192
193// Maintain performs housekeeping for Mothballs.
194func (m *Mothballs) Maintain(updateInterval time.Duration) {
195 m.refresh()
196 for range time.NewTicker(updateInterval).C {
197 m.refresh()
198 }
199}