package web import ( "bytes" "context" "errors" "html/template" "log/slog" "net/http" "strings" "git.wilk.cx/cr-web/registry" "github.com/go-chi/httplog/v3" ) 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 httplog.RequestLogger(slog.Default(), &httplog.Options{ Level: slog.LevelInfo, Schema: httplog.SchemaECS.Concise(true), })(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 { slog.Error("fetch repositories", "error", 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 { slog.Error("render repositories", "error", 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 } slog.Error("fetch repository", "error", 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 { slog.Error("render repository", "error", 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 } slog.Error("fetch manifest", "error", 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 { slog.Error("render manifest", "error", 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()) }