using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using CredentialManager.Data; using CredentialManager.Models; using CredentialManager.Services; using Data_Coupler.Models; namespace Data_Coupler.Services; /// /// Interfaccia per il servizio di backup e ripristino /// public interface IBackupService { /// /// Esporta tutti i dati del sistema in un file di backup /// Task ExportBackupAsync(BackupOptions options); /// /// Importa i dati da un file di backup /// Task ImportBackupAsync(string backupFilePath, RestoreOptions options); /// /// Importa i dati da contenuto JSON /// Task ImportBackupFromJsonAsync(string jsonContent, RestoreOptions options); /// /// Valida un file di backup /// Task ValidateBackupAsync(string backupFilePath); /// /// Ottiene le informazioni su un backup senza importarlo /// Task GetBackupInfoAsync(string backupFilePath); } /// /// Servizio per la gestione dei backup e ripristini del sistema /// public class BackupService : IBackupService { private readonly CredentialDbContext _context; private readonly IDataCouplerProfileService _profileService; private readonly ICredentialService _credentialService; private readonly IKeyAssociationService _keyAssociationService; private readonly ILogger _logger; public BackupService( CredentialDbContext context, IDataCouplerProfileService profileService, ICredentialService credentialService, IKeyAssociationService keyAssociationService, ILogger logger) { _context = context; _profileService = profileService; _credentialService = credentialService; _keyAssociationService = keyAssociationService; _logger = logger; } /// /// Esporta tutti i dati del sistema /// public async Task ExportBackupAsync(BackupOptions options) { var result = new BackupOperationResult(); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); try { _logger.LogInformation("Avvio backup con opzioni: {@Options}", options); var backupData = new SystemBackupData { Metadata = new BackupMetadata { CreatedAt = DateTime.UtcNow, CreatedBy = options.CreatedBy, Description = options.Description, ApplicationVersion = "1.0.0" } }; // Export Profiles if (options.IncludeProfiles) { _logger.LogDebug("Esportazione profili in corso..."); backupData.Profiles = await ExportProfilesAsync(options.IncludeOnlyActiveRecords); result.ProcessedCounts.Profiles = backupData.Profiles.Count; _logger.LogDebug("Esportati {Count} profili", backupData.Profiles.Count); } // Export Credentials (senza dati sensibili) if (options.IncludeCredentials) { _logger.LogDebug("Esportazione credenziali in corso..."); backupData.Credentials = await ExportCredentialsAsync(options.IncludeOnlyActiveRecords); result.ProcessedCounts.Credentials = backupData.Credentials.Count; result.Warnings.Add("Le credenziali esportate non includono password, API keys o token per motivi di sicurezza"); _logger.LogDebug("Esportate {Count} credenziali", backupData.Credentials.Count); } // Export Key Associations if (options.IncludeKeyAssociations) { _logger.LogDebug("Esportazione associazioni chiavi in corso..."); backupData.KeyAssociations = await ExportKeyAssociationsAsync(options.IncludeOnlyActiveRecords); result.ProcessedCounts.KeyAssociations = backupData.KeyAssociations.Count; _logger.LogDebug("Esportate {Count} associazioni", backupData.KeyAssociations.Count); } // Export Profile Schedules if (options.IncludeProfileSchedules) { _logger.LogDebug("Esportazione schedule profili in corso..."); backupData.ProfileSchedules = await ExportProfileSchedulesAsync(options.IncludeOnlyActiveRecords); result.ProcessedCounts.ProfileSchedules = backupData.ProfileSchedules.Count; _logger.LogDebug("Esportate {Count} schedule", backupData.ProfileSchedules.Count); } // Aggiorna metadata con conteggi backupData.Metadata.RecordCounts = result.ProcessedCounts; // Serializza e salva var jsonOptions = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var json = JsonSerializer.Serialize(backupData, jsonOptions); var fileName = $"data_coupler_backup_{DateTime.Now:yyyyMMdd_HHmmss}.json"; var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); var backupFolder = Path.Combine(documentsPath, "DataCoupler", "Backups"); Directory.CreateDirectory(backupFolder); var filePath = Path.Combine(backupFolder, fileName); await File.WriteAllTextAsync(filePath, json); result.FilePath = filePath; result.Success = true; result.Message = $"Backup completato con successo. File salvato: {filePath}"; _logger.LogInformation("Backup completato: {FilePath}, Record totali: {Total}", filePath, result.ProcessedCounts.Profiles + result.ProcessedCounts.Credentials + result.ProcessedCounts.KeyAssociations + result.ProcessedCounts.ProfileSchedules); } catch (Exception ex) { result.Success = false; result.Message = $"Errore durante il backup: {ex.Message}"; result.Errors.Add(ex.ToString()); _logger.LogError(ex, "Errore durante l'esportazione del backup"); } finally { stopwatch.Stop(); result.Duration = stopwatch.Elapsed; } return result; } /// /// Importa dati da file backup /// public async Task ImportBackupAsync(string backupFilePath, RestoreOptions options) { if (!File.Exists(backupFilePath)) { return new BackupOperationResult { Success = false, Message = "File di backup non trovato" }; } var json = await File.ReadAllTextAsync(backupFilePath); return await ImportBackupFromJsonAsync(json, options); } /// /// Importa dati da contenuto JSON /// public async Task ImportBackupFromJsonAsync(string jsonContent, RestoreOptions options) { var result = new BackupOperationResult(); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); try { _logger.LogInformation("Avvio import backup con opzioni: {@Options}", options); // Parse JSON var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var backupData = JsonSerializer.Deserialize(jsonContent, jsonOptions); if (backupData == null) { throw new InvalidOperationException("Impossibile deserializzare i dati del backup"); } _logger.LogInformation("Backup da importare: {Metadata}", backupData.Metadata); // Crea backup automatico prima del restore se richiesto if (options.CreateBackupBeforeRestore) { _logger.LogInformation("Creazione backup di sicurezza pre-restore..."); var preRestoreBackup = await ExportBackupAsync(new BackupOptions { Description = "Backup automatico pre-restore", CreatedBy = options.ImportedBy }); if (preRestoreBackup.Success) { result.Warnings.Add($"Backup di sicurezza creato: {preRestoreBackup.FilePath}"); } } using var transaction = await _context.Database.BeginTransactionAsync(); try { // Import Profiles if (options.RestoreProfiles && backupData.Profiles.Any()) { _logger.LogDebug("Importazione profili in corso..."); var importedProfiles = await ImportProfilesAsync(backupData.Profiles, options); result.ProcessedCounts.Profiles = importedProfiles; _logger.LogDebug("Importati {Count} profili", importedProfiles); } // Import Credentials (solo metadati, nessun dato sensibile) if (options.RestoreCredentials && backupData.Credentials.Any()) { _logger.LogDebug("Importazione credenziali in corso..."); var importedCredentials = await ImportCredentialsAsync(backupData.Credentials, options); result.ProcessedCounts.Credentials = importedCredentials; result.Warnings.Add("Le credenziali importate richiedono configurazione manuale di password/API keys"); _logger.LogDebug("Importate {Count} credenziali", importedCredentials); } // Import Key Associations if (options.RestoreKeyAssociations && backupData.KeyAssociations.Any()) { _logger.LogDebug("Importazione associazioni chiavi in corso..."); var importedAssociations = await ImportKeyAssociationsAsync(backupData.KeyAssociations, options); result.ProcessedCounts.KeyAssociations = importedAssociations; _logger.LogDebug("Importate {Count} associazioni", importedAssociations); } // Import Profile Schedules if (options.RestoreProfileSchedules && backupData.ProfileSchedules.Any()) { _logger.LogDebug("Importazione schedule profili in corso..."); var importedSchedules = await ImportProfileSchedulesAsync(backupData.ProfileSchedules, options); result.ProcessedCounts.ProfileSchedules = importedSchedules; _logger.LogDebug("Importate {Count} schedule", importedSchedules); } await transaction.CommitAsync(); result.Success = true; result.Message = "Import completato con successo"; _logger.LogInformation("Import completato con successo. Record totali: {Total}", result.ProcessedCounts.Profiles + result.ProcessedCounts.Credentials + result.ProcessedCounts.KeyAssociations + result.ProcessedCounts.ProfileSchedules); } catch { await transaction.RollbackAsync(); throw; } } catch (Exception ex) { result.Success = false; result.Message = $"Errore durante l'import: {ex.Message}"; result.Errors.Add(ex.ToString()); _logger.LogError(ex, "Errore durante l'importazione del backup"); } finally { stopwatch.Stop(); result.Duration = stopwatch.Elapsed; } return result; } /// /// Valida un file di backup /// public async Task ValidateBackupAsync(string backupFilePath) { var result = new BackupOperationResult(); try { if (!File.Exists(backupFilePath)) { result.Errors.Add("File non trovato"); return result; } var json = await File.ReadAllTextAsync(backupFilePath); var backupData = JsonSerializer.Deserialize(json); if (backupData == null) { result.Errors.Add("Formato backup non valido"); return result; } // Validazioni if (backupData.Metadata == null) { result.Errors.Add("Metadata mancanti"); } if (string.IsNullOrEmpty(backupData.Metadata?.Version)) { result.Warnings.Add("Versione backup non specificata"); } result.ProcessedCounts.Profiles = backupData.Profiles?.Count ?? 0; result.ProcessedCounts.Credentials = backupData.Credentials?.Count ?? 0; result.ProcessedCounts.KeyAssociations = backupData.KeyAssociations?.Count ?? 0; result.ProcessedCounts.ProfileSchedules = backupData.ProfileSchedules?.Count ?? 0; result.Success = result.Errors.Count == 0; result.Message = result.Success ? "Backup valido" : "Backup contiene errori"; } catch (Exception ex) { result.Success = false; result.Message = $"Errore validazione: {ex.Message}"; result.Errors.Add(ex.ToString()); } return result; } /// /// Ottiene informazioni backup senza importare /// public async Task GetBackupInfoAsync(string backupFilePath) { try { if (!File.Exists(backupFilePath)) return null; var json = await File.ReadAllTextAsync(backupFilePath); return JsonSerializer.Deserialize(json); } catch (Exception ex) { _logger.LogWarning(ex, "Errore lettura info backup da {FilePath}", backupFilePath); return null; } } #region Private Export Methods private async Task> ExportProfilesAsync(bool onlyActive) { var query = _context.DataCouplerProfiles .Include(p => p.SourceCredential) .Include(p => p.DestinationCredential) .AsQueryable(); if (onlyActive) query = query.Where(p => p.IsActive); var profiles = await query.ToListAsync(); return profiles.Select(p => new DataCouplerProfileBackup { Id = p.Id, Name = p.Name, Description = p.Description, SourceType = p.SourceType, SourceCredentialName = p.SourceCredential?.Name, SourceDatabaseName = p.SourceDatabaseName, SourceSchema = p.SourceSchema, SourceTable = p.SourceTable, SourceCustomQuery = p.SourceCustomQuery, SourceFilePath = p.SourceFilePath, DestinationType = p.DestinationType, DestinationCredentialName = p.DestinationCredential?.Name, DestinationSchema = p.DestinationSchema, DestinationTable = p.DestinationTable, DestinationEndpoint = p.DestinationEndpoint, FieldMappings = !string.IsNullOrEmpty(p.FieldMappingJson) ? System.Text.Json.JsonSerializer.Deserialize>(p.FieldMappingJson) ?? new List() : new List(), SourceKeyField = p.SourceKeyField, UseRecordAssociations = p.UseRecordAssociations, CreatedBy = p.CreatedBy, CreatedAt = p.CreatedAt, LastUsedAt = p.LastUsedAt, IsActive = p.IsActive }).ToList(); } private async Task> ExportCredentialsAsync(bool onlyActive) { var query = _context.Credentials.AsQueryable(); if (onlyActive) query = query.Where(c => c.IsActive); var credentials = await query.ToListAsync(); return credentials.Select(c => new CredentialBackup { Id = c.Id, Name = c.Name, Type = c.Type, DatabaseType = c.DatabaseType, Host = c.Host, Port = c.Port, DatabaseName = c.DatabaseName, Username = c.Username, // Password, API Keys e Token NON inclusi per sicurezza CommandTimeout = c.CommandTimeout, TimeoutSeconds = c.TimeoutSeconds, IgnoreSslErrors = c.IgnoreSslErrors, RestServiceType = c.RestServiceType, Headers = c.Headers, AdditionalParameters = c.AdditionalParameters, CreatedAt = c.CreatedAt, UpdatedAt = c.UpdatedAt, CreatedBy = c.CreatedBy, IsActive = c.IsActive }).ToList(); } private async Task> ExportKeyAssociationsAsync(bool onlyActive) { var query = _context.KeyAssociations.AsQueryable(); if (onlyActive) query = query.Where(ka => ka.IsActive); var associations = await query.ToListAsync(); return associations.Select(ka => new KeyAssociationBackup { Id = ka.Id, KeyValue = ka.KeyValue, SourceKeyField = ka.SourceKeyField, DestinationKeyField = ka.DestinationKeyField, DestinationEntity = ka.DestinationEntity, DestinationId = ka.DestinationId, RestCredentialName = ka.RestCredentialName, CreatedAt = ka.CreatedAt, UpdatedAt = ka.UpdatedAt, LastVerifiedAt = ka.LastVerifiedAt, IsActive = ka.IsActive, DataHash = ka.Data_Hash, SourcesInfo = ka.SourcesInfo, AdditionalInfo = ka.AdditionalInfo }).ToList(); } private Task> ExportProfileSchedulesAsync(bool onlyActive) { // Nota: Assumendo che esista una tabella ProfileSchedules // Se non esiste, questo metodo restituirà una lista vuota var schedules = new List(); try { // TODO: Implementare quando la tabella ProfileSchedules sarà disponibile // var query = _context.ProfileSchedules.AsQueryable(); // if (onlyActive) // query = query.Where(ps => ps.IsActive); // var profileSchedules = await query.ToListAsync(); // schedules = profileSchedules.Select(ps => new ProfileScheduleBackup { ... }).ToList(); } catch (Exception ex) { _logger.LogWarning(ex, "Tabella ProfileSchedules non disponibile, saltando export"); } return Task.FromResult(schedules); } #endregion #region Private Import Methods private async Task ImportProfilesAsync(List profiles, RestoreOptions options) { int imported = 0; foreach (var profileBackup in profiles) { try { var existing = await _context.DataCouplerProfiles .FirstOrDefaultAsync(p => p.Name == profileBackup.Name); if (existing != null && !options.OverwriteExisting) { _logger.LogDebug("Profilo {Name} già esistente, saltando", profileBackup.Name); continue; } var profile = new DataCouplerProfile { Name = profileBackup.Name, Description = profileBackup.Description, SourceType = profileBackup.SourceType, SourceDatabaseName = profileBackup.SourceDatabaseName, SourceSchema = profileBackup.SourceSchema, SourceTable = profileBackup.SourceTable, SourceCustomQuery = profileBackup.SourceCustomQuery, SourceFilePath = profileBackup.SourceFilePath, DestinationType = profileBackup.DestinationType, DestinationSchema = profileBackup.DestinationSchema, DestinationTable = profileBackup.DestinationTable, DestinationEndpoint = profileBackup.DestinationEndpoint, FieldMappingJson = profileBackup.FieldMappings != null ? System.Text.Json.JsonSerializer.Serialize(profileBackup.FieldMappings) : string.Empty, SourceKeyField = profileBackup.SourceKeyField, UseRecordAssociations = profileBackup.UseRecordAssociations, CreatedBy = options.ImportedBy ?? profileBackup.CreatedBy, CreatedAt = DateTime.UtcNow, IsActive = profileBackup.IsActive }; // Risolvi credential IDs per nome se esistenti if (!string.IsNullOrEmpty(profileBackup.SourceCredentialName)) { var sourceCred = await _context.Credentials .FirstOrDefaultAsync(c => c.Name == profileBackup.SourceCredentialName); profile.SourceCredentialId = sourceCred?.Id; } if (!string.IsNullOrEmpty(profileBackup.DestinationCredentialName)) { var destCred = await _context.Credentials .FirstOrDefaultAsync(c => c.Name == profileBackup.DestinationCredentialName); profile.DestinationCredentialId = destCred?.Id; } if (existing != null) { // Update existing _context.Entry(existing).CurrentValues.SetValues(profile); existing.Id = profileBackup.Id; // Mantieni ID originale se sovrascriviamo } else { // Add new _context.DataCouplerProfiles.Add(profile); } imported++; } catch (Exception ex) { _logger.LogWarning(ex, "Errore importazione profilo {Name}", profileBackup.Name); } } await _context.SaveChangesAsync(); return imported; } private async Task ImportCredentialsAsync(List credentials, RestoreOptions options) { int imported = 0; foreach (var credBackup in credentials) { try { var existing = await _context.Credentials .FirstOrDefaultAsync(c => c.Name == credBackup.Name); if (existing != null && !options.OverwriteExisting) { _logger.LogDebug("Credenziale {Name} già esistente, saltando", credBackup.Name); continue; } var credential = new CredentialEntity { Name = credBackup.Name, Type = credBackup.Type, DatabaseType = credBackup.DatabaseType, Host = credBackup.Host, Port = credBackup.Port, DatabaseName = credBackup.DatabaseName, Username = credBackup.Username, // Password, API Keys e Token dovranno essere riconfigurati manualmente CommandTimeout = credBackup.CommandTimeout, TimeoutSeconds = credBackup.TimeoutSeconds, IgnoreSslErrors = credBackup.IgnoreSslErrors, RestServiceType = credBackup.RestServiceType, Headers = credBackup.Headers, AdditionalParameters = credBackup.AdditionalParameters, CreatedAt = DateTime.UtcNow, CreatedBy = options.ImportedBy ?? credBackup.CreatedBy, IsActive = credBackup.IsActive }; if (existing != null) { // Update existing (preserva password esistenti) var oldPassword = existing.EncryptedPassword; var oldApiKey = existing.EncryptedApiKey; var oldAuthToken = existing.EncryptedAuthToken; _context.Entry(existing).CurrentValues.SetValues(credential); existing.Id = credBackup.Id; existing.EncryptedPassword = oldPassword; existing.EncryptedApiKey = oldApiKey; existing.EncryptedAuthToken = oldAuthToken; } else { // Add new _context.Credentials.Add(credential); } imported++; } catch (Exception ex) { _logger.LogWarning(ex, "Errore importazione credenziale {Name}", credBackup.Name); } } await _context.SaveChangesAsync(); return imported; } private async Task ImportKeyAssociationsAsync(List associations, RestoreOptions options) { int imported = 0; foreach (var assocBackup in associations) { try { var existing = await _context.KeyAssociations .FirstOrDefaultAsync(ka => ka.KeyValue == assocBackup.KeyValue && ka.DestinationEntity == assocBackup.DestinationEntity && ka.RestCredentialName == assocBackup.RestCredentialName); if (existing != null && !options.OverwriteExisting) { _logger.LogDebug("Associazione {Key}-{Entity} già esistente, saltando", assocBackup.KeyValue, assocBackup.DestinationEntity); continue; } var association = new KeyAssociation { KeyValue = assocBackup.KeyValue, SourceKeyField = assocBackup.SourceKeyField, DestinationKeyField = assocBackup.DestinationKeyField, DestinationEntity = assocBackup.DestinationEntity, DestinationId = assocBackup.DestinationId, RestCredentialName = assocBackup.RestCredentialName, CreatedAt = DateTime.UtcNow, IsActive = assocBackup.IsActive, Data_Hash = assocBackup.DataHash, SourcesInfo = assocBackup.SourcesInfo, AdditionalInfo = assocBackup.AdditionalInfo }; if (existing != null) { // Update existing _context.Entry(existing).CurrentValues.SetValues(association); existing.Id = assocBackup.Id; } else { // Add new _context.KeyAssociations.Add(association); } imported++; } catch (Exception ex) { _logger.LogWarning(ex, "Errore importazione associazione {Key}-{Entity}", assocBackup.KeyValue, assocBackup.DestinationEntity); } } await _context.SaveChangesAsync(); return imported; } private Task ImportProfileSchedulesAsync(List schedules, RestoreOptions options) { int imported = 0; try { // TODO: Implementare quando la tabella ProfileSchedules sarà disponibile _logger.LogInformation("Import ProfileSchedules saltato - tabella non ancora implementata"); } catch (Exception ex) { _logger.LogWarning(ex, "Errore importazione schedule profili"); } return Task.FromResult(imported); } #endregion }