aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd/mbf/internal/pkgserver
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-09-15 00:06:21 +0900
committerOphestra <cat@gensokyo.uk>2026-09-15 00:06:21 +0900
commit834db962958652502aba88362dbe91df2ee618ee (patch)
tree9c06fbcab83ad56e2d90f24a8d2cf016c6c1d005 /cmd/mbf/internal/pkgserver
parentcce7a996b06e516f5afcd41d2d67c2392000b6ab (diff)
cmd/mbf: rename pkgsite
This name is less ambiguous than pkgserver. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd/mbf/internal/pkgserver')
-rw-r--r--cmd/mbf/internal/pkgserver/api.go202
-rw-r--r--cmd/mbf/internal/pkgserver/api_test.go111
-rw-r--r--cmd/mbf/internal/pkgserver/index.go108
-rw-r--r--cmd/mbf/internal/pkgserver/index_test.go96
-rw-r--r--cmd/mbf/internal/pkgserver/search.go81
-rw-r--r--cmd/mbf/internal/pkgserver/ui/index.html58
-rw-r--r--cmd/mbf/internal/pkgserver/ui/index.ts331
-rw-r--r--cmd/mbf/internal/pkgserver/ui/style.css21
-rw-r--r--cmd/mbf/internal/pkgserver/ui/tsconfig.json8
-rw-r--r--cmd/mbf/internal/pkgserver/ui/ui.go9
-rw-r--r--cmd/mbf/internal/pkgserver/ui/ui_full.go21
-rw-r--r--cmd/mbf/internal/pkgserver/ui/ui_stub.go7
12 files changed, 0 insertions, 1053 deletions
diff --git a/cmd/mbf/internal/pkgserver/api.go b/cmd/mbf/internal/pkgserver/api.go
deleted file mode 100644
index 68ccd471..00000000
--- a/cmd/mbf/internal/pkgserver/api.go
+++ /dev/null
@@ -1,202 +0,0 @@
-// Package pkgserver implements the package metadata service backend.
-package pkgserver
-
-import (
- "context"
- "encoding/json"
- "log"
- "net/http"
- "net/url"
- "path"
- "strconv"
- "sync"
- "time"
-
- "hakurei.app/internal/info"
- "hakurei.app/internal/rosa"
-)
-
-// for lazy initialisation of serveInfo
-var (
- infoPayload struct {
- // Current package count.
- Count int `json:"count"`
- // Hakurei version, set at link time.
- HakureiVersion string `json:"hakurei_version"`
- }
- infoPayloadOnce sync.Once
-)
-
-// handleInfo writes constant system information.
-func handleInfo(w http.ResponseWriter, _ *http.Request) {
- infoPayloadOnce.Do(func() {
- infoPayload.Count = len(rosa.Collect())
- infoPayload.HakureiVersion = info.Version()
- })
- // TODO(mae): cache entire response if no additional fields are planned
- writeAPIPayload(w, infoPayload)
-}
-
-// newStatusHandler returns a [http.HandlerFunc] that offers status files for
-// viewing or download, if available.
-func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- m, ok := index.names[path.Base(r.URL.Path)]
- if !ok || !m.HasReport {
- http.NotFound(w, r)
- return
- }
-
- contentType := "text/plain; charset=utf-8"
- if disposition {
- contentType = "application/octet-stream"
-
- // quoting like this is unsound, but okay, because metadata is hardcoded
- contentDisposition := `attachment; filename="`
- contentDisposition += m.Name + "-"
- if m.Version != "" {
- contentDisposition += m.Version + "-"
- }
- contentDisposition += m.ids + `.log"`
- w.Header().Set("Content-Disposition", contentDisposition)
- }
- w.Header().Set("Content-Type", contentType)
- w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
- if err := func() (err error) {
- defer index.handleAccess(&err)()
- _, err = w.Write(m.status)
- return
- }(); err != nil {
- log.Println(err)
- http.Error(
- w, "cannot deliver status, contact maintainers",
- http.StatusInternalServerError,
- )
- }
- }
-}
-
-// handleGet writes a slice of metadata with specified order.
-func (index *packageIndex) handleGet(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, "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, "index must be an integer between 0 and "+
- strconv.Itoa(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, "sort must be an integer between 0 and "+
- strconv.Itoa(sortOrderEnd),
- http.StatusBadRequest,
- )
- return
- }
- values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
- writeAPIPayload(w, &struct {
- Values []*metadata `json:"values"`
- }{values})
-}
-
-func (index *packageIndex) handleSearch(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, "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, "index must be an integer between 0 and "+
- strconv.Itoa(len(index.sorts[0])-1),
- http.StatusBadRequest,
- )
- return
- }
- search, err := url.QueryUnescape(q.Get("search"))
- if len(search) > 100 || err != nil {
- http.Error(
- w, "search must be a string between 0 and 100 characters long",
- http.StatusBadRequest,
- )
- return
- }
- desc := q.Get("desc") == "true"
- n, res, err := index.performSearchQuery(limit, i, search, desc)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- writeAPIPayload(w, &struct {
- Count int `json:"count"`
- Values []searchResult `json:"values"`
- }{n, res})
-}
-
-// apiVersion is the name of the current API revision, as part of the pattern.
-const apiVersion = "v1"
-
-// registerAPI registers API handler functions.
-func (index *packageIndex) registerAPI(mux *http.ServeMux) {
- mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
- mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
- mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
- mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
- mux.HandleFunc("GET /status/", index.newStatusHandler(true))
-}
-
-// Register arranges for mux to service API requests.
-func Register(ctx context.Context, mux *http.ServeMux, report *rosa.Report) error {
- var index packageIndex
- index.search = make(searchCache)
- if err := index.populate(report); err != nil {
- return err
- }
- ticker := time.NewTicker(1 * time.Minute)
- go func() {
- for {
- select {
- case <-ctx.Done():
- ticker.Stop()
- return
- case <-ticker.C:
- index.search.clean()
- }
- }
- }()
- index.registerAPI(mux)
- return nil
-}
-
-// writeAPIPayload sets headers common to API responses and encodes payload as
-// JSON for the response body.
-func writeAPIPayload(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")
-
- if err := json.NewEncoder(w).Encode(payload); err != nil {
- log.Println(err)
- http.Error(
- w, "cannot encode payload, contact maintainers",
- http.StatusInternalServerError,
- )
- }
-}
diff --git a/cmd/mbf/internal/pkgserver/api_test.go b/cmd/mbf/internal/pkgserver/api_test.go
deleted file mode 100644
index 1552fec9..00000000
--- a/cmd/mbf/internal/pkgserver/api_test.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package pkgserver
-
-import (
- "net/http"
- "net/http/httptest"
- "strconv"
- "testing"
-
- "hakurei.app/internal/info"
- "hakurei.app/internal/rosa"
-)
-
-// prefix is prepended to every API path.
-const prefix = "/api/" + apiVersion + "/"
-
-func TestAPIInfo(t *testing.T) {
- t.Parallel()
-
- w := httptest.NewRecorder()
- handleInfo(w, httptest.NewRequestWithContext(
- t.Context(),
- http.MethodGet,
- prefix+"info",
- nil,
- ))
-
- resp := w.Result()
- checkStatus(t, resp, http.StatusOK)
- checkAPIHeader(t, w.Header())
-
- checkPayload(t, resp, struct {
- Count int `json:"count"`
- HakureiVersion string `json:"hakurei_version"`
- }{len(rosa.Collect()), info.Version()})
-}
-
-func TestAPIGet(t *testing.T) {
- t.Parallel()
- const target = prefix + "get"
-
- index := newIndex(t)
- newRequest := func(suffix string) *httptest.ResponseRecorder {
- w := httptest.NewRecorder()
- index.handleGet(w, httptest.NewRequestWithContext(
- t.Context(),
- http.MethodGet,
- target+suffix,
- nil,
- ))
- return w
- }
-
- checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
- t.Run("invalid", func(t *testing.T) {
- t.Parallel()
-
- w := newRequest("?" + suffix + "=invalid")
- resp := w.Result()
- checkError(t, resp, wantErr, http.StatusBadRequest)
- })
-
- t.Run("min", func(t *testing.T) {
- t.Parallel()
-
- w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
- resp := w.Result()
- checkError(t, resp, wantErr, http.StatusBadRequest)
-
- w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
- resp = w.Result()
- checkStatus(t, resp, http.StatusOK)
- })
-
- t.Run("max", func(t *testing.T) {
- t.Parallel()
-
- w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
- resp := w.Result()
- checkError(t, resp, wantErr, http.StatusBadRequest)
-
- w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
- resp = w.Result()
- checkStatus(t, resp, http.StatusOK)
- })
- }
-
- t.Run("limit", func(t *testing.T) {
- t.Parallel()
- checkValidate(
- t, "index=0&sort=0&limit", 1, 100,
- "limit must be an integer between 1 and 100",
- )
- })
-
- count := len(rosa.Collect())
- t.Run("index", func(t *testing.T) {
- t.Parallel()
- checkValidate(
- t, "limit=1&sort=0&index", 0, count-1,
- "index must be an integer between 0 and "+strconv.Itoa(count-1),
- )
- })
-
- t.Run("sort", func(t *testing.T) {
- t.Parallel()
- checkValidate(
- t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
- "sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
- )
- })
-}
diff --git a/cmd/mbf/internal/pkgserver/index.go b/cmd/mbf/internal/pkgserver/index.go
deleted file mode 100644
index 2a9b6de5..00000000
--- a/cmd/mbf/internal/pkgserver/index.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package pkgserver
-
-import (
- "cmp"
- "errors"
- "slices"
- "strings"
-
- "hakurei.app/internal/rosa"
- "hakurei.app/pkg"
-)
-
-const (
- declarationAscending = iota
- declarationDescending
- nameAscending
- nameDescending
- sizeAscending
- sizeDescending
-
- sortOrderEnd = iota - 1
-)
-
-// packageIndex refers to metadata by name and various sort orders.
-type packageIndex struct {
- sorts [sortOrderEnd + 1][]*metadata
- names map[string]*metadata
- search searchCache
- // Taken from [rosa.Report] if available.
- handleAccess func(*error) func()
-}
-
-// metadata holds [rosa.Metadata] extended with additional information.
-type metadata struct {
- handle rosa.ArtifactH
- *rosa.Metadata
-
- // Copied from [rosa.Metadata], [rosa.Unversioned] is equivalent to the zero
- // value. Otherwise, the zero value is invalid.
- Version string `json:"version,omitempty"`
- // Output data size, available if present in report.
- Size int64 `json:"size,omitempty"`
- // Whether the underlying [pkg.Artifact] is present in the report.
- HasReport bool `json:"report"`
-
- // Ident string encoded ahead of time.
- ids string
- // Backed by [rosa.Report], access must be prepared by HandleAccess.
- status []byte
-}
-
-// populate deterministically populates packageIndex, optionally with a report.
-func (index *packageIndex) populate(report *rosa.Report) (err error) {
- if report != nil {
- defer report.HandleAccess(&err)()
- index.handleAccess = report.HandleAccess
- }
-
- handles := rosa.Collect()
- work := make([]*metadata, len(handles))
- index.names = make(map[string]*metadata)
- ir := pkg.NewIR()
- for i, handle := range handles {
- meta, a := rosa.MustLoad(handle)
- m := metadata{
- handle: handle,
-
- Metadata: meta,
- Version: meta.Version,
- }
- if m.Version == "" {
- return errors.New("invalid version from " + m.Name)
- }
- if m.Version == rosa.Unversioned {
- m.Version = ""
- }
-
- if report != nil {
- id := ir.Ident(a)
- m.ids = pkg.Encode(id.Value())
- m.status, m.Size = report.ArtifactOf(id)
- m.HasReport = m.Size >= 0
- }
-
- work[i] = &m
- index.names[m.Name] = &m
- }
-
- index.sorts[declarationAscending] = work
- index.sorts[declarationDescending] = slices.Clone(work)
- slices.Reverse(index.sorts[declarationDescending][:])
-
- index.sorts[nameAscending] = slices.Clone(work)
- slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
- return strings.Compare(a.Name, b.Name)
- })
- index.sorts[nameDescending] = slices.Clone(index.sorts[nameAscending])
- slices.Reverse(index.sorts[nameDescending][:])
-
- index.sorts[sizeAscending] = slices.Clone(work)
- slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
- return cmp.Compare(a.Size, b.Size)
- })
- index.sorts[sizeDescending] = slices.Clone(index.sorts[sizeAscending])
- slices.Reverse(index.sorts[sizeDescending][:])
-
- return
-}
diff --git a/cmd/mbf/internal/pkgserver/index_test.go b/cmd/mbf/internal/pkgserver/index_test.go
deleted file mode 100644
index 8f3b5530..00000000
--- a/cmd/mbf/internal/pkgserver/index_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package pkgserver
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "reflect"
- "testing"
-)
-
-// newIndex returns the address of a newly populated packageIndex.
-func newIndex(t *testing.T) *packageIndex {
- t.Helper()
-
- var index packageIndex
- if err := index.populate(nil); err != nil {
- t.Fatalf("populate: error = %v", err)
- }
- return &index
-}
-
-// checkStatus checks response status code.
-func checkStatus(t *testing.T, resp *http.Response, want int) {
- t.Helper()
-
- if resp.StatusCode != want {
- t.Errorf(
- "StatusCode: %s, want %s",
- http.StatusText(resp.StatusCode),
- http.StatusText(want),
- )
- }
-}
-
-// checkHeader checks the value of a header entry.
-func checkHeader(t *testing.T, h http.Header, key, want string) {
- t.Helper()
-
- if got := h.Get(key); got != want {
- t.Errorf("%s: %q, want %q", key, got, want)
- }
-}
-
-// checkAPIHeader checks common entries set for API endpoints.
-func checkAPIHeader(t *testing.T, h http.Header) {
- t.Helper()
-
- checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
- checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
- checkHeader(t, h, "Pragma", "no-cache")
- checkHeader(t, h, "Expires", "0")
-}
-
-// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
-func checkPayloadFunc[T any](
- t *testing.T,
- resp *http.Response,
- f func(got *T) bool,
-) {
- t.Helper()
-
- var got T
- r := io.Reader(resp.Body)
- if testing.Verbose() {
- var buf bytes.Buffer
- r = io.TeeReader(r, &buf)
- defer func() { t.Helper(); t.Log(buf.String()) }()
- }
- if err := json.NewDecoder(r).Decode(&got); err != nil {
- t.Fatalf("Decode: error = %v", err)
- }
-
- if !f(&got) {
- t.Errorf("Body: %#v", got)
- }
-}
-
-// checkPayload checks the JSON response of an API endpoint.
-func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
- t.Helper()
-
- checkPayloadFunc(t, resp, func(got *T) bool {
- return reflect.DeepEqual(got, &want)
- })
-}
-
-func checkError(t *testing.T, resp *http.Response, error string, code int) {
- t.Helper()
-
- checkStatus(t, resp, code)
- if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
- t.Errorf("Body: %q, want %q", string(got), error)
- }
-}
diff --git a/cmd/mbf/internal/pkgserver/search.go b/cmd/mbf/internal/pkgserver/search.go
deleted file mode 100644
index 15947804..00000000
--- a/cmd/mbf/internal/pkgserver/search.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package pkgserver
-
-import (
- "cmp"
- "maps"
- "regexp"
- "slices"
- "time"
-)
-
-type searchCache map[string]searchCacheEntry
-type searchResult struct {
- NameIndices [][]int `json:"name_matches"`
- DescIndices [][]int `json:"desc_matches,omitempty"`
- Score float64 `json:"score"`
- *metadata
-}
-type searchCacheEntry struct {
- query string
- results []searchResult
- expiry time.Time
-}
-
-func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
- query := search
- if desc {
- query += ";withDesc"
- }
- entry, ok := index.search[query]
- if ok && len(entry.results) > 0 {
- return len(entry.results), entry.results[min(i, len(entry.results)-1):min(i+limit, len(entry.results))], nil
- }
-
- regex, err := regexp.Compile(search)
- if err != nil {
- return 0, make([]searchResult, 0), err
- }
- res := make([]searchResult, 0)
- for p := range maps.Values(index.names) {
- nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
- var descIndices [][]int = nil
- if desc {
- descIndices = regex.FindAllIndex([]byte(p.Description), -1)
- }
- if nameIndices == nil && descIndices == nil {
- continue
- }
- score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
- if desc {
- score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
- }
- res = append(res, searchResult{
- NameIndices: nameIndices,
- DescIndices: descIndices,
- Score: score,
- metadata: p,
- })
- }
- slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
- expiry := time.Now().Add(1 * time.Minute)
- entry = searchCacheEntry{
- query: search,
- results: res,
- expiry: expiry,
- }
- index.search[query] = entry
-
- return len(res), res[i:min(i+limit, len(entry.results))], nil
-}
-func (s *searchCache) clean() {
- maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
- return v.expiry.Before(time.Now())
- })
-}
-func indexsum(in [][]int) int {
- sum := 0
- for i := range in {
- sum += in[i][1] - in[i][0]
- }
- return sum
-}
diff --git a/cmd/mbf/internal/pkgserver/ui/index.html b/cmd/mbf/internal/pkgserver/ui/index.html
deleted file mode 100644
index 6a5d72b4..00000000
--- a/cmd/mbf/internal/pkgserver/ui/index.html
+++ /dev/null
@@ -1,58 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link rel="stylesheet" href="style.css">
- <link rel="icon" href="https://hakurei.app/favicon.ico"/>
- <title>Rosa OS Packages</title>
- <script src="index.js"></script>
-</head>
-<body>
-<h1>Rosa OS Packages</h1>
-<div class="top-controls" id="top-controls-regular">
- <p>Showing entries <span id="entry-counter"></span>.</p>
- <span id="search-bar">
- <label for="search">Search: </label>
- <input type="text" name="search" id="search"/>
- <button onclick="doSearch()">Find</button>
- <label for="include-desc">Include descriptions: </label>
- <input type="checkbox" name="include-desc" id="include-desc" checked/>
- </span>
- <div><label for="count">Entries per page: </label><select name="count" id="count">
- <option value="10">10</option>
- <option value="20">20</option>
- <option value="30">30</option>
- <option value="50">50</option>
- </select></div>
- <div><label for="sort">Sort by: </label><select name="sort" id="sort">
- <option value="0">Definition (ascending)</option>
- <option value="1">Definition (descending)</option>
- <option value="2">Name (ascending)</option>
- <option value="3">Name (descending)</option>
- <option value="4">Size (ascending)</option>
- <option value="5">Size (descending)</option>
- </select></div>
-</div>
-<div class="top-controls" id="search-top-controls" hidden>
- <p>Showing search results <span id="search-entry-counter"></span> for query "<span id="search-query"></span>".</p>
- <button onclick="exitSearch()">Back</button>
- <div><label for="search-count">Entries per page: </label><select name="search-count" id="search-count">
- <option value="10">10</option>
- <option value="20">20</option>
- <option value="30">30</option>
- <option value="50">50</option>
- </select></div>
- <p>Sorted by best match</p>
-</div>
-<div class="page-controls"><a href="javascript:prevPage()">&laquo; Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next &raquo;</a></div>
-<table id="pkg-list">
- <tr><td>Loading...</td></tr>
-</table>
-<div class="page-controls"><a href="javascript:prevPage()">&laquo; Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next &raquo;</a></div>
-<footer>
- <p>&copy;<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
-</footer>
-<script>main();</script>
-</body>
-</html>
diff --git a/cmd/mbf/internal/pkgserver/ui/index.ts b/cmd/mbf/internal/pkgserver/ui/index.ts
deleted file mode 100644
index 0784b81f..00000000
--- a/cmd/mbf/internal/pkgserver/ui/index.ts
+++ /dev/null
@@ -1,331 +0,0 @@
-interface PackageIndexEntry {
- name: string
- size?: number
- description?: string
- website?: string
- version?: string
- report?: boolean
-}
-
-function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement {
- let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
- let s = entry.size != null && entry.size > 0 ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
- let n: string
- let d: string
- if ('name_matches' in entry) {
- n = `<h2>${nameMatches(entry as SearchResult)} ${v}</h2>`
- } else {
- n = `<h2>${escapeHtml(entry.name)} ${v}</h2>`
- }
- if ('desc_matches' in entry && STATE.getIncludeDescriptions()) {
- d = descMatches(entry as SearchResult)
- } else {
- d = (entry as PackageIndexEntry).description != null ? `<p>${escapeHtml((entry as PackageIndexEntry).description)}</p>` : ""
- }
- let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
- let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
- let row = <HTMLTableRowElement>(document.createElement('tr'))
- row.innerHTML = `<td>
- ${n}
- ${d}
- ${s}
- ${w}
- ${r}
- </td>`
- return row
-}
-
-function nameMatches(sr: SearchResult): string {
- return markMatches(sr.name, sr.name_matches)
-}
-
-function descMatches(sr: SearchResult): string {
- return markMatches(sr.description!, sr.desc_matches)
-}
-
-function markMatches(str: string, indices: [number, number][]): string {
- if (indices == null) {
- return str
- }
- let out: string = ""
- let j = 0
- for (let i = 0; i < str.length; i++) {
- if (j < indices.length) {
- if (i === indices[j][0]) {
- out += `<mark>${escapeHtmlChar(str[i])}`
- continue
- }
- if (i === indices[j][1]) {
- out += `</mark>${escapeHtmlChar(str[i])}`
- j++
- continue
- }
- }
- out += escapeHtmlChar(str[i])
- }
- if (indices[j] !== undefined) {
- out += "</mark>"
- }
- return out
-}
-
-function toByteSizeString(bytes: number): string {
- if (bytes == null) return `unspecified`
- if (bytes < 1024) return `${bytes}B`
- if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB`
- if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB`
- if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB`
- if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB`
- return "not only is it big, it's large"
-}
-
-const API_VERSION = 1
-const ENDPOINT = `/api/v${API_VERSION}`
-
-interface InfoPayload {
- count?: number
- hakurei_version?: string
-}
-
-async function infoRequest(): Promise<InfoPayload> {
- const res = await fetch(`${ENDPOINT}/info`)
- const payload = await res.json()
- return payload as InfoPayload
-}
-
-interface GetPayload {
- values?: PackageIndexEntry[]
-}
-
-enum SortOrders {
- DeclarationAscending,
- DeclarationDescending,
- NameAscending,
- NameDescending
-}
-
-async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
- const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
- const payload = await res.json()
- return payload as GetPayload
-}
-
-interface SearchResult extends PackageIndexEntry {
- name_matches: [number, number][]
- desc_matches: [number, number][]
- score: number
-}
-
-interface SearchPayload {
- count?: number
- values?: SearchResult[]
-}
-
-async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise<SearchPayload> {
- const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`)
- if (!res.ok) {
- exitSearch()
- alert("invalid search query!")
- return Promise.reject(res.statusText)
- }
- const payload = await res.json()
- return payload as SearchPayload
-}
-
-class State {
- entriesPerPage: number = 10
- entryIndex: number = 0
- maxTotal: number = 0
- maxEntries: number = 0
- sort: SortOrders = SortOrders.DeclarationAscending
- search: boolean = false
-
- getEntriesPerPage(): number {
- return this.entriesPerPage
- }
-
- setEntriesPerPage(entriesPerPage: number) {
- this.entriesPerPage = entriesPerPage
- this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
- }
-
- getEntryIndex(): number {
- return this.entryIndex
- }
-
- setEntryIndex(entryIndex: number) {
- this.entryIndex = entryIndex
- this.updatePage()
- this.updateRange()
- this.updateListings()
- }
-
- getMaxTotal(): number {
- return this.maxTotal
- }
-
- setMaxTotal(max: number) {
- this.maxTotal = max
- }
-
- getSortOrder(): SortOrders {
- return this.sort
- }
-
- setSortOrder(sortOrder: SortOrders) {
- this.sort = sortOrder
- this.setEntryIndex(0)
- }
-
- updatePage() {
- let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
- for (let e of document.getElementsByClassName("page-number")) {
- (e as HTMLInputElement).value = String(page)
- }
- }
-
- updateRange() {
- let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal())
- document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}`
- if (this.search) {
- document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}`
- document.getElementById("search-query")!.innerHTML = `<code>${escapeHtml(this.getSearchQuery())}</code>`
- }
- }
-
- getSearchQuery(): string {
- let queryString = document.getElementById("search")!;
- return (queryString as HTMLInputElement).value
- }
-
- getIncludeDescriptions(): boolean {
- let includeDesc = document.getElementById("include-desc")!;
- return (includeDesc as HTMLInputElement).checked
- }
-
- updateListings() {
- if (this.search) {
- searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions())
- .then(res => {
- let table = document.getElementById("pkg-list")!
- table.innerHTML = ''
- for (let row of res.values!) {
- table.appendChild(entryToHTML(row))
- }
- STATE.maxTotal = res.count!
- STATE.updateRange()
- if(res.count! < 1) {
- exitSearch()
- alert("no results found!")
- }
- })
- } else {
- getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
- .then(res => {
- let table = document.getElementById("pkg-list")!
- table.innerHTML = ''
- for (let row of res.values!) {
- table.appendChild(entryToHTML(row))
- }
- })
- }
- }
-}
-
-let STATE: State
-
-
-function lastPageIndex(): number {
- return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()
-}
-
-function setPage(page: number) {
- STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex())))
-}
-
-
-function escapeHtml(str?: string): string {
- let out: string = ''
- if (str == undefined) return ""
- for (let i = 0; i < str.length; i++) {
- out += escapeHtmlChar(str[i])
- }
- return out
-}
-
-function escapeHtmlChar(char: string): string {
- if (char.length != 1) return char
- switch (char[0]) {
- case '&':
- return "&amp;"
- case '<':
- return "&lt;"
- case '>':
- return "&gt;"
- case '"':
- return "&quot;"
- case "'":
- return "&apos;"
- default:
- return char
- }
-}
-
-function firstPage() {
- STATE.setEntryIndex(0)
-}
-
-function prevPage() {
- let index = STATE.getEntryIndex()
- STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
-}
-
-function lastPage() {
- STATE.setEntryIndex(lastPageIndex())
-}
-
-function nextPage() {
- let index = STATE.getEntryIndex()
- STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage()))
-}
-
-function doSearch() {
- document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
- document.getElementById("search-top-controls")!.toggleAttribute("hidden");
- STATE.search = true;
- STATE.setEntryIndex(0);
-}
-
-function exitSearch() {
- document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
- document.getElementById("search-top-controls")!.toggleAttribute("hidden");
- STATE.search = false;
- STATE.setMaxTotal(STATE.maxEntries)
- STATE.setEntryIndex(0)
-}
-
-function main() {
- STATE = new State()
- infoRequest()
- .then(res => {
- STATE.maxEntries = res.count!
- STATE.setMaxTotal(STATE.maxEntries)
- document.getElementById("hakurei-version")!.textContent = res.hakurei_version!
- STATE.updateRange()
- STATE.updateListings()
- })
- for (let e of document.getElementsByClassName("page-number")) {
- e.addEventListener("change", (_) => {
- setPage(parseInt((e as HTMLInputElement).value))
- })
- }
- document.getElementById("count")?.addEventListener("change", (event) => {
- STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
- })
- document.getElementById("sort")?.addEventListener("change", (event) => {
- STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
- })
- document.getElementById("search")?.addEventListener("keyup", (event) => {
- if (event.key === 'Enter') doSearch()
- })
-} \ No newline at end of file
diff --git a/cmd/mbf/internal/pkgserver/ui/style.css b/cmd/mbf/internal/pkgserver/ui/style.css
deleted file mode 100644
index b4f281ac..00000000
--- a/cmd/mbf/internal/pkgserver/ui/style.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.page-number {
- width: 2em;
- text-align: center;
-}
-.page-number {
- width: 2em;
- text-align: center;
-}
-
-@media (prefers-color-scheme: dark) {
- html {
- background-color: #2c2c2c;
- color: ghostwhite;
- }
-}
-@media (prefers-color-scheme: light) {
- html {
- background-color: #d3d3d3;
- color: black;
- }
-} \ No newline at end of file
diff --git a/cmd/mbf/internal/pkgserver/ui/tsconfig.json b/cmd/mbf/internal/pkgserver/ui/tsconfig.json
deleted file mode 100644
index 24df4936..00000000
--- a/cmd/mbf/internal/pkgserver/ui/tsconfig.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2024",
- "strict": true,
- "alwaysStrict": true,
- "outDir": "static"
- }
-} \ No newline at end of file
diff --git a/cmd/mbf/internal/pkgserver/ui/ui.go b/cmd/mbf/internal/pkgserver/ui/ui.go
deleted file mode 100644
index 3fc0d89c..00000000
--- a/cmd/mbf/internal/pkgserver/ui/ui.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// Package ui holds the static web UI.
-package ui
-
-import "net/http"
-
-// Register arranges for mux to serve the embedded frontend.
-func Register(mux *http.ServeMux) {
- mux.Handle("GET /", http.FileServer(http.FS(static)))
-}
diff --git a/cmd/mbf/internal/pkgserver/ui/ui_full.go b/cmd/mbf/internal/pkgserver/ui/ui_full.go
deleted file mode 100644
index 564787dc..00000000
--- a/cmd/mbf/internal/pkgserver/ui/ui_full.go
+++ /dev/null
@@ -1,21 +0,0 @@
-//go:build frontend
-
-package ui
-
-import (
- "embed"
- "io/fs"
-)
-
-//go:generate tsc
-//go:generate cp index.html style.css static
-//go:embed static
-var _static embed.FS
-
-var static = func() fs.FS {
- if f, err := fs.Sub(_static, "static"); err != nil {
- panic(err)
- } else {
- return f
- }
-}()
diff --git a/cmd/mbf/internal/pkgserver/ui/ui_stub.go b/cmd/mbf/internal/pkgserver/ui/ui_stub.go
deleted file mode 100644
index daf010f8..00000000
--- a/cmd/mbf/internal/pkgserver/ui/ui_stub.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build !frontend
-
-package ui
-
-import "testing/fstest"
-
-var static fstest.MapFS