diff --git a/CHANGELOG.md b/CHANGELOG.md
index b71cf13..cb3b2f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,13 @@
## Changelog
-### Unreleased
+### 0.7
* When using MySQL for event storage, do not leak connections.
* Last events were not shown when viewing a repo of non-default namespace.
+* Support repos with slash in the name.
+* Enable Sonatype Nexus compatibility.
+* Add `base_path` option to the config to run UI from non-root.
+* Add built-in cron feature for purging tags task.
### 0.6
diff --git a/main.go b/main.go
index b5cbe3f..f0e6969 100644
--- a/main.go
+++ b/main.go
@@ -3,7 +3,6 @@ package main
import (
"flag"
"fmt"
- "io"
"io/ioutil"
"net/http"
"net/url"
@@ -107,44 +106,8 @@ func main() {
a.eventListener = events.NewEventListener(a.config.EventDatabaseDriver, a.config.EventDatabaseLocation, a.config.EventRetentionDays)
// Template engine init.
- view := jet.NewHTMLSet("templates")
- view.SetDevelopmentMode(a.config.Debug)
- view.AddGlobal("base_path", a.config.BasePath)
- view.AddGlobal("registryHost", u.Host)
- view.AddGlobal("pretty_size", func(size interface{}) string {
- var value float64
- switch i := size.(type) {
- case gjson.Result:
- value = float64(i.Int())
- case int64:
- value = float64(i)
- }
- return registry.PrettySize(value)
- })
- view.AddGlobal("pretty_time", func(datetime interface{}) string {
- d := strings.Replace(datetime.(string), "T", " ", 1)
- d = strings.Replace(d, "Z", "", 1)
- return strings.Split(d, ".")[0]
- })
- view.AddGlobal("parse_map", func(m interface{}) string {
- var res string
- for _, k := range registry.SortedMapKeys(m) {
- res = res + fmt.Sprintf(`
%s | %v |
`, k, m.(map[string]interface{})[k])
- }
- return res
- })
- view.AddGlobal("url_encoded_path", func(m interface{}) string {
- return url.PathEscape(m.(string))
- })
- view.AddGlobal("url_decoded_path", func(m interface{}) string {
- res, err := url.PathUnescape(m.(string))
- if err != nil {
- return m.(string)
- }
- return res
- })
e := echo.New()
- e.Renderer = &template{View: view}
+ e.Renderer = setupRenderer(a.config.Debug, u.Host, a.config.BasePath)
// Web routes.
e.Static(a.config.BasePath+"/static", "static")
@@ -162,28 +125,11 @@ func main() {
return token == a.config.EventListenerToken, nil
}),
}))
- p.POST(a.config.BasePath+"/events", a.receiveEvents)
+ p.POST("/events", a.receiveEvents)
e.Logger.Fatal(e.Start(a.config.ListenAddr))
}
-// Render render template.
-func (r *template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
- t, err := r.View.GetTemplate(name)
- if err != nil {
- panic(fmt.Errorf("Fatal error template file: %s", err))
- }
- vars, ok := data.(jet.VarMap)
- if !ok {
- vars = jet.VarMap{}
- }
- err = t.Execute(w, vars, nil)
- if err != nil {
- panic(fmt.Errorf("Error rendering template %s: %s", name, err))
- }
- return nil
-}
-
func (a *apiClient) viewRepositories(c echo.Context) error {
namespace := c.Param("namespace")
if namespace == "" {
@@ -233,7 +179,7 @@ func (a *apiClient) viewTagInfo(c echo.Context) error {
sha256, infoV1, infoV2 := a.client.TagInfo(repoPath, tag, false)
if infoV1 == "" || infoV2 == "" {
- return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/%s/%s", namespace, repo))
+ return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
}
var imageSize int64
@@ -293,7 +239,7 @@ func (a *apiClient) deleteTag(c echo.Context) error {
a.client.DeleteTag(repoPath, tag)
}
- return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/%s/%s", namespace, repo))
+ return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
}
// checkDeletePermission check if tag deletion is allowed whether by anyone or permitted users.
diff --git a/registry/client.go b/registry/client.go
index c94ed59..b162323 100644
--- a/registry/client.go
+++ b/registry/client.go
@@ -69,7 +69,7 @@ func NewClient(url string, verifyTLS bool, username, password string) *Client {
c.logger.Warn("No token auth service discovered from ", c.url)
return nil
}
- } else if strings.HasPrefix(strings.ToLower(authHeader), strings.ToLower("Basic")) {
+ } else if strings.HasPrefix(strings.ToLower(authHeader), "basic") {
c.request = c.request.SetBasicAuth(c.username, c.password)
c.logger.Info("It was discovered the registry is configured with HTTP basic auth.")
}
diff --git a/template.go b/template.go
new file mode 100644
index 0000000..becd351
--- /dev/null
+++ b/template.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/url"
+ "strings"
+
+ "github.com/CloudyKit/jet"
+ "github.com/labstack/echo"
+ "github.com/quiq/docker-registry-ui/registry"
+ "github.com/tidwall/gjson"
+)
+
+// Template Jet template.
+type Template struct {
+ View *jet.Set
+}
+
+// Render render template.
+func (r *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
+ t, err := r.View.GetTemplate(name)
+ if err != nil {
+ panic(fmt.Errorf("Fatal error template file: %s", err))
+ }
+ vars, ok := data.(jet.VarMap)
+ if !ok {
+ vars = jet.VarMap{}
+ }
+ err = t.Execute(w, vars, nil)
+ if err != nil {
+ panic(fmt.Errorf("Error rendering template %s: %s", name, err))
+ }
+ return nil
+}
+
+// setupRenderer template engine init.
+func setupRenderer(debug bool, registryHost, basePath string) *Template {
+ view := jet.NewHTMLSet("templates")
+ view.SetDevelopmentMode(debug)
+
+ view.AddGlobal("basePath", basePath)
+ view.AddGlobal("registryHost", registryHost)
+ view.AddGlobal("pretty_size", func(size interface{}) string {
+ var value float64
+ switch i := size.(type) {
+ case gjson.Result:
+ value = float64(i.Int())
+ case int64:
+ value = float64(i)
+ }
+ return registry.PrettySize(value)
+ })
+ view.AddGlobal("pretty_time", func(datetime interface{}) string {
+ d := strings.Replace(datetime.(string), "T", " ", 1)
+ d = strings.Replace(d, "Z", "", 1)
+ return strings.Split(d, ".")[0]
+ })
+ view.AddGlobal("parse_map", func(m interface{}) string {
+ var res string
+ for _, k := range registry.SortedMapKeys(m) {
+ res = res + fmt.Sprintf(`%s | %v |
`, k, m.(map[string]interface{})[k])
+ }
+ return res
+ })
+ view.AddGlobal("url_decode", func(m interface{}) string {
+ res, err := url.PathUnescape(m.(string))
+ if err != nil {
+ return m.(string)
+ }
+ return res
+ })
+
+ return &Template{View: view}
+}
diff --git a/templates/base.html b/templates/base.html
index 5a0abfd..8ef0a93 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -15,7 +15,7 @@
Docker Registry UI
diff --git a/templates/event_log.html b/templates/event_log.html
index a62fc1a..9c4c400 100644
--- a/templates/event_log.html
+++ b/templates/event_log.html
@@ -14,7 +14,7 @@
{{block body()}}
- - Home
+ - Home
- Event Log
diff --git a/templates/repositories.html b/templates/repositories.html
index dfeb0bd..687c403 100644
--- a/templates/repositories.html
+++ b/templates/repositories.html
@@ -8,10 +8,10 @@
"stateSave": true
});
$('#namespace').on('change', function (e) {
- window.location = '{{ base_path }}/' + this.value;
+ window.location = '{{ basePath }}/' + this.value;
});
namespace = window.location.pathname;
- namespace = namespace.replace("{{ base_path }}", "");
+ namespace = namespace.replace("{{ basePath }}", "");
if (namespace == '/') {
namespace = 'library';
} else {
@@ -37,7 +37,7 @@
- - Home
+ - Home
@@ -50,7 +50,7 @@
{{range repo := repos}}
- {{ repo }} |
+ {{ repo }} |
{{ tagCounts[namespace+"/"+repo] }} |
{{end}}
diff --git a/templates/tag_info.html b/templates/tag_info.html
index 4310629..6095394 100644
--- a/templates/tag_info.html
+++ b/templates/tag_info.html
@@ -4,11 +4,11 @@
{{block body()}}
- - Home
+ - Home
{{if namespace != "library"}}
- - {{ namespace }}
+ - {{ namespace }}
{{end}}
- - {{ repo }}
+ - {{ repo|url_decode }}
- {{ tag }}
diff --git a/templates/tags.html b/templates/tags.html
index c93f522..84429a6 100644
--- a/templates/tags.html
+++ b/templates/tags.html
@@ -1,7 +1,7 @@
{{extends "base.html"}}
{{block head()}}
-
+