using PlotLine.Models; using SkiaSharp; namespace PlotLine.Services; public interface IFloorPlanBackgroundImageService { Task UploadAsync(FloorPlan floorPlan, FloorPlanFloor floor, IFormFile upload); void DeleteBackgroundFile(string? backgroundImagePath); } public sealed record FloorPlanBackgroundImageUploadResult( bool Succeeded, string? StoragePath = null, string? OriginalFileName = null, string? ErrorMessage = null); public sealed class FloorPlanBackgroundImageService( IUploadStorageService uploadStorage, ILogger logger) : IFloorPlanBackgroundImageService { private const long MaxUploadBytes = 10L * 1024L * 1024L; private static readonly HashSet SupportedExtensions = new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp" }; public async Task UploadAsync(FloorPlan floorPlan, FloorPlanFloor floor, IFormFile upload) { if (upload.Length <= 0) { return new(false, ErrorMessage: "Choose a background image to upload."); } if (upload.Length > MaxUploadBytes) { return new(false, ErrorMessage: "Background image must be 10 MB or smaller."); } var extension = Path.GetExtension(upload.FileName); if (!SupportedExtensions.Contains(extension)) { return new(false, ErrorMessage: "Background image must be a PNG, JPG, JPEG or WebP file."); } var originalFileName = Path.GetFileName(upload.FileName); if (string.IsNullOrWhiteSpace(originalFileName)) { originalFileName = $"background{extension}"; } byte[] bytes; await using (var stream = upload.OpenReadStream()) using (var memory = new MemoryStream()) { await stream.CopyToAsync(memory); bytes = memory.ToArray(); } try { using var codec = SKCodec.Create(new MemoryStream(bytes)) ?? throw new InvalidDataException(); if (codec.EncodedFormat is not (SKEncodedImageFormat.Jpeg or SKEncodedImageFormat.Png or SKEncodedImageFormat.Webp)) { return new(false, ErrorMessage: "Background image must be a valid PNG, JPG, JPEG or WebP file."); } } catch (Exception ex) when (ex is InvalidDataException or ArgumentException) { return new(false, ErrorMessage: "Background image must be a valid PNG, JPG, JPEG or WebP file."); } var storedName = $"background-{Guid.NewGuid():N}{extension.ToLowerInvariant()}"; var uploadRoot = uploadStorage.GetDirectory("floor-plans", floorPlan.ProjectID.ToString(), floorPlan.FloorPlanID.ToString(), "backgrounds"); var absolutePath = Path.Combine(uploadRoot, storedName); var storagePath = uploadStorage.GetPublicPath("floor-plans", floorPlan.ProjectID.ToString(), floorPlan.FloorPlanID.ToString(), "backgrounds", storedName); try { await File.WriteAllBytesAsync(absolutePath, bytes); return new(true, storagePath, originalFileName); } catch (Exception ex) { logger.LogWarning(ex, "Floor plan background upload failed for floor {FloorPlanFloorID}.", floor.FloorPlanFloorID); return new(false, ErrorMessage: "Background image could not be saved. Try another image."); } } public void DeleteBackgroundFile(string? backgroundImagePath) { uploadStorage.TryDeleteFile(backgroundImagePath, "uploads/floor-plans"); } }