From 15d8780d7a5c6e3e1bddd291ac185274d6530786 Mon Sep 17 00:00:00 2001 From: mae Date: Mon, 9 Mar 2026 15:41:21 -0500 Subject: [PATCH] cmd/pkgserver: add count endpoint and restructure --- cmd/pkgserver/api.go | 59 +++++++++++++++ cmd/pkgserver/index.go | 79 ++++++++++++++++++++ cmd/pkgserver/main.go | 163 +---------------------------------------- cmd/pkgserver/ui.go | 48 ++++++++++++ 4 files changed, 188 insertions(+), 161 deletions(-) create mode 100644 cmd/pkgserver/api.go create mode 100644 cmd/pkgserver/index.go create mode 100644 cmd/pkgserver/ui.go diff --git a/cmd/pkgserver/api.go b/cmd/pkgserver/api.go new file mode 100644 index 0000000..3049604 --- /dev/null +++ b/cmd/pkgserver/api.go @@ -0,0 +1,59 @@ +package main + +import ( + "bytes" + "io" + "net/http" + "path" + "strconv" + "strings" + + "hakurei.app/internal/rosa" +) + +func serveCount(index *PackageIndex) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + count := len(index.names) + w.Write([]byte(strconv.Itoa(count))) + } +} + +func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if index == nil { + http.Error(w, "index is nil", http.StatusInternalServerError) + return + } + base := path.Base(r.URL.Path) + name := strings.TrimSuffix(base, ".log") + p, ok := rosa.ResolveName(name) + if !ok { + http.NotFound(w, r) + return + } + m := rosa.GetMetadata(p) + pk, ok := index.names[m.Name] + if !ok { + http.NotFound(w, r) + return + } + if len(pk.status) > 0 { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.WriteHeader(http.StatusOK) + _, err := io.Copy(w, bytes.NewReader(pk.status)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.NotFound(w, r) + } + } +} + +func apiRoutes(index *PackageIndex) { + http.HandleFunc("GET /api/count", serveCount(index)) + http.HandleFunc("GET /api/status/", serveStatus(index)) +} diff --git a/cmd/pkgserver/index.go b/cmd/pkgserver/index.go new file mode 100644 index 0000000..46ee713 --- /dev/null +++ b/cmd/pkgserver/index.go @@ -0,0 +1,79 @@ +package main + +import ( + "cmp" + "slices" + + "hakurei.app/internal/pkg" + "hakurei.app/internal/rosa" +) + +type SortOrders int + +const ( + DeclarationAscending SortOrders = iota + DeclarationDescending + NameAscending + NameDescending + limitSortOrders +) + +type PackageIndex struct { + sorts [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry + names map[string]*PackageIndexEntry +} + +type PackageIndexEntry struct { + Name string `json:"name"` + Description string `json:"description"` + Website string `json:"website"` + Version string `json:"version"` + status []byte +} + +func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) { + index := new(PackageIndex) + index.names = make(map[string]*PackageIndexEntry, rosa.PresetUnexportedStart) + work := make([]PackageIndexEntry, rosa.PresetUnexportedStart) + defer report.HandleAccess(&err)() + for p := range rosa.PresetUnexportedStart { + m := rosa.GetMetadata(p) + v := rosa.Std.Version(p) + a := rosa.Std.Load(p) + id := cache.Ident(a) + st, n := report.ArtifactOf(id) + var status []byte + if n < 1 { + status = nil + } else { + status = st + } + entry := PackageIndexEntry{ + Name: m.Name, + Description: m.Description, + Website: m.Website, + Version: v, + status: status, + } + work[p] = entry + index.names[m.Name] = &entry + } + for i, p := range work { + index.sorts[DeclarationAscending][i] = &p + } + slices.Reverse(work) + for i, p := range work { + index.sorts[DeclarationDescending][i] = &p + } + slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int { + return cmp.Compare(a.Name, b.Name) + }) + for i, p := range work { + index.sorts[NameAscending][i] = &p + } + slices.Reverse(work) + for i, p := range work { + index.sorts[NameDescending][i] = &p + } + return index, err +} diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index 8ae00c9..070a21e 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -1,19 +1,12 @@ package main import ( - "bytes" - "cmp" "context" - "embed" "fmt" - "io" "log" "net/http" "os" "os/signal" - "path" - "slices" - "strings" "syscall" "hakurei.app/command" @@ -23,150 +16,6 @@ import ( "hakurei.app/message" ) -//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts" -//go:embed ui/* -var content embed.FS - -func serveWebUI(w http.ResponseWriter, r *http.Request) { - fmt.Printf("serveWebUI: %s\n", r.URL.Path) - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("X-XSS-Protection", "1") - w.Header().Set("X-Frame-Options", "DENY") - - http.ServeFileFS(w, r, content, "ui/index.html") -} -func serveStaticContent(w http.ResponseWriter, r *http.Request) { - fmt.Printf("serveStaticContent: %s\n", r.URL.Path) - switch r.URL.Path { - case "/static/style.css": - darkTheme := r.CookiesNamed("dark_theme") - if len(darkTheme) > 0 && darkTheme[0].Value == "true" { - http.ServeFileFS(w, r, content, "ui/static/dark.css") - } else { - http.ServeFileFS(w, r, content, "ui/static/light.css") - } - break - case "/favicon.ico": - http.ServeFileFS(w, r, content, "ui/static/favicon.ico") - break - case "/static/index.js": - http.ServeFileFS(w, r, content, "ui/static/index.js") - break - default: - http.NotFound(w, r) - - } -} -func serveAPI(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) {} -} - -func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if index == nil { - http.Error(w, "index is nil", http.StatusInternalServerError) - return - } - base := path.Base(r.URL.Path) - name := strings.TrimSuffix(base, ".log") - p, ok := rosa.ResolveName(name) - if !ok { - http.NotFound(w, r) - return - } - m := rosa.GetMetadata(p) - pk, ok := index.names[m.Name] - if !ok { - http.NotFound(w, r) - return - } - if len(pk.status) > 0 { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.WriteHeader(http.StatusOK) - _, err := io.Copy(w, bytes.NewReader(pk.status)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - http.NotFound(w, r) - } - } -} - -type SortOrders int - -const ( - DeclarationAscending SortOrders = iota - DeclarationDescending - NameAscending - NameDescending - limitSortOrders -) - -type PackageIndex struct { - sorts [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry - names map[string]*PackageIndexEntry -} - -type PackageIndexEntry struct { - Name string `json:"name"` - Description string `json:"description"` - Website string `json:"website"` - Version string `json:"version"` - status []byte -} - -func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) { - index := new(PackageIndex) - index.names = make(map[string]*PackageIndexEntry, rosa.PresetUnexportedStart) - work := make([]PackageIndexEntry, rosa.PresetUnexportedStart) - defer report.HandleAccess(&err)() - for p := range rosa.PresetUnexportedStart { - m := rosa.GetMetadata(p) - v := rosa.Std.Version(p) - a := rosa.Std.Load(p) - id := cache.Ident(a) - st, n := report.ArtifactOf(id) - var status []byte - if n < 1 { - status = nil - } else { - status = st - } - log.Printf("Processing package %s...\n", m.Name) - entry := PackageIndexEntry{ - Name: m.Name, - Description: m.Description, - Website: m.Website, - Version: v, - status: status, - } - work[p] = entry - index.names[m.Name] = &entry - } - for i, p := range work { - index.sorts[DeclarationAscending][i] = &p - } - slices.Reverse(work) - for i, p := range work { - index.sorts[DeclarationDescending][i] = &p - } - slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int { - return cmp.Compare(a.Name, b.Name) - }) - for i, p := range work { - index.sorts[NameAscending][i] = &p - } - slices.Reverse(work) - for i, p := range work { - index.sorts[NameDescending][i] = &p - } - return index, err -} func main() { log.SetFlags(0) log.SetPrefix("pkgserver: ") @@ -186,7 +35,6 @@ func main() { if err != nil { return err } - log.Println("baseDir:", baseDir) cache, err := pkg.Open(ctx, msg, 0, baseDir) if err != nil { return err @@ -195,19 +43,12 @@ func main() { if err != nil { return err } - log.Println("reportPath:", reportPath) - log.Println("indexing packages...") index, err := createPackageIndex(cache, report) if err != nil { return err } - log.Println("created package index") - http.HandleFunc("GET /{$}", serveWebUI) - http.HandleFunc("GET /favicon.ico", serveStaticContent) - http.HandleFunc("GET /static/", serveStaticContent) - http.HandleFunc("GET /api/", serveAPI(index)) - http.HandleFunc("GET /api/status/", serveStatus(index)) - log.Println("listening on", flagPort) + uiRoutes() + apiRoutes(index) err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) if err != nil { return err diff --git a/cmd/pkgserver/ui.go b/cmd/pkgserver/ui.go new file mode 100644 index 0000000..0205b67 --- /dev/null +++ b/cmd/pkgserver/ui.go @@ -0,0 +1,48 @@ +package main + +import ( + "embed" + "net/http" +) + +//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts" +//go:embed ui/* +var content embed.FS + +func serveWebUI(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-XSS-Protection", "1") + w.Header().Set("X-Frame-Options", "DENY") + + http.ServeFileFS(w, r, content, "ui/index.html") +} +func serveStaticContent(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/static/style.css": + darkTheme := r.CookiesNamed("dark_theme") + if len(darkTheme) > 0 && darkTheme[0].Value == "true" { + http.ServeFileFS(w, r, content, "ui/static/dark.css") + } else { + http.ServeFileFS(w, r, content, "ui/static/light.css") + } + break + case "/favicon.ico": + http.ServeFileFS(w, r, content, "ui/static/favicon.ico") + break + case "/static/index.js": + http.ServeFileFS(w, r, content, "ui/static/index.js") + break + default: + http.NotFound(w, r) + + } +} + +func uiRoutes() { + http.HandleFunc("GET /{$}", serveWebUI) + http.HandleFunc("GET /favicon.ico", serveStaticContent) + http.HandleFunc("GET /static/", serveStaticContent) +}