From d14605d1388aaa7cc9ef1c230eae5ba14c9cef44 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 21 Apr 2024 18:46:40 -0700 Subject: initial commit --- api/backups/backups.go | 58 ++++++++++++++++++++++++++++++ api/serve.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ api/template/template.go | 73 ++++++++++++++++++++++++++++++++++++++ api/types/types.go | 26 ++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 api/backups/backups.go create mode 100644 api/serve.go create mode 100644 api/template/template.go create mode 100644 api/types/types.go (limited to 'api') diff --git a/api/backups/backups.go b/api/backups/backups.go new file mode 100644 index 0000000..d389582 --- /dev/null +++ b/api/backups/backups.go @@ -0,0 +1,58 @@ +package backups + +import ( + "log" + "net/http" + "time" + + "git.simponic.xyz/simponic/backup-notify/api/types" + "git.simponic.xyz/simponic/backup-notify/database" +) + +func getHostStatusOverTime(backups []database.Backup) map[string][]bool { + hostnameBackups := make(map[string][]database.Backup) + statusOverTime := make(map[string][]bool) + + if len(backups) == 0 { + return statusOverTime + } + + firstReceivedBackup := backups[0].ReceivedOn + for _, backup := range backups { + if backup.ReceivedOn.Before(firstReceivedBackup) { + firstReceivedBackup = backup.ReceivedOn + } + + if _, ok := hostnameBackups[backup.Hostname]; !ok { + hostnameBackups[backup.Hostname] = []database.Backup{} + } + hostnameBackups[backup.Hostname] = append(hostnameBackups[backup.Hostname], backup) + } + + daysSinceFirstBackup := int(time.Since(firstReceivedBackup).Hours()/24) + 1 + + for hostname := range hostnameBackups { + statusOverTime[hostname] = make([]bool, daysSinceFirstBackup) + for _, backup := range hostnameBackups[hostname] { + dayReceivedOn := int(time.Since(backup.ReceivedOn).Hours() / 24) + statusOverTime[hostname][dayReceivedOn] = true + } + } + + return statusOverTime +} + +func ListBackupsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + backups, err := database.ListBackups(context.DBConn) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + + hostStatusOverTime := getHostStatusOverTime(backups) + (*context.TemplateData)["HostStatusOverTime"] = hostStatusOverTime + return success(context, req, resp) + } +} diff --git a/api/serve.go b/api/serve.go new file mode 100644 index 0000000..5772373 --- /dev/null +++ b/api/serve.go @@ -0,0 +1,92 @@ +package api + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "time" + + "git.simponic.xyz/simponic/backup-notify/api/backups" + "git.simponic.xyz/simponic/backup-notify/api/template" + "git.simponic.xyz/simponic/backup-notify/api/types" + "git.simponic.xyz/simponic/backup-notify/args" + "git.simponic.xyz/simponic/backup-notify/utils" +) + +func LogRequestContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain { + context.Start = time.Now() + context.Id = utils.RandomId() + + log.Println(req.Method, req.URL.Path, req.RemoteAddr, context.Id) + return success(context, req, resp) + } +} + +func LogExecutionTimeContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain { + end := time.Now() + log.Println(context.Id, "took", end.Sub(context.Start)) + + return success(context, req, resp) + } +} + +func HealthCheckContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain { + resp.WriteHeader(200) + resp.Write([]byte("healthy")) + return success(context, req, resp) + } +} + +func FailurePassingContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(_success types.Continuation, failure types.Continuation) types.ContinuationChain { + return failure(context, req, resp) + } +} + +func IdContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain { + return success(context, req, resp) + } +} + +func CacheControlMiddleware(next http.Handler, maxAge int) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + header := fmt.Sprintf("public, max-age=%d", maxAge) + w.Header().Set("Cache-Control", header) + next.ServeHTTP(w, r) + }) +} + +func MakeServer(argv *args.Arguments, dbConn *sql.DB) *http.Server { + mux := http.NewServeMux() + + staticFileServer := http.FileServer(http.Dir(argv.StaticPath)) + mux.Handle("GET /static/", http.StripPrefix("/static/", CacheControlMiddleware(staticFileServer, 3600))) + + makeRequestContext := func() *types.RequestContext { + return &types.RequestContext{ + DBConn: dbConn, + Args: argv, + TemplateData: &map[string]interface{}{}, + } + } + + mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(HealthCheckContinuation, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + + mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + requestContext := makeRequestContext() + LogRequestContinuation(requestContext, r, w)(backups.ListBackupsContinuation, backups.ListBackupsContinuation)(template.TemplateContinuation("backup_list.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation) + }) + + return &http.Server{ + Addr: ":" + fmt.Sprint(argv.Port), + Handler: mux, + } +} diff --git a/api/template/template.go b/api/template/template.go new file mode 100644 index 0000000..81cb9e2 --- /dev/null +++ b/api/template/template.go @@ -0,0 +1,73 @@ +package template + +import ( + "bytes" + "errors" + "html/template" + "log" + "net/http" + "os" + + "git.simponic.xyz/simponic/backup-notify/api/types" +) + +func renderTemplate(context *types.RequestContext, templateName string, showBaseHtml bool) (bytes.Buffer, error) { + templatePath := context.Args.TemplatePath + basePath := templatePath + "/base_empty.html" + if showBaseHtml { + basePath = templatePath + "/base.html" + } + + templateLocation := templatePath + "/" + templateName + tmpl, err := template.New("").ParseFiles(templateLocation, basePath) + if err != nil { + return bytes.Buffer{}, err + } + + dataPtr := context.TemplateData + if dataPtr == nil { + dataPtr = &map[string]interface{}{} + } + + data := *dataPtr + + var buffer bytes.Buffer + err = tmpl.ExecuteTemplate(&buffer, "base", data) + + if err != nil { + return bytes.Buffer{}, err + } + return buffer, nil +} + +func TemplateContinuation(path string, showBase bool) types.Continuation { + return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + html, err := renderTemplate(context, path, true) + if errors.Is(err, os.ErrNotExist) { + resp.WriteHeader(404) + html, err = renderTemplate(context, "404.html", true) + if err != nil { + log.Println("error rendering 404 template", err) + resp.WriteHeader(500) + return failure(context, req, resp) + } + + resp.Header().Set("Content-Type", "text/html") + resp.Write(html.Bytes()) + return failure(context, req, resp) + } + + if err != nil { + log.Println("error rendering template", err) + resp.WriteHeader(500) + resp.Write([]byte("error rendering template")) + return failure(context, req, resp) + } + + resp.Header().Set("Content-Type", "text/html") + resp.Write(html.Bytes()) + return success(context, req, resp) + } + } +} diff --git a/api/types/types.go b/api/types/types.go new file mode 100644 index 0000000..1898867 --- /dev/null +++ b/api/types/types.go @@ -0,0 +1,26 @@ +package types + +import ( + "database/sql" + "net/http" + "time" + + "git.simponic.xyz/simponic/backup-notify/args" +) + +type RequestContext struct { + DBConn *sql.DB + Args *args.Arguments + + Id string + Start time.Time + + TemplateData *map[string]interface{} +} + +type BannerMessages struct { + Messages []string +} + +type Continuation func(*RequestContext, *http.Request, http.ResponseWriter) ContinuationChain +type ContinuationChain func(Continuation, Continuation) ContinuationChain -- cgit v1.2.3-70-g09d2