moth/cmd/mothd/mothballs.go

182 lines
4.1 KiB
Go
Raw Normal View History

2019-09-02 19:47:24 -06:00
package main
import (
2020-08-17 20:33:23 -06:00
"archive/zip"
2020-03-01 13:27:49 -07:00
"bufio"
2020-08-17 17:43:57 -06:00
"fmt"
2020-08-17 20:33:23 -06:00
"io"
2020-08-17 17:43:57 -06:00
"log"
2020-08-28 14:15:19 -06:00
"sort"
2020-03-01 13:27:49 -07:00
"strconv"
2020-08-17 17:43:57 -06:00
"strings"
2020-08-17 20:33:23 -06:00
"sync"
2020-03-01 14:03:46 -07:00
"time"
2020-08-17 17:43:57 -06:00
"github.com/spf13/afero"
2020-08-17 20:33:23 -06:00
"github.com/spf13/afero/zipfs"
2019-09-02 19:47:24 -06:00
)
2020-08-17 20:33:23 -06:00
type zipCategory struct {
afero.Fs
io.Closer
}
2020-08-17 17:43:57 -06:00
// Mothballs provides a collection of active mothball files (puzzle categories)
2019-09-02 19:47:24 -06:00
type Mothballs struct {
afero.Fs
2020-08-17 20:33:23 -06:00
categories map[string]zipCategory
categoryLock *sync.RWMutex
2019-09-02 19:47:24 -06:00
}
2020-08-17 17:43:57 -06:00
// NewMothballs returns a new Mothballs structure backed by the provided directory
2020-02-22 15:49:58 -07:00
func NewMothballs(fs afero.Fs) *Mothballs {
2019-09-02 19:47:24 -06:00
return &Mothballs{
2020-08-17 20:33:23 -06:00
Fs: fs,
categories: make(map[string]zipCategory),
categoryLock: new(sync.RWMutex),
2019-09-02 19:47:24 -06:00
}
}
2020-08-17 20:33:23 -06:00
func (m *Mothballs) getCat(cat string) (zipCategory, bool) {
m.categoryLock.RLock()
defer m.categoryLock.RUnlock()
ret, ok := m.categories[cat]
return ret, ok
}
2020-08-17 17:43:57 -06:00
// Open returns a ReadSeekCloser corresponding to the filename in a puzzle's category and points
2020-03-01 16:10:55 -07:00
func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
2020-08-17 20:33:23 -06:00
zc, ok := m.getCat(cat)
2020-08-17 17:43:57 -06:00
if !ok {
2020-03-01 16:10:55 -07:00
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat)
2020-03-01 14:03:46 -07:00
}
2020-08-17 20:33:23 -06:00
f, err := zc.Open(fmt.Sprintf("content/%d/%s", points, filename))
if err != nil {
return nil, time.Time{}, err
}
fInfo, err := f.Stat()
return f, fInfo.ModTime(), err
}
2020-08-17 17:43:57 -06:00
// Inventory returns the list of current categories
func (m *Mothballs) Inventory() []Category {
2020-08-17 20:33:23 -06:00
m.categoryLock.RLock()
defer m.categoryLock.RUnlock()
2020-03-01 13:27:49 -07:00
categories := make([]Category, 0, 20)
for cat, zfs := range m.categories {
2020-03-01 13:27:49 -07:00
pointsList := make([]int, 0, 20)
pf, err := zfs.Open("puzzles.txt")
if err != nil {
// No puzzles = no category
continue
}
scanner := bufio.NewScanner(pf)
for scanner.Scan() {
line := scanner.Text()
if pointval, err := strconv.Atoi(line); err != nil {
log.Printf("Reading points for %s: %s", cat, err.Error())
} else {
pointsList = append(pointsList, pointval)
}
}
2020-08-28 14:15:19 -06:00
sort.Ints(pointsList)
2020-03-01 13:27:49 -07:00
categories = append(categories, Category{cat, pointsList})
}
2020-03-01 13:27:49 -07:00
return categories
}
2020-08-17 17:43:57 -06:00
// CheckAnswer returns an error if the provided answer is in any way incorrect for the given category and points
2020-09-08 17:49:02 -06:00
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) {
2020-08-17 20:33:23 -06:00
zfs, ok := m.getCat(cat)
2020-08-17 17:43:57 -06:00
if !ok {
2020-09-08 17:49:02 -06:00
return false, fmt.Errorf("No such category: %s", cat)
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
2020-03-01 16:10:55 -07:00
af, err := zfs.Open("answers.txt")
if err != nil {
2020-09-08 17:49:02 -06:00
return false, fmt.Errorf("No answers.txt file")
2020-03-01 16:10:55 -07:00
}
defer af.Close()
2020-08-17 17:43:57 -06:00
2020-03-01 16:10:55 -07:00
needle := fmt.Sprintf("%d %s", points, answer)
scanner := bufio.NewScanner(af)
for scanner.Scan() {
if scanner.Text() == needle {
2020-09-08 17:49:02 -06:00
return true, nil
2020-03-01 16:10:55 -07:00
}
}
2020-09-08 17:49:02 -06:00
return false, nil
2020-03-01 16:10:55 -07:00
}
2020-08-18 17:04:23 -06:00
// refresh refreshes internal state.
2020-08-17 17:43:57 -06:00
// It looks for changes to the directory listing, and caches any new mothballs.
2020-08-18 17:04:23 -06:00
func (m *Mothballs) refresh() {
2020-08-17 20:33:23 -06:00
m.categoryLock.Lock()
defer m.categoryLock.Unlock()
2019-09-02 19:47:24 -06:00
// Any new categories?
files, err := afero.ReadDir(m.Fs, "/")
2019-09-02 19:47:24 -06:00
if err != nil {
2020-08-17 20:33:23 -06:00
log.Println("Error listing mothballs:", err)
2019-09-02 19:47:24 -06:00
return
}
2020-08-17 20:33:23 -06:00
found := make(map[string]bool)
2019-09-02 19:47:24 -06:00
for _, f := range files {
filename := f.Name()
if !strings.HasSuffix(filename, ".mb") {
continue
}
categoryName := strings.TrimSuffix(filename, ".mb")
2020-08-17 20:33:23 -06:00
found[categoryName] = true
2019-09-02 19:47:24 -06:00
if _, ok := m.categories[categoryName]; !ok {
2020-08-17 20:33:23 -06:00
f, err := m.Fs.Open(filename)
2019-09-02 19:47:24 -06:00
if err != nil {
2020-08-17 20:33:23 -06:00
log.Println(err)
2019-09-02 19:47:24 -06:00
continue
}
2020-08-17 20:33:23 -06:00
fi, err := f.Stat()
if err != nil {
f.Close()
log.Println(err)
continue
}
zrc, err := zip.NewReader(f, fi.Size())
if err != nil {
f.Close()
log.Println(err)
continue
}
m.categories[categoryName] = zipCategory{
Fs: zipfs.New(zrc),
Closer: f,
}
log.Println("Adding category:", categoryName)
}
}
// Delete anything in the list that wasn't found
for categoryName, zc := range m.categories {
if !found[categoryName] {
zc.Close()
delete(m.categories, categoryName)
log.Println("Removing category:", categoryName)
2019-09-02 19:47:24 -06:00
}
}
}
2020-08-18 17:04:23 -06:00
// Maintain performs housekeeping for Mothballs.
func (m *Mothballs) Maintain(updateInterval time.Duration) {
m.refresh()
for range time.NewTicker(updateInterval).C {
m.refresh()
}
}