using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using PowderCoating.Application.Configuration; using PowderCoating.Application.Interfaces; namespace PowderCoating.Application.Services; /// /// Manages equipment manual documents stored in Azure Blob Storage. /// Manuals are stored in the manuals container under the path /// {companyId}/equipment-manuals/{equipmentId}/{sanitizedFilename}{ext}. /// /// The 50 MB limit (5× larger than other upload types) is intentional — /// equipment OEM manuals are often large PDF scans. Only document formats /// are accepted; image uploads are rejected to keep this container clean. /// /// public class EquipmentManualService : IEquipmentManualService { private readonly IAzureBlobStorageService _blobService; private readonly StorageSettings _settings; /// Maximum manual file size accepted on upload (50 MB). private const long MaxFileSize = 50 * 1024 * 1024; // 50 MB /// /// Document formats permitted for equipment manuals. /// Images and spreadsheets are deliberately excluded to keep /// the container purpose-specific. /// private static readonly string[] AllowedExtensions = [".pdf", ".doc", ".docx", ".txt"]; /// /// Initialises the service with the blob storage provider and storage /// configuration (container names, etc.). /// public EquipmentManualService( IAzureBlobStorageService blobService, IOptions settings) { _blobService = blobService; _settings = settings.Value; } /// /// Validates, sanitizes, and uploads an equipment manual to Azure Blob Storage. /// The original filename (minus invalid characters) is preserved in the blob /// name so operators can recognise the document from the path alone. /// /// The uploaded file from the HTTP request. /// The tenant company's database ID (for path scoping). /// The equipment record's database ID. /// /// A tuple with a success flag, the stored blob path (on success), and a /// human-readable error message (on failure). /// public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveEquipmentManualAsync(IFormFile file, int companyId, int equipmentId) { var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize); if (!isValid) return (false, string.Empty, error); // Sanitize filename — replace OS-invalid characters with underscores to // prevent path traversal and blob naming errors in Azure. var fileName = BlobFileHelper.SanitizeFileName(Path.GetFileNameWithoutExtension(file.FileName)); var blobName = $"{companyId}/equipment-manuals/{equipmentId}/{fileName}{extension}"; var contentType = BlobFileHelper.GetContentType(extension); using var stream = file.OpenReadStream(); var result = await _blobService.UploadAsync(_settings.Containers.Manuals, blobName, stream, contentType); if (!result.Success) return (false, string.Empty, result.ErrorMessage); return (true, blobName, string.Empty); } /// /// Deletes the equipment manual blob at the given path from Azure Blob Storage. /// /// Blob-relative path previously returned by . /// Success flag and an error message on failure. public async Task<(bool Success, string ErrorMessage)> DeleteEquipmentManualAsync(string filePath) { if (string.IsNullOrEmpty(filePath)) return (false, "File path is empty"); return await _blobService.DeleteAsync(_settings.Containers.Manuals, filePath); } /// /// Downloads the raw bytes of an equipment manual so the controller can /// stream it to the browser with the appropriate content-disposition header. /// /// Blob-relative path of the manual. /// /// A tuple with a success flag, the raw file bytes, the MIME content type, /// and an error message on failure. /// public async Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetEquipmentManualAsync(string filePath) { if (string.IsNullOrEmpty(filePath)) return (false, Array.Empty(), string.Empty, "File path is empty"); return await _blobService.DownloadAsync(_settings.Containers.Manuals, filePath); } /// /// Checks whether a manual blob exists at the given path without downloading it. /// Used by the Equipment Details view to determine whether a "Download Manual" /// button should be rendered. /// /// Blob-relative path to check. /// true if the blob exists; otherwise false. public async Task EquipmentManualExistsAsync(string filePath) { if (string.IsNullOrEmpty(filePath)) return false; return await _blobService.ExistsAsync(_settings.Containers.Manuals, filePath); } }