From b03b00869391cd70ef1e40561f72dffbd2f47b29 Mon Sep 17 00:00:00 2001 From: Colin Wilk Date: Fri, 30 Oct 2026 10:04:02 +0100 Subject: Initial Commit --- web/server.go | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 web/server.go (limited to 'web/server.go') 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()) +} -- cgit v1.2.3