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 }