Files
hakurei/cmd/pkgserver/api.go
Ophestra 112c32fee2 cmd/pkgserver: log instead of write encoding error
This message is unlikely to be useful to the user, and output may be partially written at this point, causing the error to be even less intelligible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-03-11 02:10:40 +09:00

138 lines
3.9 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"path"
"strconv"
"sync"
"hakurei.app/internal/info"
"hakurei.app/internal/pkg"
"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
)
// serveInfo returns constant system information.
func serveInfo(w http.ResponseWriter, _ *http.Request) {
infoPayloadOnce.Do(func() {
infoPayload.Count = int(rosa.PresetUnexportedStart)
infoPayload.HakureiVersion = info.Version()
})
// TODO(mae): cache entire response if no additional fields are planned
writeAPIPayload(w, infoPayload)
}
func (index *packageIndex) serveStatus() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
download := path.Dir(r.URL.Path) == "/status"
if index == nil {
http.Error(w, "index is nil", http.StatusInternalServerError)
return
}
name := path.Base(r.URL.Path)
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 {
if download {
w.Header().Set("Content-Type", "application/octet-stream")
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
}
if download {
var version string
if pk.Version != "\u0000" {
version = pk.Version
} else {
version = "unknown"
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s-%s.log\"", pk.Name, version, pkg.Encode(pk.ident.Value())))
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
_, err := io.Copy(w, bytes.NewReader(pk.status))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
http.NotFound(w, r)
}
}
}
func (index *packageIndex) serveGet() http.HandlerFunc {
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]))]
// TODO(mae): remove count field
writeAPIPayload(w, &struct {
Count int `json:"count"`
Values []*metadata `json:"values"`
}{len(values), values})
}
}
const ApiVersion = "v1"
func apiRoutes(mux *http.ServeMux, index *packageIndex) {
mux.HandleFunc(fmt.Sprintf("GET /api/%s/info", ApiVersion), serveInfo)
mux.HandleFunc(fmt.Sprintf("GET /api/%s/get", ApiVersion), index.serveGet())
mux.HandleFunc(fmt.Sprintf("GET /api/%s/status/", ApiVersion), index.serveStatus())
mux.HandleFunc("GET /status/", index.serveStatus())
}
// 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,
)
}
}