OMG it runs, haven't tested yet but exciting stuff here folks

This commit is contained in:
Neale Pickett 2020-02-29 16:51:32 -07:00
parent cbc69f5647
commit 0bc8faa5ec
7 changed files with 137 additions and 164 deletions

View File

@ -1,28 +1,36 @@
package main package main
import ( import (
"path/filepath" "io"
"strings"
"time" "time"
) )
type Component struct { type Category struct {
baseDir string Name string
Puzzles []int
} }
func (c *Component) path(parts ...string) string { type ReadSeekCloser interface {
path := filepath.Clean(filepath.Join(parts...)) io.Reader
parts = filepath.SplitList(path) io.Seeker
for i, part := range parts { io.Closer
part = strings.TrimLeft(part, "./\\:")
parts[i] = part
}
parts = append([]string{c.baseDir}, parts...)
path = filepath.Join(parts...)
path = filepath.Clean(path)
return path
} }
func (c *Component) Run(updateInterval time.Duration) { type PuzzleProvider interface {
// Stub! 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()
} }

View File

@ -5,10 +5,23 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
"log" "log"
"mime" "mime"
"net/http"
"time" "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() { func main() {
log.Print("Started") log.Print("Started")
@ -22,7 +35,7 @@ func main() {
"state", "state",
"Path to state files", "Path to state files",
) )
puzzlePath := flag.String( mothballPath := flag.String(
"mothballs", "mothballs",
"mothballs", "mothballs",
"Path to mothballs to host", "Path to mothballs to host",
@ -37,25 +50,23 @@ func main() {
":8000", ":8000",
"Bind [host]:port for HTTP service", "Bind [host]:port for HTTP service",
) )
base := flag.String(
"base",
"/",
"Base URL of this instance",
)
stateFs := afero.NewBasePathFs(afero.NewOsFs(), *statePath) theme := NewTheme(afero.NewBasePathFs(afero.NewOsFs(), *themePath))
themeFs := afero.NewBasePathFs(afero.NewOsFs(), *themePath) state := NewState(afero.NewBasePathFs(afero.NewOsFs(), *statePath))
mothballFs := afero.NewBasePathFs(afero.NewOsFs(), *mothballPath) puzzles := NewMothballs(afero.NewBasePathFs(afero.NewOsFs(), *mothballPath))
theme := NewTheme(themeFs)
state := NewState(stateFs)
puzzles := NewMothballs(mothballFs)
go state.Run(*refreshInterval)
go puzzles.Run(*refreshInterval)
// Add some MIME extensions // Add some MIME extensions
// Doing this avoids decompressing a mothball entry twice per request // Doing this avoids decompressing a mothball entry twice per request
mime.AddExtensionType(".json", "application/json") mime.AddExtensionType(".json", "application/json")
mime.AddExtensionType(".zip", "application/zip") mime.AddExtensionType(".zip", "application/zip")
http.HandleFunc("/", theme.staticHandler) go custodian(*refreshInterval, []Component{theme, state, puzzles})
log.Printf("Listening on %s", *bindStr) httpd := NewHTTPServer(*base, theme, state, puzzles)
log.Fatal(http.ListenAndServe(*bindStr, nil)) httpd.Run(*bindStr)
} }

View File

@ -2,43 +2,56 @@ package main
import ( import (
"github.com/spf13/afero" "github.com/spf13/afero"
"io/ioutil"
"log" "log"
"io"
"strings" "strings"
"time"
) )
type Mothballs struct { type Mothballs struct {
fs afero.Fs
categories map[string]*Zipfs categories map[string]*Zipfs
afero.Fs
} }
func NewMothballs(fs afero.Fs) *Mothballs { func NewMothballs(fs afero.Fs) *Mothballs {
return &Mothballs{ return &Mothballs{
fs: fs, Fs: fs,
categories: make(map[string]*Zipfs), 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? // Any new categories?
files, err := afero.ReadDir(m.fs, "/") files, err := afero.ReadDir(m.Fs, "/")
if err != nil { if err != nil {
log.Print("Error listing mothballs: ", err) log.Print("Error listing mothballs: ", err)
return return
} }
for _, f := range files { for _, f := range files {
filename := f.Name() filename := f.Name()
filepath := m.path(filename)
if !strings.HasSuffix(filename, ".mb") { if !strings.HasSuffix(filename, ".mb") {
continue continue
} }
categoryName := strings.TrimSuffix(filename, ".mb") categoryName := strings.TrimSuffix(filename, ".mb")
if _, ok := m.categories[categoryName]; !ok { if _, ok := m.categories[categoryName]; !ok {
zfs, err := OpenZipfs(filepath) zfs, err := OpenZipfs(m.Fs, filename)
if err != nil { if err != nil {
log.Print("Error opening ", filepath, ": ", err) log.Print("Error opening ", filename, ": ", err)
continue continue
} }
log.Print("New mothball: ", filename) 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()
}
}
}

View File

@ -35,28 +35,26 @@ type StateExport struct {
// The only thing State methods need to know is the path to the state directory. // The only thing State methods need to know is the path to the state directory.
type State struct { type State struct {
Enabled bool Enabled bool
update chan bool afero.Fs
fs afero.Fs
} }
func NewState(fs afero.Fs) *State { func NewState(fs afero.Fs) *State {
return &State{ return &State{
Enabled: true, Enabled: true,
update: make(chan bool, 10), Fs: fs,
fs: fs,
} }
} }
// Check a few things to see if this state directory is "enabled". // Check a few things to see if this state directory is "enabled".
func (s *State) UpdateEnabled() { func (s *State) UpdateEnabled() {
if _, err := s.fs.Stat("enabled"); os.IsNotExist(err) { if _, err := s.Stat("enabled"); os.IsNotExist(err) {
s.Enabled = false s.Enabled = false
log.Print("Suspended: enabled file missing") log.Print("Suspended: enabled file missing")
return return
} }
nextEnabled := true nextEnabled := true
untilFile, err := s.fs.Open("hours") untilFile, err := s.Open("hours")
if err != nil { if err != nil {
return return
} }
@ -101,7 +99,7 @@ func (s *State) UpdateEnabled() {
// Returns team name given a team ID. // Returns team name given a team ID.
func (s *State) TeamName(teamId string) (string, error) { func (s *State) TeamName(teamId string) (string, error) {
teamFile := filepath.Join("teams", teamId) teamFile := filepath.Join("teams", teamId)
teamNameBytes, err := afero.ReadFile(s.fs, teamFile) teamNameBytes, err := afero.ReadFile(s, teamFile)
teamName := strings.TrimSpace(string(teamNameBytes)) teamName := strings.TrimSpace(string(teamNameBytes))
if os.IsNotExist(err) { 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. // Write out team name. This can only be done once.
func (s *State) SetTeamName(teamId string, teamName string) error { 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") return fmt.Errorf("Team IDs file does not exist")
} else { } else {
found := false found := false
@ -133,7 +131,7 @@ func (s *State) SetTeamName(teamId string, teamName string) error {
} }
teamFile := filepath.Join("teams", teamId) 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) { if os.IsExist(err) {
return fmt.Errorf("Team ID is already registered") 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 // Retrieve the current points log
func (s *State) PointsLog() []*Award { func (s *State) PointsLog() []*Award {
f, err := s.fs.Open("points.log") f, err := s.Open("points.log")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return nil return nil
@ -178,7 +176,7 @@ func (s *State) Export(teamId string) *StateExport {
} }
// Read in messages // 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) log.Print(err)
} else { } else {
defer f.Close() defer f.Close()
@ -243,29 +241,29 @@ func (s *State) AwardPoints(teamId, category string, points int) error {
tmpfn := filepath.Join("points.tmp", fn) tmpfn := filepath.Join("points.tmp", fn)
newfn := filepath.Join("points.new", 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 return err
} }
if err := s.fs.Rename(tmpfn, newfn); err != nil { if err := s.Rename(tmpfn, newfn); err != nil {
return err return err
} }
s.update <- true // XXX: update everything
return nil return nil
} }
// collectPoints gathers up files in points.new/ and appends their contents to points.log, // collectPoints gathers up files in points.new/ and appends their contents to points.log,
// removing each points.new/ file as it goes. // removing each points.new/ file as it goes.
func (s *State) collectPoints() { func (s *State) collectPoints() {
files, err := afero.ReadDir(s.fs, "points.new") files, err := afero.ReadDir(s, "points.new")
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return return
} }
for _, f := range files { for _, f := range files {
filename := filepath.Join("points.new", f.Name()) filename := filepath.Join("points.new", f.Name())
awardstr, err := afero.ReadFile(s.fs, filename) awardstr, err := afero.ReadFile(s, filename)
if err != nil { if err != nil {
log.Print("Opening new points: ", err) log.Print("Opening new points: ", err)
continue continue
@ -289,7 +287,7 @@ func (s *State) collectPoints() {
} else { } else {
log.Print("Award: ", award.String()) 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 { if err != nil {
log.Print("Can't append to points log: ", err) log.Print("Can't append to points log: ", err)
return return
@ -298,7 +296,7 @@ func (s *State) collectPoints() {
logf.Close() 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) log.Print("Unable to remove new points file: ", err)
} }
} }
@ -306,28 +304,28 @@ func (s *State) collectPoints() {
func (s *State) maybeInitialize() { func (s *State) maybeInitialize() {
// Are we supposed to re-initialize? // Are we supposed to re-initialize?
if _, err := s.fs.Stat("initialized"); !os.IsNotExist(err) { if _, err := s.Stat("initialized"); !os.IsNotExist(err) {
return return
} }
log.Print("initialized file missing, re-initializing") log.Print("initialized file missing, re-initializing")
// Remove any extant control and state files // Remove any extant control and state files
s.fs.Remove("enabled") s.Remove("enabled")
s.fs.Remove("hours") s.Remove("hours")
s.fs.Remove("points.log") s.Remove("points.log")
s.fs.Remove("messages.txt") s.Remove("messages.txt")
s.fs.RemoveAll("points.tmp") s.RemoveAll("points.tmp")
s.fs.RemoveAll("points.new") s.RemoveAll("points.new")
s.fs.RemoveAll("teams") s.RemoveAll("teams")
// Make sure various subdirectories exist // Make sure various subdirectories exist
s.fs.Mkdir("points.tmp", 0755) s.Mkdir("points.tmp", 0755)
s.fs.Mkdir("points.new", 0755) s.Mkdir("points.new", 0755)
s.fs.Mkdir("teams", 0755) s.Mkdir("teams", 0755)
// Preseed available team ids if file doesn't exist // 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() defer f.Close()
for i := 0; i < 100; i += 1 { for i := 0; i < 100; i += 1 {
fmt.Fprintln(f, mktoken()) fmt.Fprintln(f, mktoken())
@ -336,19 +334,19 @@ func (s *State) maybeInitialize() {
// Create some files // Create some files
afero.WriteFile( afero.WriteFile(
s.fs, s,
"initialized", "initialized",
[]byte("state/initialized: remove to re-initialize the contest\n"), []byte("state/initialized: remove to re-initialize the contest\n"),
0644, 0644,
) )
afero.WriteFile( afero.WriteFile(
s.fs, s,
"enabled", "enabled",
[]byte("state/enabled: remove to suspend the contest\n"), []byte("state/enabled: remove to suspend the contest\n"),
0644, 0644,
) )
afero.WriteFile( afero.WriteFile(
s.fs, s,
"hours", "hours",
[]byte( []byte(
"# state/hours: when the contest is enabled\n"+ "# state/hours: when the contest is enabled\n"+
@ -360,33 +358,23 @@ func (s *State) maybeInitialize() {
0644, 0644,
) )
afero.WriteFile( afero.WriteFile(
s.fs, s,
"messages.txt", "messages.txt",
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))), []byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))),
0644, 0644,
) )
afero.WriteFile( afero.WriteFile(
s.fs, s,
"points.log", "points.log",
[]byte(""), []byte(""),
0644, 0644,
) )
} }
func (s *State) Cleanup() { func (s *State) Update() {
s.maybeInitialize() s.maybeInitialize()
s.UpdateEnabled() s.UpdateEnabled()
if s.Enabled { if s.Enabled {
s.collectPoints() s.collectPoints()
} }
} }
func (s *State) Run(updateInterval time.Duration) {
for {
s.Cleanup()
select {
case <-s.update:
case <-time.After(updateInterval):
}
}
}

View File

@ -2,42 +2,32 @@ package main
import ( import (
"github.com/spf13/afero" "github.com/spf13/afero"
"net/http" "time"
"strings"
) )
type Theme struct { type Theme struct {
fs afero.Fs afero.Fs
} }
func NewTheme(fs afero.Fs) *Theme { func NewTheme(fs afero.Fs) *Theme {
return &Theme{ return &Theme{
fs: fs, Fs: fs,
} }
} }
func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) { // I don't understand why I need this. The type checking system is weird here.
path := req.URL.Path func (t *Theme) Open(name string) (ReadSeekCloser, error) {
if strings.Contains(path, "/.") { return t.Fs.Open(name)
http.Error(w, "Invalid path", http.StatusBadRequest) }
return
} func (t *Theme) ModTime(name string) (mt time.Time, err error) {
if path == "/" { fi, err := t.Fs.Stat(name)
path = "/index.html" if err == nil {
} mt = fi.ModTime()
}
f, err := t.fs.Open(path) return
if err != nil { }
http.NotFound(w, req)
return func (t *Theme) Update() {
} // No periodic tasks for a theme
defer f.Close()
d, err := f.Stat()
if err != nil {
http.NotFound(w, req)
return
}
http.ServeContent(w, req, path, d.ModTime(), f)
} }

View File

@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "github.com/spf13/afero"
"strings" "strings"
"time" "time"
) )
type Zipfs struct { type Zipfs struct {
zf *zip.ReadCloser f io.Closer
fs afero.Fs zf *zip.Reader
filename string filename string
mtime time.Time mtime time.Time
fs afero.Fs
} }
type ZipfsFile struct { type ZipfsFile struct {
@ -112,7 +113,7 @@ func (zfsf *ZipfsFile) Close() error {
return zfsf.f.Close() 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 var zfs Zipfs
zfs.fs = fs zfs.fs = fs
@ -127,7 +128,7 @@ func OpenZipfs(fs afero.fs, filename string) (*Zipfs, error) {
} }
func (zfs *Zipfs) Close() error { func (zfs *Zipfs) Close() error {
return zfs.zf.Close() return zfs.f.Close()
} }
func (zfs *Zipfs) Refresh() error { func (zfs *Zipfs) Refresh() error {
@ -146,15 +147,18 @@ func (zfs *Zipfs) Refresh() error {
return err return err
} }
zf, err := zip.OpenReader(zfs.filename) zf, err := zip.NewReader(f, info.Size())
if err != nil { if err != nil {
f.Close()
return err return err
} }
// Clean up the last one
if zfs.zf != nil { if zfs.zf != nil {
zfs.zf.Close() zfs.f.Close()
} }
zfs.zf = zf zfs.zf = zf
zfs.f = f
zfs.mtime = mtime zfs.mtime = mtime
return nil return nil

View File

@ -12,37 +12,7 @@ import (
"strings" "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. // hasLine returns true if line appears in r.
// The entire line must match. // The entire line must match.
@ -120,9 +90,9 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
return return
} }
points, err := strconv.Atoi(pointstr)
if err != nil { if err != nil {
respond( respond(
points, err := strconv.Atoi(pointstr)
w, req, JSendFail, w, req, JSendFail,
"Cannot parse point value", "Cannot parse point value",
"This doesn't look like an integer: %s", pointstr, "This doesn't look like an integer: %s", pointstr,