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
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()
}

View File

@ -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)
}

View File

@ -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()
}
}
}

View File

@ -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):
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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,