mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-23 11:51:09 +08:00
871 lines
32 KiB
C#
871 lines
32 KiB
C#
// ------------------------------------------------------------------------
|
||
// 版权信息
|
||
// 版权归百小僧及百签科技(广东)有限公司所有。
|
||
// 所有权利保留。
|
||
// 官方网站:https://baiqian.com
|
||
//
|
||
// 许可证信息
|
||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
|
||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||
// ------------------------------------------------------------------------
|
||
|
||
using System.Globalization;
|
||
using System.Net.Http.Headers;
|
||
using System.Net.Mime;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Threading.Channels;
|
||
|
||
using ThingsGateway.Extension;
|
||
using ThingsGateway.Utilities;
|
||
|
||
namespace ThingsGateway.HttpRemote;
|
||
|
||
/// <summary>
|
||
/// <see cref="MultipartFormDataContent" /> 构建器
|
||
/// </summary>
|
||
public sealed class HttpMultipartFormDataBuilder
|
||
{
|
||
/// <inheritdoc cref="HttpRequestBuilder" />
|
||
internal readonly HttpRequestBuilder _httpRequestBuilder;
|
||
|
||
/// <summary>
|
||
/// <see cref="MultipartFormDataItem" /> 集合
|
||
/// </summary>
|
||
internal readonly List<MultipartFormDataItem> _partContents;
|
||
|
||
/// <summary>
|
||
/// <inheritdoc cref="HttpMultipartFormDataBuilder" />
|
||
/// </summary>
|
||
/// <param name="httpRequestBuilder">
|
||
/// <see cref="HttpRequestBuilder" />
|
||
/// </param>
|
||
internal HttpMultipartFormDataBuilder(HttpRequestBuilder httpRequestBuilder)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(httpRequestBuilder);
|
||
|
||
_httpRequestBuilder = httpRequestBuilder;
|
||
_partContents = [];
|
||
}
|
||
|
||
/// <summary>
|
||
/// 多部分表单内容的边界
|
||
/// </summary>
|
||
public string? Boundary { get; set; } = $"--------------------------{DateTime.Now.Ticks:x}";
|
||
|
||
/// <summary>
|
||
/// 是否移除默认的多部分内容的 <c>Content-Type</c>
|
||
/// </summary>
|
||
/// <remarks>默认值为:<c>true</c>。</remarks>
|
||
public bool OmitContentType { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// 用于处理在添加 <see cref="HttpContent" /> 表单项内容时的操作
|
||
/// </summary>
|
||
internal Action<HttpContent, string>? OnPreAddContent { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 设置多部分表单内容的边界
|
||
/// </summary>
|
||
/// <param name="boundary">多部分表单内容的边界</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder SetBoundary(string? boundary)
|
||
{
|
||
Boundary = boundary;
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置用于处理在添加 <see cref="HttpContent" /> 表单项内容时的操作
|
||
/// </summary>
|
||
/// <remarks>支持多次调用。</remarks>
|
||
/// <param name="configure">自定义配置委托</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder SetOnPreAddContent(Action<HttpContent, string> configure)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(configure);
|
||
|
||
// 如果 OnPreAddContent 未设置则直接赋值
|
||
if (OnPreAddContent is null)
|
||
{
|
||
OnPreAddContent = configure;
|
||
}
|
||
// 否则创建级联调用委托
|
||
else
|
||
{
|
||
// 复制一个新的委托避免死循环
|
||
var originalOnPreAddContent = OnPreAddContent;
|
||
|
||
OnPreAddContent = (content, name) =>
|
||
{
|
||
originalOnPreAddContent.Invoke(content, name);
|
||
configure.Invoke(content, name);
|
||
};
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加 JSON 内容
|
||
/// </summary>
|
||
/// <param name="rawJson">JSON 字符串/原始对象</param>
|
||
/// <param name="name">表单名称。该值不为空时作为表单的一项。否则将遍历对象类型的每一个公开属性作为表单的项。</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
/// <exception cref="JsonException"></exception>
|
||
public HttpMultipartFormDataBuilder AddJson(object? rawJson, string? name = null, Encoding? contentEncoding = null,
|
||
string? contentType = null)
|
||
{
|
||
// 检查是否配置表单名或不是字符串类型
|
||
if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString)
|
||
{
|
||
return AddObject(rawJson, name, contentType ?? MediaTypeNames.Application.Json, contentEncoding);
|
||
}
|
||
|
||
// 尝试验证并获取 JsonDocument 实例(需 using)
|
||
var jsonDocument = JsonUtility.Parse(rawString);
|
||
|
||
// 添加请求结束时需要释放的对象
|
||
_httpRequestBuilder.AddDisposable(jsonDocument);
|
||
|
||
return AddObject(jsonDocument, name, contentType ?? MediaTypeNames.Application.Json, contentEncoding);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加单个表单项内容
|
||
/// </summary>
|
||
/// <param name="value">表单值</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddFormItem(object? value, string name, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
return AddObject(value, name, MediaTypeNames.Text.Plain, contentEncoding);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加 HTML 内容
|
||
/// </summary>
|
||
/// <param name="htmlString">HTML 字符串</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddHtml(string? htmlString, string name, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
return AddObject(htmlString, name, MediaTypeNames.Text.Html, contentEncoding);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加 XML 内容
|
||
/// </summary>
|
||
/// <param name="xmlString">XML 字符串</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddXml(string? xmlString, string name, Encoding? contentEncoding = null,
|
||
string? contentType = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
return AddObject(xmlString, name, contentType ?? MediaTypeNames.Text.Xml, contentEncoding);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加文本内容
|
||
/// </summary>
|
||
/// <param name="text">文本</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddText(string? text, string name, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
return AddObject(text, name, MediaTypeNames.Text.Plain, contentEncoding);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加对象内容
|
||
/// </summary>
|
||
/// <param name="rawObject">原始对象</param>
|
||
/// <param name="name">表单名称。该值不为空时作为表单的一项。否则将遍历对象类型的每一个公开属性作为表单的项。</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddObject(object? rawObject, string? name = null, string? contentType = null,
|
||
Encoding? contentEncoding = null)
|
||
{
|
||
// 解析内容类型字符串
|
||
Encoding? encoding = null;
|
||
var mediaType = string.IsNullOrWhiteSpace(contentType)
|
||
? Constants.TEXT_PLAIN_MIME_TYPE
|
||
: ParseContentType(contentType, contentEncoding, out encoding);
|
||
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(mediaType);
|
||
|
||
// 检查是否配置表单名
|
||
if (!string.IsNullOrWhiteSpace(name))
|
||
{
|
||
_partContents.Add(new MultipartFormDataItem(name)
|
||
{
|
||
ContentType = mediaType,
|
||
RawContent = rawObject,
|
||
ContentEncoding = encoding
|
||
});
|
||
|
||
return this;
|
||
}
|
||
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(rawObject);
|
||
|
||
// 将对象转换为字典集合再追加
|
||
var formDataItems = rawObject.ObjectToDictionary()!;
|
||
|
||
// 遍历字典集合并逐条追加
|
||
foreach (var (key, rawContent) in formDataItems)
|
||
{
|
||
// 检查原始请求内容是否是 MultipartFile 类型
|
||
if (rawContent is MultipartFile multipartFile)
|
||
{
|
||
AddFile(multipartFile);
|
||
}
|
||
else
|
||
{
|
||
_partContents.Add(new MultipartFormDataItem(key.ToCultureString(CultureInfo.InvariantCulture)!)
|
||
{
|
||
ContentType = Helpers.GetContentTypeOrDefault(rawContent, MediaTypeNames.Text.Plain),
|
||
RawContent = rawContent,
|
||
ContentEncoding = encoding
|
||
});
|
||
}
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从互联网 URL 中添加文件
|
||
/// </summary>
|
||
/// <remarks>文件大小限制在 <c>100MB</c> 以内。</remarks>
|
||
/// <param name="url">互联网 URL 地址</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="fileName">文件的名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
/// <exception cref="ArgumentException"></exception>
|
||
/// <exception cref="InvalidOperationException"></exception>
|
||
public HttpMultipartFormDataBuilder AddFileFromRemote(string url, string name = "file", string? fileName = null,
|
||
string? contentType = null, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(url);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
// 尝试获取文件的名称
|
||
var newFileName = fileName ?? Helpers.GetFileNameFromUri(new Uri(url, UriKind.Absolute));
|
||
|
||
// 从互联网 URL 地址中加载流
|
||
var fileStream = Helpers.GetStreamFromRemote(url);
|
||
|
||
return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
|
||
true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 Base64 字符串中添加文件
|
||
/// </summary>
|
||
/// <remarks>文件大小限制在 <c>100MB</c> 以内。</remarks>
|
||
/// <param name="base64String">Base64 字符串</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="fileName">文件的名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
/// <exception cref="InvalidOperationException"></exception>
|
||
public HttpMultipartFormDataBuilder AddFileFromBase64String(string base64String, string name = "file",
|
||
string? fileName = null, string? contentType = null, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(base64String);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
// 将 Base64 字符串转换成字节数组
|
||
var bytes = Convert.FromBase64String(base64String);
|
||
|
||
// 获取字节数组长度
|
||
var fileLength = bytes.Length;
|
||
|
||
// 限制文件字节数组大小在 100MB 以内
|
||
const long maxFileSizeInBytes = 104857600L;
|
||
if (fileLength > maxFileSizeInBytes)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"The file size exceeds the maximum allowed size of `{maxFileSizeInBytes.ToSizeUnits("MB"):F2}MB`.");
|
||
}
|
||
|
||
return AddByteArray(bytes, name, fileName, contentType, contentEncoding);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从本地路径中添加文件
|
||
/// </summary>
|
||
/// <param name="filePath">文件路径</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="fileName">文件的名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddFileAsStream(string filePath, string name = "file", string? fileName = null,
|
||
string? contentType = null, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
// 检查文件是否存在
|
||
if (!File.Exists(filePath))
|
||
{
|
||
throw new FileNotFoundException($"The specified file `{filePath}` does not exist.");
|
||
}
|
||
|
||
// 获取文件的名称
|
||
var newFileName = fileName ?? Path.GetFileName(filePath);
|
||
|
||
// 读取文件流(没有 using)
|
||
var fileStream = File.OpenRead(filePath);
|
||
|
||
return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
|
||
true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从本地路径中添加文件(带文件传输进度)
|
||
/// </summary>
|
||
/// <param name="filePath">文件路径</param>
|
||
/// <param name="progressChannel">文件传输进度信息的通道</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="fileName">文件的名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddFileWithProgressAsStream(string filePath,
|
||
Channel<FileTransferProgress> progressChannel, string name = "file", string? fileName = null,
|
||
string? contentType = null, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
ArgumentNullException.ThrowIfNull(progressChannel);
|
||
|
||
// 检查文件是否存在
|
||
if (!File.Exists(filePath))
|
||
{
|
||
throw new FileNotFoundException($"The specified file `{filePath}` does not exist.");
|
||
}
|
||
|
||
// 获取文件的名称
|
||
var newFileName = fileName ?? Path.GetFileName(filePath);
|
||
|
||
// 读取文件流(没有 using)
|
||
var fileStream = File.OpenRead(filePath);
|
||
|
||
// 初始化带读写进度的文件流
|
||
var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName);
|
||
|
||
return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding,
|
||
true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从本地路径中添加文件
|
||
/// </summary>
|
||
/// <param name="filePath">文件路径</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="fileName">文件的名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddFileAsByteArray(string filePath, string name = "file",
|
||
string? fileName = null, string? contentType = null, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
// 检查文件是否存在
|
||
if (!File.Exists(filePath))
|
||
{
|
||
throw new FileNotFoundException($"The specified file `{filePath}` does not exist.");
|
||
}
|
||
|
||
// 获取文件的名称
|
||
var newFileName = fileName ?? Path.GetFileName(filePath);
|
||
|
||
// 读取文件字节数组
|
||
var bytes = File.ReadAllBytes(filePath);
|
||
|
||
return AddByteArray(bytes, name, newFileName, contentType, contentEncoding);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加文件
|
||
/// </summary>
|
||
/// <remarks>使用 <c>MultipartFile.CreateFrom[Source]</c> 静态方法创建。</remarks>
|
||
/// <param name="multipartFile">
|
||
/// <see cref="MultipartFile" />
|
||
/// </param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddFile(MultipartFile multipartFile)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(multipartFile);
|
||
|
||
switch (multipartFile.FileSourceType)
|
||
{
|
||
// 字节数组
|
||
case FileSourceType.ByteArray:
|
||
return AddByteArray((byte[])multipartFile.Source!, multipartFile.Name!, multipartFile.FileName,
|
||
multipartFile.ContentType, multipartFile.ContentEncoding);
|
||
// Stream
|
||
case FileSourceType.Stream:
|
||
return AddStream((Stream)multipartFile.Source!, multipartFile.Name!, multipartFile.FileName,
|
||
multipartFile.ContentType, multipartFile.ContentEncoding);
|
||
// 本地文件路径
|
||
case FileSourceType.Path:
|
||
return AddFileAsStream((string)multipartFile.Source!, multipartFile.Name!, multipartFile.FileName,
|
||
multipartFile.ContentType, multipartFile.ContentEncoding);
|
||
// Base64 字符串文件
|
||
case FileSourceType.Base64String:
|
||
return AddFileFromBase64String((string)multipartFile.Source!, multipartFile.Name!,
|
||
multipartFile.FileName, multipartFile.ContentType, multipartFile.ContentEncoding);
|
||
// 互联网文件地址
|
||
case FileSourceType.Remote:
|
||
return AddFileFromRemote((string)multipartFile.Source!, multipartFile.Name!, multipartFile.FileName,
|
||
multipartFile.ContentType, multipartFile.ContentEncoding);
|
||
// 不做处理
|
||
case FileSourceType.None:
|
||
default:
|
||
return this;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加流
|
||
/// </summary>
|
||
/// <param name="stream">
|
||
/// <see cref="Stream" />
|
||
/// </param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="fileName">文件的名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <param name="disposeStreamOnRequestCompletion">是否在请求结束后自动释放流。默认值为:<c>false</c></param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddStream(Stream stream, string name = "file", string? fileName = null,
|
||
string? contentType = null, Encoding? contentEncoding = null, bool disposeStreamOnRequestCompletion = false)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(stream);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
// 解析内容类型字符串
|
||
var mediaType = ParseContentType(contentType, contentEncoding, out var encoding);
|
||
|
||
// 获取文件 MIME 类型
|
||
var mimeType = !string.IsNullOrWhiteSpace(mediaType) ? mediaType :
|
||
string.IsNullOrWhiteSpace(fileName) ? MediaTypeNames.Application.Octet :
|
||
FileTypeMapper.GetContentType(fileName);
|
||
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(mimeType);
|
||
|
||
_partContents.Add(new MultipartFormDataItem(name)
|
||
{
|
||
ContentType = mimeType,
|
||
RawContent = stream,
|
||
ContentEncoding = encoding,
|
||
FileName = fileName
|
||
});
|
||
|
||
// 是否在请求结束后自动释放流
|
||
if (disposeStreamOnRequestCompletion)
|
||
{
|
||
_httpRequestBuilder.AddDisposable(stream);
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加字节数组
|
||
/// </summary>
|
||
/// <param name="byteArray">字节数组</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="fileName">文件的名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddByteArray(byte[] byteArray, string name = "file", string? fileName = null,
|
||
string? contentType = null, Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(byteArray);
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
// 解析内容类型字符串
|
||
var mediaType = ParseContentType(contentType, contentEncoding, out var encoding);
|
||
|
||
// 获取文件 MIME 类型
|
||
var mimeType = !string.IsNullOrWhiteSpace(mediaType) ? mediaType :
|
||
string.IsNullOrWhiteSpace(fileName) ? MediaTypeNames.Application.Octet :
|
||
FileTypeMapper.GetContentType(fileName);
|
||
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(mimeType);
|
||
|
||
_partContents.Add(new MultipartFormDataItem(name)
|
||
{
|
||
ContentType = mimeType,
|
||
RawContent = byteArray,
|
||
ContentEncoding = encoding,
|
||
FileName = fileName
|
||
});
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加 URL 编码表单
|
||
/// </summary>
|
||
/// <param name="rawObject">原始对象</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <param name="useStringContent">
|
||
/// 是否使用 <see cref="StringContent" /> 构建
|
||
/// <see cref="FormUrlEncodedContent" />。默认 <c>false</c>。
|
||
/// </param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddFormUrlEncoded(object? rawObject, string name,
|
||
Encoding? contentEncoding = null, bool useStringContent = false)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
_partContents.Add(new MultipartFormDataItem(name)
|
||
{
|
||
ContentType = MediaTypeNames.Application.FormUrlEncoded,
|
||
RawContent = rawObject,
|
||
ContentEncoding = contentEncoding
|
||
});
|
||
|
||
// 检查是否启用 StringContent 方式构建 application/x-www-form-urlencoded 请求内容
|
||
if (useStringContent)
|
||
{
|
||
_httpRequestBuilder.AddStringContentForFormUrlEncodedContentProcessor();
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加多部分表单内容
|
||
/// </summary>
|
||
/// <param name="rawObject">原始对象</param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder AddMultipartFormData(object? rawObject, string name,
|
||
Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||
|
||
_partContents.Add(new MultipartFormDataItem(name)
|
||
{
|
||
ContentType = MediaTypeNames.Multipart.FormData,
|
||
RawContent = rawObject,
|
||
ContentEncoding = contentEncoding
|
||
});
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加 <see cref="HttpContent" />
|
||
/// </summary>
|
||
/// <param name="httpContent">
|
||
/// <see cref="HttpContent" />
|
||
/// </param>
|
||
/// <param name="name">表单名称</param>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder Add(HttpContent httpContent, string? name, string? contentType = null,
|
||
Encoding? contentEncoding = null)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(httpContent);
|
||
|
||
// 尝试从 ContentDisposition 中解析 Name
|
||
var formName = string.IsNullOrWhiteSpace(name) ? httpContent.Headers.ContentDisposition?.Name : name;
|
||
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(formName, nameof(name));
|
||
|
||
string? mediaType;
|
||
Encoding? encoding = null;
|
||
MediaTypeHeaderValue? mediaTypeHeaderValue = null;
|
||
|
||
// 空检查
|
||
if (!string.IsNullOrWhiteSpace(contentType))
|
||
{
|
||
mediaType = ParseContentType(contentType, contentEncoding, out encoding);
|
||
}
|
||
else
|
||
{
|
||
mediaTypeHeaderValue = httpContent.Headers.ContentType;
|
||
mediaType = mediaTypeHeaderValue?.MediaType;
|
||
}
|
||
|
||
// 尝试从 FileName 中解析 MediaType
|
||
if (string.IsNullOrWhiteSpace(mediaType))
|
||
{
|
||
mediaType = FileTypeMapper.GetContentType(
|
||
httpContent.Headers.ContentDisposition?.FileName?.TrimStart('"').TrimEnd('"')!,
|
||
null!);
|
||
}
|
||
|
||
// 空检查
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(mediaType, nameof(contentType));
|
||
|
||
// 设置或解析内容编码
|
||
encoding = contentEncoding ?? encoding ?? (string.IsNullOrWhiteSpace(mediaTypeHeaderValue?.CharSet)
|
||
? null
|
||
: Encoding.GetEncoding(mediaTypeHeaderValue.CharSet));
|
||
|
||
_partContents.Add(new MultipartFormDataItem(formName)
|
||
{
|
||
ContentType = mediaType,
|
||
RawContent = httpContent,
|
||
ContentEncoding = encoding
|
||
});
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置是否移除默认的多部分内容的 <c>Content-Type</c>
|
||
/// </summary>
|
||
/// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
|
||
/// <returns>
|
||
/// <see cref="HttpMultipartFormDataBuilder" />
|
||
/// </returns>
|
||
public HttpMultipartFormDataBuilder SetOmitContentType(bool omit)
|
||
{
|
||
OmitContentType = omit;
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建 <see cref="MultipartFormDataContent" /> 实例
|
||
/// </summary>
|
||
/// <param name="httpRemoteOptions">
|
||
/// <see cref="HttpRemoteOptions" />
|
||
/// </param>
|
||
/// <param name="httpContentProcessorFactory">
|
||
/// <see cref="IHttpContentProcessorFactory" />
|
||
/// </param>
|
||
/// <param name="processors"><see cref="IHttpContentProcessor" /> 集合</param>
|
||
/// <returns>
|
||
/// <see cref="MultipartFormDataContent" />
|
||
/// </returns>
|
||
internal MultipartFormDataContent? Build(HttpRemoteOptions httpRemoteOptions,
|
||
IHttpContentProcessorFactory httpContentProcessorFactory,
|
||
params IHttpContentProcessor[]? processors)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(httpRemoteOptions);
|
||
ArgumentNullException.ThrowIfNull(httpContentProcessorFactory);
|
||
|
||
// 空检查
|
||
if (_partContents.IsNullOrEmpty())
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// 获取多部分表单内容的边界;注意:这里可能出现前后双引号问题
|
||
var boundary = Boundary?.TrimStart('"').TrimEnd('"');
|
||
|
||
// 初始化 multipartFormDataContent 实例
|
||
var multipartFormDataContent = string.IsNullOrWhiteSpace(boundary)
|
||
? new MultipartFormDataContent()
|
||
: new MultipartFormDataContent(boundary);
|
||
|
||
// 处理 OSS 对象存储服务必须设置 Content-Type 问题
|
||
if (!string.IsNullOrWhiteSpace(boundary))
|
||
{
|
||
multipartFormDataContent.Headers.ContentType =
|
||
MediaTypeHeaderValue.Parse($"{MediaTypeNames.Multipart.FormData}; boundary={boundary}");
|
||
}
|
||
|
||
// 逐条遍历添加
|
||
foreach (var dataItem in _partContents)
|
||
{
|
||
// 构建 HttpContent 实例
|
||
var httpContent = BuildHttpContent(dataItem, httpContentProcessorFactory, processors);
|
||
|
||
// 空检查
|
||
if (httpContent is null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 检查是否移除默认的多部分内容的 Content-Type,解决对接 Java 程序时可能出现失败问题
|
||
if (OmitContentType)
|
||
{
|
||
httpContent.Headers.ContentType = null;
|
||
}
|
||
|
||
// 调用用于处理在添加 HttpContent 表单项内容时的操作
|
||
OnPreAddContent?.Invoke(httpContent, dataItem.Name);
|
||
|
||
// 添加 HttpContent 表单项内容
|
||
multipartFormDataContent.Add(httpContent, dataItem.Name);
|
||
}
|
||
|
||
return multipartFormDataContent;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建 <see cref="HttpContent" /> 实例
|
||
/// </summary>
|
||
/// <param name="multipartFormDataItem">
|
||
/// <see cref="MultipartFormDataItem" />
|
||
/// </param>
|
||
/// <param name="httpContentProcessorFactory">
|
||
/// <see cref="IHttpContentProcessorFactory" />
|
||
/// </param>
|
||
/// <param name="processors"><see cref="IHttpContentProcessor" /> 集合</param>
|
||
/// <returns>
|
||
/// <see cref="HttpContent" />
|
||
/// </returns>
|
||
internal static HttpContent? BuildHttpContent(MultipartFormDataItem multipartFormDataItem,
|
||
IHttpContentProcessorFactory httpContentProcessorFactory, params IHttpContentProcessor[]? processors)
|
||
{
|
||
// 空检查
|
||
ArgumentNullException.ThrowIfNull(multipartFormDataItem);
|
||
ArgumentNullException.ThrowIfNull(httpContentProcessorFactory);
|
||
|
||
// 空检查
|
||
var contentType = multipartFormDataItem.ContentType;
|
||
ArgumentException.ThrowIfNullOrWhiteSpace(contentType);
|
||
|
||
// 构建 HttpContent 实例
|
||
var httpContent = httpContentProcessorFactory.Build(multipartFormDataItem.RawContent, contentType,
|
||
multipartFormDataItem.ContentEncoding, processors);
|
||
|
||
// 空检查
|
||
if (httpContent is not null && httpContent.Headers.ContentDisposition is null)
|
||
{
|
||
// 设置表单项内容 Content-Disposition 标头
|
||
httpContent.Headers.ContentDisposition =
|
||
new ContentDispositionHeaderValue(Constants.FORM_DATA_DISPOSITION_TYPE)
|
||
{
|
||
Name = multipartFormDataItem.Name.AddQuotes(),
|
||
FileName =
|
||
(string.IsNullOrWhiteSpace(multipartFormDataItem.FileName) &&
|
||
contentType.IsIn([MediaTypeNames.Application.Octet], StringComparer.OrdinalIgnoreCase)
|
||
// 解决发送文件或二进制【未设置】文件名问题
|
||
? $"Unnamed_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid().ToString("N")[..8]}"
|
||
: multipartFormDataItem.FileName).AddQuotes()
|
||
};
|
||
}
|
||
|
||
return httpContent;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析内容类型字符串
|
||
/// </summary>
|
||
/// <param name="contentType">内容类型</param>
|
||
/// <param name="contentEncoding">内容编码</param>
|
||
/// <param name="encoding">内容编码</param>
|
||
/// <returns>
|
||
/// <see cref="string" />
|
||
/// </returns>
|
||
internal static string? ParseContentType(string? contentType, Encoding? contentEncoding, out Encoding? encoding)
|
||
{
|
||
// 空检查
|
||
if (string.IsNullOrWhiteSpace(contentType))
|
||
{
|
||
encoding = null;
|
||
return null;
|
||
}
|
||
|
||
// 解析内容类型字符串
|
||
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
|
||
|
||
// 解析/设置内容编码
|
||
encoding = contentEncoding ?? (!string.IsNullOrWhiteSpace(mediaTypeHeaderValue.CharSet)
|
||
? Encoding.GetEncoding(mediaTypeHeaderValue.CharSet)
|
||
: contentEncoding);
|
||
|
||
return mediaTypeHeaderValue.MediaType;
|
||
}
|
||
} |