moth/cmd/mothd/mothballs.go

114 lines
2.8 KiB
Go

package main
import (
"bufio"
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/spf13/afero"
)
// Mothballs provides a collection of active mothball files (puzzle categories)
type Mothballs struct {
categories map[string]*Zipfs
afero.Fs
}
// NewMothballs returns a new Mothballs structure backed by the provided directory
func NewMothballs(fs afero.Fs) *Mothballs {
return &Mothballs{
Fs: fs,
categories: make(map[string]*Zipfs),
}
}
// Open returns a ReadSeekCloser corresponding to the filename in a puzzle's category and points
func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
mb, ok := m.categories[cat]
if !ok {
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat)
}
f, err := mb.Open(fmt.Sprintf("content/%d/%s", points, filename))
return f, mb.ModTime(), err
}
// Inventory returns the list of current categories
func (m *Mothballs) Inventory() []Category {
categories := make([]Category, 0, 20)
for cat, zfs := range m.categories {
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)
}
}
categories = append(categories, Category{cat, pointsList})
}
return categories
}
// CheckAnswer returns an error if the provided answer is in any way incorrect for the given category and points
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) error {
zfs, ok := m.categories[cat]
if !ok {
return fmt.Errorf("No such category: %s", cat)
}
af, err := zfs.Open("answers.txt")
if err != nil {
return fmt.Errorf("No answers.txt file")
}
defer af.Close()
needle := fmt.Sprintf("%d %s", points, answer)
scanner := bufio.NewScanner(af)
for scanner.Scan() {
if scanner.Text() == needle {
return nil
}
}
return fmt.Errorf("Invalid answer")
}
// Update refreshes internal state.
// It looks for changes to the directory listing, and caches any new mothballs.
func (m *Mothballs) Update() {
// Any new categories?
files, err := afero.ReadDir(m.Fs, "/")
if err != nil {
log.Print("Error listing mothballs: ", err)
return
}
for _, f := range files {
filename := f.Name()
if !strings.HasSuffix(filename, ".mb") {
continue
}
categoryName := strings.TrimSuffix(filename, ".mb")
if _, ok := m.categories[categoryName]; !ok {
zfs, err := OpenZipfs(m.Fs, filename)
if err != nil {
log.Print("Error opening ", filename, ": ", err)
continue
}
log.Print("New mothball: ", filename)
m.categories[categoryName] = zfs
}
}
}