package main import ( "archive/zip" "bufio" "fmt" "io" "log" "strconv" "strings" "sync" "time" "github.com/spf13/afero" "github.com/spf13/afero/zipfs" ) type zipCategory struct { afero.Fs io.Closer } // Mothballs provides a collection of active mothball files (puzzle categories) type Mothballs struct { afero.Fs categories map[string]zipCategory categoryLock *sync.RWMutex } // 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]zipCategory), categoryLock: new(sync.RWMutex), } } func (m *Mothballs) getCat(cat string) (zipCategory, bool) { m.categoryLock.RLock() defer m.categoryLock.RUnlock() ret, ok := m.categories[cat] return ret, ok } // 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) { zc, ok := m.getCat(cat) if !ok { return nil, time.Time{}, fmt.Errorf("No such category: %s", cat) } 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 } // Inventory returns the list of current categories func (m *Mothballs) Inventory() []Category { m.categoryLock.RLock() defer m.categoryLock.RUnlock() 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.getCat(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") } // refresh refreshes internal state. // It looks for changes to the directory listing, and caches any new mothballs. func (m *Mothballs) refresh() { m.categoryLock.Lock() defer m.categoryLock.Unlock() // Any new categories? files, err := afero.ReadDir(m.Fs, "/") if err != nil { log.Println("Error listing mothballs:", err) return } found := make(map[string]bool) for _, f := range files { filename := f.Name() if !strings.HasSuffix(filename, ".mb") { continue } categoryName := strings.TrimSuffix(filename, ".mb") found[categoryName] = true if _, ok := m.categories[categoryName]; !ok { f, err := m.Fs.Open(filename) if err != nil { log.Println(err) continue } 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) } } } // Maintain performs housekeeping for Mothballs. func (m *Mothballs) Maintain(updateInterval time.Duration) { m.refresh() for range time.NewTicker(updateInterval).C { m.refresh() } }