package httpapp import ( "encoding/json" "fmt" "log" "net/http" "strings" "gitea.avt.data-center.id/othman.suseno/atlas/internal/backup" "gitea.avt.data-center.id/othman.suseno/atlas/internal/errors" "gitea.avt.data-center.id/othman.suseno/atlas/internal/models" ) // Backup Handlers func (a *App) handleCreateBackup(w http.ResponseWriter, r *http.Request) { var req struct { Description string `json:"description,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { // Description is optional, so we'll continue even if body is empty _ = err } // Collect all configuration data backupData := backup.BackupData{ Users: a.userStore.List(), SMBShares: a.smbStore.List(), NFSExports: a.nfsStore.List(), ISCSITargets: a.iscsiStore.List(), Policies: a.snapshotPolicy.List(), Config: map[string]interface{}{ "database_path": a.cfg.DatabasePath, }, } // Create backup backupID, err := a.backupService.CreateBackup(backupData, req.Description) if err != nil { log.Printf("create backup error: %v", err) writeError(w, errors.ErrInternal("failed to create backup").WithDetails(err.Error())) return } // Get backup metadata metadata, err := a.backupService.GetBackup(backupID) if err != nil { log.Printf("get backup metadata error: %v", err) writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": backupID, "message": "backup created", }) return } writeJSON(w, http.StatusCreated, metadata) } func (a *App) handleListBackups(w http.ResponseWriter, r *http.Request) { backups, err := a.backupService.ListBackups() if err != nil { log.Printf("list backups error: %v", err) writeError(w, errors.ErrInternal("failed to list backups").WithDetails(err.Error())) return } writeJSON(w, http.StatusOK, backups) } func (a *App) handleGetBackup(w http.ResponseWriter, r *http.Request) { backupID := pathParam(r, "/api/v1/backups/") if backupID == "" { writeError(w, errors.ErrBadRequest("backup id required")) return } metadata, err := a.backupService.GetBackup(backupID) if err != nil { log.Printf("get backup error: %v", err) writeError(w, errors.ErrNotFound("backup").WithDetails(backupID)) return } writeJSON(w, http.StatusOK, metadata) } func (a *App) handleRestoreBackup(w http.ResponseWriter, r *http.Request) { // Extract backup ID from path path := r.URL.Path backupID := "" // Handle both /api/v1/backups/{id} and /api/v1/backups/{id}/restore if strings.Contains(path, "/restore") { // Path: /api/v1/backups/{id}/restore prefix := "/api/v1/backups/" suffix := "/restore" if strings.HasPrefix(path, prefix) && strings.HasSuffix(path, suffix) { backupID = path[len(prefix) : len(path)-len(suffix)] } } else { // Path: /api/v1/backups/{id} backupID = pathParam(r, "/api/v1/backups/") } if backupID == "" { writeError(w, errors.ErrBadRequest("backup id required")) return } var req struct { DryRun bool `json:"dry_run,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { // Dry run is optional, default to false req.DryRun = false } // Verify backup first if err := a.backupService.VerifyBackup(backupID); err != nil { log.Printf("verify backup error: %v", err) writeError(w, errors.ErrBadRequest("backup verification failed").WithDetails(err.Error())) return } // Restore backup backupData, err := a.backupService.RestoreBackup(backupID) if err != nil { log.Printf("restore backup error: %v", err) writeError(w, errors.ErrInternal("failed to restore backup").WithDetails(err.Error())) return } if req.DryRun { // Return what would be restored without actually restoring writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "dry run - no changes made", "backup_id": backupID, "backup_data": backupData, }) return } // Restore users (skip default admin user - user-1) // Note: Passwords cannot be restored as they're hashed and not stored in user model // Users will need to reset their passwords after restore for _, user := range backupData.Users { // Skip default admin user if user.ID == "user-1" { log.Printf("skipping default admin user") continue } // Check if user already exists if _, err := a.userStore.GetByID(user.ID); err == nil { log.Printf("user %s already exists, skipping", user.ID) continue } // Create user with temporary password (user must reset password) // Use a secure random password that user must change tempPassword := fmt.Sprintf("restore-%s", user.ID) if _, err := a.userStore.Create(user.Username, user.Email, tempPassword, user.Role); err != nil { log.Printf("restore user error: %v", err) // Continue with other users } else { log.Printf("restored user %s - password reset required", user.Username) } } // Restore SMB shares for _, share := range backupData.SMBShares { // Check if share already exists if _, err := a.smbStore.Get(share.ID); err == nil { log.Printf("SMB share %s already exists, skipping", share.ID) continue } // Create share if _, err := a.smbStore.Create(share.Name, share.Path, share.Dataset, share.Description, share.ReadOnly, share.GuestOK, share.ValidUsers); err != nil { log.Printf("restore SMB share error: %v", err) // Continue with other shares } } // Restore NFS exports for _, export := range backupData.NFSExports { // Check if export already exists if _, err := a.nfsStore.Get(export.ID); err == nil { log.Printf("NFS export %s already exists, skipping", export.ID) continue } // Create export if _, err := a.nfsStore.Create(export.Path, export.Dataset, export.Clients, export.ReadOnly, export.RootSquash); err != nil { log.Printf("restore NFS export error: %v", err) // Continue with other exports } } // Restore iSCSI targets for _, target := range backupData.ISCSITargets { // Check if target already exists if _, err := a.iscsiStore.Get(target.ID); err == nil { log.Printf("iSCSI target %s already exists, skipping", target.ID) continue } // Create target if _, err := a.iscsiStore.Create(target.IQN, target.Initiators); err != nil { log.Printf("restore iSCSI target error: %v", err) // Continue with other targets } // Restore LUNs for _, lun := range target.LUNs { if _, err := a.iscsiStore.AddLUN(target.ID, lun.ZVOL, lun.Size); err != nil { log.Printf("restore iSCSI LUN error: %v", err) // Continue with other LUNs } } } // Restore snapshot policies for _, policy := range backupData.Policies { // Check if policy already exists if existing, _ := a.snapshotPolicy.Get(policy.Dataset); existing != nil { log.Printf("snapshot policy for dataset %s already exists, skipping", policy.Dataset) continue } // Set policy (uses Dataset as key) a.snapshotPolicy.Set(&policy) } // Apply service configurations shares := a.smbStore.List() if err := a.smbService.ApplyConfiguration(shares); err != nil { log.Printf("apply SMB configuration after restore error: %v", err) } exports := a.nfsStore.List() if err := a.nfsService.ApplyConfiguration(exports); err != nil { log.Printf("apply NFS configuration after restore error: %v", err) } targets := a.iscsiStore.List() for _, target := range targets { if err := a.iscsiService.ApplyConfiguration([]models.ISCSITarget{target}); err != nil { log.Printf("apply iSCSI configuration after restore error: %v", err) } } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "backup restored successfully", "backup_id": backupID, }) } func (a *App) handleDeleteBackup(w http.ResponseWriter, r *http.Request) { backupID := pathParam(r, "/api/v1/backups/") if backupID == "" { writeError(w, errors.ErrBadRequest("backup id required")) return } if err := a.backupService.DeleteBackup(backupID); err != nil { log.Printf("delete backup error: %v", err) writeError(w, errors.ErrInternal("failed to delete backup").WithDetails(err.Error())) return } writeJSON(w, http.StatusOK, map[string]string{ "message": "backup deleted", "backup_id": backupID, }) } func (a *App) handleVerifyBackup(w http.ResponseWriter, r *http.Request) { backupID := pathParam(r, "/api/v1/backups/") if backupID == "" { writeError(w, errors.ErrBadRequest("backup id required")) return } if err := a.backupService.VerifyBackup(backupID); err != nil { writeError(w, errors.ErrBadRequest("backup verification failed").WithDetails(err.Error())) return } metadata, err := a.backupService.GetBackup(backupID) if err != nil { writeError(w, errors.ErrNotFound("backup").WithDetails(backupID)) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "backup is valid", "backup_id": backupID, "metadata": metadata, }) }