Files
nyaKazuha 786b739f99
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
fix(release): 收紧分发热路径与事务边界
2026-09-12 22:22:11 +08:00

254 lines
8.1 KiB
Go

package api
import (
"net/http"
"strconv"
"strings"
"bat-api/internal/backendrpc"
)
func (s *Server) handleReleaseList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
backend, ok := s.backend.(ReleaseBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "release_backend_unavailable", "Rust bat release backend is unavailable")
return
}
channel := r.URL.Query().Get("channel")
result, err := backend.ReleaseList(r.Context(), backendrpc.ReleaseListParams{Channel: channel})
if err != nil {
s.writeControlBackendError(w, "release-list", err)
return
}
writeNoStoreJSON(w, http.StatusOK, result)
}
func (s *Server) handleReleaseDistribution(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
channel, releaseID, err := releaseSelector(r)
if err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid_release_selector", err.Error())
return
}
params, err := releaseDistributionParams(r, channel, releaseID)
if err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid_release_query", err.Error())
return
}
page, err := s.requestReleaseDistribution(r, params)
if err != nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "release_backend_unavailable", err.Error())
return
}
if page == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "release_backend_unavailable", "Rust release distribution returned no result")
return
}
status := http.StatusOK
if !page.Available {
status = http.StatusConflict
}
writeNoStoreJSON(w, status, page)
}
func (s *Server) handleAdminReleaseStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
if !s.requireAdminToken(w, r) {
return
}
backend, ok := s.backend.(ReleaseBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "release_backend_unavailable", "Rust bat release backend is unavailable")
return
}
result, err := backend.ReleaseStatus(r.Context())
if err != nil {
s.writeControlBackendError(w, "release-status", err)
return
}
if r.Method == http.MethodHead {
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
return
}
writeNoStoreJSON(w, http.StatusOK, result)
}
func (s *Server) handleAdminReleaseList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
if !s.requireAdminToken(w, r) {
return
}
backend, ok := s.backend.(ReleaseBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "release_backend_unavailable", "Rust bat release backend is unavailable")
return
}
result, err := backend.ReleaseList(r.Context(), backendrpc.ReleaseListParams{
Channel: r.URL.Query().Get("channel"),
})
if err != nil {
s.writeControlBackendError(w, "release-list", err)
return
}
if r.Method == http.MethodHead {
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
return
}
writeNoStoreJSON(w, http.StatusOK, result)
}
func releaseSelector(r *http.Request) (string, string, error) {
channel := strings.TrimSpace(r.URL.Query().Get("channel"))
releaseID := strings.TrimSpace(r.URL.Query().Get("release_id"))
if channel == "" {
channel = "official"
}
if channel != "official" && channel != "localized" {
return "", "", &releaseSelectorError{message: "channel must be official or localized"}
}
if releaseID == "." || releaseID == ".." ||
strings.Contains(releaseID, "/") ||
strings.Contains(releaseID, "\\") ||
strings.Contains(releaseID, ":") ||
strings.ContainsRune(releaseID, 0) {
return "", "", &releaseSelectorError{message: "release_id contains an unsafe path character"}
}
return channel, releaseID, nil
}
type releaseSelectorError struct {
message string
}
func (e *releaseSelectorError) Error() string {
return e.message
}
func (s *Server) loadReleaseDistribution(r *http.Request, channel, releaseID, destination string) (*backendrpc.ReleaseDistributionPage, error) {
return s.loadReleaseDistributionFrom(r, backendrpc.ReleaseDistributionParams{
Channel: channel,
ReleaseID: releaseID,
Destination: destination,
Offset: 0,
Limit: 1000,
})
}
func releaseDistributionParams(r *http.Request, channel, releaseID string) (backendrpc.ReleaseDistributionParams, error) {
params := backendrpc.ReleaseDistributionParams{
Channel: channel,
ReleaseID: releaseID,
}
query := r.URL.Query()
if destination := strings.TrimSpace(query.Get("destination")); destination != "" {
params.Destination = destination
}
if raw := strings.TrimSpace(query.Get("offset")); raw != "" {
offset, err := strconv.ParseUint(raw, 10, 64)
if err != nil || uint64(int(^uint(0)>>1)) < offset {
return backendrpc.ReleaseDistributionParams{}, &releaseSelectorError{
message: "offset must be a non-negative integer",
}
}
params.Offset = int(offset)
}
if raw := strings.TrimSpace(query.Get("limit")); raw != "" {
limit, err := strconv.ParseUint(raw, 10, 64)
if err != nil || limit == 0 || limit > 1000 {
return backendrpc.ReleaseDistributionParams{}, &releaseSelectorError{
message: "limit must be in 1..=1000",
}
}
params.Limit = int(limit)
}
return params, nil
}
func (s *Server) requestReleaseDistribution(r *http.Request, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) {
backend, ok := s.backend.(ReleaseBackend)
if !ok || backend == nil {
return nil, &releaseSelectorError{message: "Rust bat release backend is unavailable"}
}
return backend.ReleaseDistribution(r.Context(), params)
}
func (s *Server) loadReleaseDistributionFrom(r *http.Request, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) {
backend, ok := s.backend.(ReleaseBackend)
if !ok || backend == nil {
return nil, &releaseSelectorError{message: "Rust bat release backend is unavailable"}
}
pageSize := 1000
result, err := backend.ReleaseDistribution(r.Context(), params)
if err != nil || result == nil || !result.Available {
return result, err
}
if params.Destination != "" {
if result.Total != 1 || result.Offset != 0 || result.Limit != 1 || len(result.Entries) != 1 {
return nil, &releaseSelectorError{message: "Rust single-entry release distribution response is invalid"}
}
return result, nil
}
if result.Total <= len(result.Entries) {
return result, nil
}
all := append([]backendrpc.ReleaseDistributionEntry(nil), result.Entries...)
for offset := len(all); offset < result.Total; {
next, nextErr := backend.ReleaseDistribution(r.Context(), backendrpc.ReleaseDistributionParams{
Channel: params.Channel,
ReleaseID: params.ReleaseID,
Destination: params.Destination,
Offset: offset,
Limit: pageSize,
})
if nextErr != nil {
return nil, nextErr
}
if next == nil || !next.Available || len(next.Entries) == 0 {
return nil, &releaseSelectorError{message: "Rust release distribution page is incomplete"}
}
all = append(all, next.Entries...)
offset = len(all)
if len(all) > result.Total {
all = all[:result.Total]
break
}
}
result.Entries = all
result.Offset = 0
result.Limit = len(all)
return result, nil
}
func releaseDistributionEntry(page *backendrpc.ReleaseDistributionPage, rel string) (ResourceEntry, bool) {
rel = strings.TrimPrefix(strings.ReplaceAll(rel, "\\", "/"), "/")
for _, entry := range page.Entries {
destination := strings.TrimPrefix(strings.ReplaceAll(entry.Destination, "\\", "/"), "/")
if destination == rel {
return ResourceEntry{
URL: entry.URL,
RelativePath: destination,
Bytes: entry.Bytes,
BLAKE3: entry.BLAKE3,
Present: true,
SizeMatch: true,
}, true
}
}
return ResourceEntry{}, false
}