multipart/form-data support

This commit is contained in:
Tsanie Lily 2023-08-25 10:53:13 +08:00
parent e49ee1551d
commit b501dc5e76
10 changed files with 194 additions and 26 deletions

View File

@ -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<T> From<T>(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<T> : 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;
}
}

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0-ios</TargetFrameworks>
<TargetFrameworks>net8.0-ios</TargetFrameworks>
<RootNamespace>Blahblah.Library.Network</RootNamespace>
<UseMaui>true</UseMaui>
<Nullable>enable</Nullable>
@ -12,4 +13,9 @@
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.0-preview.6.8686" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.0-preview.6.8686" />
</ItemGroup>
</Project>

View File

@ -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;

View File

@ -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<NSUrlSessionTask, NetworkTask> tasks = new();
@ -71,12 +85,7 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate
public partial Task<NetworkResult<string>> 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<NetworkResult<string>>();
@ -94,12 +103,7 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate
public Task<NetworkResult<bool>> GetImageAsync(string url, string filePath, StepHandler<CGImage>? 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<NetworkResult<bool>>();
@ -115,15 +119,44 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate
return taskSource.Task;
}
public Task<NetworkResult<string>> PostAsync(string url, Action<NSMutableUrlRequest>? prepare = null, CancellationToken token = default)
public Task<NetworkResult<string>> 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<NetworkResult<string>> 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<NetworkResult<string>> RequestAsync(string url, Action<NSMutableUrlRequest>? 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");

View File

@ -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}--"));
}
}

View File

@ -2,7 +2,7 @@
namespace Blahblah.Library.Network;
public abstract class ContentTask<T, TResult> : NetworkTask
abstract class ContentTask<T, TResult> : NetworkTask
{
public StepHandler<T>? Step { get; set; }

View File

@ -2,7 +2,7 @@
namespace Blahblah.Library.Network;
public class DownloadTask<T> : ContentTask<T, bool>
class DownloadTask<T> : ContentTask<T, bool>
{
public string FilePath { get; }

View File

@ -1,6 +1,6 @@
namespace Blahblah.Library.Network;
public class FileTask : DownloadTask<float>
class FileTask : DownloadTask<float>
{
public FileTask(string url, string filePath, TaskCompletionSource<NetworkResult<bool>> source, CancellationToken token) : base(url, filePath, source, token)
{

View File

@ -3,7 +3,7 @@ using ImageIO;
namespace Blahblah.Library.Network;
public class ImageTask : DownloadTask<CGImage>
class ImageTask : DownloadTask<CGImage>
{
CGImageSource? imageSource;

View File

@ -2,7 +2,7 @@
namespace Blahblah.Library.Network;
public class StringTask : ContentTask<string, string>
class StringTask : ContentTask<string, string>
{
public StringHandler? Process { get; set; }