From a1f35b97d3fc5b7defced4fd270fb497afda93b1 Mon Sep 17 00:00:00 2001
From: Tom Wiesing
Date: Fri, 16 Sep 2022 17:54:40 +0200
Subject: [PATCH] Initial status page
---
cmd/pathbuilders.go | 69 +++++++
cmd/wdcli/main.go | 1 +
internal/component/dis/html/index.html | 55 +++++
internal/component/dis/html/instance.html | 16 ++
.../component/dis/html/static/autolink.css | 20 ++
.../component/dis/html/static/autolink.js | 24 +++
internal/component/dis/html/static/dis.css | 67 ++++++
internal/component/dis/info.go | 194 +++++++++++++++---
internal/component/dis/stack/Dockerfile | 2 +-
.../export_pathbuilder.php | 38 ++--
.../instances/wisski_pathbuilders.go | 41 ++--
internal/component/instances/wisski_php.go | 39 ++--
internal/component/instances/wisski_status.go | 19 +-
pkg/httpx/basic.go | 24 +++
pkg/httpx/html.go | 41 ++++
pkg/httpx/httpx.go | 6 +
pkg/httpx/json.go | 45 ++++
17 files changed, 618 insertions(+), 83 deletions(-)
create mode 100644 cmd/pathbuilders.go
create mode 100644 internal/component/dis/html/index.html
create mode 100644 internal/component/dis/html/instance.html
create mode 100644 internal/component/dis/html/static/autolink.css
create mode 100644 internal/component/dis/html/static/autolink.js
create mode 100644 internal/component/dis/html/static/dis.css
rename internal/component/instances/{instances/barrel/wisskiutils => php}/export_pathbuilder.php (67%)
create mode 100644 pkg/httpx/basic.go
create mode 100644 pkg/httpx/html.go
create mode 100644 pkg/httpx/httpx.go
create mode 100644 pkg/httpx/json.go
diff --git a/cmd/pathbuilders.go b/cmd/pathbuilders.go
new file mode 100644
index 0000000..e5e9509
--- /dev/null
+++ b/cmd/pathbuilders.go
@@ -0,0 +1,69 @@
+package cmd
+
+import (
+ wisski_distillery "github.com/FAU-CDI/wisski-distillery"
+ "github.com/FAU-CDI/wisski-distillery/internal/core"
+ "github.com/tkw1536/goprogram/exit"
+)
+
+// Pathbuilders is the 'pathbuilders' command
+var Pathbuilders wisski_distillery.Command = pathbuilders{}
+
+type pathbuilders struct {
+ Positionals struct {
+ Slug string `positional-arg-name:"SLUG" required:"1-1" description:"slug of instance to export pathbuilders of"`
+ Name string `positional-arg-name:"NAME" description:"Name of pathbuilder to get. If omitted, show a list of all pathbuilders"`
+ } `positional-args:"true"`
+}
+
+func (pathbuilders) Description() wisski_distillery.Description {
+ return wisski_distillery.Description{
+ Requirements: core.Requirements{
+ NeedsDistillery: true,
+ },
+ Command: "pathbuilder",
+ Description: "Lists of displays pathbuilders of a specific WissKI instance",
+ }
+}
+
+var errPathbuilders = exit.Error{
+ Message: "Unable to export pathbuilder: %s",
+ ExitCode: exit.ExitGeneric,
+}
+
+var errNoPathbuilder = exit.Error{
+ Message: "Pathbuilder %q does not exist",
+ ExitCode: exit.ExitGeneric,
+}
+
+func (pb pathbuilders) Run(context wisski_distillery.Context) error {
+ // get the wisski
+ instance, err := context.Environment.Instances().WissKI(pb.Positionals.Slug)
+ if err != nil {
+ return err
+ }
+
+ // get all of the pathbuilders
+ if pb.Positionals.Name == "" {
+ names, err := instance.Pathbuilders()
+ if err != nil {
+ return errPathbuilders.WithMessageF(err)
+ }
+ for _, name := range names {
+ context.Println(name)
+ }
+ return nil
+ }
+
+ // get all the pathbuilders
+ xml, err := instance.Pathbuilder(pb.Positionals.Name)
+ if xml == "" {
+ return errNoPathbuilder.WithMessageF(pb.Positionals.Name)
+ }
+ if err != nil {
+ return errPathbuilders.WithMessageF(err)
+ }
+ context.Printf("%s", xml)
+
+ return nil
+}
diff --git a/cmd/wdcli/main.go b/cmd/wdcli/main.go
index bc53c8c..3033788 100644
--- a/cmd/wdcli/main.go
+++ b/cmd/wdcli/main.go
@@ -42,6 +42,7 @@ func init() {
wdcli.Register(cmd.Shell)
wdcli.Register(cmd.BlindUpdate)
wdcli.Register(cmd.UpdatePrefixConfig) // TODO: Move into post-instance configuration
+ wdcli.Register(cmd.Pathbuilders)
// backup & cron
wdcli.Register(cmd.Snapshot)
diff --git a/internal/component/dis/html/index.html b/internal/component/dis/html/index.html
new file mode 100644
index 0000000..a219ee5
--- /dev/null
+++ b/internal/component/dis/html/index.html
@@ -0,0 +1,55 @@
+
+
+
+
+Distillery Status Page
+Distillery Status Page
+
+Overview
+
+
+ Domain: {{.Config.DefaultDomain}}
+ Legacy Domain(s): {{.Config.SelfExtraDomains}}
+ HTTPS Email: {{.Config.CertbotEmail}}
+
+ Homepage Redirect:{{.Config.SelfRedirect}}
+
+ Backup Age: {{.Config.MaxBackupAge}} Day(s)
+
+ Base Directory: {{.Config.DeployRoot}}
+ Configuration File: {{.Config.ConfigPath}}
+ Authorized_Keys File: {{.Config.GlobalAuthorizedKeysFile}}
+
+ MySQL User Prefix: {{.Config.MysqlUserPrefix}}
+ MySQL Database Prefix: {{.Config.MysqlDatabasePrefix}}
+ GraphDB User Prefix: {{.Config.GraphDBUserPrefix}}
+ GraphDB Database Prefix: {{.Config.GraphDBRepoPrefix}}
+
+ Bookkeeping Database: {{.Config.DistilleryBookkeepingDatabase}}
+ Bookkeeping Table: {{.Config.DistilleryBookkeepingTable}}
+
+
+Instances
+
+
+ {{ .TotalCount }} instance(s) = {{ .RunningCount }} running + {{ .StoppedCount }} stopped
+
+
+{{range .Instances}}
+
+{{end}}
+
+
+
+
\ No newline at end of file
diff --git a/internal/component/dis/html/instance.html b/internal/component/dis/html/instance.html
new file mode 100644
index 0000000..0d7b8b9
--- /dev/null
+++ b/internal/component/dis/html/instance.html
@@ -0,0 +1,16 @@
+
+
+
+Distillery Status Page - {{ .Info.Slug }}
+Distillery Status Page - {{ .Info.Slug }}
+
+ Back to index
+
+
+{{ . }}
+
+
+
+
\ No newline at end of file
diff --git a/internal/component/dis/html/static/autolink.css b/internal/component/dis/html/static/autolink.css
new file mode 100644
index 0000000..f44f737
--- /dev/null
+++ b/internal/component/dis/html/static/autolink.css
@@ -0,0 +1,20 @@
+.header-link {
+ position: relative;
+ left: 0.5em;
+ opacity: 0;
+ font-size: 0.8em;
+
+ -webkit-transition: opacity 0.2s ease-in-out 0.1s;
+ -moz-transition: opacity 0.2s ease-in-out 0.1s;
+ -ms-transition: opacity 0.2s ease-in-out 0.1s;
+
+ text-decoration: none;
+}
+
+h2:hover .header-link,
+h3:hover .header-link,
+h4:hover .header-link,
+h5:hover .header-link,
+h6:hover .header-link {
+ opacity: 1;
+}
\ No newline at end of file
diff --git a/internal/component/dis/html/static/autolink.js b/internal/component/dis/html/static/autolink.js
new file mode 100644
index 0000000..970d570
--- /dev/null
+++ b/internal/component/dis/html/static/autolink.js
@@ -0,0 +1,24 @@
+/** adding links to each item, see http://blog.parkermoore.de/2014/08/01/header-anchor-links-in-vanilla-javascript-for-github-pages-and-jekyll/ */
+var anchorForId = function (id) {
+ var anchor = document.createElement("a");
+ anchor.className = "header-link";
+ anchor.href = "#" + id;
+ anchor.innerHTML = "#";
+ return anchor;
+};
+
+var linkifyAnchors = function (level) {
+ var headers = document.getElementsByTagName("h" + level);
+ for (var h = 0; h < headers.length; h++) {
+ var header = headers[h];
+
+ if (typeof header.id !== "undefined" && header.id !== "") {
+ header.appendChild(anchorForId(header.id));
+ }
+ }
+};
+
+
+for (var level = 1; level <= 6; level++) {
+ linkifyAnchors(level);
+}
\ No newline at end of file
diff --git a/internal/component/dis/html/static/dis.css b/internal/component/dis/html/static/dis.css
new file mode 100644
index 0000000..5eabc84
--- /dev/null
+++ b/internal/component/dis/html/static/dis.css
@@ -0,0 +1,67 @@
+html {
+ line-height: 1.5;
+ font-family: Roboto;
+ font-size: 20px;
+ color: #1a1a1a;
+ background-color: #fdfdfd;
+}
+body {
+ margin: 0 auto;
+ max-width: 36em;
+ padding-left: 50px;
+ padding-right: 50px;
+ padding-top: 50px;
+ padding-bottom: 50px;
+ hyphens: auto;
+ overflow-wrap: break-word;
+ text-rendering: optimizeLegibility;
+ font-kerning: normal;
+}
+
+@media (max-width: 600px) {
+ body {
+ font-size: 0.9em;
+ padding: 1em;
+ }
+}
+
+h1 {
+ margin-top: 1.4em;
+}
+
+h2,h3 {
+ margin-top: 1em;
+}
+
+code {
+ font-family: Roboto Mono;
+ color: blue;
+}
+
+p {
+ margin: 1em 0;
+ text-align: justify;
+}
+
+a, a:visited {
+ color: #1a1a1a;
+}
+
+footer {
+ border-top: 1px solid #1a1a1a;
+ font-size: small;
+ text-align: center;
+}
+
+.wisski {
+ padding-left: 5px;
+}
+
+.wisski.running {
+ background-color: green;
+}
+
+.wisski.stopped {
+ background-color: red;
+}
+
diff --git a/internal/component/dis/info.go b/internal/component/dis/info.go
index e9cf567..dbf9706 100644
--- a/internal/component/dis/info.go
+++ b/internal/component/dis/info.go
@@ -1,70 +1,198 @@
package dis
import (
- "encoding/json"
+ "embed"
+ "html/template"
+ "io/fs"
"net/http"
+ "strings"
+ "time"
+ "github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
+ "github.com/FAU-CDI/wisski-distillery/internal/config"
+ "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/tkw1536/goprogram/stream"
"golang.org/x/sync/errgroup"
+
+ _ "embed"
)
func (dis *Dis) info(io stream.IOStream) (http.Handler, error) {
- return http.HandlerFunc(dis.handleDis), nil
+ mux := http.NewServeMux()
+
+ // handle everything under /dis/!
+ mux.HandleFunc("/dis/", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/dis/" {
+ http.Redirect(w, r, "/dis/index", http.StatusTemporaryRedirect)
+ return
+ }
+ http.NotFound(w, r)
+ })
+
+ // static stuff
+ static, err := dis.disStatic()
+ if err != nil {
+ return nil, err
+ }
+ mux.Handle("/dis/static/", static)
+
+ // render everything
+ mux.Handle("/dis/index", httpx.HTMLHandler[disIndex]{
+ Handler: dis.disIndex,
+ Template: indexTemplate,
+ })
+
+ mux.Handle("/dis/instance/", httpx.HTMLHandler[disInstance]{
+ Handler: dis.disInstance,
+ Template: instanceTemplate,
+ })
+
+ // api -- for future usage
+ mux.Handle("/dis/api/v1/instance/get/", httpx.JSON(dis.getinstance))
+ mux.Handle("/dis/api/v1/instance/all", httpx.JSON(dis.allinstances))
+
+ // ensure that everyone is logged in!
+ return httpx.BasicAuth(mux, "WissKI Distillery Admin", func(user, pass string) bool {
+ return user == dis.Config.DisAdminUser && pass == dis.Config.DisAdminPassword
+ }), nil
}
-const disLimit = 2
+// disIndex is the context of the "/dis/index" page
+type disIndex struct {
+ Time time.Time
-func (dis *Dis) handleDis(w http.ResponseWriter, r *http.Request) {
- // make sure the user is authorized
- if !dis.authDis(r) {
- w.Header().Add("WWW-Authenticate", `Basic realm="WissKI Distillery Admin"`)
- w.WriteHeader(http.StatusUnauthorized)
- w.Write([]byte("Unauthorized"))
+ Config *config.Config
+
+ Instances []instances.Info
+ TotalCount int
+ RunningCount int
+ StoppedCount int
+}
+
+func (dis *Dis) disIndex(r *http.Request) (idx disIndex, err error) {
+ // load instances
+ idx.Instances, err = dis.allinstances(r)
+ if err != nil {
return
}
- // create a new error group
+ // count how many are running and how many are stopped
+ for _, i := range idx.Instances {
+ if i.Running {
+ idx.RunningCount++
+ } else {
+ idx.StoppedCount++
+ }
+ }
+ idx.TotalCount = len(idx.Instances)
+
+ // get the static properties
+ idx.Config = dis.Config
+
+ // current time
+ idx.Time = time.Now()
+
+ return
+}
+
+// disInstance is the context of the "/dis/instance/*" page
+type disInstance struct {
+ Time time.Time
+
+ Instance bookkeeping.Instance
+ Info instances.Info
+}
+
+func (dis *Dis) disInstance(r *http.Request) (is disInstance, err error) {
+ // find the slug as the last component of path!
+ slug := strings.TrimSuffix(r.URL.Path, "/")
+ slug = slug[strings.LastIndex(slug, "/")+1:]
+
+ // find the instance itself!
+ instance, err := dis.Instances.WissKI(slug)
+ if err == instances.ErrWissKINotFound {
+ return is, httpx.ErrNotFound
+ }
+ if err != nil {
+ return is, err
+ }
+ is.Instance = instance.Instance
+
+ // get some more info about the wisski
+ is.Info, err = instance.Info(false)
+ if err != nil {
+ return is, err
+ }
+
+ // current time
+ is.Time = time.Now()
+
+ return
+}
+
+//go:embed html/static
+var htmlStaticFS embed.FS
+
+func (*Dis) disStatic() (http.Handler, error) {
+ fs, err := fs.Sub(htmlStaticFS, "html/static")
+ if err != nil {
+ return nil, err
+ }
+
+ return http.StripPrefix("/dis/static/", http.FileServer(http.FS(fs))), nil
+}
+
+//go:embed "html/index.html"
+var indexTemplateStr string
+var indexTemplate = template.Must(template.New("index.html").Parse(indexTemplateStr))
+
+//go:embed "html/instance.html"
+var instanceTemplateString string
+var instanceTemplate = template.Must(template.New("instance.html").Parse(instanceTemplateString))
+
+func (dis *Dis) getinstance(r *http.Request) (info instances.Info, err error) {
+ // find the slug as the last component of path!
+ slug := strings.TrimSuffix(r.URL.Path, "/")
+ slug = slug[strings.LastIndex(slug, "/")+1:]
+
+ // load the wisski instance!
+ wisski, err := dis.Instances.WissKI(strings.TrimSuffix(slug, "/"))
+ if err == instances.ErrWissKINotFound {
+ return info, httpx.ErrNotFound
+ }
+ if err != nil {
+ return info, err
+ }
+
+ // get info about it!
+ return wisski.Info(false)
+}
+
+func (dis *Dis) allinstances(*http.Request) (infos []instances.Info, err error) {
var errgroup errgroup.Group
- errgroup.SetLimit(disLimit)
// list all the instances
all, err := dis.Instances.All()
if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte("internal server error"))
- return
+ return nil, err
}
// get all of their info!
- infos := make([]instances.Info, len(all))
+ infos = make([]instances.Info, len(all))
for i, instance := range all {
{
i := i
instance := instance
errgroup.Go(func() (err error) {
- infos[i], err = instance.Info()
+ infos[i], err = instance.Info(true)
return err
})
}
}
- // if some info call failed
- if err := errgroup.Wait(); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte("internal server error"))
- w.Write([]byte("\n"))
- return
- }
-
- // and return the json
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- json.NewEncoder(w).Encode(infos)
-}
-
-func (dis *Dis) authDis(r *http.Request) bool {
- user, pass, ok := r.BasicAuth()
- return ok && user == dis.Config.DisAdminUser && pass == dis.Config.DisAdminPassword
+ // wait for the results, and return
+ err = errgroup.Wait()
+ return
}
diff --git a/internal/component/dis/stack/Dockerfile b/internal/component/dis/stack/Dockerfile
index 272586a..b141487 100644
--- a/internal/component/dis/stack/Dockerfile
+++ b/internal/component/dis/stack/Dockerfile
@@ -2,4 +2,4 @@ FROM docker.io/library/docker:20.10-cli
COPY wdcli /wdcli
EXPOSE 8888
-CMD ["/wdcli","--internal-in-docker","--config","${CONFIG_PATH}","server","--bind","0.0.0.0:8888"]
\ No newline at end of file
+CMD ["/wdcli","--internal-in-docker","--config", "${CONFIG_PATH}","server","--bind","0.0.0.0:8888"]
\ No newline at end of file
diff --git a/internal/component/instances/instances/barrel/wisskiutils/export_pathbuilder.php b/internal/component/instances/php/export_pathbuilder.php
similarity index 67%
rename from internal/component/instances/instances/barrel/wisskiutils/export_pathbuilder.php
rename to internal/component/instances/php/export_pathbuilder.php
index 983e655..aeae448 100644
--- a/internal/component/instances/instances/barrel/wisskiutils/export_pathbuilder.php
+++ b/internal/component/instances/php/export_pathbuilder.php
@@ -1,17 +1,33 @@
getStorage('wisski_pathbuilder')->loadMultiple();
+/** all_xml lists all pathbuilders, and returns the corresponding xml */
+function all_xml(): object {
+ $all = \Drupal::entityTypeManager()->getStorage('wisski_pathbuilder')->loadMultiple();
+ return (object)array_map("entity_to_xml", $all);
+}
-// map over the pathbuilders
-$xmls = array_map(function($pb) {
+
+/** all_list lists the ids of all pathbuilders */
+function all_list(): Array {
+ return array_keys(\Drupal::entityQuery('wisski_pathbuilder')->execute());
+}
+
+/** one_xml serializes a single pathbuilder as xml */
+function one_xml(string $id): string {
+ $pb = \Drupal::entityTypeManager()->getStorage('wisski_pathbuilder')->load($id);
+ if ($pb === NULL) {
+ return "";
+ }
+ return entity_to_xml($pb);
+}
+
+// =================================================================================
+// =================================================================================
+
+
+function entity_to_xml($pb) {
$xml = new \SimpleXMLElement("");
$paths = $pb->getAllPaths();
@@ -58,6 +74,4 @@ $xmls = array_map(function($pb) {
$dom = dom_import_simplexml($xml)->ownerDocument;
$dom->formatOutput = TRUE;
return $dom->saveXML();
-}, $pbs);
-
-echo json_encode($xmls);
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/internal/component/instances/wisski_pathbuilders.go b/internal/component/instances/wisski_pathbuilders.go
index 60edcdc..7cf7d8b 100644
--- a/internal/component/instances/wisski_pathbuilders.go
+++ b/internal/component/instances/wisski_pathbuilders.go
@@ -1,34 +1,45 @@
package instances
import (
- "bytes"
- "encoding/json"
- "errors"
"fmt"
"io/fs"
"os"
"path/filepath"
+ _ "embed"
+
"github.com/tkw1536/goprogram/stream"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
-var errPathbuildersExecFailed = errors.New("ExportPathbuilders: Failed to call export_pathbuilder")
+//go:embed php/export_pathbuilder.php
+var exportPathbuilderPHP string
+
+// Pathbuilders returns the ids of all pathbuilders in consistent order.
+func (wisski *WissKI) Pathbuilders() (ids []string, err error) {
+ err = wisski.ExecPHPScript(stream.FromNil(), &ids, exportPathbuilderPHP, "all_list")
+ slices.Sort(ids)
+ return
+}
+
+// Pathbuilder returns a single pathbuilder as xml.
+// If it does not exist, it returns the empty string and nil error.
+func (wisski *WissKI) Pathbuilder(id string) (xml string, err error) {
+ err = wisski.ExecPHPScript(stream.FromNil(), &xml, exportPathbuilderPHP, "one_xml", id)
+ return
+}
+
+// AllPathbuilders returns all pathbuilders serialized as xml
+func (wisski *WissKI) AllPathbuilders() (pathbuilders map[string]string, err error) {
+ err = wisski.ExecPHPScript(stream.FromNil(), &pathbuilders, exportPathbuilderPHP, "all_xml")
+ return
+}
// ExportPathbuilders writes pathbuilders into the directory dest
func (wisski *WissKI) ExportPathbuilders(dest string) error {
- // export all the pathbuilders into the buffer
- var buffer bytes.Buffer
- wu := stream.NewIOStream(&buffer, nil, nil, 0)
- code, err := wisski.Barrel().Exec(wu, "barrel", "/bin/bash", "/user_shell.sh", "-c", "drush php:script /wisskiutils/export_pathbuilder.php")
- if err != nil || code != 0 {
- return errPathbuildersExecFailed
- }
-
- // decode them as a json array
- var pathbuilders map[string]string
- if err := json.NewDecoder(&buffer).Decode(&pathbuilders); err != nil {
+ pathbuilders, err := wisski.AllPathbuilders()
+ if err != nil {
return err
}
diff --git a/internal/component/instances/wisski_php.go b/internal/component/instances/wisski_php.go
index cc585aa..4ebf071 100644
--- a/internal/component/instances/wisski_php.go
+++ b/internal/component/instances/wisski_php.go
@@ -13,11 +13,12 @@ var ErrExecInvalidCode = errors.New("invalid code to execute")
var ErrExecNonZero = errors.New("script returned non-zero code")
// ExecPHPScript executes the PHP code as a script within the wisski instance.
-// The script should define a function "main", and may define additional functions.
+// The script should define a function called entrypoint, and may define additional functions.
//
// Code must start with "" + code)
if err != nil {
- return nil, err
+ return err
+ }
+
+ entrypointEscape, err := marshalPHP(entrypoint)
+ if err != nil {
+ return err
}
argsEscape, err := marshalPHP(args)
if err != nil {
- return nil, err
+ return err
}
// assemble the script
@@ -55,7 +61,7 @@ func (wisski *WissKI) ExecPHPScript(io stream.IOStream, code string, args ...any
call_user_func(function(){
ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE);
- $result = call_user_func_array("main", ` + argsEscape + `);
+ $result = call_user_func_array(` + entrypointEscape + `, ` + argsEscape + `);
ob_end_clean();
echo json_encode($result);
});
@@ -65,22 +71,19 @@ func (wisski *WissKI) ExecPHPScript(io stream.IOStream, code string, args ...any
var output bytes.Buffer
res, err := wisski.Shell(io.Streams(&output, nil, strings.NewReader(script), 0), "-c", "drush php:script -")
if res != 0 {
- return nil, ErrExecNonZero
+ return ErrExecNonZero
}
if err != nil {
- return nil, err
+ return err
+ }
+
+ // did not request to receive a result
+ if result == nil {
+ return nil
}
// decode the output
- var result any
- err = json.NewDecoder(&output).Decode(&result)
- return result, err
-}
-
-// EvalPHP is similar to ExecPHPScript, except that it evaluates a single line of php.
-// A single parameter may be passed, which can be accessed using the name $arg inside the expression.
-func (wisski *WissKI) EvalPHP(expr string, arg any) (any, error) {
- return wisski.ExecPHPScript(stream.FromEnv(), "function main($arg){return "+expr+";}", arg)
+ return json.NewDecoder(&output).Decode(result)
}
const marshalRune = 'F' // press to pay respect
diff --git a/internal/component/instances/wisski_status.go b/internal/component/instances/wisski_status.go
index df52b16..46eea7e 100644
--- a/internal/component/instances/wisski_status.go
+++ b/internal/component/instances/wisski_status.go
@@ -8,25 +8,36 @@ import (
// Info represents some info about this WissKI
type Info struct {
Slug string // The slug of the instance
+ URL string // The public URL of this instance
- Running bool // is the instance running?
-
- DrupalVersion interface{} // version of drupal being used
+ Running bool // is the instance running?
+ Pathbuilders []string // list of pathbuilders
}
// Info returns information about this WissKI instance.
-func (wisski *WissKI) Info() (info Info, err error) {
+func (wisski *WissKI) Info(quick bool) (info Info, err error) {
// static properties
info.Slug = wisski.Slug
+ info.URL = wisski.URL().String()
// dynamic properties, TODO: Add more properties here!
var group errgroup.Group
+ // quick check if this wisski is running
group.Go(func() (err error) {
info.Running, err = wisski.Alive()
return
})
+ // slower checks for extra properties.
+ // these execute php code
+ if !quick {
+ group.Go(func() (err error) {
+ info.Pathbuilders, err = wisski.Pathbuilders()
+ return
+ })
+ }
+
err = group.Wait()
return
}
diff --git a/pkg/httpx/basic.go b/pkg/httpx/basic.go
new file mode 100644
index 0000000..65d5404
--- /dev/null
+++ b/pkg/httpx/basic.go
@@ -0,0 +1,24 @@
+package httpx
+
+import "net/http"
+
+var basicUnauthorized = []byte("Unauthorized")
+
+// BasicAuth returns a new [http.Handler] that requires any credentials to pass the check function
+func BasicAuth(handler http.Handler, realm string, check func(username, password string) bool) http.Handler {
+ var authenticateHeader = `Basic realm="` + realm + `"`
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // if the basic authentication passes
+ // we can just use the handler!
+ user, pass, ok := r.BasicAuth()
+ if ok && check(user, pass) {
+ handler.ServeHTTP(w, r)
+ return
+ }
+
+ // http authentication did not pass
+ w.Header().Add("WWW-Authenticate", authenticateHeader)
+ w.WriteHeader(http.StatusUnauthorized)
+ w.Write(basicUnauthorized)
+ })
+}
diff --git a/pkg/httpx/html.go b/pkg/httpx/html.go
new file mode 100644
index 0000000..af8f580
--- /dev/null
+++ b/pkg/httpx/html.go
@@ -0,0 +1,41 @@
+package httpx
+
+import (
+ "html/template"
+ "net/http"
+)
+
+type HTMLHandler[T any] struct {
+ Handler func(r *http.Request) (T, error)
+ Template *template.Template // called with T
+}
+
+var htmlInternalServerErr = []byte(`Internal Server ErrorInternal Server Error`)
+var htmlNotFound = []byte(`Not FoundNot Found`)
+
+// ServeHTTP calls j(r) and returns json
+func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // call the function
+ result, err := h.Handler(r)
+
+ // entity not found
+ if err == ErrNotFound {
+ w.Header().Set("Content-Type", "text/html")
+ w.WriteHeader(http.StatusNotFound)
+ w.Write(htmlNotFound)
+ return
+ }
+
+ // handle other errors
+ if err != nil {
+ w.Header().Set("Content-Type", "text/html")
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(htmlInternalServerErr)
+ return
+ }
+
+ // write out the response as json
+ w.Header().Set("Content-Type", "text/html")
+ w.WriteHeader(http.StatusOK)
+ h.Template.Execute(w, result)
+}
diff --git a/pkg/httpx/httpx.go b/pkg/httpx/httpx.go
new file mode 100644
index 0000000..d6b64fb
--- /dev/null
+++ b/pkg/httpx/httpx.go
@@ -0,0 +1,6 @@
+package httpx
+
+import "errors"
+
+// ErrNotFound should be returned from any httpx error to indicate that the item was not found
+var ErrNotFound = errors.New("httpx: Error 404")
diff --git a/pkg/httpx/json.go b/pkg/httpx/json.go
new file mode 100644
index 0000000..bbacc15
--- /dev/null
+++ b/pkg/httpx/json.go
@@ -0,0 +1,45 @@
+package httpx
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+var jsonInternalServerErr = []byte(`{"status":"internal server error"}`)
+var jsonNotFound = []byte(`{"status":"not found"}`)
+
+// JSON creates a new JSONHandler
+func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] {
+ return JSONHandler[T](f)
+}
+
+// JSONHandler implements [http.Handler] by returning values as json to the caller.
+// In case of an error, a generic "internal server error" message is returned.
+type JSONHandler[T any] func(r *http.Request) (T, error)
+
+// ServeHTTP calls j(r) and returns json
+func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // call the function
+ result, err := j(r)
+
+ // entity not found
+ if err == ErrNotFound {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ w.Write(jsonNotFound)
+ return
+ }
+
+ // handle other errors
+ if err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(jsonInternalServerErr)
+ return
+ }
+
+ // write out the response as json
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(result)
+}