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/step/providers"
import "git.tebibyte.media/sashakoshka/goutil/container" import "git.tebibyte.media/sashakoshka/goutil/container"
import "git.tebibyte.media/sashakoshka/go-service/daemon" 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 stephttp"git.tebibyte.media/sashakoshka/step/http"
import "git.tebibyte.media/sashakoshka/go-service/routines" import "git.tebibyte.media/sashakoshka/go-service/routines"
@ -26,6 +27,10 @@ func main () {
'p', "pid-file", 'p', "pid-file",
"Write the PID to the specified file.", "Write the PID to the specified file.",
"", cli.ValString) "", cli.ValString)
flagLogDirectory := cli.NewInputFlag (
'l', "log-directory",
"Write logs to the specified directory.",
"", cli.ValString)
flagHTTPAddress := cli.NewInputFlag ( flagHTTPAddress := cli.NewInputFlag (
'h', "http-address", 'h', "http-address",
"The address to host the HTTP server on.", "The address to host the HTTP server on.",
@ -47,6 +52,7 @@ func main () {
cmd := cli.New ( cmd := cli.New (
"Run an HTTP server that automaticaly executes STEP files.", "Run an HTTP server that automaticaly executes STEP files.",
flagPidFile, flagPidFile,
flagLogDirectory,
flagHTTPAddress, flagHTTPAddress,
flagHTTPErrorDocument, flagHTTPErrorDocument,
flagHTTPDirectoryDocument, flagHTTPDirectoryDocument,
@ -68,7 +74,17 @@ func main () {
pluginPath[index], _ = filepath.Abs(pat) 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(`==========| STEP |===========`)
log.Println(`Scriptable Template Processor`) log.Println(`Scriptable Template Processor`)
log.Println(`... initializing`) 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 // TODO catch template errors here and offset their row number by the
// number of rows taken up by the front matter // number of rows taken up by the front matter
err := this.template.Execute(&outputBuilder, data) err := this.template.Execute(&outputBuilder, data)
// ignore ErrExecutionCanceled, because it's meant to gracefully stop
// the template execution
if err != nil { return err } if err != nil { return err }
// execute parent with this document's result // 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 } if err != nil { return err }
return parent.Execute(output, ExecutionData { return parent.Execute(output, ExecutionData {
Data: data.Data, Data: data.Data,

View File

@ -57,17 +57,17 @@ func (this *Environment) Init (ctx context.Context) error {
return ctx.Err() 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 // 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. // same name.
func (this *Environment) Parse (name string) (*Document, error) { func (this *Environment) Load (name string) (*Document, error) {
return this.ParseRelative(name, nil) 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. // 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 { if document == nil {
name = filepath.Clean(name) name = filepath.Clean(name)
} else { } else {
@ -86,14 +86,14 @@ func (this *Environment) ParseRelative (name string, document *Document) (*Docum
defer input.Close() defer input.Close()
info, err := input.Stat() info, err := input.Stat()
if err != nil { return nil, err } 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 // Parse is like Load, but parses a reader and just takes your word for it that
// it that it corresponds to the file of the given name. It always re-parses. // it corresponds to the file of the given name. It always re-parses.
func (this *Environment) ParseReader (name string, input io.Reader) (*Document, error) { func (this *Environment) Parse (name string, input io.Reader) (*Document, error) {
name = filepath.Clean(name) 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. // Unload removes a named document. It will be reloaded if necessary.
@ -103,7 +103,7 @@ func (this *Environment) Unload (name string) {
delete(documents, name) 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() documents, done := this.documents.Borrow()
defer done() defer done()

View File

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

View File

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

View File

@ -1,9 +1,27 @@
package http package http
import "io" import "io"
import "fmt"
import "bytes" import "bytes"
import "net/http" 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 // HTTPData represents information about an ongoing HTTP request that is made
// available to templates as they are being executed. // available to templates as they are being executed.
type HTTPData struct { type HTTPData struct {

View File

@ -1,9 +1,10 @@
package http package http
import "net/http"
import "net/url" import "net/url"
import "net/http"
import "html/template" import "html/template"
import "git.tebibyte.media/sashakoshka/step" import "git.tebibyte.media/sashakoshka/step"
import shttp "git.tebibyte.media/sashakoshka/step/http"
var _ step.FuncProvider = new(Provider) var _ step.FuncProvider = new(Provider)
@ -23,6 +24,8 @@ func (this *Provider) FuncMap () template.FuncMap {
"statusText": http.StatusText, "statusText": http.StatusText,
"parseQuery": funcParseQuery, "parseQuery": funcParseQuery,
"parseForm": funcParseForm, "parseForm": funcParseForm,
"error": funcError,
"redirect": funcRedirect,
} }
} }
@ -35,8 +38,22 @@ func funcParseQuery (query string) url.Values {
return 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 { func funcParseForm (req *http.Request) url.Values {
// FIXME there is already a parse form method lol this can be removed
err := req.ParseForm() err := req.ParseForm()
if err != nil { return nil } if err != nil { return nil }
return req.Form return req.Form

View File

@ -25,6 +25,9 @@ func (this *Provider) FuncMapFor (document *step.Document) template.FuncMap {
} }
return template.FuncMap { return template.FuncMap {
"panic": stat.funcPanic, "panic": stat.funcPanic,
"cancel": stat.funcCancel,
"create": stat.funcCreate,
"createHTML": stat.funcCreateHTML,
"execute": stat.funcExecute, "execute": stat.funcExecute,
"include": stat.funcInclude, "include": stat.funcInclude,
"includeHTML": stat.funcIncludeHTML, "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) { func (this *state) funcExecute (name string, data any) (step.ExecutionResult, error) {
name, err := this.document.Rel(name) name, err := this.document.Rel(name)
if err != nil { return step.ExecutionResult { }, err } 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 } if err != nil { return step.ExecutionResult { }, err }
builder := strings.Builder { } builder := strings.Builder { }
err = document.Execute(&builder, step.ExecutionData { Data: data }) err = document.Execute(&builder, step.ExecutionData { Data: data })