multipart/form-data support
This commit is contained in:
parent
e49ee1551d
commit
b501dc5e76
59
Network/MultipartFormData.cs
Normal file
59
Network/MultipartFormData.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net7.0-ios</TargetFrameworks>
|
<TargetFrameworks>net8.0-ios</TargetFrameworks>
|
||||||
|
|
||||||
<RootNamespace>Blahblah.Library.Network</RootNamespace>
|
<RootNamespace>Blahblah.Library.Network</RootNamespace>
|
||||||
<UseMaui>true</UseMaui>
|
<UseMaui>true</UseMaui>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@ -12,4 +13,9 @@
|
|||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
|
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
|
||||||
</PropertyGroup>
|
</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>
|
</Project>
|
||||||
|
@ -8,8 +8,12 @@ public partial class NetworkHelper
|
|||||||
public const string AcceptJpegImage = "image/jpeg,image/*,*/*;q=0.8";
|
public const string AcceptJpegImage = "image/jpeg,image/*,*/*;q=0.8";
|
||||||
public const string AcceptJson = "application/json";
|
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 AcceptHeader = "Accept";
|
||||||
const string ReferrerHeader = "Referer";
|
const string ReferrerHeader = "Referer";
|
||||||
|
const string ContentTypeHeader = "Content-Type";
|
||||||
|
|
||||||
const int Timeout = 30;
|
const int Timeout = 30;
|
||||||
const int ImageTimeout = 60;
|
const int ImageTimeout = 60;
|
||||||
|
@ -10,6 +10,20 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate
|
|||||||
return (int)response.StatusCode is >= 200 and < 300;
|
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;
|
NSUrlSession? urlSession;
|
||||||
|
|
||||||
readonly Dictionary<NSUrlSessionTask, NetworkTask> tasks = new();
|
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)
|
public partial Task<NetworkResult<string>> GetContentAsync(string url, StringHandler? process, CancellationToken token)
|
||||||
{
|
{
|
||||||
var uri = NSUrl.FromString(url) ?? throw new ArgumentNullException(nameof(url));
|
var request = CreateUrlRequest(url, Accept ?? AcceptHttp, Referrer);
|
||||||
var request = new NSMutableUrlRequest(uri)
|
|
||||||
{
|
|
||||||
[AcceptHeader] = Accept ?? AcceptHttp,
|
|
||||||
[ReferrerHeader] = Referrer
|
|
||||||
};
|
|
||||||
|
|
||||||
var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null");
|
var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null");
|
||||||
var taskSource = new TaskCompletionSource<NetworkResult<string>>();
|
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)
|
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 = CreateUrlRequest(url, Accept ?? AcceptImage, Referrer);
|
||||||
var request = new NSMutableUrlRequest(uri)
|
|
||||||
{
|
|
||||||
[AcceptHeader] = Accept ?? AcceptImage,
|
|
||||||
[ReferrerHeader] = Referrer
|
|
||||||
};
|
|
||||||
|
|
||||||
var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null");
|
var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null");
|
||||||
var taskSource = new TaskCompletionSource<NetworkResult<bool>>();
|
var taskSource = new TaskCompletionSource<NetworkResult<bool>>();
|
||||||
@ -115,15 +119,44 @@ partial class NetworkHelper : NSObject, INSUrlSessionDataDelegate
|
|||||||
return taskSource.Task;
|
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 = CreateUrlRequest(url, Accept ?? AcceptJson, Referrer);
|
||||||
var request = new NSMutableUrlRequest(uri)
|
request.HttpMethod = "POST";
|
||||||
{
|
|
||||||
HttpMethod = "POST",
|
|
||||||
[AcceptHeader] = Accept ?? AcceptImage,
|
|
||||||
[ReferrerHeader] = Referrer
|
|
||||||
};
|
|
||||||
prepare?.Invoke(request);
|
prepare?.Invoke(request);
|
||||||
|
|
||||||
var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null");
|
var task = (urlSession?.CreateDataTask(request)) ?? throw new NullReferenceException("Url request is null");
|
||||||
|
66
Network/Platforms/iOS/Structs/NSMultipartFormData.cs
Normal file
66
Network/Platforms/iOS/Structs/NSMultipartFormData.cs
Normal 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}--"));
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Blahblah.Library.Network;
|
namespace Blahblah.Library.Network;
|
||||||
|
|
||||||
public abstract class ContentTask<T, TResult> : NetworkTask
|
abstract class ContentTask<T, TResult> : NetworkTask
|
||||||
{
|
{
|
||||||
public StepHandler<T>? Step { get; set; }
|
public StepHandler<T>? Step { get; set; }
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Blahblah.Library.Network;
|
namespace Blahblah.Library.Network;
|
||||||
|
|
||||||
public class DownloadTask<T> : ContentTask<T, bool>
|
class DownloadTask<T> : ContentTask<T, bool>
|
||||||
{
|
{
|
||||||
public string FilePath { get; }
|
public string FilePath { get; }
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
namespace Blahblah.Library.Network;
|
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)
|
public FileTask(string url, string filePath, TaskCompletionSource<NetworkResult<bool>> source, CancellationToken token) : base(url, filePath, source, token)
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,7 @@ using ImageIO;
|
|||||||
|
|
||||||
namespace Blahblah.Library.Network;
|
namespace Blahblah.Library.Network;
|
||||||
|
|
||||||
public class ImageTask : DownloadTask<CGImage>
|
class ImageTask : DownloadTask<CGImage>
|
||||||
{
|
{
|
||||||
CGImageSource? imageSource;
|
CGImageSource? imageSource;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Blahblah.Library.Network;
|
namespace Blahblah.Library.Network;
|
||||||
|
|
||||||
public class StringTask : ContentTask<string, string>
|
class StringTask : ContentTask<string, string>
|
||||||
{
|
{
|
||||||
public StringHandler? Process { get; set; }
|
public StringHandler? Process { get; set; }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user