mirror of https://github.com/dirtbags/moth.git
Push out mothballs in dev mode
This commit is contained in:
parent
cc93eb164b
commit
05bfa17a71
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/dirtbags/moth/pkg/jsend"
|
"github.com/dirtbags/moth/pkg/jsend"
|
||||||
)
|
)
|
||||||
|
@ -29,6 +30,10 @@ func NewHTTPServer(base string, server *MothServer) *HTTPServer {
|
||||||
h.HandleMothFunc("/register", h.RegisterHandler)
|
h.HandleMothFunc("/register", h.RegisterHandler)
|
||||||
h.HandleMothFunc("/answer", h.AnswerHandler)
|
h.HandleMothFunc("/answer", h.AnswerHandler)
|
||||||
h.HandleMothFunc("/content/", h.ContentHandler)
|
h.HandleMothFunc("/content/", h.ContentHandler)
|
||||||
|
|
||||||
|
if server.Config.Devel {
|
||||||
|
h.HandleMothFunc("/mothballer/", h.MothballerHandler)
|
||||||
|
}
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,16 +133,16 @@ func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter,
|
||||||
|
|
||||||
// ContentHandler returns static content from a given puzzle
|
// ContentHandler returns static content from a given puzzle
|
||||||
func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
trimLen := len(h.base) + len("/content/")
|
parts := strings.SplitN(req.URL.Path[len(h.base)+1:], "/", 4)
|
||||||
parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3)
|
if len(parts) < 4 {
|
||||||
if len(parts) < 3 {
|
http.NotFound(w, req)
|
||||||
http.Error(w, "Not Found", http.StatusNotFound)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cat := parts[0]
|
// parts[0] == "content"
|
||||||
pointsStr := parts[1]
|
cat := parts[1]
|
||||||
filename := parts[2]
|
pointsStr := parts[2]
|
||||||
|
filename := parts[3]
|
||||||
|
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
filename = "puzzle.json"
|
filename = "puzzle.json"
|
||||||
|
@ -154,3 +159,23 @@ func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter
|
||||||
|
|
||||||
http.ServeContent(w, req, filename, mtime, mf)
|
http.ServeContent(w, req, filename, mtime, mf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MothballerHandler returns a mothball
|
||||||
|
func (h *HTTPServer) MothballerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
|
parts := strings.SplitN(req.URL.Path[len(h.base)+1:], "/", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parts[0] == "mothballer"
|
||||||
|
filename := parts[1]
|
||||||
|
cat := strings.TrimSuffix(filename, ".mb")
|
||||||
|
mothball, err := mh.Mothball(cat)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeContent(w, req, filename, time.Now(), mothball)
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TestParticipantID = "shipox"
|
const TestParticipantID = "shipox"
|
||||||
|
@ -123,3 +125,39 @@ func TestHttpd(t *testing.T) {
|
||||||
t.Error("Unexpected body", r.Body.String())
|
t.Error("Unexpected body", r.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDevelMemHttpd(t *testing.T) {
|
||||||
|
srv := NewTestServer()
|
||||||
|
|
||||||
|
{
|
||||||
|
hs := NewHTTPServer("/", srv)
|
||||||
|
|
||||||
|
if r := hs.TestRequest("/mothballer/pategory.md", nil); r.Result().StatusCode != 404 {
|
||||||
|
t.Error("Should have gotten a 404 for mothballer in prod mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
srv.Config.Devel = true
|
||||||
|
hs := NewHTTPServer("/", srv)
|
||||||
|
|
||||||
|
if r := hs.TestRequest("/mothballer/pategory.md", nil); r.Result().StatusCode != 500 {
|
||||||
|
t.Log(r.Body.String())
|
||||||
|
t.Log(r.Result())
|
||||||
|
t.Error("Should have given us an internal server error, since category is a mothball")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDevelFsHttps(t *testing.T) {
|
||||||
|
fs := afero.NewBasePathFs(afero.NewOsFs(), "testdata")
|
||||||
|
transpilerProvider := NewTranspilerProvider(fs)
|
||||||
|
srv := NewMothServer(Configuration{Devel: true}, NewTestTheme(), NewTestState(), transpilerProvider)
|
||||||
|
hs := NewHTTPServer("/", srv)
|
||||||
|
|
||||||
|
if r := hs.TestRequest("/mothballer/cat0.mb", nil); r.Result().StatusCode != 200 {
|
||||||
|
t.Log(r.Body.String())
|
||||||
|
t.Log(r.Result())
|
||||||
|
t.Error("Didn't get a Mothball")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
@ -44,8 +46,22 @@ func main() {
|
||||||
"/",
|
"/",
|
||||||
"Base URL of this instance",
|
"Base URL of this instance",
|
||||||
)
|
)
|
||||||
|
seed := flag.String(
|
||||||
|
"seed",
|
||||||
|
"",
|
||||||
|
"Random seed to use, overrides $SEED",
|
||||||
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// Set random seed
|
||||||
|
if *seed == "" {
|
||||||
|
*seed = os.Getenv("SEED")
|
||||||
|
}
|
||||||
|
if *seed == "" {
|
||||||
|
*seed = fmt.Sprintf("%d%d", os.Getpid(), time.Now().Unix())
|
||||||
|
}
|
||||||
|
os.Setenv("SEED", *seed)
|
||||||
|
|
||||||
osfs := afero.NewOsFs()
|
osfs := afero.NewOsFs()
|
||||||
theme := NewTheme(afero.NewBasePathFs(osfs, *themePath))
|
theme := NewTheme(afero.NewBasePathFs(osfs, *themePath))
|
||||||
state := NewState(afero.NewBasePathFs(osfs, *statePath))
|
state := NewState(afero.NewBasePathFs(osfs, *statePath))
|
||||||
|
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -172,6 +173,11 @@ func (m *Mothballs) refresh() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mothball just returns an error
|
||||||
|
func (m *Mothballs) Mothball(cat string) (*bytes.Reader, error) {
|
||||||
|
return nil, fmt.Errorf("Can't repackage a compiled mothball")
|
||||||
|
}
|
||||||
|
|
||||||
// Maintain performs housekeeping for Mothballs.
|
// Maintain performs housekeeping for Mothballs.
|
||||||
func (m *Mothballs) Maintain(updateInterval time.Duration) {
|
func (m *Mothballs) Maintain(updateInterval time.Duration) {
|
||||||
m.refresh()
|
m.refresh()
|
||||||
|
|
|
@ -4,6 +4,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
@ -122,6 +123,11 @@ func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bo
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mothball just returns an error
|
||||||
|
func (pc ProviderCommand) Mothball(cat string) (*bytes.Reader, error) {
|
||||||
|
return nil, fmt.Errorf("Can't package a command-generated category")
|
||||||
|
}
|
||||||
|
|
||||||
// Maintain does nothing: a command puzzle ProviderCommand has no housekeeping
|
// Maintain does nothing: a command puzzle ProviderCommand has no housekeeping
|
||||||
func (pc ProviderCommand) Maintain(updateInterval time.Duration) {
|
func (pc ProviderCommand) Maintain(updateInterval time.Duration) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -41,6 +42,7 @@ type PuzzleProvider interface {
|
||||||
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
|
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
|
||||||
Inventory() []Category
|
Inventory() []Category
|
||||||
CheckAnswer(cat string, points int, answer string) (bool, error)
|
CheckAnswer(cat string, points int, answer string) (bool, error)
|
||||||
|
Mothball(cat string) (*bytes.Reader, error)
|
||||||
Maintainer
|
Maintainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,3 +231,16 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||||
|
|
||||||
return &export
|
return &export
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mothball generates a mothball for the given category.
|
||||||
|
func (mh *MothRequestHandler) Mothball(cat string) (r *bytes.Reader, err error) {
|
||||||
|
if !mh.Config.Devel {
|
||||||
|
return nil, fmt.Errorf("Cannot mothball in production mode")
|
||||||
|
}
|
||||||
|
for _, provider := range mh.PuzzleProviders {
|
||||||
|
if r, err = provider.Mothball(cat); err == nil {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,12 @@ func (p TranspilerProvider) CheckAnswer(cat string, points int, answer string) (
|
||||||
return c.Answer(points, answer), nil
|
return c.Answer(points, answer), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mothball packages up a category into a mothball.
|
||||||
|
func (p TranspilerProvider) Mothball(cat string) (*bytes.Reader, error) {
|
||||||
|
c := transpile.NewFsCategory(p.fs, cat)
|
||||||
|
return transpile.Mothball(c)
|
||||||
|
}
|
||||||
|
|
||||||
// Maintain performs housekeeping.
|
// Maintain performs housekeeping.
|
||||||
func (p TranspilerProvider) Maintain(updateInterval time.Duration) {
|
func (p TranspilerProvider) Maintain(updateInterval time.Duration) {
|
||||||
// Nothing to do here.
|
// Nothing to do here.
|
||||||
|
|
|
@ -13,7 +13,7 @@ pre:
|
||||||
- Buster
|
- Buster
|
||||||
- DW
|
- DW
|
||||||
attachments:
|
attachments:
|
||||||
- filename: moo.txt
|
- moo.txt
|
||||||
---
|
---
|
||||||
YAML body
|
YAML body
|
||||||
`)
|
`)
|
||||||
|
|
|
@ -92,6 +92,26 @@ type StaticAttachment struct {
|
||||||
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
|
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML allows a StaticAttachment to be specified as a single string.
|
||||||
|
// The way the yaml library works is weird.
|
||||||
|
func (sa *StaticAttachment) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
if err := unmarshal(&sa.Filename); err == nil {
|
||||||
|
sa.FilesystemPath = sa.Filename
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := new(struct {
|
||||||
|
Filename string
|
||||||
|
FilesystemPath string
|
||||||
|
})
|
||||||
|
if err := unmarshal(parts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sa.Filename = parts.Filename
|
||||||
|
sa.FilesystemPath = parts.FilesystemPath
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReadSeekCloser provides io.Reader, io.Seeker, and io.Closer.
|
// ReadSeekCloser provides io.Reader, io.Seeker, and io.Closer.
|
||||||
type ReadSeekCloser interface {
|
type ReadSeekCloser interface {
|
||||||
io.Reader
|
io.Reader
|
||||||
|
@ -307,8 +327,16 @@ func rfc822HeaderParser(r io.Reader) (StaticPuzzle, error) {
|
||||||
p.Debug.Summary = val[0]
|
p.Debug.Summary = val[0]
|
||||||
case "hint":
|
case "hint":
|
||||||
p.Debug.Hints = val
|
p.Debug.Hints = val
|
||||||
|
case "solution":
|
||||||
|
p.Debug.Hints = val
|
||||||
case "ksa":
|
case "ksa":
|
||||||
p.Post.KSAs = val
|
p.Post.KSAs = val
|
||||||
|
case "objective":
|
||||||
|
p.Post.Objective = val[0]
|
||||||
|
case "success.acceptable":
|
||||||
|
p.Post.Success.Acceptable = val[0]
|
||||||
|
case "success.mastery":
|
||||||
|
p.Post.Success.Mastery = val[0]
|
||||||
default:
|
default:
|
||||||
return p, fmt.Errorf("Unknown header field: %s", key)
|
return p, fmt.Errorf("Unknown header field: %s", key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,3 +136,35 @@ func TestFsPuzzle(t *testing.T) {
|
||||||
t.Error("Error answer marked correct")
|
t.Error("Error answer marked correct")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAttachment(t *testing.T) {
|
||||||
|
buf := bytes.NewBufferString(`
|
||||||
|
pre:
|
||||||
|
attachments:
|
||||||
|
- simple
|
||||||
|
- filename: complex
|
||||||
|
filesystempath: backingfile
|
||||||
|
`)
|
||||||
|
p, err := yamlHeaderParser(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
att := p.Pre.Attachments
|
||||||
|
if len(att) != 2 {
|
||||||
|
t.Error("Wrong number of attachments", att)
|
||||||
|
}
|
||||||
|
if att[0].Filename != "simple" {
|
||||||
|
t.Error("Attachment 0 wrong")
|
||||||
|
}
|
||||||
|
if att[0].Filename != att[0].FilesystemPath {
|
||||||
|
t.Error("Attachment 0 wrong")
|
||||||
|
}
|
||||||
|
if att[1].Filename != "complex" {
|
||||||
|
t.Error("Attachment 1 wrong")
|
||||||
|
}
|
||||||
|
if att[1].FilesystemPath != "backingfile" {
|
||||||
|
t.Error("Attachment 2 wrong")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue