feat: infra backup GFS retention + version history
New infra_backup_versions table with GFS pruning (~14 versions per customer). Recovery endpoint supports ?version=ID. New /versions API. Dashboard shows collapsible backup history with app names and disk count. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleNotify(w, r)
|
||||
case r.Method == http.MethodPost && path == "/infra-backup":
|
||||
h.handleInfraBackupPush(w, r)
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(path, "/infra-backup/") && strings.HasSuffix(path, "/versions"):
|
||||
customerID := strings.TrimPrefix(path, "/infra-backup/")
|
||||
customerID = strings.TrimSuffix(customerID, "/versions")
|
||||
h.handleInfraBackupVersions(w, r, customerID)
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(path, "/infra-backup/"):
|
||||
h.handleInfraBackupGet(w, r, strings.TrimPrefix(path, "/infra-backup/"))
|
||||
case r.Method == http.MethodPost && path == "/preferences":
|
||||
@@ -586,6 +590,33 @@ func (h *Handler) handleInfraBackupGet(w http.ResponseWriter, r *http.Request, c
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// handleInfraBackupVersions returns a list of backup versions for a customer.
|
||||
// Auth: Bearer token.
|
||||
func (h *Handler) handleInfraBackupVersions(w http.ResponseWriter, r *http.Request, customerID string) {
|
||||
if !h.checkAuth(r) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if customerID == "" {
|
||||
http.Error(w, "Missing customer_id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
versions, err := h.store.ListInfraBackupVersions(customerID)
|
||||
if err != nil {
|
||||
h.logger.Printf("[ERROR] Failed to list infra backup versions for %s: %v", customerID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if versions == nil {
|
||||
versions = []store.InfraBackupVersion{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(versions)
|
||||
}
|
||||
|
||||
// handleRecovery returns both the generated controller.yaml and the infra backup for disaster recovery.
|
||||
// Auth: X-Retrieval-Password header (same as config retrieval).
|
||||
func (h *Handler) handleRecovery(w http.ResponseWriter, r *http.Request, customerID string) {
|
||||
@@ -631,24 +662,44 @@ func (h *Handler) handleRecovery(w http.ResponseWriter, r *http.Request, custome
|
||||
// Fetch infra backup (optional — may not exist for new customers)
|
||||
var infraBackup json.RawMessage
|
||||
hasInfraBackup := false
|
||||
if data, err := h.store.GetInfraBackup(customerID); err == nil && data != nil {
|
||||
infraBackup = data
|
||||
hasInfraBackup = true
|
||||
|
||||
// Support ?version=ID for selecting a specific backup version
|
||||
if versionStr := r.URL.Query().Get("version"); versionStr != "" {
|
||||
var versionID int64
|
||||
if _, err := fmt.Sscanf(versionStr, "%d", &versionID); err == nil {
|
||||
if data, err := h.store.GetInfraBackupByID(versionID); err == nil && data != nil {
|
||||
infraBackup = data
|
||||
hasInfraBackup = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if data, err := h.store.GetInfraBackup(customerID); err == nil && data != nil {
|
||||
infraBackup = data
|
||||
hasInfraBackup = true
|
||||
}
|
||||
}
|
||||
|
||||
// Include version list for version picker
|
||||
var backupVersions []store.InfraBackupVersion
|
||||
if versions, err := h.store.ListInfraBackupVersions(customerID); err == nil {
|
||||
backupVersions = versions
|
||||
}
|
||||
|
||||
resp := struct {
|
||||
CustomerID string `json:"customer_id"`
|
||||
ConfigYAML string `json:"config_yaml"`
|
||||
InfraBackup json.RawMessage `json:"infra_backup"`
|
||||
HasInfraBackup bool `json:"has_infra_backup"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
ConfigYAML string `json:"config_yaml"`
|
||||
InfraBackup json.RawMessage `json:"infra_backup"`
|
||||
HasInfraBackup bool `json:"has_infra_backup"`
|
||||
BackupVersions []store.InfraBackupVersion `json:"backup_versions,omitempty"`
|
||||
}{
|
||||
CustomerID: customerID,
|
||||
ConfigYAML: configYAML,
|
||||
InfraBackup: infraBackup,
|
||||
HasInfraBackup: hasInfraBackup,
|
||||
BackupVersions: backupVersions,
|
||||
}
|
||||
|
||||
h.logger.Printf("[INFO] Recovery data downloaded for customer %s (has_infra_backup=%v)", customerID, hasInfraBackup)
|
||||
h.logger.Printf("[INFO] Recovery data downloaded for customer %s (has_infra_backup=%v, versions=%d)", customerID, hasInfraBackup, len(backupVersions))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user