diff --git a/maintenance.go b/maintenance.go
deleted file mode 100644
index f352c33..0000000
--- a/maintenance.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package main
-
-import (
- "log"
- "io/ioutil"
- "time"
- "os"
- "strings"
-)
-
-func cacheMothball(filepath string, categoryName string) {
- log.Printf("I'm exploding a mothball %s %s", filepath, categoryName)
-}
-
-// maintenance runs
-func tidy() {
- // Skip if we've been disabled
- if _, err := os.Stat(statePath("disabled")); err == nil {
- log.Print("disabled file found, suspending maintenance")
- return
- }
-
- // Skip if we've expired
- untilspec, err := ioutil.ReadFile(statePath("until"))
- if err == nil {
- until, err := time.Parse(time.RFC3339, string(untilspec))
- if err != nil {
- log.Print("Unparseable date in until file: %s", until)
- } else {
- if until.Before(time.Now()) {
- log.Print("until file time reached, suspending maintenance")
- return
- }
- }
- }
-
- // Get current list of categories
- newCategories := []string{}
- files, err := ioutil.ReadDir(modulesPath())
- if err != nil {
- log.Printf("Error reading packages: %s", err)
- }
- for _, f := range files {
- filename := f.Name()
- filepath := modulesPath(filename)
- if ! strings.HasSuffix(filename, ".mb") {
- continue
- }
-
- categoryName := strings.TrimSuffix(filename, ".mb")
- newCategories = append(newCategories, categoryName)
-
- // Uncompress into cache directory
- cacheMothball(filepath, categoryName)
- }
- categories = newCategories
-
- collectPoints()
-}
-
-// maintenance is the goroutine that runs a periodic maintenance task
-func maintenance(maintenanceInterval time.Duration) {
- for ;; time.Sleep(maintenanceInterval) {
- tidy()
- }
-}
diff --git a/mothd.go b/mothd.go
deleted file mode 100644
index 18f42a6..0000000
--- a/mothd.go
+++ /dev/null
@@ -1,167 +0,0 @@
-package main
-
-import (
- "bufio"
- "github.com/namsral/flag"
- "fmt"
- "log"
- "net/http"
- "os"
- "path"
- "strings"
- "time"
-)
-
-var moduleDir string
-var stateDir string
-var cacheDir string
-var categories = []string{}
-
-// anchoredSearch looks for needle in filename,
-// skipping the first skip space-delimited words
-func anchoredSearch(filename string, needle string, skip int) bool {
- f, err := os.Open(filename)
- if err != nil {
- log.Print("Can't open %s: %s", filename, err)
- return false
- }
- defer f.Close()
-
- scanner := bufio.NewScanner(f)
- for scanner.Scan() {
- line := scanner.Text()
- parts := strings.SplitN(" ", line, skip+1)
- if parts[skip+1] == needle {
- return true
- }
- }
-
- return false
-}
-
-func showPage(w http.ResponseWriter, title string, body string) {
- w.WriteHeader(http.StatusOK)
-
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "
")
- fmt.Fprintf(w, "%s", title)
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "%s
", title)
- fmt.Fprintf(w, "", body)
- fmt.Fprintf(w, "")
- fmt.Fprintf(w, "")
-}
-
-func modulesPath(parts ...string) string {
- tail := path.Join(parts...)
- return path.Join(moduleDir, tail)
-}
-
-func statePath(parts ...string) string {
- tail := path.Join(parts...)
- return path.Join(stateDir, tail)
-}
-
-func cachePath(parts ...string) string {
- tail := path.Join(parts...)
- return path.Join(cacheDir, tail)
-}
-
-func logRequest(handler http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- log.Printf("HTTP %s %s %s\n", r.RemoteAddr, r.Method, r.URL)
- handler.ServeHTTP(w, r)
- })
-}
-
-func setup() error {
- // Roll over and die if directories aren't even set up
- if _, err := os.Stat(modulesPath()); os.IsNotExist(err) {
- return err
- }
- if _, err := os.Stat(statePath()); os.IsNotExist(err) {
- return err
- }
- if _, err := os.Stat(cachePath()); os.IsNotExist(err) {
- return err
- }
-
- // Make sure points directories exist
- os.Mkdir(statePath("points.tmp"), 0755)
- os.Mkdir(statePath("points.new"), 0755)
-
- // Preseed available team ids if file doesn't exist
- if f, err := os.OpenFile(statePath("teamids.txt"), os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0644); err == nil {
- defer f.Close()
- for i := 0; i <= 9999; i += 1 {
- fmt.Fprintf(f, "%04d\n", i)
- }
- }
-
- return nil
-}
-
-func main() {
- var maintenanceInterval time.Duration
- var listen string
-
- fs := flag.NewFlagSetWithEnvPrefix(os.Args[0], "MOTH", flag.ExitOnError)
- fs.StringVar(
- &moduleDir,
- "modules",
- "/moth/modules",
- "Path where your moth modules live",
- )
- fs.StringVar(
- &stateDir,
- "state",
- "/moth/state",
- "Path where state should be written",
- )
- fs.StringVar(
- &cacheDir,
- "cache",
- "/moth/cache",
- "Path for ephemeral cache",
- )
- fs.DurationVar(
- &maintenanceInterval,
- "maint",
- 20 * time.Second,
- "Maintenance interval",
- )
- fs.StringVar(
- &listen,
- "listen",
- ":8080",
- "[host]:port to bind and listen",
- )
- fs.Parse(os.Args[1:])
-
- if err := setup(); err != nil {
- log.Fatal(err)
- }
- go maintenance(maintenanceInterval)
-
- fileserver := http.FileServer(http.Dir(cacheDir))
- http.HandleFunc("/", rootHandler)
- http.Handle("/static/", http.StripPrefix("/static", fileserver))
-
- http.HandleFunc("/register", registerHandler)
- http.HandleFunc("/token", tokenHandler)
- http.HandleFunc("/answer", answerHandler)
-
- http.HandleFunc("/puzzles.json", puzzlesHandler)
- http.HandleFunc("/points.json", pointsHandler)
-
- log.Printf("Listening on %s", listen)
- log.Fatal(http.ListenAndServe(listen, logRequest(http.DefaultServeMux)))
-}
diff --git a/points.go b/points.go
deleted file mode 100644
index f6f16c0..0000000
--- a/points.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package main
-
-import (
- "bufio"
- "fmt"
- "io/ioutil"
- "log"
- "os"
- "strconv"
- "strings"
- "time"
-)
-
-type Award struct {
- when time.Time
- teamid string
- category string
- points int
-}
-
-func ParseAward(s string) (*Award, error) {
- ret := Award{}
-
- parts := strings.SplitN(s, " ", 5)
- if len(parts) < 4 {
- return nil, fmt.Errorf("Malformed award string")
- }
-
- whenEpoch, err := strconv.ParseInt(parts[0], 10, 64)
- if (err != nil) {
- return nil, fmt.Errorf("Malformed timestamp: %s", parts[0])
- }
- ret.when = time.Unix(whenEpoch, 0)
-
- ret.teamid = parts[1]
- ret.category = parts[2]
-
- points, err := strconv.Atoi(parts[3])
- if (err != nil) {
- return nil, fmt.Errorf("Malformed points: %s", parts[3])
- }
- ret.points = points
-
- return &ret, nil
-}
-
-func (a *Award) String() string {
- return fmt.Sprintf("%d %s %s %d", a.when.Unix(), a.teamid, a.category, a.points)
-}
-
-func pointsLog() []Award {
- var ret []Award
-
- fn := statePath("points.log")
- f, err := os.Open(fn)
- if err != nil {
- log.Printf("Unable to open %s: %s", fn, err)
- return ret
- }
- defer f.Close()
-
- scanner := bufio.NewScanner(f)
- for scanner.Scan() {
- line := scanner.Text()
- cur, err := ParseAward(line)
- if err != nil {
- log.Printf("Skipping malformed award line %s: %s", line, err)
- continue
- }
- ret = append(ret, *cur)
- }
-
- return ret
-}
-
-// awardPoints gives points points to team teamid in category category
-func awardPoints(teamid string, category string, points int) error {
- fn := fmt.Sprintf("%s-%s-%d", teamid, category, points)
- tmpfn := statePath("points.tmp", fn)
- newfn := statePath("points.new", fn)
-
- contents := fmt.Sprintf("%d %s %s %d\n", time.Now().Unix(), teamid, points)
-
- if err := ioutil.WriteFile(tmpfn, []byte(contents), 0644); err != nil {
- return err
- }
-
- if err := os.Rename(tmpfn, newfn); err != nil {
- return err
- }
-
- log.Printf("Award %s %s %d", teamid, category, points)
- 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 collectPoints() {
- logf, err := os.OpenFile(statePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil {
- log.Printf("Can't append to points log: %s", err)
- return
- }
- defer logf.Close()
-
- files, err := ioutil.ReadDir(statePath("points.new"))
- if err != nil {
- log.Printf("Error reading packages: %s", err)
- }
- for _, f := range files {
- filename := statePath("points.new", f.Name())
- s, err := ioutil.ReadFile(filename)
- if err != nil {
- log.Printf("Can't read points file %s: %s", filename, err)
- continue
- }
- award, err := ParseAward(string(s))
- if err != nil {
- log.Printf("Can't parse award file %s: %s", filename, err)
- continue
- }
- fmt.Fprintf(logf, "%s\n", award.String())
- log.Print(award.String())
- logf.Sync()
- if err := os.Remove(filename); err != nil {
- log.Printf("Unable to remove %s: %s", filename, err)
- }
- }
-}
\ No newline at end of file
diff --git a/src/award.go b/src/award.go
new file mode 100644
index 0000000..e975599
--- /dev/null
+++ b/src/award.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type Award struct {
+ When time.Time
+ TeamId string
+ Category string
+ Points int
+}
+
+func (a *Award) String() string {
+ return fmt.Sprintf("%d %s %s %d", a.When.Unix(), a.TeamId, a.Category, a.Points)
+}
+
+func ParseAward(s string) (*Award, error) {
+ ret := Award{}
+
+ parts := strings.SplitN(s, " ", 5)
+ if len(parts) < 4 {
+ return nil, fmt.Errorf("Malformed award string")
+ }
+
+ whenEpoch, err := strconv.ParseInt(parts[0], 10, 64)
+ if (err != nil) {
+ return nil, fmt.Errorf("Malformed timestamp: %s", parts[0])
+ }
+ ret.When = time.Unix(whenEpoch, 0)
+
+ ret.TeamId = parts[1]
+ ret.Category = parts[2]
+
+ points, err := strconv.Atoi(parts[3])
+ if (err != nil) {
+ return nil, fmt.Errorf("Malformed Points: %s", parts[3])
+ }
+ ret.Points = points
+
+ return &ret, nil
+}
+
diff --git a/src/award_test.go b/src/award_test.go
new file mode 100644
index 0000000..ba3ff06
--- /dev/null
+++ b/src/award_test.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "testing"
+)
+
+func TestAward(t *testing.T) {
+ entry := "1536958399 1a2b3c4d counting 1"
+ a, err := ParseAward(entry)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if a.TeamId != "1a2b3c4d" {
+ t.Error("TeamID parsed wrong")
+ }
+ if a.Category != "counting" {
+ t.Error("Category parsed wrong")
+ }
+ if a.Points != 1 {
+ t.Error("Points parsed wrong")
+ }
+
+ if a.String() != entry {
+ t.Error("String conversion wonky")
+ }
+
+ if _, err := ParseAward("bad bad bad 1"); err == nil {
+ t.Error("Not throwing error on bad timestamp")
+ }
+ if _, err := ParseAward("1 bad bad bad"); err == nil {
+ t.Error("Not throwing error on bad points")
+ }
+}
diff --git a/handlers.go b/src/handlers.go
similarity index 53%
rename from handlers.go
rename to src/handlers.go
index a1edddd..f3b60ea 100644
--- a/handlers.go
+++ b/src/handlers.go
@@ -7,9 +7,38 @@ import (
"regexp"
"strings"
"strconv"
+ "io"
+ "log"
+ "bufio"
)
-func registerHandler(w http.ResponseWriter, req *http.Request) {
+// anchoredSearch looks for needle in r,
+// skipping the first skip space-delimited words
+func anchoredSearch(r io.Reader, needle string, skip int) bool {
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ parts := strings.SplitN(line, " ", skip+1)
+ if (len(parts) > skip) && (parts[skip] == needle) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func anchoredSearchFile(filename string, needle string, skip int) bool {
+ r, err := os.Open(filename)
+ if err != nil {
+ return false
+ }
+ defer r.Close()
+
+ return anchoredSearch(r, needle, skip)
+}
+
+
+func (ctx Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
teamname := req.FormValue("n")
teamid := req.FormValue("h")
@@ -22,12 +51,12 @@ func registerHandler(w http.ResponseWriter, req *http.Request) {
return
}
- if ! anchoredSearch(statePath("teamids.txt"), teamid, 0) {
+ if ! anchoredSearchFile(ctx.StatePath("teamids.txt"), teamid, 0) {
showPage(w, "Invalid Team ID", "I don't have a record of that team ID. Maybe you used capital letters accidentally?")
return
}
- f, err := os.OpenFile(statePath("state", teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+ f, err := os.OpenFile(ctx.StatePath(teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err != nil {
showPage(
w,
@@ -41,12 +70,12 @@ func registerHandler(w http.ResponseWriter, req *http.Request) {
showPage(w, "Success", "Okay, your team has been named and you may begin using your team ID!")
}
-func tokenHandler(w http.ResponseWriter, req *http.Request) {
+func (ctx Instance) tokenHandler(w http.ResponseWriter, req *http.Request) {
teamid := req.FormValue("t")
token := req.FormValue("k")
// Check answer
- if ! anchoredSearch(token, statePath("tokens.txt"), 0) {
+ if ! anchoredSearchFile(ctx.StatePath("tokens.txt"), token, 0) {
showPage(w, "Unrecognized token", "I don't recognize that token. Did you type in the whole thing?")
return
}
@@ -72,14 +101,14 @@ func tokenHandler(w http.ResponseWriter, req *http.Request) {
return
}
- if err := awardPoints(teamid, category, points); err != nil {
+ if err := ctx.AwardPoints(teamid, category, points); err != nil {
showPage(w, "Error awarding points", err.Error())
return
}
showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
}
-func answerHandler(w http.ResponseWriter, req *http.Request) {
+func (ctx Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
teamid := req.FormValue("t")
category := req.FormValue("c")
pointstr := req.FormValue("p")
@@ -90,41 +119,51 @@ func answerHandler(w http.ResponseWriter, req *http.Request) {
points = 0
}
- // Defang category name; prevent directory traversal
- if matched, _ := regexp.MatchString("^[A-Za-z0-9_-]", category); matched {
- category = ""
+ catmb, ok := ctx.Categories[category]
+ if ! ok {
+ showPage(w, "Category does not exist", "The specified category does not exist. Sorry!")
+ return
}
- // Check answer
- needle := fmt.Sprintf("%s %s", points, answer)
- haystack := cachePath(category, "answers.txt")
+ // Get the answers
+ haystack, err := catmb.Open("answers.txt")
+ if err != nil {
+ showPage(w, "Answers do not exist",
+ "Please tell the contest people that the mothball for this category has no answers.txt in it!")
+ return
+ }
+ defer haystack.Close()
+
+ // Look for the answer
+ needle := fmt.Sprintf("%d %s", points, answer)
if ! anchoredSearch(haystack, needle, 0) {
showPage(w, "Wrong answer", err.Error())
+ return
}
- if err := awardPoints(teamid, category, points); err != nil {
+ if err := ctx.AwardPoints(teamid, category, points); err != nil {
showPage(w, "Error awarding points", err.Error())
return
}
showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
}
-func puzzlesHandler(w http.ResponseWriter, req *http.Request) {
+func (ctx Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
puzzles := map[string][]interface{}{}
// v := map[string][]interface{}{"Moo": {1, "0177f85ae895a33e2e7c5030c3dc484e8173e55c"}}
// j, _ := json.Marshal(v)
- for _, category := range categories {
-
+ for _, category := range ctx.Categories {
+ log.Print(puzzles, category)
}
}
-func pointsHandler(w http.ResponseWriter, req *http.Request) {
+func (ctx Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
}
// staticHandler serves up static files.
-func rootHandler(w http.ResponseWriter, req *http.Request) {
+func (ctx Instance) rootHandler(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/" {
showPage(
w,
@@ -150,3 +189,12 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
http.NotFound(w, req)
}
+
+func (ctx Instance) BindHandlers(mux *http.ServeMux) {
+ mux.HandleFunc(ctx.Base + "/", ctx.rootHandler)
+ mux.HandleFunc(ctx.Base + "/register", ctx.registerHandler)
+ mux.HandleFunc(ctx.Base + "/token", ctx.tokenHandler)
+ mux.HandleFunc(ctx.Base + "/answer", ctx.answerHandler)
+ mux.HandleFunc(ctx.Base + "/puzzles.json", ctx.puzzlesHandler)
+ mux.HandleFunc(ctx.Base + "/points.json", ctx.pointsHandler)
+}
diff --git a/src/instance.go b/src/instance.go
new file mode 100644
index 0000000..01011bc5
--- /dev/null
+++ b/src/instance.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "os"
+ "log"
+ "bufio"
+ "fmt"
+ "time"
+ "io/ioutil"
+ "path"
+ "strings"
+)
+
+type Instance struct {
+ Base string
+ MothballDir string
+ StateDir string
+ Categories map[string]*Mothball
+}
+
+func NewInstance(base, mothballDir, stateDir string) (*Instance, error) {
+ ctx := &Instance{
+ Base: strings.TrimRight(base, "/"),
+ MothballDir: mothballDir,
+ StateDir: stateDir,
+ }
+
+ // Roll over and die if directories aren't even set up
+ if _, err := os.Stat(mothballDir); err != nil {
+ return nil, err
+ }
+ if _, err := os.Stat(stateDir); err != nil {
+ return nil, err
+ }
+
+ ctx.Initialize()
+
+ return ctx, nil
+}
+
+func (ctx *Instance) Initialize () {
+ // Make sure points directories exist
+ os.Mkdir(ctx.StatePath("points.tmp"), 0755)
+ os.Mkdir(ctx.StatePath("points.new"), 0755)
+
+ // Preseed available team ids if file doesn't exist
+ if f, err := os.OpenFile(ctx.StatePath("teamids.txt"), os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0644); err == nil {
+ defer f.Close()
+ for i := 0; i <= 9999; i += 1 {
+ fmt.Fprintf(f, "%04d\n", i)
+ }
+ }
+
+ if f, err := os.OpenFile(ctx.StatePath("initialized"), os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0644); err == nil {
+ defer f.Close()
+ fmt.Println("Remove this file to reinitialize the contest")
+ }
+}
+
+func (ctx Instance) MothballPath(parts ...string) string {
+ tail := path.Join(parts...)
+ return path.Join(ctx.MothballDir, tail)
+}
+
+func (ctx *Instance) StatePath(parts ...string) string {
+ tail := path.Join(parts...)
+ return path.Join(ctx.StateDir, tail)
+}
+
+
+func (ctx *Instance) PointsLog() []Award {
+ var ret []Award
+
+ fn := ctx.StatePath("points.log")
+ f, err := os.Open(fn)
+ if err != nil {
+ log.Printf("Unable to open %s: %s", fn, err)
+ return ret
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line := scanner.Text()
+ cur, err := ParseAward(line)
+ if err != nil {
+ log.Printf("Skipping malformed award line %s: %s", line, err)
+ continue
+ }
+ ret = append(ret, *cur)
+ }
+
+ return ret
+}
+
+// awardPoints gives points points to team teamid in category category
+func (ctx *Instance) AwardPoints(teamid string, category string, points int) error {
+ fn := fmt.Sprintf("%s-%s-%d", teamid, category, points)
+ tmpfn := ctx.StatePath("points.tmp", fn)
+ newfn := ctx.StatePath("points.new", fn)
+
+ contents := fmt.Sprintf("%d %s %s %d\n", time.Now().Unix(), teamid, category, points)
+
+ if err := ioutil.WriteFile(tmpfn, []byte(contents), 0644); err != nil {
+ return err
+ }
+
+ if err := os.Rename(tmpfn, newfn); err != nil {
+ return err
+ }
+
+ log.Printf("Award %s %s %d", teamid, category, points)
+ return nil
+}
diff --git a/src/instance_test.go b/src/instance_test.go
new file mode 100644
index 0000000..06ab7d0
--- /dev/null
+++ b/src/instance_test.go
@@ -0,0 +1 @@
+package main
diff --git a/src/maintenance.go b/src/maintenance.go
new file mode 100644
index 0000000..b359e63
--- /dev/null
+++ b/src/maintenance.go
@@ -0,0 +1,105 @@
+package main
+
+import (
+ "log"
+ "io/ioutil"
+ "time"
+ "os"
+ "strings"
+ "fmt"
+)
+
+// maintenance runs
+func (ctx *Instance) Tidy() {
+ // Skip if we've been disabled
+ if _, err := os.Stat(ctx.StatePath("disabled")); err == nil {
+ log.Print("disabled file found, suspending maintenance")
+ return
+ }
+
+ // Skip if we've expired
+ untilspec, err := ioutil.ReadFile(ctx.StatePath("until"))
+ if err == nil {
+ until, err := time.Parse(time.RFC3339, string(untilspec))
+ if err != nil {
+ log.Printf("Unparseable date in until file: %v", until)
+ } else {
+ if until.Before(time.Now()) {
+ log.Print("until file time reached, suspending maintenance")
+ return
+ }
+ }
+ }
+
+ // Any new categories?
+ files, err := ioutil.ReadDir(ctx.MothballPath())
+ if err != nil {
+ log.Printf("Error listing mothballs: %s", err)
+ }
+ for _, f := range files {
+ filename := f.Name()
+ filepath := ctx.MothballPath(filename)
+ if ! strings.HasSuffix(filename, ".mb") {
+ continue
+ }
+ categoryName := strings.TrimSuffix(filename, ".mb")
+
+ if _, ok := ctx.Categories[categoryName]; !ok {
+ mb, err := OpenMothball(filepath)
+ if err != nil {
+ log.Printf("Error opening %s: %s", filepath, err)
+ continue
+ }
+ ctx.Categories[categoryName] = mb
+ }
+ }
+
+ // Any old categories?
+ log.Print("XXX: Check for and reap old categories")
+
+ ctx.CollectPoints()
+}
+
+// collectPoints gathers up files in points.new/ and appends their contents to points.log,
+// removing each points.new/ file as it goes.
+func (ctx *Instance) CollectPoints() {
+ logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ log.Printf("Can't append to points log: %s", err)
+ return
+ }
+ defer logf.Close()
+
+ files, err := ioutil.ReadDir(ctx.StatePath("points.new"))
+ if err != nil {
+ log.Printf("Error reading packages: %s", err)
+ }
+ for _, f := range files {
+ filename := ctx.StatePath("points.new", f.Name())
+ s, err := ioutil.ReadFile(filename)
+ if err != nil {
+ log.Printf("Can't read points file %s: %s", filename, err)
+ continue
+ }
+ award, err := ParseAward(string(s))
+ if err != nil {
+ log.Printf("Can't parse award file %s: %s", filename, err)
+ continue
+ }
+ fmt.Fprintf(logf, "%s\n", award.String())
+ log.Print("XXX: check for duplicates", award.String())
+ logf.Sync()
+ if err := os.Remove(filename); err != nil {
+ log.Printf("Unable to remove %s: %s", filename, err)
+ }
+ }
+}
+
+
+
+// maintenance is the goroutine that runs a periodic maintenance task
+func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
+ for ;; time.Sleep(maintenanceInterval) {
+ ctx.Tidy()
+ }
+}
diff --git a/src/moth-init b/src/moth-init
deleted file mode 100755
index e0cd4d1..0000000
--- a/src/moth-init
+++ /dev/null
@@ -1,9 +0,0 @@
-#! /bin/sh
-
-while true; do
- /moth/bin/once
- sleep 20
-done &
-
-cd /moth/www
-s6-tcpserver -u $(id -u www) -g $(id -g www) 0.0.0.0 80 /usr/bin/eris -c -d -.
diff --git a/src/mothball/mothball.go b/src/mothball.go
similarity index 73%
rename from src/mothball/mothball.go
rename to src/mothball.go
index 1f63776..1a5dfcd 100644
--- a/src/mothball/mothball.go
+++ b/src/mothball.go
@@ -1,9 +1,10 @@
-package mothball
+package main
import (
"archive/zip"
"fmt"
"io"
+ "io/ioutil"
"os"
"time"
)
@@ -14,7 +15,7 @@ type Mothball struct {
mtime time.Time
}
-func Open(filename string) (*Mothball, error) {
+func OpenMothball(filename string) (*Mothball, error) {
var m Mothball
m.filename = filename
@@ -38,7 +39,7 @@ func (m *Mothball) Refresh() (error) {
}
mtime := info.ModTime()
- if mtime == m.mtime {
+ if ! mtime.After(m.mtime) {
return nil
}
@@ -65,3 +66,14 @@ func (m *Mothball) Open(filename string) (io.ReadCloser, error) {
}
return nil, fmt.Errorf("File not found: %s in %s", filename, m.filename)
}
+
+func (m *Mothball) ReadFile(filename string) ([]byte, error) {
+ f, err := m.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ bytes, err := ioutil.ReadAll(f)
+ return bytes, err
+}
diff --git a/src/mothball/mothball_test.go b/src/mothball_test.go
similarity index 94%
rename from src/mothball/mothball_test.go
rename to src/mothball_test.go
index f740e27..d1b46bb 100644
--- a/src/mothball/mothball_test.go
+++ b/src/mothball_test.go
@@ -1,4 +1,4 @@
-package mothball
+package main
import (
"archive/zip"
@@ -35,7 +35,7 @@ func TestMothball(t *testing.T) {
tf.Close()
// Now read it in
- mb, err := Open(tf.Name())
+ mb, err := OpenMothball(tf.Name())
if err != nil {
t.Error(err)
return
diff --git a/src/mothd b/src/mothd
deleted file mode 100755
index ffad247..0000000
--- a/src/mothd
+++ /dev/null
@@ -1,14 +0,0 @@
-#! /bin/sh
-
-cd ${1:-$(dirname $0)}
-KOTH_BASE=$(pwd)
-
-echo "Running koth instances in $KOTH_BASE"
-
-while true; do
- for i in $KOTH_BASE/*/assigned.txt; do
- dir=${i%/*}
- $dir/bin/once
- done
- sleep 5
-done
diff --git a/src/mothd.go b/src/mothd.go
new file mode 100644
index 0000000..93c27b8
--- /dev/null
+++ b/src/mothd.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+)
+
+func showPage(w http.ResponseWriter, title string, body string) {
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "%s", title)
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "%s
", title)
+ fmt.Fprintf(w, "", body)
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, "")
+}
+
+
+func logRequest(handler http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ log.Printf("HTTP %s %s %s\n", r.RemoteAddr, r.Method, r.URL)
+ handler.ServeHTTP(w, r)
+ })
+}
+
+func setup() error {
+ return nil
+}
+
+func main() {
+ base := flag.String(
+ "base",
+ "/",
+ "Base URL of this instance",
+ )
+ mothballDir := flag.String(
+ "mothballs",
+ "/moth/mothballs",
+ "Path to read mothballs",
+ )
+ stateDir := flag.String(
+ "state",
+ "/moth/state",
+ "Path to write state",
+ )
+ maintenanceInterval := flag.Duration(
+ "maint",
+ 20 * time.Second,
+ "Maintenance interval",
+ )
+ listen := flag.String(
+ "listen",
+ ":80",
+ "[host]:port to bind and listen",
+ )
+ flag.Parse()
+
+ if err := setup(); err != nil {
+ log.Fatal(err)
+ }
+
+ ctx, err := NewInstance(*base, *mothballDir, *stateDir)
+ if err != nil {
+ log.Fatal(err)
+ }
+ ctx.BindHandlers(http.DefaultServeMux)
+
+ go ctx.Maintenance(*maintenanceInterval)
+
+ log.Printf("Listening on %s", *listen)
+ log.Fatal(http.ListenAndServe(*listen, logRequest(http.DefaultServeMux)))
+}