using System; using System.IO; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using DMSDAL.CustomCode.Migration; using DMSDAL.DTO.Migration; using GB5Shared.DTO.Framework.Login; using GB5Shared.Storage; using Microsoft.Extensions.Logging; namespace DMSBLL.Migration { public class AlfrescoMigrationBLL : IAlfrescoMigrationBLL { private readonly IAlfrescoMigrationDAL _dal; private readonly IStorageProvider _storage; private readonly ILogger _log; private const int DefaultDocumentSetDetailId = -1; public AlfrescoMigrationBLL( IAlfrescoMigrationDAL dal, IStorageProvider storage, ILogger log) { _dal = dal; _storage = storage; _log = log; } public async Task StartMigrationAsync( int objectTypeId, int batchSize, bool dryRun, LoginDTO login, CancellationToken ct = default) { // ── 1. Entity table info ──────────────────────────── var entityInfo = await _dal.GetEntityTableInfoAsync(objectTypeId, login, ct) .ConfigureAwait(false); if (entityInfo is null) throw new InvalidOperationException( $"ObjectTypeId {objectTypeId} not found in MENTITY."); // ── 2. Alfresco settings ──────────────────────────── var settings = await _dal.GetAlfrescoSettingsAsync(login, ct).ConfigureAwait(false); if (settings is null) throw new InvalidOperationException( "AlfrescoCMS config keys not found in MCOMMONCONFIG. Cannot connect to Alfresco."); byte attachmentOption = _storage.ProviderType == StorageProviderType.S3 ? (byte)2 // AmazonS3 : (byte)3; // FileBased // ── 3. Page through source entity table ───────────── int offset = 0; while (true) { ct.ThrowIfCancellationRequested(); var objectIds = await _dal.GetEntityIdsBatchAsync( entityInfo, batchSize, offset, login, ct).ConfigureAwait(false); bool anyRows = false; foreach (var objectId in objectIds) { anyRows = true; await ProcessEntityAsync( entityInfo, settings, objectTypeId, objectId, dryRun, attachmentOption, login, ct).ConfigureAwait(false); } if (!anyRows) break; offset += batchSize; } // ── 4. Return final status ────────────────────────── return await _dal.GetMigrationStatusAsync(objectTypeId, login, ct) .ConfigureAwait(false) ?? new MigrationStatusDTO { ObjectTypeId = objectTypeId }; } public async Task GetMigrationStatusAsync( int objectTypeId, LoginDTO login, CancellationToken ct = default) => await _dal.GetMigrationStatusAsync(objectTypeId, login, ct).ConfigureAwait(false); // ── Per-entity processing ──────────────────────────────────────── private async Task ProcessEntityAsync( EntityTableInfoDTO entityInfo, AlfrescoSettings settings, int objectTypeId, int objectId, bool dryRun, byte attachmentOption, LoginDTO login, CancellationToken ct) { if (await _dal.IsAlreadyMigratedAsync(objectTypeId, objectId, login, ct) .ConfigureAwait(false)) return; var attachments = await _dal.DiscoverAlfrescoAttachmentsAsync( settings, objectTypeId, objectId, login.ClientId, ct).ConfigureAwait(false); foreach (var att in attachments) { var logDto = new MigrationLogDTO { ObjectTypeId = objectTypeId, ObjectId = objectId, AlfrescoNodeId = att.NodeId, DisplayFileName = att.DisplayFileName, Status = 0, // Pending ClientId = login.ClientId }; var logId = await _dal.InsertMigrationLogAsync(logDto, login, ct) .ConfigureAwait(false); await MigrateFileAsync( entityInfo, settings, att, objectTypeId, objectId, logId, dryRun, attachmentOption, login, ct).ConfigureAwait(false); } } private async Task MigrateFileAsync( EntityTableInfoDTO entityInfo, AlfrescoSettings settings, AlfrescoAttachmentInfoDTO att, int objectTypeId, int objectId, long logId, bool dryRun, byte attachmentOption, LoginDTO login, CancellationToken ct) { await _dal.UpdateMigrationLogStatusAsync(logId, 1, null, null, login, ct) .ConfigureAwait(false); try { if (dryRun) { // Skipped — no writes await _dal.UpdateMigrationLogStatusAsync(logId, 4, null, null, login, ct) .ConfigureAwait(false); return; } // Download from Alfresco await using var alfStream = await _dal.DownloadFromAlfrescoAsync(settings, att.NodeId, ct) .ConfigureAwait(false); // Buffer, compute hash + size using var ms = new MemoryStream(); await alfStream.CopyToAsync(ms, ct).ConfigureAwait(false); ms.Position = 0; var sizeBytes = ms.Length; var contentHash = ComputeMd5Hex(ms.GetBuffer(), (int)ms.Length); ms.Position = 0; // Build archival storage path var ext = Path.GetExtension(att.DisplayFileName).ToLowerInvariant(); var folderName = $"{entityInfo.EntityCode}/ALF_MIGRATED/{objectId}/"; var fileName = $"{att.NodeId}{ext}"; var storageKey = folderName + fileName; await _storage.SaveAsync(ms, storageKey, att.MimeType, ct).ConfigureAwait(false); // Insert TATTACHMENT var attachmentId = await _dal.InsertTAttachmentAsync( objectTypeId, objectId, DefaultDocumentSetDetailId, folderName, fileName, att.DisplayFileName, storageKey, att.MimeType, sizeBytes, contentHash, attachmentOption, login, ct).ConfigureAwait(false); await _dal.UpdateMigrationLogStatusAsync(logId, 2, attachmentId, null, login, ct) .ConfigureAwait(false); _log.LogInformation( "Migrated attachment. OT={OT} OId={OId} NodeId={Node} AttachmentId={Aid}", objectTypeId, objectId, att.NodeId, attachmentId); } catch (Exception ex) { _log.LogError(ex, "Migration failed. OT={OT} OId={OId} NodeId={Node}", objectTypeId, objectId, att.NodeId); await _dal.UpdateMigrationLogStatusAsync(logId, 3, null, ex.Message, login, ct) .ConfigureAwait(false); } } private static string ComputeMd5Hex(byte[] buffer, int length) { Span hash = stackalloc byte[16]; MD5.HashData(buffer.AsSpan(0, length), hash); return Convert.ToHexString(hash).ToLowerInvariant(); } } }