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 @@ + + + + +
+ Domain: {{.Config.DefaultDomain}}
+ Legacy Domain(s): {{.Config.SelfExtraDomains}}
+ HTTPS Email: {{.Config.CertbotEmail}}
+
{{.Config.MaxBackupAge}} Day(s){{.Config.DeployRoot}}{{.Config.ConfigPath}}{{.Config.GlobalAuthorizedKeysFile}}{{.Config.MysqlUserPrefix}}{{.Config.MysqlDatabasePrefix}}{{.Config.GraphDBUserPrefix}}{{.Config.GraphDBRepoPrefix}}{{.Config.DistilleryBookkeepingDatabase}}{{.Config.DistilleryBookkeepingTable}}
+ {{ .TotalCount }} instance(s) = {{ .RunningCount }} running + {{ .StoppedCount }} stopped
+
+ {{.URL}}
+
+
+ More Details
+
+
+ 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("