Files
Alessio d042863a56 feat: Implementazione completa sistema schedulazione con intervalli personalizzati
- Aggiunto supporto schedulazione con intervalli flessibili (secondi/minuti/ore/giorni/settimane/mesi)
- Esteso modello ProfileSchedule con campi IntervalValue e IntervalUnit
- Ottimizzato ScheduledJobService per controlli ogni 30s con esecuzione parallela
- Implementata interfaccia UI completa con anteprima real-time in italiano
- Aggiunta migrazione database AddIntervalSchedulingFields
- Implementati metodi calcolo NextExecutionTime per intervalli
- Aggiunta gestione tracking anti-duplicati e cleanup automatico
- Creata documentazione completa (6 file, 2500+ righe)

Modifiche tecniche:
- ProfileSchedule.cs: Nuovi campi e metodi CalculateNextInterval/GetScheduleDescription
- ScheduledJobService.cs: Ridotto check interval a 30s, aggiunto parallel processing
- ProfileScheduleService.cs: Supporto calcolo intervalli in UpdateNextExecutionTimeAsync
- Scheduling.razor: Aggiunta sezione UI per configurazione intervalli
- Scheduling.razor.cs: Implementato GetIntervalPreview() e gestione stato campi
2025-10-02 01:12:39 +02:00

733 lines
28 KiB
C#

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;
/// <summary>
/// Interfaccia per il servizio di backup e ripristino
/// </summary>
public interface IBackupService
{
/// <summary>
/// Esporta tutti i dati del sistema in un file di backup
/// </summary>
Task<BackupOperationResult> ExportBackupAsync(BackupOptions options);
/// <summary>
/// Importa i dati da un file di backup
/// </summary>
Task<BackupOperationResult> ImportBackupAsync(string backupFilePath, RestoreOptions options);
/// <summary>
/// Importa i dati da contenuto JSON
/// </summary>
Task<BackupOperationResult> ImportBackupFromJsonAsync(string jsonContent, RestoreOptions options);
/// <summary>
/// Valida un file di backup
/// </summary>
Task<BackupOperationResult> ValidateBackupAsync(string backupFilePath);
/// <summary>
/// Ottiene le informazioni su un backup senza importarlo
/// </summary>
Task<SystemBackupData?> GetBackupInfoAsync(string backupFilePath);
}
/// <summary>
/// Servizio per la gestione dei backup e ripristini del sistema
/// </summary>
public class BackupService : IBackupService
{
private readonly CredentialDbContext _context;
private readonly IDataCouplerProfileService _profileService;
private readonly ICredentialService _credentialService;
private readonly IKeyAssociationService _keyAssociationService;
private readonly ILogger<BackupService> _logger;
public BackupService(
CredentialDbContext context,
IDataCouplerProfileService profileService,
ICredentialService credentialService,
IKeyAssociationService keyAssociationService,
ILogger<BackupService> logger)
{
_context = context;
_profileService = profileService;
_credentialService = credentialService;
_keyAssociationService = keyAssociationService;
_logger = logger;
}
/// <summary>
/// Esporta tutti i dati del sistema
/// </summary>
public async Task<BackupOperationResult> 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;
}
/// <summary>
/// Importa dati da file backup
/// </summary>
public async Task<BackupOperationResult> 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);
}
/// <summary>
/// Importa dati da contenuto JSON
/// </summary>
public async Task<BackupOperationResult> 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<SystemBackupData>(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;
}
/// <summary>
/// Valida un file di backup
/// </summary>
public async Task<BackupOperationResult> 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<SystemBackupData>(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;
}
/// <summary>
/// Ottiene informazioni backup senza importare
/// </summary>
public async Task<SystemBackupData?> GetBackupInfoAsync(string backupFilePath)
{
try
{
if (!File.Exists(backupFilePath))
return null;
var json = await File.ReadAllTextAsync(backupFilePath);
return JsonSerializer.Deserialize<SystemBackupData>(json);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore lettura info backup da {FilePath}", backupFilePath);
return null;
}
}
#region Private Export Methods
private async Task<List<DataCouplerProfileBackup>> 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<List<FieldMappingDto>>(p.FieldMappingJson) ?? new List<FieldMappingDto>() :
new List<FieldMappingDto>(),
SourceKeyField = p.SourceKeyField,
UseRecordAssociations = p.UseRecordAssociations,
CreatedBy = p.CreatedBy,
CreatedAt = p.CreatedAt,
LastUsedAt = p.LastUsedAt,
IsActive = p.IsActive
}).ToList();
}
private async Task<List<CredentialBackup>> 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<List<KeyAssociationBackup>> 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<List<ProfileScheduleBackup>> ExportProfileSchedulesAsync(bool onlyActive)
{
// Nota: Assumendo che esista una tabella ProfileSchedules
// Se non esiste, questo metodo restituirà una lista vuota
var schedules = new List<ProfileScheduleBackup>();
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<int> ImportProfilesAsync(List<DataCouplerProfileBackup> 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<int> ImportCredentialsAsync(List<CredentialBackup> 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<int> ImportKeyAssociationsAsync(List<KeyAssociationBackup> 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<int> ImportProfileSchedulesAsync(List<ProfileScheduleBackup> 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
}