using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using PowderCoating.Application.Configuration; using PowderCoating.Application.Interfaces; namespace PowderCoating.Application.Services; /// /// Manages company logo files stored in Azure Blob Storage. /// Logos are stored in the companylogos container using the path /// {companyId}/company-logo{ext} (e.g. 7/company-logo.png). /// The service enforces a 10 MB size limit and restricts uploads to common /// web-safe image formats. It also handles the "replace" case by deleting /// blobs of every other allowed extension before writing the new one, so a /// company can never end up with multiple active logos at different paths. /// public class CompanyLogoService : ICompanyLogoService { private readonly IAzureBlobStorageService _blobService; private readonly StorageSettings _settings; /// Maximum logo file size accepted on upload (10 MB). private const long MaxFileSize = 10 * 1024 * 1024; // 10 MB /// /// Exhaustive list of image extensions permitted for company logos. /// SVG is included because some companies upload vector-format brand assets. /// private static readonly string[] AllowedExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]; /// /// Initialises the service with the blob storage provider and storage /// configuration (container names, etc.). /// public CompanyLogoService( IAzureBlobStorageService blobService, IOptions settings) { _blobService = blobService; _settings = settings.Value; } /// /// Returns the deterministic blob path for a company logo given its file /// extension. Using a fixed name (rather than a GUID) allows the path to /// be derived from companyId alone — useful for PDF embedding and /// sidebar rendering without an extra DB lookup. /// /// The tenant company's database ID. /// File extension including the leading dot (e.g. .png). /// Blob-relative path such as 7/company-logo.png. public string GetCompanyLogoPath(int companyId, string extension) { return $"{companyId}/company-logo{extension}"; } /// /// Validates and uploads a new company logo, replacing any existing logo /// regardless of its extension. Old blobs at alternative extensions are /// deleted first so the container never holds stale copies. /// /// The uploaded file from the HTTP request. /// The tenant company's database ID. /// /// A tuple indicating success, the stored blob path (on success), and a /// human-readable error message (on failure). /// public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveCompanyLogoAsync(IFormFile file, int companyId) { var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize); if (!isValid) return (false, string.Empty, error); // Delete old logo (any extension) before saving new one await DeleteOldLogosAsync(companyId, extension); var blobName = GetCompanyLogoPath(companyId, extension); var contentType = BlobFileHelper.GetContentType(extension); using var stream = file.OpenReadStream(); var result = await _blobService.UploadAsync(_settings.Containers.CompanyLogos, blobName, stream, contentType); if (!result.Success) return (false, string.Empty, result.ErrorMessage); return (true, blobName, string.Empty); } /// /// Deletes the company logo 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)> DeleteCompanyLogoAsync(string filePath) { if (string.IsNullOrEmpty(filePath)) return (false, "File path is empty"); return await _blobService.DeleteAsync(_settings.Containers.CompanyLogos, filePath); } /// /// Downloads the raw bytes of the company logo for embedding in PDFs or /// serving through the /CompanyLogo controller endpoint. /// /// Blob-relative path of the logo. /// /// 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)> GetCompanyLogoAsync(string filePath) { if (string.IsNullOrEmpty(filePath)) return (false, Array.Empty(), string.Empty, "File path is empty"); return await _blobService.DownloadAsync(_settings.Containers.CompanyLogos, filePath); } /// /// Checks whether a logo blob exists at the given path without downloading it. /// Used by the sidebar to decide whether to show the tenant logo or fall back /// to the default PCL logo. /// /// Blob-relative path to check. /// true if the blob exists; otherwise false. public async Task CompanyLogoExistsAsync(string filePath) { if (string.IsNullOrEmpty(filePath)) return false; return await _blobService.ExistsAsync(_settings.Containers.CompanyLogos, filePath); } /// /// Deletes any pre-existing logo blobs at extensions other than /// so that uploading a PNG logo /// automatically removes a previously stored JPG logo (and vice versa). /// Errors are intentionally swallowed — a missing old blob is not a failure. /// /// The tenant company's database ID. /// /// The extension of the incoming upload; blobs with this extension are /// skipped because they will be overwritten by the caller. /// private async Task DeleteOldLogosAsync(int companyId, string currentExtension) { foreach (var ext in AllowedExtensions) { if (ext == currentExtension) continue; var oldBlobName = GetCompanyLogoPath(companyId, ext); await _blobService.DeleteAsync(_settings.Containers.CompanyLogos, oldBlobName); } } }