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, "
%s
", 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, "
%s
", 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))) +}