hako/internal/server/handlers/system.go
2026-01-12 19:33:08 +01:00

286 lines
9.3 KiB
Go

package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"git.nakama.town/fmartingr/hako/internal/archival/archiver"
archivalRules "git.nakama.town/fmartingr/hako/internal/archival/rules"
archivalStore "git.nakama.town/fmartingr/hako/internal/archival/store"
"git.nakama.town/fmartingr/hako/internal/extractors"
"git.nakama.town/fmartingr/hako/internal/model"
"git.nakama.town/fmartingr/hako/internal/server/webcontext"
)
// SystemHandler handles system-related requests
type SystemHandler struct {
getVersion func(ctx context.Context) (string, error)
extractorMgr *extractors.Manager
archiverMgr *archiver.Manager
archiverConfigStore *archivalStore.ArchiverConfigStore
settingsStore *archivalStore.SettingsStore
}
// NewSystemHandler creates a new SystemHandler
func NewSystemHandler(getVersion func(ctx context.Context) (string, error), extractorMgr *extractors.Manager, archiverMgr *archiver.Manager, archiverConfigStore *archivalStore.ArchiverConfigStore, settingsStore *archivalStore.SettingsStore) *SystemHandler {
return &SystemHandler{
getVersion: getVersion,
extractorMgr: extractorMgr,
archiverMgr: archiverMgr,
archiverConfigStore: archiverConfigStore,
settingsStore: settingsStore,
}
}
// SystemResponse represents a system response
type SystemResponse struct {
Version string `json:"version,omitempty"`
Commit string `json:"commit,omitempty"`
Date string `json:"date,omitempty"`
}
// HandleSystem handles GET /system
func (h *SystemHandler) HandleSystem(w http.ResponseWriter, r *http.Request) {
// Create webcontext for this request
c := webcontext.NewWebContext(w, r)
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
response := SystemResponse{
Version: model.BuildVersion,
Commit: model.BuildCommit,
Date: model.BuildDate,
}
_ = json.NewEncoder(c.ResponseWriter()).Encode(response)
}
// HandleExtractors handles GET /system/extractors
func (h *SystemHandler) HandleExtractors(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Get status of all extractors
statuses := h.extractorMgr.GetStatus()
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
_ = json.NewEncoder(c.ResponseWriter()).Encode(statuses)
}
// HandleArchivers handles GET /system/archivers
func (h *SystemHandler) HandleArchivers(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Get status of all archivers
statuses := h.archiverMgr.GetStatus(c.Context(), h.archiverConfigStore)
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
_ = json.NewEncoder(c.ResponseWriter()).Encode(statuses)
}
// HandleGetArchiverConfig handles GET /system/archivers/{key}/config
func (h *SystemHandler) HandleGetArchiverConfig(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Extract archiver key from path
key := r.PathValue("key")
if key == "" {
http.Error(w, "Archiver key is required", http.StatusBadRequest)
return
}
// Get archiver to access default config
arch, ok := h.archiverMgr.GetArchiver(key)
if !ok {
http.Error(w, fmt.Sprintf("Archiver not found: %s", key), http.StatusNotFound)
return
}
// Try to get stored config
storedConfig, err := h.archiverConfigStore.Get(c.Context(), key)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get archiver config: %v", err), http.StatusInternalServerError)
return
}
var config any
if storedConfig != nil && storedConfig.ConfigJSON != "" {
// Parse stored config JSON
if err := json.Unmarshal([]byte(storedConfig.ConfigJSON), &config); err != nil {
// If parsing fails, fall back to default
config = arch.GetDefaultConfig()
}
} else {
// No stored config, use default
config = arch.GetDefaultConfig()
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
_ = json.NewEncoder(c.ResponseWriter()).Encode(config)
}
// HandleSaveArchiverConfig handles PUT /system/archivers/{key}/config
func (h *SystemHandler) HandleSaveArchiverConfig(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Extract archiver key from path
key := r.PathValue("key")
if key == "" {
http.Error(w, "Archiver key is required", http.StatusBadRequest)
return
}
// Verify archiver exists
arch, ok := h.archiverMgr.GetArchiver(key)
if !ok {
http.Error(w, fmt.Sprintf("Archiver not found: %s", key), http.StatusNotFound)
return
}
// Parse request body
var config map[string]any
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
return
}
// Validate config by trying to apply it
if err := arch.ApplyConfig(config); err != nil {
http.Error(w, fmt.Sprintf("Invalid config: %v", err), http.StatusBadRequest)
return
}
// Marshal config back to JSON
configJSON, err := json.Marshal(config)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to marshal config: %v", err), http.StatusInternalServerError)
return
}
// Save to database
archiverConfig := &model.ArchiverConfig{
ArchiverKey: key,
ConfigJSON: string(configJSON),
UpdatedAt: time.Now(),
}
if err := h.archiverConfigStore.Upsert(c.Context(), archiverConfig); err != nil {
http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
return
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
_ = json.NewEncoder(c.ResponseWriter()).Encode(map[string]string{"message": "Config saved successfully"})
}
// HandleDeleteArchiverConfig handles DELETE /system/archivers/{key}/config
func (h *SystemHandler) HandleDeleteArchiverConfig(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Extract archiver key from path
key := r.PathValue("key")
if key == "" {
http.Error(w, "Archiver key is required", http.StatusBadRequest)
return
}
// Verify archiver exists
_, ok := h.archiverMgr.GetArchiver(key)
if !ok {
http.Error(w, fmt.Sprintf("Archiver not found: %s", key), http.StatusNotFound)
return
}
// Delete from database
if err := h.archiverConfigStore.Delete(c.Context(), key); err != nil {
http.Error(w, fmt.Sprintf("Failed to delete config: %v", err), http.StatusInternalServerError)
return
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
_ = json.NewEncoder(c.ResponseWriter()).Encode(map[string]string{"message": "Config reset to default successfully"})
}
// HandleGetRulesConfig handles GET /system/rules/config
func (h *SystemHandler) HandleGetRulesConfig(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Get rules config from settings store
rulesConfig, err := h.settingsStore.GetRulesConfig(c.Context())
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get rules config: %v", err), http.StatusInternalServerError)
return
}
// Convert to JSON format for frontend
// We need to manually construct JSON since Rule is an interface
rulesJSON := make([]json.RawMessage, len(rulesConfig.Rules))
for i, rule := range rulesConfig.Rules {
ruleBytes, err := json.Marshal(rule)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to marshal rule %d: %v", i, err), http.StatusInternalServerError)
return
}
rulesJSON[i] = ruleBytes
}
response := map[string]interface{}{
"rules": rulesJSON,
"default_extractors": rulesConfig.DefaultArchivers, // JSON key kept for backward compatibility
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
_ = json.NewEncoder(c.ResponseWriter()).Encode(response)
}
// HandleSaveRulesConfig handles PUT /system/rules/config
func (h *SystemHandler) HandleSaveRulesConfig(w http.ResponseWriter, r *http.Request) {
c := webcontext.NewWebContext(w, r)
// Read request body
var requestBody map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
return
}
// Convert to JSON bytes for unmarshaling
requestBytes, err := json.Marshal(requestBody)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to marshal request: %v", err), http.StatusBadRequest)
return
}
// Unmarshal and validate structure
rulesConfig, err := archivalRules.UnmarshalRuleConfig(requestBytes)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid rules config structure: %v", err), http.StatusBadRequest)
return
}
// Validate each rule
for i, rule := range rulesConfig.Rules {
if err := rule.IsValid(); err != nil {
http.Error(w, fmt.Sprintf("Rule %d is invalid: %v", i, err), http.StatusBadRequest)
return
}
}
// Save to database
if err := h.settingsStore.SetRulesConfig(c.Context(), rulesConfig); err != nil {
http.Error(w, fmt.Sprintf("Failed to save rules config: %v", err), http.StatusInternalServerError)
return
}
c.ResponseWriter().Header().Set("Content-Type", "application/json")
c.ResponseWriter().WriteHeader(http.StatusOK)
_ = json.NewEncoder(c.ResponseWriter()).Encode(map[string]string{"message": "Rules config saved successfully"})
}