mirror of https://github.com/deuill/grawkit.git
172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
// Standard library
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"syscall"
|
|
"text/template"
|
|
"time"
|
|
|
|
// Internal packages.
|
|
"go.deuill.org/grawkit/play/static"
|
|
|
|
// Third-party packages
|
|
"github.com/benhoyt/goawk/interp"
|
|
"github.com/benhoyt/goawk/parser"
|
|
)
|
|
|
|
const (
|
|
// Error messages.
|
|
errReadRequest = "Error reading request, please try again"
|
|
errValidate = "Error validating content"
|
|
errRender = "Error rendering preview"
|
|
|
|
// The maximum content size we're going to parse.
|
|
maxContentSize = 4096
|
|
)
|
|
|
|
var (
|
|
scriptPath = flag.String("script-path", "../grawkit", "The path to the Grawkit script")
|
|
listenAddress = flag.String("listen-address", "localhost:8080", "The default address to listen on")
|
|
|
|
index *template.Template // The base template to render.
|
|
program *parser.Program // The parsed version of the Grawkit script.
|
|
)
|
|
|
|
// ParseContent accepts un-filtered POST form content, and returns the content to render as a string.
|
|
// An error is returned if the content is missing or otherwise invalid.
|
|
func parseContent(form url.Values) (string, error) {
|
|
if _, ok := form["content"]; !ok || len(form["content"]) == 0 {
|
|
return "", errors.New("missing or empty content")
|
|
}
|
|
|
|
var content = form["content"][0]
|
|
switch true {
|
|
case len(content) > maxContentSize:
|
|
return "", errors.New("content too large")
|
|
}
|
|
|
|
return content, nil
|
|
}
|
|
|
|
// HandleRequest accepts a GET or POST HTTP request and responds appropriately based on given data
|
|
// and pre-parsed template files. For GET requests not against the document root, HandleRequest will
|
|
// attempt to find and return the contents of an equivalent file under the configured static directory.
|
|
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
|
// Handle template rendering on root path.
|
|
if r.URL.Path == "/" {
|
|
var data struct {
|
|
Content string
|
|
Preview string
|
|
Error string
|
|
}
|
|
var outbuf, errbuf bytes.Buffer
|
|
|
|
switch r.Method {
|
|
case "POST":
|
|
if err := r.ParseForm(); err != nil {
|
|
data.Error = errReadRequest
|
|
} else if data.Content, err = parseContent(r.PostForm); err != nil {
|
|
data.Error = errValidate + ": " + err.Error()
|
|
} else {
|
|
config := &interp.Config{
|
|
Stdin: bytes.NewReader([]byte(data.Content)),
|
|
Output: &outbuf,
|
|
Error: &errbuf,
|
|
}
|
|
|
|
// Render generated preview from content given.
|
|
if n, err := interp.ExecProgram(program, config); err != nil {
|
|
data.Error = errRender
|
|
} else if n != 0 {
|
|
data.Error = "Error: " + string(errbuf.Bytes())
|
|
} else if _, ok := r.PostForm["generate"]; ok {
|
|
data.Preview = string(outbuf.Bytes())
|
|
} else if _, ok = r.PostForm["download"]; ok {
|
|
w.Header().Set("Content-Disposition", `attachment; filename="generated.svg"`)
|
|
http.ServeContent(w, r, "generated.svg", time.Now(), bytes.NewReader(outbuf.Bytes()))
|
|
return
|
|
}
|
|
}
|
|
|
|
fallthrough
|
|
case "GET":
|
|
// Set correct status code and error message, if any.
|
|
if data.Error != "" {
|
|
w.Header().Set("X-Error-Message", data.Error)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
|
|
// Render index page template.
|
|
if err := index.Execute(w, data); err != nil {
|
|
log.Printf("error rendering template: %s", err)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Serve file as fallback.
|
|
http.FileServer(http.FS(static.FS)).ServeHTTP(w, r)
|
|
}
|
|
|
|
// Setup reads configuration flags and initializes global state for the service, returning an error
|
|
// if any of the service pre-requisites are not fulfilled.
|
|
func setup() error {
|
|
// Set up command-line flags.
|
|
flag.Parse()
|
|
|
|
// Set up and parse known template files.
|
|
var err error
|
|
var files = []string{
|
|
path.Join("template", "index.template"),
|
|
path.Join("template", "default-content.template"),
|
|
path.Join("template", "default-preview.template"),
|
|
}
|
|
|
|
if index, err = template.ParseFS(static.FS, files...); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse Grawkit script into concrete representation.
|
|
if script, err := os.ReadFile(*scriptPath); err != nil {
|
|
return err
|
|
} else if program, err = parser.ParseProgram(script, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
// Set up base service dependencies.
|
|
if err := setup(); err != nil {
|
|
log.Fatalf("Failed setting up service: %s", err)
|
|
}
|
|
|
|
// Set up TCP socket and handlers for HTTP server.
|
|
ln, err := net.Listen("tcp", *listenAddress)
|
|
if err != nil {
|
|
log.Fatalf("Failed listening on address '%s': %s", *listenAddress, err)
|
|
}
|
|
|
|
// Listen on given address and wait for INT or TERM signals.
|
|
log.Println("Listening on " + *listenAddress + "...")
|
|
go http.Serve(ln, http.HandlerFunc(handleRequest))
|
|
|
|
halt := make(chan os.Signal, 1)
|
|
signal.Notify(halt, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
<-halt
|
|
log.Println("Shutting down listener...")
|
|
}
|