diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go index 2217258..2e07ea1 100644 --- a/cmd/mothd/common.go +++ b/cmd/mothd/common.go @@ -1,28 +1,36 @@ package main import ( - "path/filepath" - "strings" + "io" "time" ) -type Component struct { - baseDir string +type Category struct { + Name string + Puzzles []int } -func (c *Component) path(parts ...string) string { - path := filepath.Clean(filepath.Join(parts...)) - parts = filepath.SplitList(path) - for i, part := range parts { - part = strings.TrimLeft(part, "./\\:") - parts[i] = part - } - parts = append([]string{c.baseDir}, parts...) - path = filepath.Join(parts...) - path = filepath.Clean(path) - return path +type ReadSeekCloser interface { + io.Reader + io.Seeker + io.Closer } -func (c *Component) Run(updateInterval time.Duration) { - // Stub! +type PuzzleProvider interface { + Metadata(cat string, points int) (io.ReadCloser, error) + Open(cat string, points int, path string) (io.ReadCloser, error) + Inventory() []Category +} + +type ThemeProvider interface { + Open(path string) (ReadSeekCloser, error) + ModTime(path string) (time.Time, error) +} + +type StateProvider interface { + +} + +type Component interface { + Update() } diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 98ea8b1..b377526 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -5,10 +5,23 @@ import ( "github.com/spf13/afero" "log" "mime" - "net/http" "time" ) +func custodian(updateInterval time.Duration, components []Component) { + update := func() { + for _, c := range components { + c.Update() + } + } + + ticker := time.NewTicker(updateInterval) + update() + for _ = range ticker.C { + update() + } +} + func main() { log.Print("Started") @@ -22,7 +35,7 @@ func main() { "state", "Path to state files", ) - puzzlePath := flag.String( + mothballPath := flag.String( "mothballs", "mothballs", "Path to mothballs to host", @@ -37,25 +50,23 @@ func main() { ":8000", "Bind [host]:port for HTTP service", ) + base := flag.String( + "base", + "/", + "Base URL of this instance", + ) - stateFs := afero.NewBasePathFs(afero.NewOsFs(), *statePath) - themeFs := afero.NewBasePathFs(afero.NewOsFs(), *themePath) - mothballFs := afero.NewBasePathFs(afero.NewOsFs(), *mothballPath) - - theme := NewTheme(themeFs) - state := NewState(stateFs) - puzzles := NewMothballs(mothballFs) - - go state.Run(*refreshInterval) - go puzzles.Run(*refreshInterval) + theme := NewTheme(afero.NewBasePathFs(afero.NewOsFs(), *themePath)) + state := NewState(afero.NewBasePathFs(afero.NewOsFs(), *statePath)) + puzzles := NewMothballs(afero.NewBasePathFs(afero.NewOsFs(), *mothballPath)) // Add some MIME extensions // Doing this avoids decompressing a mothball entry twice per request mime.AddExtensionType(".json", "application/json") mime.AddExtensionType(".zip", "application/zip") - http.HandleFunc("/", theme.staticHandler) + go custodian(*refreshInterval, []Component{theme, state, puzzles}) - log.Printf("Listening on %s", *bindStr) - log.Fatal(http.ListenAndServe(*bindStr, nil)) + httpd := NewHTTPServer(*base, theme, state, puzzles) + httpd.Run(*bindStr) } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index e4c5d51..4877f26 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -2,43 +2,56 @@ package main import ( "github.com/spf13/afero" - "io/ioutil" "log" + "io" "strings" - "time" ) type Mothballs struct { - fs afero.Fs categories map[string]*Zipfs + afero.Fs } func NewMothballs(fs afero.Fs) *Mothballs { return &Mothballs{ - fs: fs, + Fs: fs, categories: make(map[string]*Zipfs), } } -func (m *Mothballs) update() { +func (m *Mothballs) Metadata(cat string, points int) (io.ReadCloser, error) { + f, err := m.Fs.Open("/dev/null") + return f, err +} + +func (m *Mothballs) Open(cat string, points int, filename string) (io.ReadCloser, error) { + f, err := m.Fs.Open("/dev/null") + return f, err +} + +func (m *Mothballs) Inventory() []Category { + return []Category{} +} + + +func (m *Mothballs) Update() { // Any new categories? - files, err := afero.ReadDir(m.fs, "/") + files, err := afero.ReadDir(m.Fs, "/") if err != nil { log.Print("Error listing mothballs: ", err) return } for _, f := range files { filename := f.Name() - filepath := m.path(filename) if !strings.HasSuffix(filename, ".mb") { continue } categoryName := strings.TrimSuffix(filename, ".mb") if _, ok := m.categories[categoryName]; !ok { - zfs, err := OpenZipfs(filepath) + zfs, err := OpenZipfs(m.Fs, filename) if err != nil { - log.Print("Error opening ", filepath, ": ", err) + log.Print("Error opening ", filename, ": ", err) continue } log.Print("New mothball: ", filename) @@ -47,14 +60,3 @@ func (m *Mothballs) update() { } } -func (m *Mothballs) Run(updateInterval time.Duration) { - ticker := time.NewTicker(updateInterval) - m.update() - for { - select { - case when := <-ticker.C: - log.Print("Tick: ", when) - m.update() - } - } -} diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 775bf85..081b644 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -35,28 +35,26 @@ type StateExport struct { // The only thing State methods need to know is the path to the state directory. type State struct { Enabled bool - update chan bool - fs afero.Fs + afero.Fs } func NewState(fs afero.Fs) *State { return &State{ Enabled: true, - update: make(chan bool, 10), - fs: fs, + Fs: fs, } } // Check a few things to see if this state directory is "enabled". func (s *State) UpdateEnabled() { - if _, err := s.fs.Stat("enabled"); os.IsNotExist(err) { + if _, err := s.Stat("enabled"); os.IsNotExist(err) { s.Enabled = false log.Print("Suspended: enabled file missing") return } nextEnabled := true - untilFile, err := s.fs.Open("hours") + untilFile, err := s.Open("hours") if err != nil { return } @@ -101,7 +99,7 @@ func (s *State) UpdateEnabled() { // Returns team name given a team ID. func (s *State) TeamName(teamId string) (string, error) { teamFile := filepath.Join("teams", teamId) - teamNameBytes, err := afero.ReadFile(s.fs, teamFile) + teamNameBytes, err := afero.ReadFile(s, teamFile) teamName := strings.TrimSpace(string(teamNameBytes)) if os.IsNotExist(err) { @@ -115,7 +113,7 @@ func (s *State) TeamName(teamId string) (string, error) { // Write out team name. This can only be done once. func (s *State) SetTeamName(teamId string, teamName string) error { - if f, err := s.fs.Open("teamids.txt"); err != nil { + if f, err := s.Open("teamids.txt"); err != nil { return fmt.Errorf("Team IDs file does not exist") } else { found := false @@ -133,7 +131,7 @@ func (s *State) SetTeamName(teamId string, teamName string) error { } teamFile := filepath.Join("teams", teamId) - err := afero.WriteFile(s.fs, teamFile, []byte(teamName), os.ModeExclusive|0644) + err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644) if os.IsExist(err) { return fmt.Errorf("Team ID is already registered") } @@ -142,7 +140,7 @@ func (s *State) SetTeamName(teamId string, teamName string) error { // Retrieve the current points log func (s *State) PointsLog() []*Award { - f, err := s.fs.Open("points.log") + f, err := s.Open("points.log") if err != nil { log.Println(err) return nil @@ -178,7 +176,7 @@ func (s *State) Export(teamId string) *StateExport { } // Read in messages - if f, err := s.fs.Open("messages.txt"); err != nil { + if f, err := s.Open("messages.txt"); err != nil { log.Print(err) } else { defer f.Close() @@ -243,29 +241,29 @@ func (s *State) AwardPoints(teamId, category string, points int) error { tmpfn := filepath.Join("points.tmp", fn) newfn := filepath.Join("points.new", fn) - if err := afero.WriteFile(s.fs, tmpfn, []byte(a.String()), 0644); err != nil { + if err := afero.WriteFile(s, tmpfn, []byte(a.String()), 0644); err != nil { return err } - if err := s.fs.Rename(tmpfn, newfn); err != nil { + if err := s.Rename(tmpfn, newfn); err != nil { return err } - s.update <- true + // XXX: update everything return nil } // collectPoints gathers up files in points.new/ and appends their contents to points.log, // removing each points.new/ file as it goes. func (s *State) collectPoints() { - files, err := afero.ReadDir(s.fs, "points.new") + files, err := afero.ReadDir(s, "points.new") if err != nil { log.Print(err) return } for _, f := range files { filename := filepath.Join("points.new", f.Name()) - awardstr, err := afero.ReadFile(s.fs, filename) + awardstr, err := afero.ReadFile(s, filename) if err != nil { log.Print("Opening new points: ", err) continue @@ -289,7 +287,7 @@ func (s *State) collectPoints() { } else { log.Print("Award: ", award.String()) - logf, err := s.fs.OpenFile("points.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + logf, err := s.OpenFile("points.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Print("Can't append to points log: ", err) return @@ -298,7 +296,7 @@ func (s *State) collectPoints() { logf.Close() } - if err := s.fs.Remove(filename); err != nil { + if err := s.Remove(filename); err != nil { log.Print("Unable to remove new points file: ", err) } } @@ -306,28 +304,28 @@ func (s *State) collectPoints() { func (s *State) maybeInitialize() { // Are we supposed to re-initialize? - if _, err := s.fs.Stat("initialized"); !os.IsNotExist(err) { + if _, err := s.Stat("initialized"); !os.IsNotExist(err) { return } log.Print("initialized file missing, re-initializing") // Remove any extant control and state files - s.fs.Remove("enabled") - s.fs.Remove("hours") - s.fs.Remove("points.log") - s.fs.Remove("messages.txt") - s.fs.RemoveAll("points.tmp") - s.fs.RemoveAll("points.new") - s.fs.RemoveAll("teams") + s.Remove("enabled") + s.Remove("hours") + s.Remove("points.log") + s.Remove("messages.txt") + s.RemoveAll("points.tmp") + s.RemoveAll("points.new") + s.RemoveAll("teams") // Make sure various subdirectories exist - s.fs.Mkdir("points.tmp", 0755) - s.fs.Mkdir("points.new", 0755) - s.fs.Mkdir("teams", 0755) + s.Mkdir("points.tmp", 0755) + s.Mkdir("points.new", 0755) + s.Mkdir("teams", 0755) // Preseed available team ids if file doesn't exist - if f, err := s.fs.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { + if f, err := s.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { defer f.Close() for i := 0; i < 100; i += 1 { fmt.Fprintln(f, mktoken()) @@ -336,19 +334,19 @@ func (s *State) maybeInitialize() { // Create some files afero.WriteFile( - s.fs, + s, "initialized", []byte("state/initialized: remove to re-initialize the contest\n"), 0644, ) afero.WriteFile( - s.fs, + s, "enabled", []byte("state/enabled: remove to suspend the contest\n"), 0644, ) afero.WriteFile( - s.fs, + s, "hours", []byte( "# state/hours: when the contest is enabled\n"+ @@ -360,33 +358,23 @@ func (s *State) maybeInitialize() { 0644, ) afero.WriteFile( - s.fs, + s, "messages.txt", []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))), 0644, ) afero.WriteFile( - s.fs, + s, "points.log", []byte(""), 0644, ) } -func (s *State) Cleanup() { +func (s *State) Update() { s.maybeInitialize() s.UpdateEnabled() if s.Enabled { s.collectPoints() } } - -func (s *State) Run(updateInterval time.Duration) { - for { - s.Cleanup() - select { - case <-s.update: - case <-time.After(updateInterval): - } - } -} diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index b7a24ee..afb7e7e 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -2,42 +2,32 @@ package main import ( "github.com/spf13/afero" - "net/http" - "strings" + "time" ) type Theme struct { - fs afero.Fs + afero.Fs } func NewTheme(fs afero.Fs) *Theme { return &Theme{ - fs: fs, + Fs: fs, } } -func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) { - path := req.URL.Path - if strings.Contains(path, "/.") { - http.Error(w, "Invalid path", http.StatusBadRequest) - return - } - if path == "/" { - path = "/index.html" - } - - f, err := t.fs.Open(path) - if err != nil { - http.NotFound(w, req) - return - } - defer f.Close() - - d, err := f.Stat() - if err != nil { - http.NotFound(w, req) - return - } - - http.ServeContent(w, req, path, d.ModTime(), f) +// I don't understand why I need this. The type checking system is weird here. +func (t *Theme) Open(name string) (ReadSeekCloser, error) { + return t.Fs.Open(name) +} + +func (t *Theme) ModTime(name string) (mt time.Time, err error) { + fi, err := t.Fs.Stat(name) + if err == nil { + mt = fi.ModTime() + } + return +} + +func (t *Theme) Update() { + // No periodic tasks for a theme } diff --git a/cmd/mothd/zipfs.go b/cmd/mothd/zipfs.go index 9c27e62..a955e35 100644 --- a/cmd/mothd/zipfs.go +++ b/cmd/mothd/zipfs.go @@ -5,16 +5,17 @@ import ( "fmt" "io" "io/ioutil" - "os" + "github.com/spf13/afero" "strings" "time" ) type Zipfs struct { - zf *zip.ReadCloser - fs afero.Fs + f io.Closer + zf *zip.Reader filename string mtime time.Time + fs afero.Fs } type ZipfsFile struct { @@ -112,7 +113,7 @@ func (zfsf *ZipfsFile) Close() error { return zfsf.f.Close() } -func OpenZipfs(fs afero.fs, filename string) (*Zipfs, error) { +func OpenZipfs(fs afero.Fs, filename string) (*Zipfs, error) { var zfs Zipfs zfs.fs = fs @@ -127,7 +128,7 @@ func OpenZipfs(fs afero.fs, filename string) (*Zipfs, error) { } func (zfs *Zipfs) Close() error { - return zfs.zf.Close() + return zfs.f.Close() } func (zfs *Zipfs) Refresh() error { @@ -146,15 +147,18 @@ func (zfs *Zipfs) Refresh() error { return err } - zf, err := zip.OpenReader(zfs.filename) + zf, err := zip.NewReader(f, info.Size()) if err != nil { + f.Close() return err } + // Clean up the last one if zfs.zf != nil { - zfs.zf.Close() + zfs.f.Close() } zfs.zf = zf + zfs.f = f zfs.mtime = mtime return nil diff --git a/cmd/mothdv3/handlers.go b/cmd/mothdv3/handlers.go index a699223..29197bd 100644 --- a/cmd/mothdv3/handlers.go +++ b/cmd/mothdv3/handlers.go @@ -12,37 +12,7 @@ import ( "strings" ) -// https://github.com/omniti-labs/jsend -type JSend struct { - Status string `json:"status"` - Data struct { - Short string `json:"short"` - Description string `json:"description"` - } `json:"data"` -} -const ( - JSendSuccess = "success" - JSendFail = "fail" - JSendError = "error" -) - -func respond(w http.ResponseWriter, req *http.Request, status string, short string, format string, a ...interface{}) { - resp := JSend{} - resp.Status = status - resp.Data.Short = short - resp.Data.Description = fmt.Sprintf(format, a...) - - respBytes, err := json.Marshal(resp) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) // RFC2616 makes it pretty clear that 4xx codes are for the user-agent - w.Write(respBytes) -} // hasLine returns true if line appears in r. // The entire line must match. @@ -120,9 +90,9 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) { return } - points, err := strconv.Atoi(pointstr) if err != nil { respond( + points, err := strconv.Atoi(pointstr) w, req, JSendFail, "Cannot parse point value", "This doesn't look like an integer: %s", pointstr,