mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
254 lines
8.1 KiB
Go
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
|
|
}
|