Initial commit
This commit is contained in:
72
cmd/step/main.go
Normal file
72
cmd/step/main.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Command step evaluates STEP files and prints the result to standard out.
|
||||
package main
|
||||
|
||||
import "os"
|
||||
import "io"
|
||||
import "fmt"
|
||||
import "context"
|
||||
import "git.tebibyte.media/sashakoshka/step"
|
||||
import "git.tebibyte.media/sashakoshka/go-cli"
|
||||
import "git.tebibyte.media/sashakoshka/step/providers"
|
||||
|
||||
func main () {
|
||||
// TODO: add some kind of way to play an http request to a STEP file,
|
||||
// and have it be executed as if the request actually happened.
|
||||
// TODO: initialize plugins on startup and tear them down on finish
|
||||
|
||||
// parse command line arguments
|
||||
flagMetadata := cli.NewFlag (
|
||||
'm', "metadata",
|
||||
"Print document metadata as front matter")
|
||||
cmd := cli.New (
|
||||
"Evaluate STEP files and print the result",
|
||||
flagMetadata,
|
||||
cli.NewHelp())
|
||||
cmd.Syntax = "[OPTIONS]... FILE"
|
||||
cmd.ParseOrExit(os.Args)
|
||||
if len(cmd.Args) > 1 {
|
||||
cmd.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// set up the environment
|
||||
environment := step.Environment { }
|
||||
environment.FuncProviders = providers.All()
|
||||
err := environment.Init(context.Background())
|
||||
handleErr(cmd, err)
|
||||
|
||||
// load and execute the document
|
||||
var document *step.Document
|
||||
if len(cmd.Args) > 0 {
|
||||
document, err = environment.Parse(cmd.Args[0])
|
||||
} else {
|
||||
document, err = environment.ParseReader(".", os.Stdin)
|
||||
}
|
||||
handleErr(cmd, err)
|
||||
if flagMetadata.Value == "true" {
|
||||
writeMetadata(os.Stdout, document)
|
||||
}
|
||||
err = document.Execute(os.Stdout, step.ExecutionData { })
|
||||
handleErr(cmd, err)
|
||||
}
|
||||
|
||||
func handleErr (cmd *cli.Cli, err error) {
|
||||
if err != nil {
|
||||
cmd.Errorln(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func writeMetadata (output io.Writer, document *step.Document) {
|
||||
fmt.Fprintln(output, "---")
|
||||
if document.Author != "" {
|
||||
fmt.Fprintf(output, "Author: %s\n", document.Author)
|
||||
}
|
||||
if document.Title != "" {
|
||||
fmt.Fprintf(output, "Title: %s\n", document.Title)
|
||||
}
|
||||
if document.Extends != "" {
|
||||
fmt.Fprintf(output, "Extends: %s\n", document.Extends)
|
||||
}
|
||||
fmt.Fprintln(output, "---")
|
||||
}
|
||||
130
cmd/stepd/handler.go
Normal file
130
cmd/stepd/handler.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import "log"
|
||||
import "fmt"
|
||||
import "path"
|
||||
import "io/fs"
|
||||
import "strings"
|
||||
import "net/http"
|
||||
import "html/template"
|
||||
import "path/filepath"
|
||||
import "git.tebibyte.media/sashakoshka/step"
|
||||
import "git.tebibyte.media/sashakoshka/goutil/container"
|
||||
|
||||
type handler struct {
|
||||
environment *step.Environment
|
||||
directories bool
|
||||
stepExt ucontainer.Set[string]
|
||||
index []string
|
||||
}
|
||||
|
||||
func (this *handler) ServeHTTP (res http.ResponseWriter, req *http.Request) {
|
||||
remoteAddr := req.RemoteAddr
|
||||
if addr := req.Header.Get("CF-Connecting-IP"); addr != "" {
|
||||
remoteAddr = fmt.Sprintf("%s --CF-> %s", addr, req.RemoteAddr)
|
||||
} else if addr := req.Header.Get("X-Forwarded-For"); addr != "" {
|
||||
remoteAddr = fmt.Sprintf("%s --??-> %s", addr, req.RemoteAddr)
|
||||
}
|
||||
log.Println("(i)", req.Method, req.URL, "from", remoteAddr)
|
||||
// TODO log all this stuff
|
||||
filesystem := this.environment.GetFS()
|
||||
|
||||
// normalize path
|
||||
pat := req.URL.Path
|
||||
if !strings.HasPrefix(pat, "/") {
|
||||
pat = "/" + pat
|
||||
req.URL.Path = pat
|
||||
}
|
||||
pat = path.Clean(req.URL.Path)
|
||||
|
||||
info, err := statFile(filesystem, pathToName(pat))
|
||||
if err != nil {
|
||||
// TODO need more detailed error, should allow specifying error
|
||||
// documents
|
||||
this.serveError(res, req, http.StatusNotFound, req.URL)
|
||||
return
|
||||
}
|
||||
if info.IsDir() {
|
||||
// try to find an index
|
||||
for _, base := range this.index {
|
||||
currentPath := path.Join(pat, base)
|
||||
info, err := statFile(filesystem, pathToName(currentPath))
|
||||
if err != nil { continue }
|
||||
if info.IsDir() { continue }
|
||||
this.serveFile(res, req, filesystem, currentPath)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: serve a directory
|
||||
// have a custom directory listing document that takes in a list
|
||||
// of the files in the directory and the path to the directory
|
||||
// as data
|
||||
|
||||
if !this.directories {
|
||||
this.serveError(res, req, http.StatusForbidden, req.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.serveFile(res, req, filesystem, pat)
|
||||
}
|
||||
|
||||
func (this *handler) serveFile (res http.ResponseWriter, req *http.Request, filesystem fs.FS, pat string) {
|
||||
name := pathToName(pat)
|
||||
if !this.stepExt.Has(filepath.Ext(name)) {
|
||||
// just a normal file
|
||||
http.ServeFileFS(res, req, this.environment.GetFS(), name)
|
||||
return
|
||||
}
|
||||
|
||||
// parse and execute
|
||||
document, err := this.environment.Parse(name)
|
||||
if err != nil {
|
||||
this.serveError(res, req, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
err = document.Execute(res, step.ExecutionData {
|
||||
Data: req,
|
||||
})
|
||||
if err != nil {
|
||||
if tmplErr, ok := err.(*template.Error); ok {
|
||||
log.Printf (
|
||||
"ERR document %s:%s: %s\n",
|
||||
name, tmplErr.Line, tmplErr.Description)
|
||||
} else {
|
||||
log.Printf("ERR document %s: %v\n", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *handler) serveError (res http.ResponseWriter, req *http.Request, status int, message any) {
|
||||
res.Header().Add("Content-Type", "text/plain")
|
||||
res.WriteHeader(status)
|
||||
// TODO: allow customization with templates
|
||||
if message == nil {
|
||||
fmt.Fprintf(res, "%d %s\n", status, http.StatusText(status))
|
||||
} else {
|
||||
fmt.Fprintf(res, "%d %s: %v\n", status, http.StatusText(status), message)
|
||||
}
|
||||
log.Printf("ERR %d %s: %v\n", status, http.StatusText(status), message)
|
||||
}
|
||||
|
||||
func statFile (filesystem fs.FS, name string) (fs.FileInfo, error) {
|
||||
if filesystem, ok := filesystem.(fs.StatFS); ok {
|
||||
return filesystem.Stat(name)
|
||||
}
|
||||
file, err := filesystem.Open(name)
|
||||
if err != nil { return nil, err }
|
||||
defer file.Close()
|
||||
return file.Stat()
|
||||
}
|
||||
|
||||
func pathToName (pat string) string {
|
||||
if strings.HasPrefix(pat, "/") {
|
||||
pat = strings.TrimPrefix(pat, "/")
|
||||
}
|
||||
if pat == "" {
|
||||
return "."
|
||||
}
|
||||
return pat
|
||||
}
|
||||
98
cmd/stepd/main.go
Normal file
98
cmd/stepd/main.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Command stepd provides an HTTP server that automatically executes STEP files.
|
||||
package main
|
||||
|
||||
import "os"
|
||||
import "log"
|
||||
import "time"
|
||||
import "errors"
|
||||
import "context"
|
||||
import "net/http"
|
||||
import "git.tebibyte.media/sashakoshka/step"
|
||||
import "git.tebibyte.media/sashakoshka/go-cli"
|
||||
import "git.tebibyte.media/sashakoshka/step/providers"
|
||||
import "git.tebibyte.media/sashakoshka/goutil/container"
|
||||
import "git.tebibyte.media/sashakoshka/go-service/daemon"
|
||||
import "git.tebibyte.media/sashakoshka/go-service/routines"
|
||||
|
||||
func main () {
|
||||
// parse command line arguments
|
||||
flagAddress := cli.NewInputFlag (
|
||||
'a', "address",
|
||||
"The address to host the server on",
|
||||
":8080", cli.ValString)
|
||||
flagDirectories := cli.NewFlag (
|
||||
'd', "directories",
|
||||
"Serve the contents of directories")
|
||||
cmd := cli.New (
|
||||
"Run an HTTP server that automaticaly executes STEP files",
|
||||
flagAddress,
|
||||
flagDirectories,
|
||||
cli.NewHelp())
|
||||
cmd.ParseOrExit(os.Args)
|
||||
if len(cmd.Args) > 0 {
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Println(`==========| STEP |===========`)
|
||||
log.Println(`Scriptable Template Processor`)
|
||||
log.Println(`... initializing`)
|
||||
|
||||
// set up the environment
|
||||
environment := step.Environment { }
|
||||
environment.FuncProviders = providers.All()
|
||||
err := environment.Init(context.Background())
|
||||
if err != nil { log.Fatal(err) }
|
||||
|
||||
// set up the HTTP handler
|
||||
handler := handler {
|
||||
environment: &environment,
|
||||
directories: flagDirectories.Value == "true",
|
||||
stepExt: ucontainer.NewSet(".step"),
|
||||
index: []string { "index.step", "index.html", "index" },
|
||||
}
|
||||
|
||||
// set up the HTTP server
|
||||
httpServer := http.Server {
|
||||
Addr: flagAddress.Value,
|
||||
Handler: &handler,
|
||||
}
|
||||
|
||||
// set up the routine manager
|
||||
httpServerRoutine := httpServerRoutine(httpServer)
|
||||
manager := routines.Manager {
|
||||
Routines: []routines.Routine {
|
||||
&httpServerRoutine,
|
||||
},
|
||||
}
|
||||
|
||||
log.Println(`.// initialized.`)
|
||||
|
||||
ctx, done := context.WithCancel(context.Background())
|
||||
daemon.OnSigint(done)
|
||||
|
||||
log.Printf("(i) listening on %s\n", httpServer.Addr)
|
||||
if err := manager.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
type httpServerRoutine http.Server
|
||||
|
||||
func (this *httpServerRoutine) Run (ctx context.Context) error {
|
||||
ctx, done := context.WithCancel(ctx)
|
||||
defer done()
|
||||
server := http.Server(*this)
|
||||
go func () {
|
||||
<- ctx.Done()
|
||||
shutdownCtx, _ := context.WithTimeout (
|
||||
context.Background(),
|
||||
16 * time.Second)
|
||||
server.Shutdown(shutdownCtx)
|
||||
} ()
|
||||
err := server.ListenAndServe()
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user