mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
1148 lines
36 KiB
Go
1148 lines
36 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"bat-api/internal/backendrpc"
|
|
)
|
|
|
|
const adminControlMaxBodyBytes = 1024
|
|
const adminScheduleMaxBodyBytes = 64 * 1024
|
|
const adminDefaultLogTail = 200
|
|
const adminMaxLogTail = 2000
|
|
|
|
type adminControlRequest struct {
|
|
Force bool `json:"force"`
|
|
}
|
|
|
|
type adminTaskRequest struct {
|
|
TaskID string `json:"task_id"`
|
|
}
|
|
|
|
func (s *Server) handleAdminIndex(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
|
|
}
|
|
body := AdminIndexResponse{
|
|
Service: "bat-api",
|
|
Panel: "admin",
|
|
Status: "available",
|
|
Links: []string{
|
|
"/healthz",
|
|
"/readyz",
|
|
"/v1/bootstrap",
|
|
"/v1/release",
|
|
"/v1/resources",
|
|
"/openapi.yaml",
|
|
"/admin/dashboard/",
|
|
"/admin/schedules",
|
|
"/admin/tasks",
|
|
"/admin/tasks/status",
|
|
"/admin/tasks/logs",
|
|
"/admin/diagnostics",
|
|
"/admin/logs",
|
|
"/admin/parse/status",
|
|
"/admin/parse/text-units",
|
|
"/admin/parse/errors",
|
|
"/admin/translation/tasks",
|
|
"/admin/translation/handoff",
|
|
"/admin/translation/memory/summary",
|
|
"/admin/translation/memory/query",
|
|
"/admin/translation/status",
|
|
},
|
|
Controls: []string{
|
|
"/admin/control/reload",
|
|
"/admin/control/refresh",
|
|
"/admin/control/restart",
|
|
"/admin/control/sync",
|
|
"/admin/control/verify",
|
|
"/admin/control/repair",
|
|
"/admin/control/catalog-refresh",
|
|
"/admin/control/schedule-add",
|
|
"/admin/control/schedule-update",
|
|
"/admin/control/schedule-remove",
|
|
"/admin/control/schedule-run",
|
|
"/admin/control/task-cancel",
|
|
"/admin/control/translation-task-update",
|
|
"/admin/control/translation-worker-run",
|
|
"/admin/control/translation-proofread",
|
|
"/admin/control/translation-memory-confirm",
|
|
"/admin/control/localized-publish",
|
|
"/admin/control/localized-rollback",
|
|
},
|
|
}
|
|
if r.Method == http.MethodHead {
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusOK, body)
|
|
}
|
|
|
|
func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
|
return
|
|
}
|
|
if !s.requireAdminToken(w, r) {
|
|
return
|
|
}
|
|
action := strings.TrimPrefix(r.URL.Path, "/admin/control/")
|
|
if action == "" || strings.Contains(action, "/") {
|
|
writeErrorJSON(w, http.StatusNotFound, "control_not_found", "unknown control action")
|
|
return
|
|
}
|
|
if strings.HasPrefix(action, "schedule-") {
|
|
s.handleAdminScheduleControl(w, r, action)
|
|
return
|
|
}
|
|
if action == "task-cancel" {
|
|
s.handleAdminTaskCancel(w, r)
|
|
return
|
|
}
|
|
if action == "translation-task-update" {
|
|
s.handleAdminTranslationTaskUpdate(w, r)
|
|
return
|
|
}
|
|
if action == "translation-worker-run" {
|
|
s.handleAdminTranslationWorkerRun(w, r)
|
|
return
|
|
}
|
|
if action == "translation-proofread" {
|
|
s.handleAdminTranslationProofread(w, r)
|
|
return
|
|
}
|
|
if action == "translation-memory-confirm" {
|
|
s.handleAdminTranslationMemoryConfirm(w, r)
|
|
return
|
|
}
|
|
if action == "localized-publish" {
|
|
s.handleAdminLocalizedPublish(w, r)
|
|
return
|
|
}
|
|
if action == "localized-rollback" {
|
|
s.handleAdminLocalizedRollback(w, r)
|
|
return
|
|
}
|
|
request, ok := decodeAdminControlRequest(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
backend, ok := s.backend.(ControlBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "control_backend_unavailable", "Rust bat control backend is unavailable")
|
|
return
|
|
}
|
|
|
|
var (
|
|
method string
|
|
result any
|
|
err error
|
|
)
|
|
switch action {
|
|
case "reload":
|
|
if request.Force {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "reload does not accept force")
|
|
return
|
|
}
|
|
method = "daemon.reload"
|
|
result, err = backend.DaemonReload(r.Context())
|
|
case "refresh":
|
|
method = "daemon.refresh"
|
|
result, err = backend.DaemonRefresh(r.Context(), request.Force)
|
|
case "restart":
|
|
if request.Force {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "restart does not accept force")
|
|
return
|
|
}
|
|
method = "daemon.restart"
|
|
result, err = backend.DaemonRestart(r.Context())
|
|
case "sync":
|
|
method = "resource.sync"
|
|
result, err = backend.ResourceSync(r.Context(), request.Force)
|
|
case "verify":
|
|
if request.Force {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "verify does not accept force")
|
|
return
|
|
}
|
|
method = "resource.verify"
|
|
result, err = backend.ResourceVerify(r.Context())
|
|
case "repair":
|
|
if request.Force {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "repair does not accept force")
|
|
return
|
|
}
|
|
method = "resource.repair"
|
|
result, err = backend.ResourceRepair(r.Context())
|
|
case "catalog-refresh":
|
|
method = "catalog.refresh"
|
|
result, err = backend.CatalogRefresh(r.Context(), request.Force)
|
|
case "stop", "clean-stable":
|
|
writeErrorJSON(w, http.StatusForbidden, "control_not_allowed", "control action is not exposed by bat-api")
|
|
return
|
|
default:
|
|
writeErrorJSON(w, http.StatusNotFound, "control_not_found", "unknown control action")
|
|
return
|
|
}
|
|
if err != nil {
|
|
s.writeControlBackendError(w, action, err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: action,
|
|
RPCMethod: method,
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http.Request) {
|
|
backend, ok := s.backend.(TranslationBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
|
|
return
|
|
}
|
|
var params backendrpc.TranslationTaskUpdateParams
|
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
if err := validateTranslationTaskUpdateParams(params); err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_params", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.TranslationTaskUpdate(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-task-update", err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: "translation-task-update",
|
|
RPCMethod: "translation.task.update",
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminTranslationWorkerRun(w http.ResponseWriter, r *http.Request) {
|
|
backend, ok := s.backend.(TranslationBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
|
|
return
|
|
}
|
|
var params backendrpc.TranslationWorkerRunParams
|
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
if err := validateTranslationWorkerRunParams(params); err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_params", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.TranslationWorkerRun(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-worker-run", err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: "translation-worker-run",
|
|
RPCMethod: "translation.worker.run",
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminTranslationProofread(w http.ResponseWriter, r *http.Request) {
|
|
backend, ok := s.backend.(TranslationBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
|
|
return
|
|
}
|
|
result, err := backend.TranslationProofread(r.Context())
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-proofread", err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: "translation-proofread",
|
|
RPCMethod: "translation.proofread",
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminTranslationMemoryConfirm(w http.ResponseWriter, r *http.Request) {
|
|
backend, ok := s.backend.(TranslationMemoryBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
|
return
|
|
}
|
|
var params backendrpc.TranslationMemoryConfirmParams
|
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
if err := validateTranslationMemoryConfirmParams(params); err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_params", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.TranslationMemoryConfirm(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-memory-confirm", err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: "translation-memory-confirm",
|
|
RPCMethod: "translation.memory.confirm",
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminLocalizedPublish(w http.ResponseWriter, r *http.Request) {
|
|
backend, ok := s.backend.(LocalizedBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "localized_backend_unavailable", "Rust bat localized backend is unavailable")
|
|
return
|
|
}
|
|
var params backendrpc.LocalizedPublishParams
|
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
if err := validateLocalizedPublishParams(params); err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_localized_params", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.LocalizedPublish(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "localized-publish", err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: "localized-publish",
|
|
RPCMethod: "localized.publish",
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminLocalizedRollback(w http.ResponseWriter, r *http.Request) {
|
|
backend, ok := s.backend.(LocalizedBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "localized_backend_unavailable", "Rust bat localized backend is unavailable")
|
|
return
|
|
}
|
|
var params backendrpc.LocalizedRollbackParams
|
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
result, err := backend.LocalizedRollback(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "localized-rollback", err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: "localized-rollback",
|
|
RPCMethod: "localized.rollback",
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminLocalizedStatus(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.(LocalizedBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "localized_backend_unavailable", "Rust bat localized backend is unavailable")
|
|
return
|
|
}
|
|
result, err := backend.LocalizedStatus(r.Context())
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "localized-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) handleAdminDiagnostics(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
|
|
}
|
|
if s.backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "diagnostics_backend_unavailable", "Rust bat diagnostics backend is unavailable")
|
|
return
|
|
}
|
|
result, err := s.backend.DaemonDoctor(r.Context())
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "diagnostics", 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) handleAdminLogs(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.(DaemonLogsBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "diagnostics_backend_unavailable", "Rust bat log backend is unavailable")
|
|
return
|
|
}
|
|
tail, err := adminLogTail(r)
|
|
if err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_log_query", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.DaemonLogs(r.Context(), tail)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "daemon-logs", 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) handleAdminTasks(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.(TaskBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
|
return
|
|
}
|
|
result, err := backend.TaskList(r.Context())
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "task-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 (s *Server) handleAdminTaskStatus(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.(TaskBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
|
return
|
|
}
|
|
taskID, err := adminTaskIDQuery(r)
|
|
if err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_task_query", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.TaskStatus(r.Context(), taskID)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "task-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) handleAdminTaskLogs(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.(TaskBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
|
return
|
|
}
|
|
taskID, err := adminTaskIDQuery(r)
|
|
if err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_task_query", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.TaskLogs(r.Context(), taskID)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "task-logs", 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) handleAdminParseStatus(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.(ParseBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "parse_backend_unavailable", "Rust bat parse backend is unavailable")
|
|
return
|
|
}
|
|
result, err := backend.ParseStatus(r.Context())
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "parse-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) handleAdminParseTextUnits(w http.ResponseWriter, r *http.Request) {
|
|
s.handleAdminParseTextUnitQuery(w, r, false)
|
|
}
|
|
|
|
func (s *Server) handleAdminParseErrors(w http.ResponseWriter, r *http.Request) {
|
|
s.handleAdminParseTextUnitQuery(w, r, true)
|
|
}
|
|
|
|
func (s *Server) handleAdminParseTextUnitQuery(w http.ResponseWriter, r *http.Request, errorsOnly bool) {
|
|
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.(ParseBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "parse_backend_unavailable", "Rust bat parse backend is unavailable")
|
|
return
|
|
}
|
|
query, err := parseTextUnitQueryParams(r)
|
|
if err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_parse_query", err.Error())
|
|
return
|
|
}
|
|
var result json.RawMessage
|
|
if errorsOnly {
|
|
result, err = backend.ParseErrors(r.Context(), query)
|
|
} else {
|
|
result, err = backend.ParseTextUnits(r.Context(), query)
|
|
}
|
|
if err != nil {
|
|
action := "parse-text-units"
|
|
if errorsOnly {
|
|
action = "parse-errors"
|
|
}
|
|
s.writeControlBackendError(w, action, 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) handleAdminTaskCancel(w http.ResponseWriter, r *http.Request) {
|
|
backend, ok := s.backend.(TaskBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "task_backend_unavailable", "Rust bat task backend is unavailable")
|
|
return
|
|
}
|
|
var params adminTaskRequest
|
|
if !decodeAdminTaskJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
taskID := strings.TrimSpace(params.TaskID)
|
|
if taskID == "" {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_task_params", "task cancel requires task_id")
|
|
return
|
|
}
|
|
result, err := backend.TaskCancel(r.Context(), taskID)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "task-cancel", err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: "task-cancel",
|
|
RPCMethod: "task.cancel",
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleAdminTranslationTasks(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.(TranslationBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
|
|
return
|
|
}
|
|
params, err := translationTaskListParams(r)
|
|
if err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_query", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.TranslationTasks(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-tasks", 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) handleAdminTranslationHandoff(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.(TranslationBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
|
|
return
|
|
}
|
|
result, err := backend.TranslationHandoff(r.Context())
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-handoff", 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) handleAdminTranslationMemorySummary(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.(TranslationMemoryBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
|
return
|
|
}
|
|
params := backendrpc.TranslationMemorySummaryParams{
|
|
TranslationMemoryPath: firstTrimmedQuery(r.URL.Query(), "translation_memory_path", "tm_path"),
|
|
}
|
|
result, err := backend.TranslationMemorySummary(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-memory-summary", 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) handleAdminTranslationMemoryQuery(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.(TranslationMemoryBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
|
return
|
|
}
|
|
params, err := translationMemoryQueryParams(r)
|
|
if err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_query", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.TranslationMemoryQuery(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "translation-memory-query", 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) handleAdminSchedules(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.(ScheduleBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "schedule_backend_unavailable", "Rust bat schedule backend is unavailable")
|
|
return
|
|
}
|
|
params, err := scheduleListParams(r)
|
|
if err != nil {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_schedule_query", err.Error())
|
|
return
|
|
}
|
|
result, err := backend.ScheduleList(r.Context(), params)
|
|
if err != nil {
|
|
s.writeControlBackendError(w, "schedule-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 scheduleListParams(r *http.Request) (backendrpc.ScheduleListParams, error) {
|
|
query := r.URL.Query()
|
|
params := backendrpc.ScheduleListParams{
|
|
ID: query.Get("id"),
|
|
Group: query.Get("group"),
|
|
}
|
|
if raw := query.Get("enabled"); raw != "" {
|
|
enabled, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return backendrpc.ScheduleListParams{}, errors.New("enabled must be a boolean")
|
|
}
|
|
params.Enabled = &enabled
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
func translationTaskListParams(r *http.Request) (backendrpc.TranslationTaskListParams, error) {
|
|
query := r.URL.Query()
|
|
params := backendrpc.TranslationTaskListParams{
|
|
TaskID: strings.TrimSpace(query.Get("task_id")),
|
|
ReleaseID: firstTrimmedQuery(query, "release_id", "official_release_id"),
|
|
Destination: strings.TrimSpace(query.Get("destination")),
|
|
PathPattern: strings.TrimSpace(query.Get("path_pattern")),
|
|
ArchiveEntry: strings.TrimSpace(query.Get("archive_entry")),
|
|
Status: firstTrimmedQuery(query, "status", "task_status"),
|
|
WorkerStatus: strings.TrimSpace(query.Get("worker_status")),
|
|
ParseStatus: strings.TrimSpace(query.Get("parse_status")),
|
|
Format: firstTrimmedQuery(query, "format", "text_unit_format"),
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("offset")); raw != "" {
|
|
offset, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil {
|
|
return backendrpc.TranslationTaskListParams{}, errors.New("offset must be a non-negative integer")
|
|
}
|
|
params.Offset = &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.TranslationTaskListParams{}, errors.New("limit must be in 1..=1000")
|
|
}
|
|
params.Limit = &limit
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("has_reason")); raw != "" {
|
|
hasReason, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return backendrpc.TranslationTaskListParams{}, errors.New("has_reason must be a boolean")
|
|
}
|
|
params.HasReason = &hasReason
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("has_failure_reason")); raw != "" {
|
|
hasFailureReason, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return backendrpc.TranslationTaskListParams{}, errors.New("has_failure_reason must be a boolean")
|
|
}
|
|
params.HasFailureReason = &hasFailureReason
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
func translationMemoryQueryParams(r *http.Request) (backendrpc.TranslationMemoryQueryParams, error) {
|
|
query := r.URL.Query()
|
|
sourceText := strings.TrimSpace(query.Get("source_text"))
|
|
if sourceText == "" {
|
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("source_text is required")
|
|
}
|
|
params := backendrpc.TranslationMemoryQueryParams{
|
|
TranslationMemoryPath: firstTrimmedQuery(query, "translation_memory_path", "tm_path"),
|
|
SourceText: sourceText,
|
|
}
|
|
if raw := firstTrimmedQuery(query, "source_context", "context"); raw != "" {
|
|
var context backendrpc.TranslationMemoryContext
|
|
if raw != "null" {
|
|
if err := json.Unmarshal([]byte(raw), &context); err != nil {
|
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("source_context must be a JSON object with string values")
|
|
}
|
|
if context == nil {
|
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("source_context must be a JSON object")
|
|
}
|
|
params.SourceContext = context
|
|
}
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("limit")); raw != "" {
|
|
limit, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil || limit == 0 || limit > 1000 {
|
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("limit must be in 1..=1000")
|
|
}
|
|
params.Limit = &limit
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
func parseTextUnitQueryParams(r *http.Request) (backendrpc.TextUnitQueryParams, error) {
|
|
query := r.URL.Query()
|
|
params := backendrpc.TextUnitQueryParams{
|
|
Destination: strings.TrimSpace(query.Get("destination")),
|
|
PathPattern: strings.TrimSpace(query.Get("path_pattern")),
|
|
ArchiveEntry: strings.TrimSpace(query.Get("archive_entry")),
|
|
FieldPath: strings.TrimSpace(query.Get("field_path")),
|
|
Format: strings.TrimSpace(query.Get("format")),
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("offset")); raw != "" {
|
|
offset, err := strconv.ParseInt(raw, 10, 32)
|
|
if err != nil || offset < 0 {
|
|
return backendrpc.TextUnitQueryParams{}, errors.New("offset must be a non-negative integer")
|
|
}
|
|
params.Offset = int(offset)
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("limit")); raw != "" {
|
|
limit, err := strconv.ParseInt(raw, 10, 32)
|
|
if err != nil || limit < 1 || limit > 1000 {
|
|
return backendrpc.TextUnitQueryParams{}, errors.New("limit must be in 1..=1000")
|
|
}
|
|
params.Limit = int(limit)
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("path_id")); raw != "" {
|
|
pathID, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil {
|
|
return backendrpc.TextUnitQueryParams{}, errors.New("path_id must be a signed integer")
|
|
}
|
|
params.PathID = &pathID
|
|
}
|
|
if raw := strings.TrimSpace(query.Get("class_id")); raw != "" {
|
|
classID, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return backendrpc.TextUnitQueryParams{}, errors.New("class_id must be a signed integer")
|
|
}
|
|
params.ClassID = &classID
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
func firstTrimmedQuery(query url.Values, keys ...string) string {
|
|
for _, key := range keys {
|
|
values := query[key]
|
|
if len(values) == 0 {
|
|
continue
|
|
}
|
|
if value := strings.TrimSpace(values[0]); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func adminLogTail(r *http.Request) (int, error) {
|
|
raw := strings.TrimSpace(r.URL.Query().Get("tail"))
|
|
if raw == "" {
|
|
return adminDefaultLogTail, nil
|
|
}
|
|
tail, err := strconv.Atoi(raw)
|
|
if err != nil || tail < 1 || tail > adminMaxLogTail {
|
|
return 0, errors.New("tail must be in 1..=2000")
|
|
}
|
|
return tail, nil
|
|
}
|
|
|
|
func adminTaskIDQuery(r *http.Request) (string, error) {
|
|
taskID := strings.TrimSpace(r.URL.Query().Get("task_id"))
|
|
if taskID == "" {
|
|
return "", errors.New("task_id is required")
|
|
}
|
|
return taskID, nil
|
|
}
|
|
|
|
func (s *Server) handleAdminScheduleControl(w http.ResponseWriter, r *http.Request, action string) {
|
|
backend, ok := s.backend.(ScheduleBackend)
|
|
if !ok || backend == nil {
|
|
writeErrorJSON(w, http.StatusServiceUnavailable, "schedule_backend_unavailable", "Rust bat schedule backend is unavailable")
|
|
return
|
|
}
|
|
|
|
var (
|
|
method string
|
|
result json.RawMessage
|
|
err error
|
|
)
|
|
switch action {
|
|
case "schedule-add", "schedule-update", "schedule-remove":
|
|
var params backendrpc.ScheduleMutationParams
|
|
if !decodeAdminScheduleJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
switch action {
|
|
case "schedule-add":
|
|
method = "schedule.add"
|
|
result, err = backend.ScheduleAdd(r.Context(), params)
|
|
case "schedule-update":
|
|
method = "schedule.update"
|
|
result, err = backend.ScheduleUpdate(r.Context(), params)
|
|
default:
|
|
method = "schedule.remove"
|
|
result, err = backend.ScheduleRemove(r.Context(), params)
|
|
}
|
|
case "schedule-run":
|
|
var params backendrpc.ScheduleRunParams
|
|
if !decodeAdminScheduleJSON(w, r, ¶ms) {
|
|
return
|
|
}
|
|
method = "schedule.run"
|
|
result, err = backend.ScheduleRun(r.Context(), params)
|
|
default:
|
|
writeErrorJSON(w, http.StatusNotFound, "control_not_found", "unknown control action")
|
|
return
|
|
}
|
|
if err != nil {
|
|
s.writeControlBackendError(w, action, err)
|
|
return
|
|
}
|
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
|
Service: "bat-api",
|
|
Action: action,
|
|
RPCMethod: method,
|
|
Status: "accepted",
|
|
Result: result,
|
|
})
|
|
}
|
|
|
|
func (s *Server) requireAdminToken(w http.ResponseWriter, r *http.Request) bool {
|
|
if s.cfg.AuthToken == "" {
|
|
writeErrorJSON(w, http.StatusForbidden, "admin_auth_required", "admin controls require BAT_API_AUTH_TOKEN")
|
|
return false
|
|
}
|
|
if !constantTimeTokenEqual(s.requestToken(r), s.cfg.AuthToken) {
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="bat-api-admin"`)
|
|
writeErrorJSON(w, http.StatusUnauthorized, "unauthorized", "missing or invalid access token")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func decodeAdminScheduleJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
|
return decodeAdminJSON(w, r, target, "invalid_schedule_params", "schedule request")
|
|
}
|
|
|
|
func decodeAdminTranslationJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
|
return decodeAdminJSON(w, r, target, "invalid_translation_params", "translation request")
|
|
}
|
|
|
|
func decodeAdminTaskJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
|
return decodeAdminJSON(w, r, target, "invalid_task_params", "task request")
|
|
}
|
|
|
|
func validateTranslationTaskUpdateParams(params backendrpc.TranslationTaskUpdateParams) error {
|
|
if strings.TrimSpace(params.TaskID) == "" || strings.TrimSpace(params.Status) == "" {
|
|
return errors.New("translation task update requires task_id and status")
|
|
}
|
|
if len(params.TranslationResults) == 0 {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(params.Status) != "completed" {
|
|
return errors.New("translation_results can only be submitted with completed status")
|
|
}
|
|
seen := make(map[string]struct{}, len(params.TranslationResults))
|
|
for _, result := range params.TranslationResults {
|
|
unitID := strings.TrimSpace(result.UnitID)
|
|
if unitID == "" {
|
|
return errors.New("translation_results unit_id is required")
|
|
}
|
|
if _, ok := seen[unitID]; ok {
|
|
return errors.New("translation_results unit_id must be unique")
|
|
}
|
|
seen[unitID] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateTranslationWorkerRunParams(params backendrpc.TranslationWorkerRunParams) error {
|
|
if params.Concurrency != nil && (*params.Concurrency < 1 || *params.Concurrency > 256) {
|
|
return errors.New("translation worker concurrency must be in 1..=256")
|
|
}
|
|
if params.MaxAttempts != nil && *params.MaxAttempts == 0 {
|
|
return errors.New("translation worker max_attempts must be greater than 0")
|
|
}
|
|
if params.LeaseSeconds != nil && *params.LeaseSeconds == 0 {
|
|
return errors.New("translation worker lease_seconds must be greater than 0")
|
|
}
|
|
if params.MaxTasks != nil && *params.MaxTasks == 0 {
|
|
return errors.New("translation worker max_tasks must be greater than 0")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateTranslationMemoryConfirmParams(params backendrpc.TranslationMemoryConfirmParams) error {
|
|
if strings.TrimSpace(params.RecordID) == "" || strings.TrimSpace(params.Reviewer) == "" {
|
|
return errors.New("Translation Memory confirm requires record_id and reviewer")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateLocalizedPublishParams(params backendrpc.LocalizedPublishParams) error {
|
|
hasFile := strings.TrimSpace(params.TranslationFile) != ""
|
|
if hasFile == params.FromWorker {
|
|
return errors.New("localized publish requires exactly one of translation_file or from_worker")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeAdminJSON(w http.ResponseWriter, r *http.Request, target any, errorCode string, subject string) bool {
|
|
if r.Body == nil {
|
|
return true
|
|
}
|
|
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, adminScheduleMaxBodyBytes))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(target); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return true
|
|
}
|
|
writeErrorJSON(w, http.StatusBadRequest, errorCode, subject+" must be a JSON object")
|
|
return false
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
|
writeErrorJSON(w, http.StatusBadRequest, errorCode, subject+" must contain exactly one JSON object")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func decodeAdminControlRequest(w http.ResponseWriter, r *http.Request) (adminControlRequest, bool) {
|
|
var request adminControlRequest
|
|
if r.Body == nil {
|
|
return request, true
|
|
}
|
|
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, adminControlMaxBodyBytes))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&request); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return request, true
|
|
}
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "control request must be a JSON object with an optional force boolean")
|
|
return adminControlRequest{}, false
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "control request must contain exactly one JSON object")
|
|
return adminControlRequest{}, false
|
|
}
|
|
return request, true
|
|
}
|
|
|
|
func (s *Server) writeControlBackendError(w http.ResponseWriter, action string, err error) {
|
|
status := http.StatusBadGateway
|
|
code := "control_backend_failed"
|
|
message := "Rust bat rejected the control request"
|
|
switch {
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
status = http.StatusGatewayTimeout
|
|
code = "control_backend_timeout"
|
|
message = "Rust bat control request timed out"
|
|
case errors.Is(err, context.Canceled):
|
|
status = http.StatusRequestTimeout
|
|
code = "control_request_canceled"
|
|
message = "control request was canceled"
|
|
default:
|
|
var apiErr *backendrpc.APIError
|
|
if errors.As(err, &apiErr) && apiErr.Kind == "not_implemented" {
|
|
status = http.StatusNotImplemented
|
|
code = "control_not_implemented"
|
|
message = "Rust bat does not implement this control action"
|
|
}
|
|
}
|
|
s.logger.Printf("bat-api control action=%s error=%v", action, err)
|
|
writeErrorJSON(w, status, code, message)
|
|
}
|