package main import ( "bytes" "cmp" "context" "embed" "fmt" "io" "log" "net/http" "os" "os/signal" "path" "slices" "strings" "syscall" "hakurei.app/command" "hakurei.app/container/check" "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" "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: ") var ( flagBaseDir string flagPort int ) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) defer stop() msg := message.New(log.Default()) c := command.New(os.Stderr, log.Printf, "pkgserver", func(args []string) error { reportPath := args[0] baseDir, err := check.NewAbs(flagBaseDir) if err != nil { return err } log.Println("baseDir:", baseDir) cache, err := pkg.Open(ctx, msg, 0, baseDir) if err != nil { return err } report, err := rosa.OpenReport(reportPath) 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) err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) if err != nil { return err } return nil }).Flag( &flagBaseDir, "b", command.StringFlag(""), "base directory for cache", ).Flag( &flagPort, "p", command.IntFlag(8067), "http listen port", ) c.MustParse(os.Args[1:], func(e error) { log.Fatal(e) }) }