summaryrefslogtreecommitdiffstats
path: root/web/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'web/server.go')
-rw-r--r--web/server.go181
1 files changed, 181 insertions, 0 deletions
diff --git a/web/server.go b/web/server.go
new file mode 100644
index 0000000..121b197
--- /dev/null
+++ b/web/server.go
@@ -0,0 +1,181 @@
+package web
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "html/template"
+ "log"
+ "net/http"
+ "strings"
+
+ "git.wilk.cx/cr-browser/registry"
+)
+
+type RepositoryLister interface {
+ Repositories(ctx context.Context) ([]registry.Repository, error)
+ Repository(ctx context.Context, name string) (registry.Repository, error)
+ Manifest(ctx context.Context, repository, digest string) (registry.Manifest, error)
+}
+
+type Server struct {
+ provider RepositoryLister
+ registryHost string
+ tmpl *template.Template
+}
+
+type PageData struct {
+ Title string
+ RegistryHost string
+ Repositories []registry.Repository
+ Repository registry.Repository
+ Manifest registry.Manifest
+ SelectedTag string
+}
+
+func NewServer(provider RepositoryLister, registryHost string) (*Server, error) {
+ tmpl, err := template.New("").
+ Funcs(templateFuncs()).
+ ParseGlob("templates/*.gohtml")
+ if err != nil {
+ return nil, err
+ }
+
+ return &Server{
+ provider: provider,
+ registryHost: registryHost,
+ tmpl: tmpl,
+ }, nil
+}
+
+func (s *Server) Handler() http.Handler {
+ mux := http.NewServeMux()
+
+ // Static assets
+ mux.Handle(
+ "/assets/",
+ http.StripPrefix(
+ "/assets/",
+ http.FileServer(http.Dir("./assets")),
+ ),
+ )
+
+ // Repository listing page
+ mux.HandleFunc("/", s.repositories)
+ mux.HandleFunc("/r/{name...}", s.repository)
+
+ return mux
+}
+
+func (s *Server) repositories(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+
+ if r.Method != http.MethodGet {
+ w.Header().Set("Allow", "GET")
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ repos, err := s.provider.Repositories(r.Context())
+ if err != nil {
+ log.Printf("fetch repositories: %v", err)
+ http.Error(w, "failed to fetch repositories", http.StatusBadGateway)
+ return
+ }
+
+ data := PageData{
+ Title: "Container Registry",
+ RegistryHost: s.registryHost,
+ Repositories: repos,
+ }
+
+ var buf bytes.Buffer
+ if err := s.tmpl.ExecuteTemplate(&buf, "repositories", data); err != nil {
+ log.Printf("render repositories: %v", err)
+ http.Error(w, "failed to render page", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write(buf.Bytes())
+}
+
+func (s *Server) repository(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ w.Header().Set("Allow", "GET")
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ name := strings.TrimSuffix(r.PathValue("name"), "/")
+ // Dispatch manifest links while allowing slashes in repository names.
+ if i := strings.LastIndex(name, "/m/"); i >= 0 {
+ digest := name[i+3:]
+ if strings.Contains(digest, ":") && !strings.Contains(digest, "/") {
+ s.manifest(w, r, name[:i], digest)
+ return
+ }
+ }
+
+ repo, err := s.provider.Repository(r.Context(), name)
+ if err != nil {
+ if errors.Is(err, registry.ErrNotFound) {
+ http.NotFound(w, r)
+ return
+ }
+
+ log.Printf("fetch repository: %v", err)
+ http.Error(w, "failed to fetch repository", http.StatusBadGateway)
+ return
+ }
+
+ data := PageData{
+ Title: repo.Name,
+ RegistryHost: s.registryHost,
+ Repository: repo,
+ }
+
+ var buf bytes.Buffer
+ if err := s.tmpl.ExecuteTemplate(&buf, "repository", data); err != nil {
+ log.Printf("render repository: %v", err)
+ http.Error(w, "failed to render page", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write(buf.Bytes())
+}
+
+func (s *Server) manifest(w http.ResponseWriter, r *http.Request, repository, digest string) {
+ manifest, err := s.provider.Manifest(r.Context(), repository, digest)
+ if err != nil {
+ if errors.Is(err, registry.ErrNotFound) {
+ http.NotFound(w, r)
+ return
+ }
+ log.Printf("fetch manifest: %v", err)
+ http.Error(w, "failed to fetch manifest", http.StatusBadGateway)
+ return
+ }
+
+ data := PageData{
+ Title: "Manifest in " + repository,
+ RegistryHost: s.registryHost,
+ Repository: registry.Repository{Name: repository},
+ Manifest: manifest,
+ SelectedTag: r.URL.Query().Get("tag"),
+ }
+
+ var buf bytes.Buffer
+ if err := s.tmpl.ExecuteTemplate(&buf, "manifest", data); err != nil {
+ log.Printf("render manifest: %v", err)
+ http.Error(w, "failed to render page", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write(buf.Bytes())
+}