diff --git a/src/Essentials/src/FileSystem/FileSystem.windows.cs b/src/Essentials/src/FileSystem/FileSystem.windows.cs index d3484157da85..23e0c723bae7 100644 --- a/src/Essentials/src/FileSystem/FileSystem.windows.cs +++ b/src/Essentials/src/FileSystem/FileSystem.windows.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Frozen; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net.Mime; using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; using Windows.Storage; @@ -84,11 +88,88 @@ Task PlatformAppPackageFileExistsAsync(string filename) public partial class FileBase { + // Static mapping for file extensions to MIME types + // Used as fallback when Windows StorageFile.ContentType doesn't provide correct MIME type + static readonly FrozenDictionary ExtensionToMimeTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // Image formats + { ".jpg", MediaTypeNames.Image.Jpeg }, + { ".jpeg", MediaTypeNames.Image.Jpeg }, + { ".png", "image/png" }, + { ".gif", MediaTypeNames.Image.Gif }, + { ".bmp", "image/bmp" }, + { ".svg", "image/svg+xml" }, + { ".webp", "image/webp" }, + { ".tiff", MediaTypeNames.Image.Tiff }, + { ".tif", MediaTypeNames.Image.Tiff }, + { ".ico", "image/x-icon" }, + + // Audio formats + { ".mp3", "audio/mpeg" }, + { ".wav", "audio/wav" }, + { ".flac", "audio/flac" }, + { ".aac", "audio/aac" }, + { ".ogg", "audio/ogg" }, + { ".wma", "audio/x-ms-wma" }, + + // Video formats + { ".mp4", "video/mp4" }, + { ".avi", "video/x-msvideo" }, + { ".mov", "video/quicktime" }, + { ".wmv", "video/x-ms-wmv" }, + { ".webm", "video/webm" }, + { ".mkv", "video/x-matroska" }, + { ".flv", "video/x-flv" }, + + // Document formats + { ".pdf", MediaTypeNames.Application.Pdf }, + { ".doc", "application/msword" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".txt", MediaTypeNames.Text.Plain }, + { ".rtf", MediaTypeNames.Application.Rtf }, + + // Web formats + { ".html", MediaTypeNames.Text.Html }, + { ".htm", MediaTypeNames.Text.Html }, + { ".css", "text/css" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".xml", MediaTypeNames.Text.Xml }, + + // Archive formats + { ".zip", MediaTypeNames.Application.Zip }, + { ".rar", "application/x-rar-compressed" }, + { ".7z", "application/x-7z-compressed" }, + { ".tar", "application/x-tar" }, + { ".tar.gz", "application/gzip" }, + { ".gz", "application/gzip" } + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + internal FileBase(IStorageFile file) : this(file?.Path) { File = file; - ContentType = file?.ContentType; + + // Set the ContentType, but prefer our mapping for known problematic extensions + var fileContentType = file?.ContentType; + var extension = Path.GetExtension(file?.Path ?? ""); + + // If Windows provides a generic "application/octet-stream" or empty ContentType, + // try to get a more specific MIME type from our extension mapping + if (string.IsNullOrWhiteSpace(fileContentType) || + fileContentType == "application/octet-stream") + { + var betterContentType = PlatformGetContentType(extension); + ContentType = betterContentType ?? fileContentType; + } + else + { + ContentType = fileContentType; + } } void PlatformInit(FileBase file) @@ -98,8 +179,39 @@ void PlatformInit(FileBase file) internal IStorageFile File { get; set; } - // we can't do anything here, but Windows will take care of it - string PlatformGetContentType(string extension) => null; + // Use extension mapping as fallback when Windows doesn't provide correct MIME type + string PlatformGetContentType(string extension) + { + if (string.IsNullOrWhiteSpace(extension)) + return null; + + // Trim whitespace and convert to lowercase for consistent matching + extension = extension.Trim().ToLowerInvariant(); + if (string.IsNullOrEmpty(extension)) + return null; + + if (!extension.StartsWith(".")) + extension = "." + extension; + + // Try exact match first + if (ExtensionToMimeTypeMap.TryGetValue(extension, out var mimeType)) + return mimeType; + + var fileName = Path.GetFileNameWithoutExtension(extension); + if (!string.IsNullOrEmpty(fileName) && fileName.Contains('.', StringComparison.Ordinal)) + { + // Try progressively longer extensions (e.g., .tar.gz, then .gz) + var parts = fileName.Split('.'); + for (int i = parts.Length - 1; i >= 0; i--) + { + var compoundExtension = "." + string.Join(".", parts.Skip(i)) + extension; + if (ExtensionToMimeTypeMap.TryGetValue(compoundExtension, out mimeType)) + return mimeType; + } + } + + return null; + } internal async virtual Task PlatformOpenReadAsync() { diff --git a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs index 90417c1cc279..4a1a068d6b42 100644 --- a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs @@ -101,5 +101,29 @@ public async Task CheckFileResultOpenReadAsyncMultipleTimes() File.Delete(filePath); } + +#if WINDOWS + [Theory] + [InlineData(".webp", "image/webp")] + [InlineData(".jpg", "image/jpeg")] + [InlineData(".JPG", "image/jpeg")] + [InlineData(".Jpg", "image/jpeg")] + [InlineData(".jPg", "image/jpeg")] + [InlineData(".jpg ", "image/jpeg")] // Trailing space + [InlineData(" .jpg", "image/jpeg")] // Leading space + [InlineData(" .jpg ", "image/jpeg")] // Leading and trailing spaces + [InlineData(".png", "image/png")] + [InlineData(".PNG", "image/png")] + [InlineData(".tar.gz", "application/gzip")] + [InlineData(".TAR.GZ", "application/gzip")] + public async Task EnsureFileResultContentType(string extension, string expectedMimeType) + { + string filePath = Path.Combine(FileSystem.CacheDirectory, $"test{extension}"); + await File.WriteAllTextAsync(filePath, $"File Content type is {expectedMimeType}"); + FileResult fileResult = new FileResult(filePath); + Assert.Equal(expectedMimeType, fileResult.ContentType); + File.Delete(filePath); + } +#endif } -} +} \ No newline at end of file