a5f8943c72
- Aggiunta validazione percorsi file prima del salvataggio profili - Implementati metodi di lettura file CSV e Excel per schedulazioni - Supporto doppia modalità: caricamento browser (preview) e percorso manuale (schedulazione) - Gestione completa deletion sync anche per file CSV/Excel - Rilevamento automatico separatori CSV (virgola, punto e virgola, tab, pipe) - Supporto formati Excel legacy (.xls) e moderni (.xlsx) - Abilitati profili file nella UI di schedulazione - Logging dettagliato per troubleshooting - Documentazione completa in CSV_SCHEDULING_IMPLEMENTATION.md - Aggiornati README.md e copilot-instructions.md con nuove feature - Rimosso testo 'TEST' dalla pagina di login
201 lines
8.7 KiB
C#
201 lines
8.7 KiB
C#
using CredentialManager.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Data_Coupler.Services;
|
|
|
|
/// <summary>
|
|
/// Servizio per l'esecuzione automatica dei trasferimenti dati basati sui profili
|
|
/// </summary>
|
|
public interface IDataTransferService
|
|
{
|
|
Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null, bool enableDeletionSync = false);
|
|
}
|
|
|
|
public class DataTransferResult
|
|
{
|
|
public bool IsSuccess { get; set; }
|
|
public int RecordsProcessed { get; set; }
|
|
public string? ErrorMessage { get; set; }
|
|
public DateTime StartTime { get; set; }
|
|
public DateTime EndTime { get; set; }
|
|
public List<string> ErrorDetails { get; set; } = new();
|
|
public Dictionary<string, object> AdditionalInfo { get; set; } = new();
|
|
|
|
public TimeSpan Duration => EndTime - StartTime;
|
|
}
|
|
|
|
public class DataTransferService : IDataTransferService
|
|
{
|
|
private readonly IScheduledProfileExecutionService _scheduledExecutionService;
|
|
private readonly ILogger<DataTransferService> _logger;
|
|
|
|
public DataTransferService(
|
|
IScheduledProfileExecutionService scheduledExecutionService,
|
|
ILogger<DataTransferService> logger)
|
|
{
|
|
_scheduledExecutionService = scheduledExecutionService ?? throw new ArgumentNullException(nameof(scheduledExecutionService));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public async Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null, bool enableDeletionSync = false)
|
|
{
|
|
var result = new DataTransferResult
|
|
{
|
|
StartTime = DateTime.Now // Usa l'ora locale per coerenza con le schedulazioni
|
|
};
|
|
|
|
try
|
|
{
|
|
_logger.LogInformation("Iniziando esecuzione profilo {ProfileName} (ID: {ProfileId})",
|
|
profile.Name, profile.Id);
|
|
|
|
// Validazione del profilo
|
|
var validationResult = ValidateProfile(profile);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
result.IsSuccess = false;
|
|
result.ErrorMessage = validationResult.ErrorMessage;
|
|
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
|
|
return result;
|
|
}
|
|
|
|
// Applica override del database se specificati
|
|
var profileToExecute = await ApplyDatabaseOverrides(profile, sourceDatabaseOverride, destinationDatabaseOverride);
|
|
|
|
// Utilizza il servizio esistente per l'esecuzione
|
|
var executionResult = await _scheduledExecutionService.ExecuteProfileAsync(profileToExecute.Id, enableDeletionSync);
|
|
|
|
result.IsSuccess = executionResult.Success;
|
|
result.RecordsProcessed = executionResult.RecordsProcessed;
|
|
result.ErrorMessage = executionResult.Success ? null : executionResult.Message;
|
|
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
|
|
|
|
if (executionResult.Success)
|
|
{
|
|
result.AdditionalInfo["ExecutionDuration"] = executionResult.Duration.ToString();
|
|
_logger.LogInformation("Profilo {ProfileName} eseguito con successo. " +
|
|
"Record processati: {RecordsProcessed}, Durata: {Duration}ms",
|
|
profile.Name, result.RecordsProcessed, result.Duration.TotalMilliseconds);
|
|
}
|
|
else
|
|
{
|
|
result.ErrorDetails.Add(executionResult.Message);
|
|
_logger.LogError("Errore nell'esecuzione del profilo {ProfileName}: {ErrorMessage}",
|
|
profile.Name, executionResult.Message);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Errore durante l'esecuzione del profilo {ProfileName} (ID: {ProfileId})",
|
|
profile.Name, profile.Id);
|
|
|
|
result.IsSuccess = false;
|
|
result.ErrorMessage = ex.Message;
|
|
result.ErrorDetails.Add($"Exception: {ex.GetType().Name} - {ex.Message}");
|
|
if (ex.InnerException != null)
|
|
{
|
|
result.ErrorDetails.Add($"Inner Exception: {ex.InnerException.Message}");
|
|
}
|
|
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private async Task<DataCouplerProfile> ApplyDatabaseOverrides(DataCouplerProfile originalProfile,
|
|
string? sourceDatabaseOverride, string? destinationDatabaseOverride)
|
|
{
|
|
// Se non ci sono override, restituisce il profilo originale
|
|
if (string.IsNullOrEmpty(sourceDatabaseOverride) && string.IsNullOrEmpty(destinationDatabaseOverride))
|
|
{
|
|
return originalProfile;
|
|
}
|
|
|
|
// Crea una copia del profilo con gli override applicati
|
|
// In un'implementazione reale, potresti voler creare una copia più sofisticata
|
|
// o utilizzare un metodo di clonazione appropriato
|
|
var profileCopy = new DataCouplerProfile
|
|
{
|
|
Id = originalProfile.Id,
|
|
Name = originalProfile.Name,
|
|
Description = originalProfile.Description,
|
|
SourceType = originalProfile.SourceType,
|
|
SourceCredentialId = originalProfile.SourceCredentialId,
|
|
SourceDatabaseName = originalProfile.SourceDatabaseName,
|
|
SourceTable = originalProfile.SourceTable,
|
|
SourceSchema = originalProfile.SourceSchema,
|
|
SourceCustomQuery = originalProfile.SourceCustomQuery,
|
|
SourceFilePath = originalProfile.SourceFilePath,
|
|
DestinationType = originalProfile.DestinationType,
|
|
DestinationCredentialId = originalProfile.DestinationCredentialId,
|
|
DestinationTable = originalProfile.DestinationTable,
|
|
DestinationSchema = originalProfile.DestinationSchema,
|
|
DestinationEndpoint = originalProfile.DestinationEndpoint,
|
|
FieldMappingJson = originalProfile.FieldMappingJson,
|
|
SourceKeyField = originalProfile.SourceKeyField,
|
|
UseRecordAssociations = originalProfile.UseRecordAssociations,
|
|
IsActive = originalProfile.IsActive,
|
|
CreatedAt = originalProfile.CreatedAt,
|
|
LastUsedAt = originalProfile.LastUsedAt,
|
|
CreatedBy = originalProfile.CreatedBy
|
|
};
|
|
|
|
// TODO: Implementare l'applicazione degli override del database
|
|
// Questo richiederebbe di modificare temporaneamente la stringa di connessione
|
|
// delle credenziali per puntare al database specificato
|
|
|
|
_logger.LogInformation("Applicazione override database - Source: {SourceDB}, Destination: {DestDB}",
|
|
sourceDatabaseOverride ?? "none", destinationDatabaseOverride ?? "none");
|
|
|
|
return await Task.FromResult(profileCopy);
|
|
}
|
|
|
|
private (bool IsValid, string? ErrorMessage) ValidateProfile(DataCouplerProfile profile)
|
|
{
|
|
if (profile == null)
|
|
return (false, "Profilo non specificato");
|
|
|
|
if (string.IsNullOrEmpty(profile.Name))
|
|
return (false, "Nome profilo non specificato");
|
|
|
|
if (string.IsNullOrEmpty(profile.SourceType))
|
|
return (false, "Tipo sorgente non specificato");
|
|
|
|
if (string.IsNullOrEmpty(profile.DestinationType))
|
|
return (false, "Tipo destinazione non specificato");
|
|
|
|
// Per le sorgenti file, la credenziale non è richiesta
|
|
if (profile.SourceType != "file" && !profile.SourceCredentialId.HasValue)
|
|
return (false, "Credenziale sorgente non specificata");
|
|
|
|
if (!profile.DestinationCredentialId.HasValue)
|
|
return (false, "Credenziale destinazione non specificata");
|
|
|
|
// Validazioni specifiche per tipo sorgente
|
|
if (profile.SourceType == "database")
|
|
{
|
|
if (string.IsNullOrEmpty(profile.SourceTable) && string.IsNullOrEmpty(profile.SourceCustomQuery))
|
|
return (false, "Tabella sorgente o query personalizzata deve essere specificata");
|
|
}
|
|
else if (profile.SourceType == "file")
|
|
{
|
|
if (string.IsNullOrEmpty(profile.SourceFilePath))
|
|
return (false, "Percorso file sorgente non specificato");
|
|
}
|
|
|
|
// Validazioni specifiche per tipo destinazione
|
|
if (profile.DestinationType == "database")
|
|
{
|
|
if (string.IsNullOrEmpty(profile.DestinationTable))
|
|
return (false, "Tabella destinazione non specificata");
|
|
}
|
|
else if (profile.DestinationType == "rest")
|
|
{
|
|
if (string.IsNullOrEmpty(profile.DestinationEndpoint))
|
|
return (false, "Endpoint REST destinazione non specificato");
|
|
}
|
|
|
|
return (true, null);
|
|
}
|
|
} |