From bf668b0cf74b81490310cb4ca16a498ddfedc80c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 10 Dec 2024 20:37:40 -0500 Subject: [PATCH] Things I did while unable to commit - Log rotation - Execution cancellation - HTTP redirect, error functions - Changed naming of document parsing/loading functions --- cmd/stepd/main.go | 18 +++++++++++++++++- document.go | 4 +++- environment.go | 24 ++++++++++++------------ error.go | 1 + http/handler.go | 25 +++++++++++++++---------- http/http.go | 18 ++++++++++++++++++ providers/http/http.go | 19 ++++++++++++++++++- providers/import/import.go | 18 +++++++++++++++++- 8 files changed, 101 insertions(+), 26 deletions(-) diff --git a/cmd/stepd/main.go b/cmd/stepd/main.go index 896f063..141e26c 100644 --- a/cmd/stepd/main.go +++ b/cmd/stepd/main.go @@ -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`) diff --git a/document.go b/document.go index ae85354..1857277 100644 --- a/document.go +++ b/document.go @@ -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, diff --git a/environment.go b/environment.go index 96075d9..f59194a 100644 --- a/environment.go +++ b/environment.go @@ -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() diff --git a/error.go b/error.go index 72d651a..faed089 100644 --- a/error.go +++ b/error.go @@ -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" diff --git a/http/handler.go b/http/handler.go index 132bb6b..2256080 100644 --- a/http/handler.go +++ b/http/handler.go @@ -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, }, diff --git a/http/http.go b/http/http.go index 0a3d3a8..b3b7ab6 100644 --- a/http/http.go +++ b/http/http.go @@ -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 { diff --git a/providers/http/http.go b/providers/http/http.go index 687b96a..d3bf988 100644 --- a/providers/http/http.go +++ b/providers/http/http.go @@ -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 diff --git a/providers/import/import.go b/providers/import/import.go index 36335e3..29e91cc 100644 --- a/providers/import/import.go +++ b/providers/import/import.go @@ -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 })