mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:04:55 +08:00
feat(release):完成双 release 运维闭环
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
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 string) (*backendrpc.ReleaseDistributionPage, error) {
|
||||
return s.loadReleaseDistributionFrom(r, backendrpc.ReleaseDistributionParams{
|
||||
Channel: channel,
|
||||
ReleaseID: releaseID,
|
||||
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 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 || result.Total <= len(result.Entries) {
|
||||
return result, err
|
||||
}
|
||||
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,
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user