Things I did while unable to commit

- Log rotation
- Execution cancellation
- HTTP redirect, error functions
- Changed naming of document parsing/loading functions
This commit is contained in:
Sasha Koshka 2024-12-10 20:37:40 -05:00
parent f112a2e564
commit bf668b0cf7
8 changed files with 101 additions and 26 deletions

View File

@ -15,6 +15,7 @@ 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/rotate"
import stephttp"git.tebibyte.media/sashakoshka/step/http"
import "git.tebibyte.media/sashakoshka/go-service/routines"
@ -26,6 +27,10 @@ func main () {
'p', "pid-file",
"Write the PID to the specified file.",
"", cli.ValString)
flagLogDirectory := cli.NewInputFlag (
'l', "log-directory",
"Write logs to the specified directory.",
"", cli.ValString)
flagHTTPAddress := cli.NewInputFlag (
'h', "http-address",
"The address to host the HTTP server on.",
@ -47,6 +52,7 @@ func main () {
cmd := cli.New (
"Run an HTTP server that automaticaly executes STEP files.",
flagPidFile,
flagLogDirectory,
flagHTTPAddress,
flagHTTPErrorDocument,
flagHTTPDirectoryDocument,
@ -68,7 +74,17 @@ func main () {
pluginPath[index], _ = filepath.Abs(pat)
}
// log header for telling apart separate program runs
// set up logging
if flagLogDirectory.Value != "" {
directory := flagLogDirectory.Value
log.Println("(i) logging to", directory)
directory, err := filepath.Abs(directory)
if err != nil { log.Fatalln("XXX", err) }
logger, err := rotate.New(directory)
if err != nil { log.Fatalln("XXX", err) }
defer logger.Close()
log.SetOutput(logger)
}
log.Println(`==========| STEP |===========`)
log.Println(`Scriptable Template Processor`)
log.Println(`... initializing`)

View File

@ -34,10 +34,12 @@ func (this *Document) Execute (output io.Writer, data ExecutionData) error {
// TODO catch template errors here and offset their row number by the
// number of rows taken up by the front matter
err := this.template.Execute(&outputBuilder, data)
// ignore ErrExecutionCanceled, because it's meant to gracefully stop
// the template execution
if err != nil { return err }
// execute parent with this document's result
parent, err := this.environment.ParseRelative(this.Extends, this)
parent, err := this.environment.LoadRelative(this.Extends, this)
if err != nil { return err }
return parent.Execute(output, ExecutionData {
Data: data.Data,

View File

@ -57,17 +57,17 @@ func (this *Environment) Init (ctx context.Context) error {
return ctx.Err()
}
// Parse parses the named document and returns it. The name is treated as a file
// Load loads the named document and returns it. The name is treated as a file
// path relative to the current working directory. Documents are cached into the
// environment once parsed, and are returned on subsequent calls that have the
// environment once loaded, and are returned on subsequent calls that have the
// same name.
func (this *Environment) Parse (name string) (*Document, error) {
return this.ParseRelative(name, nil)
func (this *Environment) Load (name string) (*Document, error) {
return this.LoadRelative(name, nil)
}
// ParseRelative is like Parse, but treats the name as a path relative to the
// LoadRelative is like Load, but treats the name as a path relative to the
// given document.
func (this *Environment) ParseRelative (name string, document *Document) (*Document, error) {
func (this *Environment) LoadRelative (name string, document *Document) (*Document, error) {
if document == nil {
name = filepath.Clean(name)
} else {
@ -86,14 +86,14 @@ func (this *Environment) ParseRelative (name string, document *Document) (*Docum
defer input.Close()
info, err := input.Stat()
if err != nil { return nil, err }
return this.parse(name, info.ModTime(), input)
return this.load(name, info.ModTime(), input)
}
// ParseReader is like Parse, but parses a reader and just takes your word for
// it that it corresponds to the file of the given name. It always re-parses.
func (this *Environment) ParseReader (name string, input io.Reader) (*Document, error) {
// Parse is like Load, but parses a reader and just takes your word for it that
// it corresponds to the file of the given name. It always re-parses.
func (this *Environment) Parse (name string, input io.Reader) (*Document, error) {
name = filepath.Clean(name)
return this.parse(name, time.Now(), input)
return this.load(name, time.Now(), input)
}
// Unload removes a named document. It will be reloaded if necessary.
@ -103,7 +103,7 @@ func (this *Environment) Unload (name string) {
delete(documents, name)
}
func (this *Environment) parse (name string, modTime time.Time, input io.Reader) (*Document, error) {
func (this *Environment) load (name string, modTime time.Time, input io.Reader) (*Document, error) {
documents, done := this.documents.Borrow()
defer done()

View File

@ -3,6 +3,7 @@ package step
// Error enumerates errors common to this package.
type Error string; const (
ErrCircularInheritance Error = "circular inheritance"
ErrExecutionCanceled Error = "execution canceled"
ErrMetaMalformed Error = "metadata is malformed"
ErrMetaNeverClosed Error = "metadata is never closed"
ErrTypeMismatch Error = "type mismatch"

View File

@ -4,6 +4,7 @@ import "log"
import "fmt"
import "path"
import "io/fs"
import "errors"
import "strings"
import "strconv"
import "net/http"
@ -11,11 +12,6 @@ import "path/filepath"
import "git.tebibyte.media/sashakoshka/step"
import "git.tebibyte.media/sashakoshka/goutil/container"
type ErrorData struct {
Status int
Message any
}
type DirectoryData struct {
Name string
Entries []fs.DirEntry
@ -120,7 +116,7 @@ func (this *Handler) serveDirectory (
this.serveFile(res, req, pat)
return
}
document, err := this.Environment.Parse(this.DirectoryDocument)
document, err := this.Environment.Load(this.DirectoryDocument)
if err != nil {
this.serveError(res, req, http.StatusInternalServerError, err, false)
return
@ -143,7 +139,7 @@ func (this *Handler) serveDocument (
name string,
) {
// parse
document, err := this.Environment.Parse(name)
document, err := this.Environment.Load(name)
if err != nil {
this.serveError(res, req, http.StatusInternalServerError, err, false)
return
@ -177,8 +173,17 @@ func (this *Handler) serveDocument (
err = document.Execute(&recorder, step.ExecutionData {
Data: data,
})
if errors.Is(err, step.ErrExecutionCanceled) { err = nil }
var httpError Error
if errors.As(err, &httpError) {
this.serveError (
res, req,
httpError.Status, httpError.Message, false)
}
if err != nil {
this.serveError(res, req, http.StatusInternalServerError, err, false)
this.serveError (
res, req,
http.StatusInternalServerError, err, false)
return
}
@ -209,13 +214,13 @@ func (this *Handler) serveError (
return
}
document, err := this.Environment.Parse(this.ErrorDocument)
document, err := this.Environment.Load(this.ErrorDocument)
if err != nil {
this.serveError(res, req, http.StatusInternalServerError, err, true)
return
}
err = document.Execute(res, step.ExecutionData {
Data: ErrorData {
Data: Error {
Status: status,
Message: message,
},

View File

@ -1,9 +1,27 @@
package http
import "io"
import "fmt"
import "bytes"
import "net/http"
// Error represents an HTTP error. It is passed to error documents as data.
// If returned from a template, the server will show the appropriate error page.
type Error struct {
Status int
Message any
}
func (err Error) Error () string {
message := fmt.Sprint(err.Message)
text := http.StatusText(err.Status)
if message == "" {
return fmt.Sprintf("%d: %s", err.Status, text)
} else {
return fmt.Sprintf("%d: %s: %s", err.Status, text, message)
}
}
// HTTPData represents information about an ongoing HTTP request that is made
// available to templates as they are being executed.
type HTTPData struct {

View File

@ -1,9 +1,10 @@
package http
import "net/http"
import "net/url"
import "net/http"
import "html/template"
import "git.tebibyte.media/sashakoshka/step"
import shttp "git.tebibyte.media/sashakoshka/step/http"
var _ step.FuncProvider = new(Provider)
@ -23,6 +24,8 @@ func (this *Provider) FuncMap () template.FuncMap {
"statusText": http.StatusText,
"parseQuery": funcParseQuery,
"parseForm": funcParseForm,
"error": funcError,
"redirect": funcRedirect,
}
}
@ -35,8 +38,22 @@ func funcParseQuery (query string) url.Values {
return values
}
func funcError (status int, message any) (string, error) {
return "", shttp.Error {
Status: status,
Message: message,
}
}
func funcRedirect (res shttp.WrappedResponseWriter, status int, pat string) (string, error) {
// TODO remove parameters
res.Header.Add("Location", pat)
res.WriteHeader(status)
return "", step.ErrExecutionCanceled
}
func funcParseForm (req *http.Request) url.Values {
// FIXME there is already a parse form method lol this can be removed
err := req.ParseForm()
if err != nil { return nil }
return req.Form

View File

@ -25,6 +25,9 @@ func (this *Provider) FuncMapFor (document *step.Document) template.FuncMap {
}
return template.FuncMap {
"panic": stat.funcPanic,
"cancel": stat.funcCancel,
"create": stat.funcCreate,
"createHTML": stat.funcCreateHTML,
"execute": stat.funcExecute,
"include": stat.funcInclude,
"includeHTML": stat.funcIncludeHTML,
@ -43,10 +46,23 @@ func (this *state) funcPanic (message any) (string, error) {
}
}
func (this *state) funcCancel () (string, error) {
return "", step.ErrExecutionCanceled
}
func (this *state) funcCreate (name string, arguments ...any) (string, error) {
return this.funcInclude(name, arguments)
}
func (this *state) funcCreateHTML (name string, arguments ...any) (template.HTML, error) {
return this.funcIncludeHTML(name, arguments)
}
func (this *state) funcExecute (name string, data any) (step.ExecutionResult, error) {
name, err := this.document.Rel(name)
if err != nil { return step.ExecutionResult { }, err }
document, err := this.document.Environment().Parse(name)
document, err := this.document.Environment().LoadRelative (
name, this.document)
if err != nil { return step.ExecutionResult { }, err }
builder := strings.Builder { }
err = document.Execute(&builder, step.ExecutionData { Data: data })