mirror of https://github.com/dirtbags/moth.git
OMG it runs, haven't tested yet but exciting stuff here folks
This commit is contained in:
parent
cbc69f5647
commit
0bc8faa5ec
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
// 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)
|
||||
}
|
||||
|
||||
f, err := t.fs.Open(path)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
func (t *Theme) ModTime(name string) (mt time.Time, err error) {
|
||||
fi, err := t.Fs.Stat(name)
|
||||
if err == nil {
|
||||
mt = fi.ModTime()
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
d, err := f.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, req, path, d.ModTime(), f)
|
||||
func (t *Theme) Update() {
|
||||
// No periodic tasks for a theme
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue