namespace SeaHaven.Services.Helpers { /// /// SH-116 client-confirmed media contract (2026-09-22): photos up to 10 MB (JPEG/PNG/HEIC), /// videos up to 100 MB and 90 seconds (MP4/MOV), at most 10 photos and 3 videos per work /// order, across the dispatcher media modal, the completion-doc media tab and the vendor /// portal upload. Documents (PDF/DOC/DOCX) keep the 50 MB document cap. Duration is only /// checkable client-side (the server cannot probe it cheaply), so it is enforced in the /// browser and documented here as part of the same contract. /// public static class WorkOrderMediaContract { public const long MaxPhotoBytes = 10_000_000; public const long MaxVideoBytes = 100_000_000; public const long MaxDocumentBytes = 50_000_000; public const int MaxPhotosPerWorkOrder = 10; public const int MaxVideosPerWorkOrder = 3; public const int MaxVideoDurationSeconds = 90; /// /// Request-level ceiling for media endpoints (RequestSizeLimit / multipart limit). It sits /// above plus multipart overhead so an oversize file reaches the /// per-kind check and gets its generic message instead of a framework 413. The EB nginx /// proxy (.platform/nginx/conf.d/01_upload_body_size.conf) must stay above this value. /// public const long MaxUploadRequestBytes = 110_000_000; public enum UploadKind { Photo, Video, Document, Unknown } private static readonly Dictionary KindByContentType = new(StringComparer.OrdinalIgnoreCase) { ["image/jpeg"] = UploadKind.Photo, ["image/jpg"] = UploadKind.Photo, ["image/png"] = UploadKind.Photo, ["image/heic"] = UploadKind.Photo, ["video/mp4"] = UploadKind.Video, ["video/quicktime"] = UploadKind.Video, ["application/pdf"] = UploadKind.Document, ["application/msword"] = UploadKind.Document, ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] = UploadKind.Document, }; private static readonly Dictionary KindByExtension = new(StringComparer.OrdinalIgnoreCase) { [".jpg"] = UploadKind.Photo, [".jpeg"] = UploadKind.Photo, [".png"] = UploadKind.Photo, [".heic"] = UploadKind.Photo, [".mp4"] = UploadKind.Video, [".mov"] = UploadKind.Video, [".pdf"] = UploadKind.Document, [".doc"] = UploadKind.Document, [".docx"] = UploadKind.Document, }; /// /// Classifies an upload. Pass the validated (resolved) content type: it decides whenever /// it is an allowlisted type, so a misleading file name cannot move a file into a larger /// size class. The extension only decides when no allowlisted type is known (for example /// counting stored attachment URLs). /// public static UploadKind ResolveKind(string? contentType, string? fileName) { var type = (contentType ?? string.Empty).Trim(); if (KindByContentType.TryGetValue(type, out var byType)) return byType; var extension = Path.GetExtension(fileName ?? string.Empty); return KindByExtension.TryGetValue(extension, out var byExtension) ? byExtension : UploadKind.Unknown; } /// Stable, generic per-kind size message; never echoes file metadata. public static string? ValidateSize(long length, UploadKind kind) { return kind switch { UploadKind.Photo when length > MaxPhotoBytes => "Photos must be 10 MB or smaller.", UploadKind.Video when length > MaxVideoBytes => "Videos must be 100 MB or smaller.", UploadKind.Document when length > MaxDocumentBytes => "Documents must be 50 MB or smaller.", UploadKind.Unknown when length > MaxVideoBytes => "Videos must be 100 MB or smaller.", _ => null }; } public static string? ValidateSize(string? contentType, string? fileName, long length) => ValidateSize(length, ResolveKind(contentType, fileName)); } }