mirror of https://github.com/dirtbags/moth.git
I think I'm done.
This commit is contained in:
parent
a9f8cba6f9
commit
ba38d1a84b
|
@ -38,12 +38,12 @@ function render(obj) {
|
||||||
l.appendChild(i);
|
l.appendChild(i);
|
||||||
|
|
||||||
if (points === 0) {
|
if (points === 0) {
|
||||||
i.textContent = "‡";
|
i.textContent = "✿";
|
||||||
} else {
|
} else {
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
i.appendChild(a);
|
i.appendChild(a);
|
||||||
a.textContent = points;
|
a.textContent = points;
|
||||||
a.href = cat + "/" + id + "/index.html";
|
a.href = "puzzle.html?cat=" + cat + "&points=" + points + "&pid=" + id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ document.addEventListener("DOMContentLoaded", init);
|
||||||
</section>
|
</section>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="puzzles.html">Puzzles</a></li>
|
<li><a href="puzzle-list.html">Puzzles</a></li>
|
||||||
<li><a href="scoreboard.html">Scoreboard</a></li>
|
<li><a href="scoreboard.html">Scoreboard</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
|
@ -140,7 +140,7 @@ document.addEventListener("DOMContentLoaded", init);
|
||||||
</section>
|
</section>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="puzzles.html">Puzzles</a></li>
|
<li><a href="puzzle-list.html">Puzzles</a></li>
|
||||||
<li><a href="scoreboard.html">Scoreboard</a></li>
|
<li><a href="scoreboard.html">Scoreboard</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -318,6 +318,42 @@ func (ctx Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
w.Write(jret)
|
w.Write(jret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx Instance) contentHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// Prevent directory traversal
|
||||||
|
if strings.Contains(req.URL.Path, "/.") {
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Be clever: use only the last three parts of the path. This may prove to be a bad idea.
|
||||||
|
parts := strings.Split(req.URL.Path, "/")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := parts[len(parts)-1]
|
||||||
|
puzzleId := parts[len(parts)-2]
|
||||||
|
categoryName := parts[len(parts)-3]
|
||||||
|
|
||||||
|
mb, ok := ctx.Categories[categoryName]
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mbFilename := fmt.Sprintf("content/%s/%s", puzzleId, fileName)
|
||||||
|
mf, err := mb.Open(mbFilename)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer mf.Close()
|
||||||
|
|
||||||
|
http.ServeContent(w, req, fileName, mf.ModTime(), mf)
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx Instance) staticHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx Instance) staticHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
ServeStatic(w, req, ctx.ResourcesDir)
|
ServeStatic(w, req, ctx.ResourcesDir)
|
||||||
}
|
}
|
||||||
|
@ -327,6 +363,7 @@ func (ctx Instance) BindHandlers(mux *http.ServeMux) {
|
||||||
mux.HandleFunc(ctx.Base+"/register", ctx.registerHandler)
|
mux.HandleFunc(ctx.Base+"/register", ctx.registerHandler)
|
||||||
mux.HandleFunc(ctx.Base+"/token", ctx.tokenHandler)
|
mux.HandleFunc(ctx.Base+"/token", ctx.tokenHandler)
|
||||||
mux.HandleFunc(ctx.Base+"/answer", ctx.answerHandler)
|
mux.HandleFunc(ctx.Base+"/answer", ctx.answerHandler)
|
||||||
|
mux.HandleFunc(ctx.Base+"/content/", ctx.contentHandler)
|
||||||
mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
|
mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
|
||||||
mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
|
mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ type Instance struct {
|
||||||
StateDir string
|
StateDir string
|
||||||
ResourcesDir string
|
ResourcesDir string
|
||||||
Categories map[string]*Mothball
|
Categories map[string]*Mothball
|
||||||
|
update chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) {
|
func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) {
|
||||||
|
@ -27,6 +28,7 @@ func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, e
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
ResourcesDir: resourcesDir,
|
ResourcesDir: resourcesDir,
|
||||||
Categories: map[string]*Mothball{},
|
Categories: map[string]*Mothball{},
|
||||||
|
update: make(chan bool, 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roll over and die if directories aren't even set up
|
// Roll over and die if directories aren't even set up
|
||||||
|
@ -145,6 +147,7 @@ func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.update <- true
|
||||||
log.Printf("Award %s %s %d", teamid, category, points)
|
log.Printf("Award %s %s %d", teamid, category, points)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,13 @@ func (ctx *Instance) CollectPoints() {
|
||||||
|
|
||||||
// maintenance is the goroutine that runs a periodic maintenance task
|
// maintenance is the goroutine that runs a periodic maintenance task
|
||||||
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||||
for ; ; time.Sleep(maintenanceInterval) {
|
for {
|
||||||
ctx.Tidy()
|
ctx.Tidy()
|
||||||
|
select {
|
||||||
|
case <-ctx.update:
|
||||||
|
log.Print("Forced update")
|
||||||
|
case <-time.After(maintenanceInterval):
|
||||||
|
log.Print("Housekeeping...")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
120
src/mothball.go
120
src/mothball.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +16,101 @@ type Mothball struct {
|
||||||
mtime time.Time
|
mtime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MothballFile struct {
|
||||||
|
f io.ReadCloser
|
||||||
|
pos int64
|
||||||
|
zf *zip.File
|
||||||
|
io.Reader
|
||||||
|
io.Seeker
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMothballFile(zf *zip.File) (*MothballFile, error) {
|
||||||
|
mf := &MothballFile{
|
||||||
|
zf: zf,
|
||||||
|
pos: 0,
|
||||||
|
f: nil,
|
||||||
|
}
|
||||||
|
if err := mf.reopen(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mf *MothballFile) reopen() error {
|
||||||
|
if mf.f != nil {
|
||||||
|
if err := mf.f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f, err := mf.zf.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mf.f = f
|
||||||
|
mf.pos = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mf *MothballFile) ModTime() time.Time {
|
||||||
|
return mf.zf.Modified
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mf *MothballFile) Read(p []byte) (int, error) {
|
||||||
|
n, err := mf.f.Read(p)
|
||||||
|
mf.pos += int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mf *MothballFile) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
var pos int64
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
pos = offset
|
||||||
|
case io.SeekCurrent:
|
||||||
|
pos = mf.pos + int64(offset)
|
||||||
|
case io.SeekEnd:
|
||||||
|
pos = int64(mf.zf.UncompressedSize64) - int64(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos < 0 {
|
||||||
|
return mf.pos, fmt.Errorf("Tried to seek %d before start of file", pos)
|
||||||
|
}
|
||||||
|
if pos >= int64(mf.zf.UncompressedSize64) {
|
||||||
|
// We don't need to decompress anything, we're at the end of the file
|
||||||
|
mf.f.Close()
|
||||||
|
mf.f = ioutil.NopCloser(strings.NewReader(""))
|
||||||
|
mf.pos = int64(mf.zf.UncompressedSize64)
|
||||||
|
return mf.pos, nil
|
||||||
|
}
|
||||||
|
if pos < mf.pos {
|
||||||
|
if err := mf.reopen(); err != nil {
|
||||||
|
return mf.pos, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 32 * 1024)
|
||||||
|
for pos > mf.pos {
|
||||||
|
l := pos - mf.pos
|
||||||
|
if l > int64(cap(buf)) {
|
||||||
|
l = int64(cap(buf)) - 1
|
||||||
|
}
|
||||||
|
p := buf[0:int(l)]
|
||||||
|
n, err := mf.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return mf.pos, err
|
||||||
|
} else if n <= 0 {
|
||||||
|
return mf.pos, fmt.Errorf("Short read (%d bytes)", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mf.pos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mf *MothballFile) Close() error {
|
||||||
|
return mf.f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func OpenMothball(filename string) (*Mothball, error) {
|
func OpenMothball(filename string) (*Mothball, error) {
|
||||||
var m Mothball
|
var m Mothball
|
||||||
|
|
||||||
|
@ -57,14 +153,30 @@ func (m *Mothball) Refresh() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mothball) Open(filename string) (io.ReadCloser, error) {
|
func (m *Mothball) get(filename string) (*zip.File, error) {
|
||||||
for _, f := range m.zf.File {
|
for _, f := range m.zf.File {
|
||||||
if filename == f.Name {
|
if filename == f.Name {
|
||||||
ret, err := f.Open()
|
return f, nil
|
||||||
return ret, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("File not found: %s in %s", filename, m.filename)
|
return nil, fmt.Errorf("File not found: %s %s", m.filename, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mothball) Header(filename string) (*zip.FileHeader, error) {
|
||||||
|
f, err := m.get(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &f.FileHeader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mothball) Open(filename string) (*MothballFile, error) {
|
||||||
|
f, err := m.get(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mf, err := NewMothballFile(f)
|
||||||
|
return mf, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mothball) ReadFile(filename string) ([]byte, error) {
|
func (m *Mothball) ReadFile(filename string) ([]byte, error) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -61,6 +62,11 @@ func main() {
|
||||||
}
|
}
|
||||||
ctx.BindHandlers(http.DefaultServeMux)
|
ctx.BindHandlers(http.DefaultServeMux)
|
||||||
|
|
||||||
|
// Add some MIME extensions
|
||||||
|
// Doing this avoids decompressing a mothball entry twice per request
|
||||||
|
mime.AddExtensionType(".json", "application/json")
|
||||||
|
mime.AddExtensionType(".zip", "application/zip")
|
||||||
|
|
||||||
go ctx.Maintenance(*maintenanceInterval)
|
go ctx.Maintenance(*maintenanceInterval)
|
||||||
|
|
||||||
log.Printf("Listening on %s", *listen)
|
log.Printf("Listening on %s", *listen)
|
||||||
|
|
|
@ -67,7 +67,7 @@ func ShowHtml(w http.ResponseWriter, status Status, title string, body string) {
|
||||||
fmt.Fprintf(w, "<section>%s</section>", body)
|
fmt.Fprintf(w, "<section>%s</section>", body)
|
||||||
fmt.Fprintf(w, "<nav>")
|
fmt.Fprintf(w, "<nav>")
|
||||||
fmt.Fprintf(w, "<ul>")
|
fmt.Fprintf(w, "<ul>")
|
||||||
fmt.Fprintf(w, "<li><a href=\"puzzles.html\">Puzzles</a></li>")
|
fmt.Fprintf(w, "<li><a href=\"puzzle-list.html\">Puzzles</a></li>")
|
||||||
fmt.Fprintf(w, "<li><a href=\"scoreboard.html\">Scoreboard</a></li>")
|
fmt.Fprintf(w, "<li><a href=\"scoreboard.html\">Scoreboard</a></li>")
|
||||||
fmt.Fprintf(w, "</ul>")
|
fmt.Fprintf(w, "</ul>")
|
||||||
fmt.Fprintf(w, "</nav>")
|
fmt.Fprintf(w, "</nav>")
|
||||||
|
@ -397,7 +397,7 @@ func ServeStatic(w http.ResponseWriter, req *http.Request, resourcesDir string)
|
||||||
staticIndex(w)
|
staticIndex(w)
|
||||||
case "/scoreboard.html":
|
case "/scoreboard.html":
|
||||||
staticScoreboard(w)
|
staticScoreboard(w)
|
||||||
case "/puzzles.html":
|
case "/puzzle-list.html":
|
||||||
staticPuzzles(w)
|
staticPuzzles(w)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
|
|
Loading…
Reference in New Issue