mirror of https://github.com/dirtbags/moth.git
Mostly refactored
This commit is contained in:
parent
54ea337447
commit
309432d05c
|
@ -36,7 +36,7 @@ And point a browser to http://localhost:8080/ (or whatever host is running the s
|
|||
The development server includes a number of Python libraries that we have found useful in writing puzzles.
|
||||
|
||||
When you're ready to create your own puzzles,
|
||||
read [the devel server documentation](docs/devel-server.md).
|
||||
read [the devel server documentation](doc/devel-server.md).
|
||||
|
||||
Click the `[mb]` link by a puzzle category to compile and download a mothball that the production server can read.
|
||||
|
||||
|
@ -49,7 +49,7 @@ Running a Production Server
|
|||
You can be more fine-grained about directories, if you like.
|
||||
Inside the container, you need the following paths:
|
||||
|
||||
* `/state` (rw) Where state is stored. Read [the overview](docs/overview.md) to learn what's what in here.
|
||||
* `/state` (rw) Where state is stored. Read [the overview](doc/overview.md) to learn what's what in here.
|
||||
* `/mothballs` (ro) Mothballs (puzzle bundles) as provided by the development server.
|
||||
* `/resources` (ro) Overrides for built-in HTML/CSS resources.
|
||||
|
||||
|
@ -73,7 +73,7 @@ Point a web browser at http://localhost:8080/
|
|||
and start hacking on things in your `puzzles` directory.
|
||||
|
||||
More on how the devel sever works in
|
||||
[the devel server documentation](docs/devel-server.md)
|
||||
[the devel server documentation](doc/devel-server.md)
|
||||
|
||||
|
||||
Running A Production Server
|
||||
|
|
|
@ -3,17 +3,26 @@ package main
|
|||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MothPath(base string, parts ...string) string {
|
||||
type Component struct {
|
||||
baseDir string
|
||||
}
|
||||
|
||||
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{base}, parts...)
|
||||
parts = append([]string{c.baseDir}, parts...)
|
||||
path = filepath.Join(parts...)
|
||||
path = filepath.Clean(path)
|
||||
return path
|
||||
}
|
||||
|
||||
func (c *Component) Run(updateInterval time.Duration) {
|
||||
// Stub!
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Print("Started")
|
||||
|
||||
theme := NewTheme("../../theme")
|
||||
state := NewState("../../state")
|
||||
puzzles := NewMothballs("../../mothballs")
|
||||
|
||||
|
||||
interval := 2 * time.Second
|
||||
go theme.Run(interval)
|
||||
go state.Run(interval)
|
||||
go puzzles.Run(interval)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Print(state.Export(""))
|
||||
time.Sleep(19 * time.Second)
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Mothballs struct {
|
||||
Component
|
||||
categories map[string]*Zipfs
|
||||
}
|
||||
|
||||
func NewMothballs(baseDir string) *Mothballs {
|
||||
return &Mothballs{
|
||||
Component: Component{
|
||||
baseDir: baseDir,
|
||||
},
|
||||
categories: make(map[string]*Zipfs),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mothballs) update() {
|
||||
// Any new categories?
|
||||
files, err := ioutil.ReadDir(m.path())
|
||||
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)
|
||||
if err != nil {
|
||||
log.Print("Error opening ", filepath, ": ", err)
|
||||
continue
|
||||
}
|
||||
log.Print("New mothball: ", filename)
|
||||
m.categories[categoryName] = zfs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"bufio"
|
||||
"time"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
||||
|
@ -34,29 +33,19 @@ type StateExport struct {
|
|||
// We use the filesystem for synchronization between threads.
|
||||
// The only thing State methods need to know is the path to the state directory.
|
||||
type State struct {
|
||||
StateDir string
|
||||
update chan bool
|
||||
Component
|
||||
update chan bool
|
||||
}
|
||||
|
||||
func NewState(stateDir string) (*State) {
|
||||
func NewState(baseDir string) *State {
|
||||
return &State{
|
||||
StateDir: stateDir,
|
||||
update: make(chan bool, 10),
|
||||
Component: Component{
|
||||
baseDir: baseDir,
|
||||
},
|
||||
update: make(chan bool, 10),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a cleaned up join of path parts relative to
|
||||
func (s *State) path(parts ...string) string {
|
||||
rel := filepath.Clean(filepath.Join(parts...))
|
||||
parts = filepath.SplitList(rel)
|
||||
for i, part := range parts {
|
||||
part = strings.TrimLeft(part, "./\\:")
|
||||
parts[i] = part
|
||||
}
|
||||
rel = filepath.Join(parts...)
|
||||
return filepath.Join(s.StateDir, rel)
|
||||
}
|
||||
|
||||
// Check a few things to see if this state directory is "enabled".
|
||||
func (s *State) Enabled() bool {
|
||||
if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) {
|
||||
|
@ -92,19 +81,19 @@ func (s *State) TeamName(teamId string) (string, error) {
|
|||
} else if err != nil {
|
||||
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err)
|
||||
}
|
||||
|
||||
|
||||
return teamName, nil
|
||||
}
|
||||
|
||||
// Write out team name. This can only be done once.
|
||||
func (s *State) SetTeamName(teamId string, teamName string) error {
|
||||
teamFile := s.path("teams", teamId)
|
||||
err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive | 0644)
|
||||
err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive|0644)
|
||||
return err
|
||||
}
|
||||
|
||||
// Retrieve the current points log
|
||||
func (s *State) PointsLog() ([]*Award) {
|
||||
func (s *State) PointsLog() []*Award {
|
||||
pointsFile := s.path("points.log")
|
||||
f, err := os.Open(pointsFile)
|
||||
if err != nil {
|
||||
|
@ -112,7 +101,7 @@ func (s *State) PointsLog() ([]*Award) {
|
|||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
|
||||
pointsLog := make([]*Award, 0, 200)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
|
@ -130,17 +119,17 @@ func (s *State) PointsLog() ([]*Award) {
|
|||
// Return an exportable points log,
|
||||
// This anonymizes teamId with either an integer, or the string "self"
|
||||
// for the requesting teamId.
|
||||
func (s *State) Export(teamId string) (*StateExport) {
|
||||
func (s *State) Export(teamId string) *StateExport {
|
||||
teamName, _ := s.TeamName(teamId)
|
||||
|
||||
pointsLog := s.PointsLog()
|
||||
|
||||
export := StateExport{
|
||||
PointsLog: make([]Award, len(pointsLog)),
|
||||
Messages: make([]string, 0, 10),
|
||||
Messages: make([]string, 0, 10),
|
||||
TeamNames: map[string]string{"self": teamName},
|
||||
}
|
||||
|
||||
|
||||
// Read in messages
|
||||
messagesFile := s.path("messages.txt")
|
||||
if f, err := os.Open(messagesFile); err != nil {
|
||||
|
@ -156,7 +145,7 @@ func (s *State) Export(teamId string) (*StateExport) {
|
|||
export.Messages = append(export.Messages, message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Read in points
|
||||
exportIds := map[string]string{teamId: "self"}
|
||||
for logno, award := range pointsLog {
|
||||
|
@ -167,7 +156,7 @@ func (s *State) Export(teamId string) (*StateExport) {
|
|||
exportId := strconv.Itoa(logno)
|
||||
exportAward.TeamId = exportId
|
||||
exportIds[award.TeamId] = exportAward.TeamId
|
||||
|
||||
|
||||
name, err := s.TeamName(award.TeamId)
|
||||
if err != nil {
|
||||
name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
|
||||
|
@ -176,7 +165,7 @@ func (s *State) Export(teamId string) (*StateExport) {
|
|||
}
|
||||
export.PointsLog[logno] = *exportAward
|
||||
}
|
||||
|
||||
|
||||
return &export
|
||||
}
|
||||
|
||||
|
@ -269,10 +258,9 @@ func (s *State) collectPoints() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func (s *State) maybeInitialize() {
|
||||
// Are we supposed to re-initialize?
|
||||
if _, err := os.Stat(s.path("initialized")); ! os.IsNotExist(err) {
|
||||
if _, err := os.Stat(s.path("initialized")); !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -318,7 +306,7 @@ func (s *State) maybeInitialize() {
|
|||
)
|
||||
ioutil.WriteFile(
|
||||
s.path("messages.txt"),
|
||||
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now())),
|
||||
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))),
|
||||
0644,
|
||||
)
|
||||
ioutil.WriteFile(
|
||||
|
@ -334,23 +322,10 @@ func (s *State) Run(updateInterval time.Duration) {
|
|||
if s.Enabled() {
|
||||
s.collectPoints()
|
||||
}
|
||||
|
||||
|
||||
select {
|
||||
case <-s.update:
|
||||
case <-time.After(updateInterval):
|
||||
case <-s.update:
|
||||
case <-time.After(updateInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
s := NewState("./state")
|
||||
go s.Run(2 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
|
||||
fmt.Println(s.Export(""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,23 +7,21 @@ import (
|
|||
)
|
||||
|
||||
type Theme struct {
|
||||
ThemeDir string
|
||||
Component
|
||||
}
|
||||
|
||||
func NewTheme(themeDir string) *Theme {
|
||||
func NewTheme(baseDir string) *Theme {
|
||||
return &Theme{
|
||||
ThemeDir: themeDir,
|
||||
Component: Component{
|
||||
baseDir: baseDir,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Theme) path(parts ...string) string {
|
||||
return MothPath(t.ThemeDir, parts...)
|
||||
}
|
||||
|
||||
func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
if strings.Contains(path, "/.") {
|
||||
http.Error(w, "Invalid URL path", http.StatusBadRequest)
|
||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if path == "/" {
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"context"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
type StubLockSystem struct {
|
||||
}
|
||||
|
||||
func (ls *StubLockSystem) Confirm(now time.Time, name0, name1 string, conditions ...webdav.Condition) (release func(), err error) {
|
||||
return nil, webdav.ErrConfirmationFailed
|
||||
}
|
||||
|
||||
func (ls *StubLockSystem) Create(now time.Time, details webdav.LockDetails) (token string, err error) {
|
||||
return "", webdav.ErrLocked
|
||||
}
|
||||
|
||||
func (ls *StubLockSystem) Refresh(now time.Time, token string, duration time.Duration) (webdav.LockDetails, error) {
|
||||
return webdav.LockDetails{}, webdav.ErrNoSuchLock
|
||||
}
|
||||
|
||||
func (ls *StubLockSystem) Unlock(now time.Time, token string) error {
|
||||
return webdav.ErrNoSuchLock
|
||||
}
|
||||
|
||||
|
||||
type MothFS struct {
|
||||
}
|
||||
|
||||
func (fs *MothFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
func (fs *MothFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
f, err := os.Open("hello.txt")
|
||||
return f, err
|
||||
}
|
||||
|
||||
func (fs *MothFS) RemoveAll(ctx context.Context, name string) error {
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
func (fs *MothFS) Rename(ctx context.Context, oldName, newName string) error {
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
func (fs *MothFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
info, err := os.Stat("hello.txt")
|
||||
return info, err
|
||||
}
|
||||
|
||||
func main() {
|
||||
//dirFlag := flag.String("d", "./", "Directory to serve from. Default is CWD")
|
||||
httpPort := flag.Int("p", 80, "Port to serve on (Plain HTTP)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
srv := &webdav.Handler{
|
||||
FileSystem: new(MothFS),
|
||||
LockSystem: new(StubLockSystem),
|
||||
Logger: func(r *http.Request, err error) {
|
||||
if err != nil {
|
||||
log.Printf("WEBDAV [%s]: %s, ERROR: %s\n", r.Method, r.URL, err)
|
||||
} else {
|
||||
log.Printf("WEBDAV [%s]: %s \n", r.Method, r.URL)
|
||||
}
|
||||
},
|
||||
}
|
||||
http.Handle("/", srv)
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil {
|
||||
log.Fatalf("Error with WebDAV server: %v", err)
|
||||
}
|
||||
|
||||
}
|
|
@ -18,18 +18,23 @@ func seedJoin(parts ...string) string {
|
|||
func usage() {
|
||||
out := flag.CommandLine.Output()
|
||||
name := flag.CommandLine.Name()
|
||||
fmt.Fprintf(out, "Usage: %s [OPTIONS] CATEGORY [CATEGORY ...]\n", name)
|
||||
fmt.Fprintf(out, "Usage: %s [OPTION]... CATEGORY [PUZZLE [FILENAME]]\n", name)
|
||||
fmt.Fprintf(out, "\n")
|
||||
fmt.Fprintf(out, "Transpile CATEGORY, or provide individual category components.\n")
|
||||
fmt.Fprintf(out, "If PUZZLE is provided, only transpile the given puzzle.\n")
|
||||
fmt.Fprintf(out, "If FILENAME is provided, output provided file.\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
// XXX: We need a way to pass in "only run this one point value puzzle"
|
||||
// XXX: Convert puzzle.py to standalone thingies
|
||||
|
||||
flag.Usage = usage
|
||||
points := flag.Int("points", 0, "Transpile only this point value puzzle")
|
||||
flag.Parse()
|
||||
|
||||
points := flag.Int("points", 0, "Transpile only this point value puzzle")
|
||||
mothball := flag.Bool("mothball", false, "Generate a mothball")
|
||||
flag.Parse()
|
||||
|
||||
baseSeedString := os.Getenv("MOTH_SEED")
|
||||
|
||||
jsenc := json.NewEncoder(os.Stdout)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
package main
|
||||
|
Loading…
Reference in New Issue