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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"io"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Component struct {
|
type Category struct {
|
||||||
baseDir string
|
Name string
|
||||||
|
Puzzles []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Component) path(parts ...string) string {
|
type ReadSeekCloser interface {
|
||||||
path := filepath.Clean(filepath.Join(parts...))
|
io.Reader
|
||||||
parts = filepath.SplitList(path)
|
io.Seeker
|
||||||
for i, part := range parts {
|
io.Closer
|
||||||
part = strings.TrimLeft(part, "./\\:")
|
|
||||||
parts[i] = part
|
|
||||||
}
|
|
||||||
parts = append([]string{c.baseDir}, parts...)
|
|
||||||
path = filepath.Join(parts...)
|
|
||||||
path = filepath.Clean(path)
|
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Component) Run(updateInterval time.Duration) {
|
type PuzzleProvider interface {
|
||||||
// Stub!
|
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"
|
"github.com/spf13/afero"
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
|
||||||
"time"
|
"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() {
|
func main() {
|
||||||
log.Print("Started")
|
log.Print("Started")
|
||||||
|
|
||||||
|
@ -22,7 +35,7 @@ func main() {
|
||||||
"state",
|
"state",
|
||||||
"Path to state files",
|
"Path to state files",
|
||||||
)
|
)
|
||||||
puzzlePath := flag.String(
|
mothballPath := flag.String(
|
||||||
"mothballs",
|
"mothballs",
|
||||||
"mothballs",
|
"mothballs",
|
||||||
"Path to mothballs to host",
|
"Path to mothballs to host",
|
||||||
|
@ -37,25 +50,23 @@ func main() {
|
||||||
":8000",
|
":8000",
|
||||||
"Bind [host]:port for HTTP service",
|
"Bind [host]:port for HTTP service",
|
||||||
)
|
)
|
||||||
|
base := flag.String(
|
||||||
|
"base",
|
||||||
|
"/",
|
||||||
|
"Base URL of this instance",
|
||||||
|
)
|
||||||
|
|
||||||
stateFs := afero.NewBasePathFs(afero.NewOsFs(), *statePath)
|
theme := NewTheme(afero.NewBasePathFs(afero.NewOsFs(), *themePath))
|
||||||
themeFs := afero.NewBasePathFs(afero.NewOsFs(), *themePath)
|
state := NewState(afero.NewBasePathFs(afero.NewOsFs(), *statePath))
|
||||||
mothballFs := afero.NewBasePathFs(afero.NewOsFs(), *mothballPath)
|
puzzles := NewMothballs(afero.NewBasePathFs(afero.NewOsFs(), *mothballPath))
|
||||||
|
|
||||||
theme := NewTheme(themeFs)
|
|
||||||
state := NewState(stateFs)
|
|
||||||
puzzles := NewMothballs(mothballFs)
|
|
||||||
|
|
||||||
go state.Run(*refreshInterval)
|
|
||||||
go puzzles.Run(*refreshInterval)
|
|
||||||
|
|
||||||
// Add some MIME extensions
|
// Add some MIME extensions
|
||||||
// Doing this avoids decompressing a mothball entry twice per request
|
// Doing this avoids decompressing a mothball entry twice per request
|
||||||
mime.AddExtensionType(".json", "application/json")
|
mime.AddExtensionType(".json", "application/json")
|
||||||
mime.AddExtensionType(".zip", "application/zip")
|
mime.AddExtensionType(".zip", "application/zip")
|
||||||
|
|
||||||
http.HandleFunc("/", theme.staticHandler)
|
go custodian(*refreshInterval, []Component{theme, state, puzzles})
|
||||||
|
|
||||||
log.Printf("Listening on %s", *bindStr)
|
httpd := NewHTTPServer(*base, theme, state, puzzles)
|
||||||
log.Fatal(http.ListenAndServe(*bindStr, nil))
|
httpd.Run(*bindStr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,43 +2,56 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Mothballs struct {
|
type Mothballs struct {
|
||||||
fs afero.Fs
|
|
||||||
categories map[string]*Zipfs
|
categories map[string]*Zipfs
|
||||||
|
afero.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMothballs(fs afero.Fs) *Mothballs {
|
func NewMothballs(fs afero.Fs) *Mothballs {
|
||||||
return &Mothballs{
|
return &Mothballs{
|
||||||
fs: fs,
|
Fs: fs,
|
||||||
categories: make(map[string]*Zipfs),
|
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?
|
// Any new categories?
|
||||||
files, err := afero.ReadDir(m.fs, "/")
|
files, err := afero.ReadDir(m.Fs, "/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("Error listing mothballs: ", err)
|
log.Print("Error listing mothballs: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
filename := f.Name()
|
filename := f.Name()
|
||||||
filepath := m.path(filename)
|
|
||||||
if !strings.HasSuffix(filename, ".mb") {
|
if !strings.HasSuffix(filename, ".mb") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
categoryName := strings.TrimSuffix(filename, ".mb")
|
categoryName := strings.TrimSuffix(filename, ".mb")
|
||||||
|
|
||||||
if _, ok := m.categories[categoryName]; !ok {
|
if _, ok := m.categories[categoryName]; !ok {
|
||||||
zfs, err := OpenZipfs(filepath)
|
zfs, err := OpenZipfs(m.Fs, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("Error opening ", filepath, ": ", err)
|
log.Print("Error opening ", filename, ": ", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Print("New mothball: ", filename)
|
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.
|
// The only thing State methods need to know is the path to the state directory.
|
||||||
type State struct {
|
type State struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
update chan bool
|
afero.Fs
|
||||||
fs afero.Fs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewState(fs afero.Fs) *State {
|
func NewState(fs afero.Fs) *State {
|
||||||
return &State{
|
return &State{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
update: make(chan bool, 10),
|
Fs: fs,
|
||||||
fs: fs,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check a few things to see if this state directory is "enabled".
|
// Check a few things to see if this state directory is "enabled".
|
||||||
func (s *State) UpdateEnabled() {
|
func (s *State) UpdateEnabled() {
|
||||||
if _, err := s.fs.Stat("enabled"); os.IsNotExist(err) {
|
if _, err := s.Stat("enabled"); os.IsNotExist(err) {
|
||||||
s.Enabled = false
|
s.Enabled = false
|
||||||
log.Print("Suspended: enabled file missing")
|
log.Print("Suspended: enabled file missing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nextEnabled := true
|
nextEnabled := true
|
||||||
untilFile, err := s.fs.Open("hours")
|
untilFile, err := s.Open("hours")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -101,7 +99,7 @@ func (s *State) UpdateEnabled() {
|
||||||
// Returns team name given a team ID.
|
// Returns team name given a team ID.
|
||||||
func (s *State) TeamName(teamId string) (string, error) {
|
func (s *State) TeamName(teamId string) (string, error) {
|
||||||
teamFile := filepath.Join("teams", teamId)
|
teamFile := filepath.Join("teams", teamId)
|
||||||
teamNameBytes, err := afero.ReadFile(s.fs, teamFile)
|
teamNameBytes, err := afero.ReadFile(s, teamFile)
|
||||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
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.
|
// Write out team name. This can only be done once.
|
||||||
func (s *State) SetTeamName(teamId string, teamName string) error {
|
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")
|
return fmt.Errorf("Team IDs file does not exist")
|
||||||
} else {
|
} else {
|
||||||
found := false
|
found := false
|
||||||
|
@ -133,7 +131,7 @@ func (s *State) SetTeamName(teamId string, teamName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
teamFile := filepath.Join("teams", teamId)
|
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) {
|
if os.IsExist(err) {
|
||||||
return fmt.Errorf("Team ID is already registered")
|
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
|
// Retrieve the current points log
|
||||||
func (s *State) PointsLog() []*Award {
|
func (s *State) PointsLog() []*Award {
|
||||||
f, err := s.fs.Open("points.log")
|
f, err := s.Open("points.log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return nil
|
return nil
|
||||||
|
@ -178,7 +176,7 @@ func (s *State) Export(teamId string) *StateExport {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read in messages
|
// 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)
|
log.Print(err)
|
||||||
} else {
|
} else {
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
@ -243,29 +241,29 @@ func (s *State) AwardPoints(teamId, category string, points int) error {
|
||||||
tmpfn := filepath.Join("points.tmp", fn)
|
tmpfn := filepath.Join("points.tmp", fn)
|
||||||
newfn := filepath.Join("points.new", 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.fs.Rename(tmpfn, newfn); err != nil {
|
if err := s.Rename(tmpfn, newfn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.update <- true
|
// XXX: update everything
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||||
// removing each points.new/ file as it goes.
|
// removing each points.new/ file as it goes.
|
||||||
func (s *State) collectPoints() {
|
func (s *State) collectPoints() {
|
||||||
files, err := afero.ReadDir(s.fs, "points.new")
|
files, err := afero.ReadDir(s, "points.new")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
filename := filepath.Join("points.new", f.Name())
|
filename := filepath.Join("points.new", f.Name())
|
||||||
awardstr, err := afero.ReadFile(s.fs, filename)
|
awardstr, err := afero.ReadFile(s, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("Opening new points: ", err)
|
log.Print("Opening new points: ", err)
|
||||||
continue
|
continue
|
||||||
|
@ -289,7 +287,7 @@ func (s *State) collectPoints() {
|
||||||
} else {
|
} else {
|
||||||
log.Print("Award: ", award.String())
|
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 {
|
if err != nil {
|
||||||
log.Print("Can't append to points log: ", err)
|
log.Print("Can't append to points log: ", err)
|
||||||
return
|
return
|
||||||
|
@ -298,7 +296,7 @@ func (s *State) collectPoints() {
|
||||||
logf.Close()
|
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)
|
log.Print("Unable to remove new points file: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,28 +304,28 @@ func (s *State) collectPoints() {
|
||||||
|
|
||||||
func (s *State) maybeInitialize() {
|
func (s *State) maybeInitialize() {
|
||||||
// Are we supposed to re-initialize?
|
// Are we supposed to re-initialize?
|
||||||
if _, err := s.fs.Stat("initialized"); !os.IsNotExist(err) {
|
if _, err := s.Stat("initialized"); !os.IsNotExist(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("initialized file missing, re-initializing")
|
log.Print("initialized file missing, re-initializing")
|
||||||
|
|
||||||
// Remove any extant control and state files
|
// Remove any extant control and state files
|
||||||
s.fs.Remove("enabled")
|
s.Remove("enabled")
|
||||||
s.fs.Remove("hours")
|
s.Remove("hours")
|
||||||
s.fs.Remove("points.log")
|
s.Remove("points.log")
|
||||||
s.fs.Remove("messages.txt")
|
s.Remove("messages.txt")
|
||||||
s.fs.RemoveAll("points.tmp")
|
s.RemoveAll("points.tmp")
|
||||||
s.fs.RemoveAll("points.new")
|
s.RemoveAll("points.new")
|
||||||
s.fs.RemoveAll("teams")
|
s.RemoveAll("teams")
|
||||||
|
|
||||||
// Make sure various subdirectories exist
|
// Make sure various subdirectories exist
|
||||||
s.fs.Mkdir("points.tmp", 0755)
|
s.Mkdir("points.tmp", 0755)
|
||||||
s.fs.Mkdir("points.new", 0755)
|
s.Mkdir("points.new", 0755)
|
||||||
s.fs.Mkdir("teams", 0755)
|
s.Mkdir("teams", 0755)
|
||||||
|
|
||||||
// Preseed available team ids if file doesn't exist
|
// 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()
|
defer f.Close()
|
||||||
for i := 0; i < 100; i += 1 {
|
for i := 0; i < 100; i += 1 {
|
||||||
fmt.Fprintln(f, mktoken())
|
fmt.Fprintln(f, mktoken())
|
||||||
|
@ -336,19 +334,19 @@ func (s *State) maybeInitialize() {
|
||||||
|
|
||||||
// Create some files
|
// Create some files
|
||||||
afero.WriteFile(
|
afero.WriteFile(
|
||||||
s.fs,
|
s,
|
||||||
"initialized",
|
"initialized",
|
||||||
[]byte("state/initialized: remove to re-initialize the contest\n"),
|
[]byte("state/initialized: remove to re-initialize the contest\n"),
|
||||||
0644,
|
0644,
|
||||||
)
|
)
|
||||||
afero.WriteFile(
|
afero.WriteFile(
|
||||||
s.fs,
|
s,
|
||||||
"enabled",
|
"enabled",
|
||||||
[]byte("state/enabled: remove to suspend the contest\n"),
|
[]byte("state/enabled: remove to suspend the contest\n"),
|
||||||
0644,
|
0644,
|
||||||
)
|
)
|
||||||
afero.WriteFile(
|
afero.WriteFile(
|
||||||
s.fs,
|
s,
|
||||||
"hours",
|
"hours",
|
||||||
[]byte(
|
[]byte(
|
||||||
"# state/hours: when the contest is enabled\n"+
|
"# state/hours: when the contest is enabled\n"+
|
||||||
|
@ -360,33 +358,23 @@ func (s *State) maybeInitialize() {
|
||||||
0644,
|
0644,
|
||||||
)
|
)
|
||||||
afero.WriteFile(
|
afero.WriteFile(
|
||||||
s.fs,
|
s,
|
||||||
"messages.txt",
|
"messages.txt",
|
||||||
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))),
|
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))),
|
||||||
0644,
|
0644,
|
||||||
)
|
)
|
||||||
afero.WriteFile(
|
afero.WriteFile(
|
||||||
s.fs,
|
s,
|
||||||
"points.log",
|
"points.log",
|
||||||
[]byte(""),
|
[]byte(""),
|
||||||
0644,
|
0644,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) Cleanup() {
|
func (s *State) Update() {
|
||||||
s.maybeInitialize()
|
s.maybeInitialize()
|
||||||
s.UpdateEnabled()
|
s.UpdateEnabled()
|
||||||
if s.Enabled {
|
if s.Enabled {
|
||||||
s.collectPoints()
|
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 (
|
import (
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"net/http"
|
"time"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Theme struct {
|
type Theme struct {
|
||||||
fs afero.Fs
|
afero.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTheme(fs afero.Fs) *Theme {
|
func NewTheme(fs afero.Fs) *Theme {
|
||||||
return &Theme{
|
return &Theme{
|
||||||
fs: fs,
|
Fs: fs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) {
|
// I don't understand why I need this. The type checking system is weird here.
|
||||||
path := req.URL.Path
|
func (t *Theme) Open(name string) (ReadSeekCloser, error) {
|
||||||
if strings.Contains(path, "/.") {
|
return t.Fs.Open(name)
|
||||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if path == "/" {
|
|
||||||
path = "/index.html"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := t.fs.Open(path)
|
func (t *Theme) ModTime(name string) (mt time.Time, err error) {
|
||||||
if err != nil {
|
fi, err := t.Fs.Stat(name)
|
||||||
http.NotFound(w, req)
|
if err == nil {
|
||||||
return
|
mt = fi.ModTime()
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
d, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, req)
|
|
||||||
return
|
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"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"github.com/spf13/afero"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Zipfs struct {
|
type Zipfs struct {
|
||||||
zf *zip.ReadCloser
|
f io.Closer
|
||||||
fs afero.Fs
|
zf *zip.Reader
|
||||||
filename string
|
filename string
|
||||||
mtime time.Time
|
mtime time.Time
|
||||||
|
fs afero.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
type ZipfsFile struct {
|
type ZipfsFile struct {
|
||||||
|
@ -112,7 +113,7 @@ func (zfsf *ZipfsFile) Close() error {
|
||||||
return zfsf.f.Close()
|
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
|
var zfs Zipfs
|
||||||
|
|
||||||
zfs.fs = fs
|
zfs.fs = fs
|
||||||
|
@ -127,7 +128,7 @@ func OpenZipfs(fs afero.fs, filename string) (*Zipfs, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (zfs *Zipfs) Close() error {
|
func (zfs *Zipfs) Close() error {
|
||||||
return zfs.zf.Close()
|
return zfs.f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (zfs *Zipfs) Refresh() error {
|
func (zfs *Zipfs) Refresh() error {
|
||||||
|
@ -146,15 +147,18 @@ func (zfs *Zipfs) Refresh() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
zf, err := zip.OpenReader(zfs.filename)
|
zf, err := zip.NewReader(f, info.Size())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up the last one
|
||||||
if zfs.zf != nil {
|
if zfs.zf != nil {
|
||||||
zfs.zf.Close()
|
zfs.f.Close()
|
||||||
}
|
}
|
||||||
zfs.zf = zf
|
zfs.zf = zf
|
||||||
|
zfs.f = f
|
||||||
zfs.mtime = mtime
|
zfs.mtime = mtime
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -12,37 +12,7 @@ import (
|
||||||
"strings"
|
"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.
|
// hasLine returns true if line appears in r.
|
||||||
// The entire line must match.
|
// The entire line must match.
|
||||||
|
@ -120,9 +90,9 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
points, err := strconv.Atoi(pointstr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respond(
|
respond(
|
||||||
|
points, err := strconv.Atoi(pointstr)
|
||||||
w, req, JSendFail,
|
w, req, JSendFail,
|
||||||
"Cannot parse point value",
|
"Cannot parse point value",
|
||||||
"This doesn't look like an integer: %s", pointstr,
|
"This doesn't look like an integer: %s", pointstr,
|
||||||
|
|
Loading…
Reference in New Issue