From b501dc5e76dfd723ae814fec5bbd87d8586d9936 Mon Sep 17 00:00:00 2001 From: Tsanie Lily Date: Fri, 25 Aug 2023 10:53:13 +0800 Subject: [PATCH] multipart/form-data support --- Network/MultipartFormData.cs | 59 +++++++++++++++ Network/Network.csproj | 8 +- Network/NetworkHelper.cs | 4 + .../Platforms/iOS/PlatformNetworkHelper.cs | 73 ++++++++++++++----- .../iOS/Structs/NSMultipartFormData.cs | 66 +++++++++++++++++ Network/Platforms/iOS/Tasks/ContentTask.cs | 2 +- Network/Platforms/iOS/Tasks/DownloadTask.cs | 2 +- Network/Platforms/iOS/Tasks/FileTask.cs | 2 +- Network/Platforms/iOS/Tasks/ImageTask.cs | 2 +- Network/Platforms/iOS/Tasks/StringTask.cs | 2 +- 10 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 Network/MultipartFormData.cs create mode 100644 Network/Platforms/iOS/Structs/NSMultipartFormData.cs diff --git a/Network/MultipartFormData.cs b/Network/MultipartFormData.cs new file mode 100644 index 0000000..0011582 --- /dev/null +++ b/Network/MultipartFormData.cs @@ -0,0 +1,59 @@ +namespace Blahblah.Library.Network; + +public abstract class MultipartFormData +{ + public string Name { get; } + + public MultipartFormData(string name) + { + Name = name; + } + + public static FieldFormData From(string name, string value) + { + return new(name, value); + } + + public static ObjectFormData From(string name, T value) + { + return new(name, value); + } + + public static FileFormData From(string name, Stream stream, string fileName, string mimeType = NSMultipartFormData.DefaultMimeType) + { + return new(name, stream, fileName, mimeType); + } +} + +public class FieldFormData : MultipartFormData +{ + public string? Value { get; } + + public FieldFormData(string name, string? value) : base(name) + { + Value = value; + } +} + +public class ObjectFormData : FieldFormData +{ + public ObjectFormData(string name, T value) : base(name, value?.ToString()) + { + } +} + +public class FileFormData : MultipartFormData +{ + public Stream Stream { get; } + + public string FileName { get; } + + public string MimeType { get; } + + public FileFormData(string name, Stream stream, string fileName, string mimeType = NSMultipartFormData.DefaultMimeType) : base(name) + { + Stream = stream; + FileName = fileName; + MimeType = mimeType; + } +} diff --git a/Network/Network.csproj b/Network/Network.csproj index 678aa39..5c68ee1 100644 --- a/Network/Network.csproj +++ b/Network/Network.csproj @@ -1,7 +1,8 @@ - net7.0-ios + net8.0-ios + Blahblah.Library.Network true enable @@ -12,4 +13,9 @@ 14.0 + + + + + diff --git a/Network/NetworkHelper.cs b/Network/NetworkHelper.cs index d3818c5..53c6f9a 100644 --- a/Network/NetworkHelper.cs +++ b/Network/NetworkHelper.cs @@ -8,8 +8,12 @@ public partial class NetworkHelper public const string AcceptJpegImage = "image/jpeg,image/*,*/*;q=0.8"; public const string AcceptJson = "application/json"; + public const string ContentJson = "application/json"; + public const string ContentFormData = "multipart/form-data"; + const string AcceptHeader = "Accept"; const string ReferrerHeader = "Referer"; + const string ContentTypeHeader = "Content-Type"; const int Timeout = 30; const int ImageTimeout = 60; diff --git a/Network/Platforms/iOS/PlatformNetworkHelper.cs b/Network/Platforms/iOS/PlatformNetworkHelper.cs index e055788..69e1697 100644 --- a/Network/Platforms/iOS/PlatformNetworkHelper.cs +++ b/Network/Platforms/iOS/PlatformNetworkHelper.cs @@ -10,6 +10,20 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate return (int)response.StatusCode is >= 200 and < 300; } + static NSMutableUrlRequest CreateUrlRequest(string url, string accept, string? referrer = null) + { + var uri = NSUrl.FromString(url) ?? throw new ArgumentNullException(nameof(url)); + var request = new NSMutableUrlRequest(uri) + { + [AcceptHeader] = accept + }; + if (!string.IsNullOrEmpty(referrer)) + { + request[ReferrerHeader] = referrer; + } + return request; + } + NSUrlSession? urlSession; readonly Dictionary tasks = new(); @@ -71,12 +85,7 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate public partial Task> GetContentAsync(string url, StringHandler? process, CancellationToken token) { - var uri = NSUrl.FromString(url) ?? throw new ArgumentNullException(nameof(url)); - var request = new NSMutableUrlRequest(uri) - { - [AcceptHeader] = Accept ?? AcceptHttp, - [ReferrerHeader] = Referrer - }; + var request = CreateUrlRequest(url, Accept ?? AcceptHttp, Referrer); var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null"); var taskSource = new TaskCompletionSource>(); @@ -94,12 +103,7 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate public Task> GetImageAsync(string url, string filePath, StepHandler? step = null, CancellationToken token = default) { - var uri = NSUrl.FromString(url) ?? throw new ArgumentNullException(nameof(url)); - var request = new NSMutableUrlRequest(uri) - { - [AcceptHeader] = Accept ?? AcceptImage, - [ReferrerHeader] = Referrer - }; + var request = CreateUrlRequest(url, Accept ?? AcceptImage, Referrer); var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null"); var taskSource = new TaskCompletionSource>(); @@ -115,15 +119,44 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate return taskSource.Task; } - public Task> PostAsync(string url, Action? prepare = null, CancellationToken token = default) + public Task> PostAsync(string url, string data, string contentType = ContentJson, CancellationToken token = default) + { + return RequestAsync(url, request => + { + request[ContentTypeHeader] = contentType; + request.Body = NSData.FromString(data, NSStringEncoding.UTF8); + }, token); + } + + public Task> PostAsync(string url, MultipartFormData[] forms, CancellationToken token = default) + { + return RequestAsync(url, request => + { + var body = new NSMultipartFormData(); + request[ContentTypeHeader] = body.ContentType; + foreach (var form in forms) + { + if (form is FieldFormData field) + { + if (!string.IsNullOrEmpty(field.Value)) + { + body.AddTextField(field.Name, field.Value); + } + } + else if (form is FileFormData file) + { + body.AddDataField(file.Name, file.Stream, file.FileName, file.MimeType); + } + } + body.AddEnd(); + request.Body = body.Data; + }, token); + } + + public Task> RequestAsync(string url, Action? prepare = null, CancellationToken token = default) { - var uri = NSUrl.FromString(url) ?? throw new ArgumentNullException(nameof(url)); - var request = new NSMutableUrlRequest(uri) - { - HttpMethod = "POST", - [AcceptHeader] = Accept ?? AcceptImage, - [ReferrerHeader] = Referrer - }; + var request = CreateUrlRequest(url, Accept ?? AcceptJson, Referrer); + request.HttpMethod = "POST"; prepare?.Invoke(request); var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null"); diff --git a/Network/Platforms/iOS/Structs/NSMultipartFormData.cs b/Network/Platforms/iOS/Structs/NSMultipartFormData.cs new file mode 100644 index 0000000..7b475b2 --- /dev/null +++ b/Network/Platforms/iOS/Structs/NSMultipartFormData.cs @@ -0,0 +1,66 @@ +using Foundation; + +namespace Blahblah.Library.Network; + +class NSMultipartFormData +{ + public const string DefaultMimeType = "application/octet-stream"; + + public NSMutableData Data => boundaryData; + + readonly string boundary; + readonly NSMutableData boundaryData; + + public string ContentType => $"{NetworkHelper.ContentFormData}; boundary={boundary}"; + + public NSMultipartFormData() + { + var data = new byte[12]; + for (var i = 0; i < 12; i++) + { + data[i] = (byte)(Random.Shared.Next(0x30, 0x3a)); + } + var id = Convert.ToBase64String(data); + boundary = $"----WebKitFormBoundary{id}"; + boundaryData = new NSMutableData(); + } + + public void AddTextField(string name, string value) + { + // Content-Type: text/plain; charset=ISO-8859-1 + // Content-Transfer-Encoding: 8bit + + var field = $"--{boundary}\r\n" + + $"Content-Disposition: form-data; name=\"{name}\"\r\n\r\n" + + $"{value}\r\n"; + boundaryData.AppendData(NSData.FromString(field, NSStringEncoding.UTF8)); + } + + public void AddDataField(string name, Stream data, string? fileName = null, string mimeType = DefaultMimeType) + { + if (fileName == null) + { + fileName = ""; + } + else if (!string.IsNullOrEmpty(fileName)) + { + fileName = $"; filename=\"{fileName}\""; + } + var field = $"--{boundary}\r\n" + + $"Content-Disposition: form-data; name=\"{name}\"{fileName}\r\n" + + $"Content-Type: {mimeType}\r\n\r\n"; + boundaryData.AppendData(NSData.FromString(field, NSStringEncoding.UTF8)); + + var stream = NSData.FromStream(data); + if (stream != null) + { + boundaryData.AppendData(stream); + } + boundaryData.AppendData(NSData.FromString("\r\n")); + } + + public void AddEnd() + { + boundaryData.AppendData(NSData.FromString($"--{boundary}--")); + } +} diff --git a/Network/Platforms/iOS/Tasks/ContentTask.cs b/Network/Platforms/iOS/Tasks/ContentTask.cs index 83b00bf..b59ef0f 100644 --- a/Network/Platforms/iOS/Tasks/ContentTask.cs +++ b/Network/Platforms/iOS/Tasks/ContentTask.cs @@ -2,7 +2,7 @@ namespace Blahblah.Library.Network; -public abstract class ContentTask : NetworkTask +abstract class ContentTask : NetworkTask { public StepHandler? Step { get; set; } diff --git a/Network/Platforms/iOS/Tasks/DownloadTask.cs b/Network/Platforms/iOS/Tasks/DownloadTask.cs index 3ed61ce..1816dce 100644 --- a/Network/Platforms/iOS/Tasks/DownloadTask.cs +++ b/Network/Platforms/iOS/Tasks/DownloadTask.cs @@ -2,7 +2,7 @@ namespace Blahblah.Library.Network; -public class DownloadTask : ContentTask +class DownloadTask : ContentTask { public string FilePath { get; } diff --git a/Network/Platforms/iOS/Tasks/FileTask.cs b/Network/Platforms/iOS/Tasks/FileTask.cs index 6131969..b74f205 100644 --- a/Network/Platforms/iOS/Tasks/FileTask.cs +++ b/Network/Platforms/iOS/Tasks/FileTask.cs @@ -1,6 +1,6 @@ namespace Blahblah.Library.Network; -public class FileTask : DownloadTask +class FileTask : DownloadTask { public FileTask(string url, string filePath, TaskCompletionSource> source, CancellationToken token) : base(url, filePath, source, token) { diff --git a/Network/Platforms/iOS/Tasks/ImageTask.cs b/Network/Platforms/iOS/Tasks/ImageTask.cs index 951cec3..150281c 100644 --- a/Network/Platforms/iOS/Tasks/ImageTask.cs +++ b/Network/Platforms/iOS/Tasks/ImageTask.cs @@ -3,7 +3,7 @@ using ImageIO; namespace Blahblah.Library.Network; -public class ImageTask : DownloadTask +class ImageTask : DownloadTask { CGImageSource? imageSource; diff --git a/Network/Platforms/iOS/Tasks/StringTask.cs b/Network/Platforms/iOS/Tasks/StringTask.cs index 46cebc2..5ba6c66 100644 --- a/Network/Platforms/iOS/Tasks/StringTask.cs +++ b/Network/Platforms/iOS/Tasks/StringTask.cs @@ -2,7 +2,7 @@ namespace Blahblah.Library.Network; -public class StringTask : ContentTask +class StringTask : ContentTask { public StringHandler? Process { get; set; }