forked from security/hakurei
cmd/pkgserver: add count endpoint and restructure
This commit is contained in:
59
cmd/pkgserver/api.go
Normal file
59
cmd/pkgserver/api.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
79
cmd/pkgserver/index.go
Normal file
79
cmd/pkgserver/index.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,19 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"cmp"
|
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
@@ -23,150 +16,6 @@ import (
|
|||||||
"hakurei.app/message"
|
"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() {
|
func main() {
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
log.SetPrefix("pkgserver: ")
|
log.SetPrefix("pkgserver: ")
|
||||||
@@ -186,7 +35,6 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println("baseDir:", baseDir)
|
|
||||||
cache, err := pkg.Open(ctx, msg, 0, baseDir)
|
cache, err := pkg.Open(ctx, msg, 0, baseDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -195,19 +43,12 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println("reportPath:", reportPath)
|
|
||||||
log.Println("indexing packages...")
|
|
||||||
index, err := createPackageIndex(cache, report)
|
index, err := createPackageIndex(cache, report)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println("created package index")
|
uiRoutes()
|
||||||
http.HandleFunc("GET /{$}", serveWebUI)
|
apiRoutes(index)
|
||||||
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)
|
err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
48
cmd/pkgserver/ui.go
Normal file
48
cmd/pkgserver/ui.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user