using System; using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Linq; using System.Threading.Tasks; using GB5Shared.DTO.ECM; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; namespace GB5Shared.FileUpload { public class FileUploadBLL : IFileUploadBLL { private readonly IAttachmentDAL _AttachmentDAL; private readonly IQueryExecutor _QueryExecutor; private readonly IConfiguration _Configuration; private static readonly Dictionary MimeTypes = new(StringComparer.OrdinalIgnoreCase) { { "txt", "text/plain" }, { "json", "application/json" }, { "xls", "application/vnd.ms-excel" }, { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, { "doc", "application/msword" }, { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, { "html", "text/html" }, { "htm", "text/html" }, { "jpg", "image/jpeg" }, { "jpeg", "image/jpeg" }, { "bmp", "image/bmp" }, { "pdf", "application/pdf" }, { "ppt", "application/vnd.ms-powerpoint" }, { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, { "xml", "application/xml" }, { "zip", "application/zip" }, { "png", "image/png" }, { "gif", "image/gif" }, { "avi", "video/x-msvideo" }, { "flv", "video/x-flv" }, { "csv", "text/csv" }, { "pfx", "application/x-pkcs12" }, { "rar", "application/x-rar-compressed" }, { "eml", "message/rfc822" }, { "msg", "application/vnd.ms-outlook" }, { "dwg", "application/acad" }, { "dxf", "application/dxf" }, { "step", "application/step" }, { "stp", "application/step" }, { "nc", "text/x-nc" } }; public FileUploadBLL( IAttachmentDAL AttachmentDAL, IQueryExecutor QueryExecutor, IConfiguration Configuration) { _AttachmentDAL = AttachmentDAL; _QueryExecutor = QueryExecutor; _Configuration = Configuration; } // ── Upload ──────────────────────────────────────────────────── public async Task FileUploadWebWithMetaData( IFormFile File, int ObjectId, int ObjectTypeId, int DocumentDetailId, LoginDTO LoginDTO, string? tempRefKey = null) { if (File == null || File.Length == 0) throw new ArgumentException("File is empty or not provided."); string originalFileName = Path.GetFileName(File.FileName); string extension = Path.GetExtension(originalFileName).TrimStart('.').ToLowerInvariant(); if (!MimeTypes.ContainsKey(extension)) throw new NotSupportedException($"File type '.{extension}' is not supported."); string basePath = GetBasePath(); string cmsId = Guid.NewGuid().ToString(); string directoryName = $"{LoginDTO.ClientId}_{ObjectTypeId}"; // Path.Combine handles the OS path separator on both Windows and Linux string folderPath = Path.Combine(basePath, directoryName, cmsId); string targetPath = Path.Combine(folderPath, originalFileName); Directory.CreateDirectory(folderPath); // ── Write file ──────────────────────────────────────────── await using (var fs = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None)) { await File.CopyToAsync(fs); } // ── Persist metadata with transaction ────────────────────── DbTransaction? tx = null; try { tx = await _QueryExecutor.BeginTransactionAsync(LoginDTO); var dto = new TAttachmentDTO { ObjectTypeId = ObjectTypeId, ObjectId = ObjectId, BizTransactionTypeId = -1, SlNo = 1, DocumentSetDetailId = DocumentDetailId, AttachmentType = 0, DocumentTypeId = -1, ValidFrom = new DateTime(1898, 12, 31), ValidTo = new DateTime(9999, 12, 31), ClassificationMarkId = -1, CMSId = cmsId, FolderName = $"{directoryName}/{cmsId}", DisplayFileName = originalFileName, AttachedFileName = originalFileName, Status = 1, Version = 1, AttachmentOption = 2, TempRefKey = tempRefKey, RowGuid = null, MimeType = MimeTypes[extension], FileSizeBytes = File.Length }; int attachmentId = await _AttachmentDAL.SaveAttachment(dto, LoginDTO, tx); await _QueryExecutor.CommitAsync(tx); return $"File successfully attached with AttachmentId = {attachmentId}"; } catch { if (tx != null) await _QueryExecutor.RollbackAsync(tx); throw; } } // ── Upload via Base64 JSON ──────────────────────────────────── public async Task FileUploadBase64Async( FileUploadBase64RequestDTO request, LoginDTO LoginDTO, string? tempRefKey = null) { if (request == null) throw new ArgumentNullException(nameof(request)); if (string.IsNullOrWhiteSpace(request.FileName)) throw new ArgumentException("FileName is required."); if (string.IsNullOrWhiteSpace(request.Base64Content)) throw new ArgumentException("Base64Content is required."); byte[] fileBytes; try { fileBytes = Convert.FromBase64String(request.Base64Content); } catch (FormatException) { throw new ArgumentException("Base64Content is not a valid Base64 string."); } if (fileBytes.Length == 0) throw new ArgumentException("File content is empty."); string originalFileName = Path.GetFileName(request.FileName); string extension = Path.GetExtension(originalFileName).TrimStart('.').ToLowerInvariant(); if (!MimeTypes.ContainsKey(extension)) throw new NotSupportedException($"File type '.{extension}' is not supported."); string basePath = GetBasePath(); string cmsId = Guid.NewGuid().ToString(); string directoryName = $"{LoginDTO.ClientId}_{request.ObjectTypeId}"; string folderPath = Path.Combine(basePath, directoryName, cmsId); string targetPath = Path.Combine(folderPath, originalFileName); Directory.CreateDirectory(folderPath); await System.IO.File.WriteAllBytesAsync(targetPath, fileBytes); DbTransaction? tx = null; try { tx = await _QueryExecutor.BeginTransactionAsync(LoginDTO); var dto = new TAttachmentDTO { ObjectTypeId = request.ObjectTypeId, ObjectId = request.ObjectId, DocumentSetDetailId = request.DocumentDetailId, AttachmentType = request.AttachmentType, DocumentTypeId = request.DocumentTypeId, ValidFrom = request.ValidFrom ?? new DateTime(1898, 12, 31), ValidTo = request.ValidTo ?? new DateTime(9999, 12, 31), ClassificationMarkId = request.ClassificationMarkId, Tags = request.Tags ?? string.Empty, LinkUrl = request.LinkUrl ?? string.Empty, Thumbnail = request.Thumbnail, ObjectHeaderId = request.ObjectHeaderId, ObjectHeaderTypeId = request.ObjectHeaderTypeId, ObjectHeaderGuid = request.ObjectHeaderGuid, Remarks = request.Remarks, SlNo = request.SlNo, BizTransactionTypeId = request.BizTransactionTypeId, CMSId = cmsId, FolderName = $"{directoryName}/{cmsId}", DisplayFileName = string.IsNullOrWhiteSpace(request.DisplayFileName) ? originalFileName : request.DisplayFileName, AttachedFileName = originalFileName, Status = request.Status > 0 ? request.Status : 1, Version = 1, AttachmentOption = 2, TempRefKey = tempRefKey, RowGuid = Guid.TryParse(request.RowGuid, out var rg) ? rg : (Guid?)null, MimeType = MimeTypes[extension], FileSizeBytes = fileBytes.LongLength }; int attachmentId = await _AttachmentDAL.SaveAttachment(dto, LoginDTO, tx); await _QueryExecutor.CommitAsync(tx); return new FileUploadTempRefKeyResponseDTO { Status = "File successfully attached.", AttachmentId = attachmentId }; } catch { if (tx != null) await _QueryExecutor.RollbackAsync(tx); throw; } } // ── Get ─────────────────────────────────────────────────────── public async Task GetAttachment(int AttachmentId, LoginDTO LoginDTO) { var result = await _AttachmentDAL.GetAttachmentDetail(AttachmentId, LoginDTO); if (result == null) return JsonConvert.SerializeObject(null); // Use stored MimeType when available; fall back to extension derivation if (!string.IsNullOrEmpty(result.FileName)) { if (string.IsNullOrEmpty(result.MimeType)) { string ext = Path.GetExtension(result.FileName).TrimStart('.').ToLowerInvariant(); result.MimeType = MimeTypes.TryGetValue(ext, out string? mime) ? mime : "application/octet-stream"; } result.FileType = result.MimeType; } // Build physical path if (!string.IsNullOrEmpty(result.FolderName)) result.PhysicalPath = $"{GetBasePath()}/{result.FolderName}//"; return JsonConvert.SerializeObject(result); } // ── List ────────────────────────────────────────────────────── public async Task> ListAttachments(int ObjectId, int ObjectTypeId, LoginDTO LoginDTO) { var list = (await _AttachmentDAL.ListAttachments(ObjectId, ObjectTypeId, LoginDTO)).ToList(); string basePath = GetBasePath(); foreach (var item in list) { EnrichAttachmentItem(item, basePath); } return list; } // ── List by ObjectId only ───────────────────────────────────── public async Task> ListAttachmentsByObjectId(int ObjectId, LoginDTO LoginDTO) { var list = (await _AttachmentDAL.ListAttachmentsByObjectId(ObjectId, LoginDTO)).ToList(); string basePath = GetBasePath(); foreach (var item in list) { EnrichAttachmentItem(item, basePath); } return list; } // ── List by ObjectHeaderId + ObjectId ──────────────────────── public async Task> ListAttachmentsByHeaderAndObjectId( int ObjectHeaderId, int ObjectId, LoginDTO LoginDTO) { var list = (await _AttachmentDAL.ListAttachmentsByHeaderAndObjectId(ObjectHeaderId, ObjectId, LoginDTO)).ToList(); string basePath = GetBasePath(); foreach (var item in list) { EnrichAttachmentItem(item, basePath); } return list; } // ── Update ──────────────────────────────────────────────────── public async Task UpdateAttachment(AttachmentUpdateDTO Dto, LoginDTO LoginDTO) { DbTransaction? tx = null; try { tx = await _QueryExecutor.BeginTransactionAsync(LoginDTO); string result = await _AttachmentDAL.UpdateAttachment(Dto, LoginDTO, tx); await _QueryExecutor.CommitAsync(tx); return result; } catch { if (tx != null) await _QueryExecutor.RollbackAsync(tx); throw; } } // ── Delete ──────────────────────────────────────────────────── public async Task DeleteAttachment(int AttachmentId, LoginDTO LoginDTO) { // Fetch the record first to know the physical file location TAttachmentDTO? attachment = await _AttachmentDAL.GetAttachment(AttachmentId, LoginDTO); if (attachment == null) throw new KeyNotFoundException($"Attachment {AttachmentId} not found."); DbTransaction? tx = null; try { tx = await _QueryExecutor.BeginTransactionAsync(LoginDTO); string result = await _AttachmentDAL.DeleteAttachment(AttachmentId, LoginDTO, tx); await _QueryExecutor.CommitAsync(tx); // ── Remove physical file after successful DB delete ──── DeletePhysicalFile(attachment); return result; } catch { if (tx != null) await _QueryExecutor.RollbackAsync(tx); throw; } } // ── Download as Base64 ──────────────────────────────────────── public async Task DownloadAttachmentAsBase64(int AttachmentId, LoginDTO LoginDTO) { TAttachmentDTO? attachment = await _AttachmentDAL.GetAttachment(AttachmentId, LoginDTO); if (attachment == null) throw new KeyNotFoundException($"Attachment {AttachmentId} not found."); string basePath = GetBasePath(); string filePath = Path.Combine(basePath, attachment.FolderName, attachment.DisplayFileName!); if (!System.IO.File.Exists(filePath)) throw new FileNotFoundException($"Physical file for AttachmentId {AttachmentId} not found on disk.", filePath); byte[] bytes = await System.IO.File.ReadAllBytesAsync(filePath); // Prefer stored MimeType; fall back to extension derivation string mimeType = attachment.MimeType ?? string.Empty; if (string.IsNullOrEmpty(mimeType)) { string extension = Path.GetExtension(attachment.DisplayFileName!).TrimStart('.').ToLowerInvariant(); MimeTypes.TryGetValue(extension, out string? derived); mimeType = derived ?? "application/octet-stream"; } return new AttachmentBase64ResponseDTO { AttachmentId = attachment.AttachmentId, FileName = attachment.DisplayFileName!, MimeType = mimeType, Base64Content = Convert.ToBase64String(bytes), FileSizeBytes = bytes.LongLength }; } // ── Helpers ─────────────────────────────────────────────────── /// /// Picks the OS-specific PhysicalFilePath from config (FileStorage:PhysicalFilePath:Windows /// or :Linux), falling back to a flat FileStorage:PhysicalFilePath for backward compatibility. /// private string GetBasePath() { string osKey = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( System.Runtime.InteropServices.OSPlatform.Windows) ? "Windows" : "Linux"; string raw = _Configuration[$"FileStorage:PhysicalFilePath:{osKey}"] ?? _Configuration["FileStorage:PhysicalFilePath"] ?? throw new InvalidOperationException( $"FileStorage:PhysicalFilePath:{osKey} (or FileStorage:PhysicalFilePath) is not configured."); // Normalise separator to the current OS (forward slash → backslash on Windows, vice versa) return raw.Replace('\\', Path.DirectorySeparatorChar) .Replace('/', Path.DirectorySeparatorChar) .TrimEnd(Path.DirectorySeparatorChar); } private void EnrichAttachmentItem(AttachmentListDTO item, string basePath) { if (!string.IsNullOrEmpty(item.FileName)) { if (string.IsNullOrEmpty(item.MimeType)) { string ext = Path.GetExtension(item.FileName).TrimStart('.').ToLowerInvariant(); item.MimeType = MimeTypes.TryGetValue(ext, out string? mime) ? mime : "application/octet-stream"; } item.FileType = item.MimeType; } if (!string.IsNullOrEmpty(item.FolderName)) item.PhysicalPath = $"{basePath}/{item.FolderName}//"; } private void DeletePhysicalFile(TAttachmentDTO attachment) { if (string.IsNullOrWhiteSpace(attachment.CMSId) || string.IsNullOrWhiteSpace(attachment.DisplayFileName)) return; string basePath = GetBasePath(); string filePath = Path.Combine(basePath, attachment.FolderName, attachment.DisplayFileName); if (System.IO.File.Exists(filePath)) System.IO.File.Delete(filePath); // Clean up the now-empty cmsId sub-directory (FolderName = {ClientId}_{ObjectTypeId}/{cmsId}) string cmsDir = Path.Combine(basePath, attachment.FolderName); if (Directory.Exists(cmsDir) && Directory.GetFileSystemEntries(cmsDir).Length == 0) Directory.Delete(cmsDir); } public async Task> ListAttachmentsByHeaderAndObjectHeaderTypeId(int ObjectHeaderTypeId, int ObjectHeaderId,LoginDTO LoginDTO) { var list = (await _AttachmentDAL .ListAttachmentsByHeaderAndObjectHeaderTypeId(ObjectHeaderTypeId, ObjectHeaderId, LoginDTO)) .ToList(); string basePath = GetBasePath(); foreach (var item in list) { EnrichAttachmentItem(item, basePath); } return list; } public async Task UpdateAttachmentObjectId(AttachmentUpdateDTO dto,LoginDTO LoginDTO) { DbTransaction? tx = null; try { tx = await _QueryExecutor.BeginTransactionAsync(LoginDTO); string result = await _AttachmentDAL.UpdateAttachmentObjectId( dto, LoginDTO, tx); await _QueryExecutor.CommitAsync(tx); return result; } catch { if (tx != null) await _QueryExecutor.RollbackAsync(tx); throw; } } } }