Compare commits
6 Commits
e24832b8c0
...
93673cb359
| Author | SHA1 | Date | |
|---|---|---|---|
| 93673cb359 | |||
| 5267455e59 | |||
| 15d8780d7a | |||
| 7641e1baa7 | |||
| 18e0cd1b0a | |||
| 11069bd103 |
130
cmd/pkgserver/api.go
Normal file
130
cmd/pkgserver/api.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
type InfoPayload struct {
|
||||
Count int `json:"count"`
|
||||
HakureiVersion string `json:"hakurei_version"`
|
||||
}
|
||||
|
||||
func NewInfoPayload(index *PackageIndex) InfoPayload {
|
||||
count := len(index.sorts[0])
|
||||
return InfoPayload{
|
||||
Count: count,
|
||||
HakureiVersion: info.Version(),
|
||||
}
|
||||
}
|
||||
|
||||
func serveInfo(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/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
WritePayload(w, NewInfoPayload(index))
|
||||
}
|
||||
}
|
||||
|
||||
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 GetPayload struct {
|
||||
Count int `json:"count"`
|
||||
Values []PackageIndexEntry `json:"values"`
|
||||
}
|
||||
|
||||
func NewGetPayload(values []*PackageIndexEntry) GetPayload {
|
||||
count := len(values)
|
||||
v := make([]PackageIndexEntry, count)
|
||||
for i, _ := range values {
|
||||
v[i] = *values[i]
|
||||
}
|
||||
return GetPayload{
|
||||
Count: count,
|
||||
Values: v,
|
||||
}
|
||||
}
|
||||
|
||||
func serveGet(index *PackageIndex) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
limit, err := strconv.Atoi(q.Get("limit"))
|
||||
if err != nil || limit > 100 || limit < 1 {
|
||||
http.Error(w, fmt.Sprintf("limit must be an integer between 1 and 100"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
i, err := strconv.Atoi(q.Get("index"))
|
||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||
http.Error(w, fmt.Sprintf("index must be an integer between 0 and %d", len(index.sorts[0])-1), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sort, err := strconv.Atoi(q.Get("sort"))
|
||||
if err != nil || sort >= len(index.sorts) || sort < 0 {
|
||||
http.Error(w, fmt.Sprintf("sort must be an integer between 0 and %d", len(index.sorts)-1), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
||||
WritePayload(w, NewGetPayload(values))
|
||||
}
|
||||
}
|
||||
|
||||
const ApiVersion = "v1"
|
||||
|
||||
func apiRoutes(index *PackageIndex) {
|
||||
http.HandleFunc(fmt.Sprintf("GET /api/%s/info", ApiVersion), serveInfo(index))
|
||||
http.HandleFunc(fmt.Sprintf("GET /api/%s/get", ApiVersion), serveGet(index))
|
||||
http.HandleFunc(fmt.Sprintf("GET /api/%s/status/", ApiVersion), serveStatus(index))
|
||||
}
|
||||
|
||||
func WritePayload(w http.ResponseWriter, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(payload)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
85
cmd/pkgserver/index.go
Normal file
85
cmd/pkgserver/index.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"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,omitempty"`
|
||||
Website string `json:"website,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"report,omitempty"`
|
||||
status []byte `json:"-"`
|
||||
}
|
||||
|
||||
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
|
||||
var statusUrl string
|
||||
if n < 1 {
|
||||
status = nil
|
||||
statusUrl = ""
|
||||
} else {
|
||||
status = st
|
||||
statusUrl = fmt.Sprintf("/api/%s/status/%s.log", ApiVersion, m.Name)
|
||||
}
|
||||
entry := PackageIndexEntry{
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Website: m.Website,
|
||||
Version: v,
|
||||
Status: statusUrl,
|
||||
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,55 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"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(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
func main() {
|
||||
http.HandleFunc("GET /{$}", serveWebUI)
|
||||
http.HandleFunc("GET /favicon.ico", serveStaticContent)
|
||||
http.HandleFunc("GET /static/", serveStaticContent)
|
||||
http.HandleFunc("GET /api/", serveAPI)
|
||||
http.ListenAndServe(":8067", nil)
|
||||
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
|
||||
}
|
||||
cache, err := pkg.Open(ctx, msg, 0, baseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report, err := rosa.OpenReport(reportPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := createPackageIndex(cache, report)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uiRoutes()
|
||||
apiRoutes(index)
|
||||
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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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