diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..509c0d16 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,22 @@ +# Copilot Instructions + +在每次任务完成之后,应该总结经验且更新此文档内容 + +## 现代化改造经验总结 + +本项目在现代化改造中,建议持续遵循以下实践: + +1. 网络层统一使用 `HttpClient` + `HttpRequestMessage`,避免 `HttpWebRequest`。 +2. JSON 处理优先使用 `System.Text.Json`,并尽量配合源生成上下文(`JsonSerializerContext`)提升 AOT 兼容性。 +3. 对关键入参增加早期校验(如 `ArgumentNullException.ThrowIfNull` / `ArgumentException.ThrowIfNullOrWhiteSpace`),减少隐式空引用风险。 +4. 常用集合初始化时尽量预估容量(如 `new List(count)`、`new Dictionary(capacity)`),减少扩容开销。 +5. 字符串拼接优先使用内插字符串(`$"..."`),替代 `string.Format`,可读性更高。 +6. 属性、方法、字段、类型名等的命名需要符合 C# 命名规范,例如: + - 属性、方法、类型名应该使用 PascalCasing 命名风格 + - 局部变量应该使用 camelCase 命名风格 + - 字段应该使用 `_camelCase` 风格,而且尽可能使用 `readonly ` 标记只读。 +7. 最好不要公开字段,请封装为属性进行公开 +8. 考虑可空。但可以对进行网络请求的纯数据结构放宽要求 +9. 结果模型统一走 `QiniuJson` + `JsonSerializerContext`,减少反射序列化路径 +10. 旧 API 的命名保留兼容入口(`[Obsolete]`),新增规范命名 API 并逐步迁移 +11. 禁止通过伪造兼容层保留 `Newtonsoft` 特性;应直接迁移为 `System.Text.Json` 特性与 API \ No newline at end of file diff --git a/README.md b/README.md index 620601ba..0c69d07f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ 5. 然后到 github 网站的该 git 远程仓库的 my-new-feature 分支下发起 Pull Request - ## 许可证 Copyright (c) 2017 [qiniu.com](www.qiniu.com) diff --git a/src/Qiniu.sln b/src/Qiniu.sln index b3a660d0..4059748d 100644 --- a/src/Qiniu.sln +++ b/src/Qiniu.sln @@ -1,12 +1,18 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29102.190 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11512.155 d18.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qiniu", "Qiniu\Qiniu.csproj", "{2F5B0328-DE8B-4B53-A500-3077E340A51B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QiniuTests", "QiniuTests\QiniuTests.csproj", "{E8CB1665-53F7-46A5-9AFD-B85AD08262D0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{552C1A84-F05A-4364-B6FC-41695F015CF8}" + ProjectSection(SolutionItems) = preProject + ..\.github\copilot-instructions.md = ..\.github\copilot-instructions.md + ..\README.md = ..\README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/Qiniu/CDN/BandwidthRequest.cs b/src/Qiniu/CDN/BandwidthRequest.cs index 16e210cc..1a0705f1 100644 --- a/src/Qiniu/CDN/BandwidthRequest.cs +++ b/src/Qiniu/CDN/BandwidthRequest.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Qiniu.Util; namespace Qiniu.CDN { /// @@ -9,25 +10,25 @@ public class BandwidthRequest /// /// 起始日期,例如2016-09-01 /// - [JsonProperty("startDate")] + [JsonPropertyName("startDate")] public string StartDate { get; set; } /// /// 结束日期,例如2016-09-10 /// - [JsonProperty("endDate")] + [JsonPropertyName("endDate")] public string EndDate { get; set; } /// /// 时间粒度((取值:5min / hour /day)) /// - [JsonProperty("granularity")] + [JsonPropertyName("granularity")] public string Granularity { get; set; } /// /// 域名列表,以西文半角分号分割 /// - [JsonProperty("domains")] + [JsonPropertyName("domains")] public string Domains { get; set; } /// @@ -62,7 +63,7 @@ public BandwidthRequest(string startDate, string endDate, string granularity, st /// 请求内容的JSON字符串 public string ToJsonStr() { - return JsonConvert.SerializeObject(this); + return QiniuJson.Serialize(this, QiniuJson.SerializerContext.BandwidthRequest); } } } diff --git a/src/Qiniu/CDN/BandwidthResult.cs b/src/Qiniu/CDN/BandwidthResult.cs index 1120b1aa..96e17105 100644 --- a/src/Qiniu/CDN/BandwidthResult.cs +++ b/src/Qiniu/CDN/BandwidthResult.cs @@ -1,6 +1,6 @@ using System.Text; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.CDN { @@ -12,14 +12,14 @@ public class BandwidthResult : HttpResult /// /// 获取带宽信息 /// - public BandwidthInfo Result + public BandwidthInfo? Result { get { - BandwidthInfo info = null; + BandwidthInfo? info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info = JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.BandwidthInfo); } return info; } diff --git a/src/Qiniu/CDN/CdnManager.cs b/src/Qiniu/CDN/CdnManager.cs index 0d7eeb61..0ec5a3b4 100644 --- a/src/Qiniu/CDN/CdnManager.cs +++ b/src/Qiniu/CDN/CdnManager.cs @@ -27,29 +27,29 @@ public CdnManager(Mac mac) httpManager = new HttpManager(); } - private string refreshEntry() + private string RefreshEntry() { - return string.Format("{0}/v2/tune/refresh", FUSION_API_HOST); + return $"{FUSION_API_HOST}/v2/tune/refresh"; } - private string prefetchEntry() + private string PrefetchEntry() { - return string.Format("{0}/v2/tune/prefetch", FUSION_API_HOST); + return $"{FUSION_API_HOST}/v2/tune/prefetch"; } - private string bandwidthEntry() + private string BandwidthEntry() { - return string.Format("{0}/v2/tune/bandwidth", FUSION_API_HOST); + return $"{FUSION_API_HOST}/v2/tune/bandwidth"; } - private string fluxEntry() + private string FluxEntry() { - return string.Format("{0}/v2/tune/flux", FUSION_API_HOST); + return $"{FUSION_API_HOST}/v2/tune/flux"; } - private string logListEntry() + private string LogListEntry() { - return string.Format("{0}/v2/tune/log/list", FUSION_API_HOST); + return $"{FUSION_API_HOST}/v2/tune/log/list"; } @@ -66,7 +66,7 @@ public RefreshResult RefreshUrlsAndDirs(string[] urls, string[] dirs) try { - string url = refreshEntry(); + string url = RefreshEntry(); string body = request.ToJsonStr(); string token = auth.CreateManageToken(url); @@ -126,7 +126,7 @@ public PrefetchResult PrefetchUrls(string[] urls) try { - string url = prefetchEntry(); + string url = PrefetchEntry(); string body = request.ToJsonStr(); string token = auth.CreateManageToken(url); @@ -173,7 +173,7 @@ public BandwidthResult GetBandwidthData(string[] domains, string startDate, stri try { - string url = bandwidthEntry(); + string url = BandwidthEntry(); string body = request.ToJsonStr(); string token = auth.CreateManageToken(url); @@ -220,7 +220,7 @@ public FluxResult GetFluxData(string[] domains, string startDate, string endDate try { - string url = fluxEntry(); + string url = FluxEntry(); string body = request.ToJsonStr(); string token = auth.CreateManageToken(url); @@ -262,7 +262,7 @@ public LogListResult GetCdnLogList(string[] domains, string day) try { - string url = logListEntry(); + string url = LogListEntry(); string body = request.ToJsonStr(); string token = auth.CreateManageToken(url); diff --git a/src/Qiniu/CDN/FluxRequest.cs b/src/Qiniu/CDN/FluxRequest.cs index 6a3cafe0..7f753454 100644 --- a/src/Qiniu/CDN/FluxRequest.cs +++ b/src/Qiniu/CDN/FluxRequest.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Qiniu.Util; namespace Qiniu.CDN { /// @@ -9,25 +10,25 @@ public class FluxRequest /// /// 起始日期,例如2016-09-01 /// - [JsonProperty("startDate")] + [JsonPropertyName("startDate")] public string StartDate { get; set; } /// /// 结束日期,例如2016-09-10 /// - [JsonProperty("endDate")] + [JsonPropertyName("endDate")] public string EndDate { get; set; } /// /// 时间粒度((取值:5min / hour /day)) /// - [JsonProperty("granularity")] + [JsonPropertyName("granularity")] public string Granularity { get; set; } /// /// 域名列表,以西文半角分号分割 /// - [JsonProperty("domains")] + [JsonPropertyName("domains")] public string Domains { get; set; } /// @@ -62,7 +63,7 @@ public FluxRequest(string startDate, string endDate, string granularity, string /// 请求内容的JSON字符串 public string ToJsonStr() { - return JsonConvert.SerializeObject(this); + return QiniuJson.Serialize(this, QiniuJson.SerializerContext.FluxRequest); } } } diff --git a/src/Qiniu/CDN/FluxResult.cs b/src/Qiniu/CDN/FluxResult.cs index a512f3b6..d7780882 100644 --- a/src/Qiniu/CDN/FluxResult.cs +++ b/src/Qiniu/CDN/FluxResult.cs @@ -1,6 +1,6 @@ using System.Text; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.CDN { @@ -12,14 +12,14 @@ public class FluxResult : HttpResult /// /// 获取流量信息 /// - public FluxInfo Result + public FluxInfo? Result { get { - FluxInfo info = null; + FluxInfo? info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info=JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.FluxInfo); } return info; } diff --git a/src/Qiniu/CDN/LogListRequest.cs b/src/Qiniu/CDN/LogListRequest.cs index 97e7167b..d1bf7eca 100644 --- a/src/Qiniu/CDN/LogListRequest.cs +++ b/src/Qiniu/CDN/LogListRequest.cs @@ -1,6 +1,7 @@ using System.Text; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Qiniu.Util; namespace Qiniu.CDN { /// @@ -11,13 +12,13 @@ public class LogListRequest /// /// 日期,例如 2016-09-01 /// - [JsonProperty("day")] + [JsonPropertyName("day")] public string Day { get; set; } /// /// 域名列表,以西文半角分号分割 /// - [JsonProperty("domains")] + [JsonPropertyName("domains")] public string Domains { get; set; } /// @@ -88,7 +89,7 @@ public LogListRequest(string day, IList domains) /// 请求内容的JSON字符串 public string ToJsonStr() { - return JsonConvert.SerializeObject(this); + return QiniuJson.Serialize(this, QiniuJson.SerializerContext.LogListRequest); } } } diff --git a/src/Qiniu/CDN/LogListResult.cs b/src/Qiniu/CDN/LogListResult.cs index db4614b8..415c359d 100644 --- a/src/Qiniu/CDN/LogListResult.cs +++ b/src/Qiniu/CDN/LogListResult.cs @@ -1,6 +1,6 @@ using System.Text; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.CDN { @@ -12,14 +12,14 @@ public class LogListResult : HttpResult /// /// 获取日志列表信息 /// - public LogListInfo Result + public LogListInfo? Result { get { - LogListInfo info = null; + LogListInfo? info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info=JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.LogListInfo); } return info; } diff --git a/src/Qiniu/CDN/PrefetchRequest.cs b/src/Qiniu/CDN/PrefetchRequest.cs index 5f7d89a7..1314a328 100644 --- a/src/Qiniu/CDN/PrefetchRequest.cs +++ b/src/Qiniu/CDN/PrefetchRequest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Text; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Qiniu.Util; namespace Qiniu.CDN { @@ -16,7 +17,8 @@ public class PrefetchRequest /// 请输入资源 url 完整的绝对路径,由 http:// 或 https:// 开始 /// 资源 url 不支持通配符,例如:不支持 http://www.test.com/abc/*.* /// - [JsonProperty("urls",NullValueHandling=NullValueHandling.Ignore)] + [JsonPropertyName("urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List Urls { get; set; } /// @@ -67,7 +69,7 @@ public void AddUrls(IList urls) /// 请求内容的JSON字符串 public string ToJsonStr() { - return JsonConvert.SerializeObject(this); + return QiniuJson.Serialize(this, QiniuJson.SerializerContext.PrefetchRequest); } } } diff --git a/src/Qiniu/CDN/PrefetchResult.cs b/src/Qiniu/CDN/PrefetchResult.cs index 5a12fb9f..9f77bb28 100644 --- a/src/Qiniu/CDN/PrefetchResult.cs +++ b/src/Qiniu/CDN/PrefetchResult.cs @@ -1,6 +1,6 @@ using System.Text; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.CDN { @@ -12,14 +12,14 @@ public class PrefetchResult : HttpResult /// /// 获取文件预取信息 /// - public PrefetchInfo Result + public PrefetchInfo? Result { get { - PrefetchInfo info = null; + PrefetchInfo? info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info=JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.PrefetchInfo); } return info; } diff --git a/src/Qiniu/CDN/RefreshRequest.cs b/src/Qiniu/CDN/RefreshRequest.cs index 08ac8139..847e896f 100644 --- a/src/Qiniu/CDN/RefreshRequest.cs +++ b/src/Qiniu/CDN/RefreshRequest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Text; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Qiniu.Util; namespace Qiniu.CDN { /// @@ -17,7 +18,8 @@ public class RefreshRequest /// 带参数的 url 刷新,根据其域名缓存配置是否忽略参数缓存决定刷新结果。 /// 如果配置了时间戳防盗链的资源 url 提交时刷新需要去掉 e 和 token 参数 /// - [JsonProperty("urls", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List Urls { get; set; } /// @@ -25,7 +27,8 @@ public class RefreshRequest /// 例如:http://bar.foo.com/dir/, /// 也支持在尾部使用通配符,例如:http://bar.foo.com/dir/* /// - [JsonProperty("dirs", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("dirs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List Dirs { get; set; } /// @@ -100,7 +103,7 @@ public void AddDirs(IList dirs) /// 请求内容的JSON字符串 public string ToJsonStr() { - return JsonConvert.SerializeObject(this); + return QiniuJson.Serialize(this, QiniuJson.SerializerContext.RefreshRequest); } } } diff --git a/src/Qiniu/CDN/RefreshResult.cs b/src/Qiniu/CDN/RefreshResult.cs index 00aa7196..5adcc2eb 100644 --- a/src/Qiniu/CDN/RefreshResult.cs +++ b/src/Qiniu/CDN/RefreshResult.cs @@ -1,6 +1,6 @@ using System.Text; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.CDN { @@ -12,14 +12,14 @@ public class RefreshResult : HttpResult /// /// 获取缓存刷新信息 /// - public RefreshInfo Result + public RefreshInfo? Result { get { - RefreshInfo info = null; + RefreshInfo? info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info=JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.RefreshInfo); } return info; } diff --git a/src/Qiniu/Http/HttpHelper.cs b/src/Qiniu/Http/HttpHelper.cs index bdd72d0c..eb758c75 100644 --- a/src/Qiniu/Http/HttpHelper.cs +++ b/src/Qiniu/Http/HttpHelper.cs @@ -8,94 +8,142 @@ namespace Qiniu.Http /// public class HttpHelper { + public const string ContentTypeTextPlain = "text/plain"; + public const string ContentTypeApplicationJson = "application/json"; + public const string ContentTypeApplicationOctet = "application/octet-stream"; + public const string ContentTypeWwwForm = "application/x-www-form-urlencoded"; + public const string ContentTypeMultipart = "multipart/form-data"; + + public const int StatusCodeOk = 200; + public const int StatusCodePartlyOk = 298; + public const int StatusCodeUndefined = -256; + public const int StatusCodeUserCanceled = -255; + public const int StatusCodeUserPaused = -254; + public const int StatusCodeUserResumed = -253; + public const int StatusCodeNeedRetry = -252; + public const int StatusCodeException = -252; + /// /// 资源类型:普通文本 /// + [Obsolete("Use ContentTypeTextPlain instead.")] public static string CONTENT_TYPE_TEXT_PLAIN = "text/plain"; /// /// 资源类型:JSON字符串 /// + [Obsolete("Use ContentTypeApplicationJson instead.")] public static string CONTENT_TYPE_APP_JSON = "application/json"; /// /// 资源类型:未知类型(数据流) /// + [Obsolete("Use ContentTypeApplicationOctet instead.")] public static string CONTENT_TYPE_APP_OCTET = "application/octet-stream"; /// /// 资源类型:表单数据(键值对) /// + [Obsolete("Use ContentTypeWwwForm instead.")] public static string CONTENT_TYPE_WWW_FORM = "application/x-www-form-urlencoded"; /// /// 资源类型:多分部数据 /// + [Obsolete("Use ContentTypeMultipart instead.")] public static string CONTENT_TYPE_MULTIPART = "multipart/form-data"; /// /// HTTP状态码200 (OK) /// + [Obsolete("Use StatusCodeOk instead.")] public static int STATUS_CODE_OK = 200; /// /// HTTP状态码298 (部分OK) /// + [Obsolete("Use StatusCodePartlyOk instead.")] public static int STATUS_CODE_PARTLY_OK = 298; /// /// 自定义HTTP状态码 (默认值) /// + [Obsolete("Use StatusCodeUndefined instead.")] public static int STATUS_CODE_UNDEF = -256; /// /// 自定义HTTP状态码 (用户取消) /// + [Obsolete("Use StatusCodeUserCanceled instead.")] public static int STATUS_CODE_USER_CANCELED = -255; /// /// 自定义HTTP状态码 (用户暂停) /// + [Obsolete("Use StatusCodeUserPaused instead.")] public static int STATUS_CODE_USER_PAUSED = -254; /// /// 自定义HTTP状态码 (用户继续) /// + [Obsolete("Use StatusCodeUserResumed instead.")] public static int STATUS_CODE_USER_RESUMED = -253; /// /// 自定义HTTP状态码 (需要重试) /// + [Obsolete("Use StatusCodeNeedRetry instead.")] public static int STATUS_CODE_NEED_RETRY= -252; /// /// 自定义HTTP状态码 (异常或错误) /// + [Obsolete("Use StatusCodeException instead.")] public static int STATUS_CODE_EXCEPTION = -252; /// /// 客户端标识 /// /// 客户端标识UA + [Obsolete("Use GetUserAgent instead.")] public static string getUserAgent() { + return GetUserAgent(); + } + + /// + /// 客户端标识 + /// + /// 客户端标识UA + public static string GetUserAgent() + { #if NetStandard string sfx = Environment.MachineName; #else var osInfo = Environment.OSVersion; string sfx = Environment.MachineName + "; " + osInfo.Platform + "; " + osInfo.Version; #endif - return string.Format("{0}/{1} ({2})", QiniuCSharpSDK.ALIAS, QiniuCSharpSDK.VERSION, sfx); + return $"{QiniuCSharpSDK.ALIAS}/{QiniuCSharpSDK.VERSION} ({sfx})"; } /// /// 多部分表单数据(multi-part form-data)的分界(boundary)标识 /// /// 多部分表单数据的boundary + [Obsolete("Use CreateFormDataBoundary instead.")] public static string createFormDataBoundary() + { + return CreateFormDataBoundary(); + } + + /// + /// 多部分表单数据(multi-part form-data)的分界(boundary)标识 + /// + /// 多部分表单数据的boundary + public static string CreateFormDataBoundary() { string now = DateTime.UtcNow.Ticks.ToString(); - return string.Format("-------{0}Boundary{1}", QiniuCSharpSDK.ALIAS, Hashing.CalcMD5(now)); + return $"-------{QiniuCSharpSDK.ALIAS}Boundary{Hashing.CalcMD5(now)}"; } } } diff --git a/src/Qiniu/Http/HttpManager.cs b/src/Qiniu/Http/HttpManager.cs index 083a6030..52bd8d69 100644 --- a/src/Qiniu/Http/HttpManager.cs +++ b/src/Qiniu/Http/HttpManager.cs @@ -1,21 +1,24 @@ -using System; +using Qiniu.Util; + +using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Text; using System.IO; using System.Linq; using System.Net; -using Qiniu.Util; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; namespace Qiniu.Http { /// /// HttpManager for .NET 2.0/3.0/3.5/4.0 /// - public class HttpManager + public class HttpManager : IDisposable { - private bool allowAutoRedirect; - private string userAgent; + private readonly bool _allowAutoRedirect; + private string _userAgent; /// /// 初始化 @@ -23,10 +26,12 @@ public class HttpManager /// 是否允许HttpWebRequest的“重定向”,默认禁止 public HttpManager(bool allowAutoRedirect = false) { - this.allowAutoRedirect = allowAutoRedirect; - userAgent = GetUserAgent(); + _allowAutoRedirect = allowAutoRedirect; + _userAgent = GetUserAgent(); } + private HttpClientHandler? _sharedHttpClientHandler; + /// /// 客户端标识(UserAgent),示例:"SepcifiedClient/1.1 (Universal)" /// @@ -34,7 +39,7 @@ public HttpManager(bool allowAutoRedirect = false) public static string GetUserAgent() { string osDesc = Environment.OSVersion.Platform + "; " + Environment.OSVersion.Version; - return string.Format("{0}/{1} ({2}; {3})", QiniuCSharpSDK.ALIAS, QiniuCSharpSDK.VERSION, QiniuCSharpSDK.RTFX, osDesc); + return $"{QiniuCSharpSDK.ALIAS}/{QiniuCSharpSDK.VERSION} ({QiniuCSharpSDK.RTFX}; {osDesc})"; } /// @@ -43,11 +48,11 @@ public static string GetUserAgent() /// /// 用户自定义的UserAgent /// 客户端标识UA - public void SetUserAgent(string userAgent) + public void SetUserAgent(string? userAgent) { if (!string.IsNullOrEmpty(userAgent)) { - this.userAgent = userAgent; + _userAgent = userAgent; } } @@ -58,14 +63,14 @@ public void SetUserAgent(string userAgent) public static string CreateFormDataBoundary() { string now = DateTime.UtcNow.Ticks.ToString(); - return string.Format("-------{0}Boundary{1}", QiniuCSharpSDK.ALIAS, Hashing.CalcMD5X(now)); + return $"-------{QiniuCSharpSDK.ALIAS}Boundary{Hashing.CalcMD5X(now)}"; } public HttpRequestOptions CreateHttpRequestOptions( string method, string url, - StringDictionary headers, - string token = null + StringDictionary? headers, + string? token = null ) { HttpRequestOptions reqOpts = new HttpRequestOptions(); @@ -78,92 +83,176 @@ public HttpRequestOptions CreateHttpRequestOptions( reqOpts.Headers = headers; } + reqOpts.Headers ??= new StringDictionary(); if (!string.IsNullOrEmpty(token)) { reqOpts.Headers.Add("Authorization", token); } - reqOpts.Headers.Add("User-Agent", userAgent); - reqOpts.AllowAutoRedirect = allowAutoRedirect; + reqOpts.Headers.Add("User-Agent", _userAgent); + reqOpts.AllowAutoRedirect = _allowAutoRedirect; return reqOpts; } - public HttpResult CreateHttpResult(HttpWebResponse wResp, bool binaryMode = false) + public async Task CreateHttpResultAsync(HttpResponseMessage? response, bool binaryMode = false) { HttpResult result = new HttpResult(); - if (wResp == null) + if (response == null) { return result; } - result.Code = (int)wResp.StatusCode; - result.RefCode = (int)wResp.StatusCode; + result.Code = (int) response.StatusCode; + result.RefCode = (int) response.StatusCode; - getHeaders(ref result, wResp); + GetHeaders(result, response); - Stream respStream = wResp.GetResponseStream(); - if (respStream == null) + var content = response.Content; + if (binaryMode) + { + result.Data = await content.ReadAsByteArrayAsync(); + } + else + { + result.Text = await content.ReadAsStringAsync(); + } + + return result; + } + + public HttpResult CreateHttpResult(HttpResponseMessage? response, bool binaryMode = false) + { + HttpResult result = new HttpResult(); + + if (response == null) { - wResp.Close(); return result; } - if (binaryMode) + result.Code = (int) response.StatusCode; + result.RefCode = (int) response.StatusCode; + + GetHeaders(result, response); + + using (response) { - int len = (int)wResp.ContentLength; - result.Data = new byte[len]; - int bytesLeft = len; - int bytesRead = 0; + var content = response.Content; + if (content == null) + { + return result; + } - using (BinaryReader br = new BinaryReader(respStream)) + if (binaryMode) { - while (bytesLeft > 0) - { - bytesRead = br.Read(result.Data, len - bytesLeft, bytesLeft); - bytesLeft -= bytesRead; - } + result.Data = content.ReadAsByteArrayAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + result.Text = content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } } + return result; + } + + public async Task SendRequestAsync(HttpRequestOptions reqOpts, Boolean binaryMode = false) + { + HttpResult result; + HttpClientHandler handler; + if (reqOpts.CanUseSharedHttpClientHandler()) + { + _sharedHttpClientHandler ??= new HttpClientHandler(); + handler = _sharedHttpClientHandler; + } else { - using (StreamReader sr = new StreamReader(respStream)) + handler = reqOpts.CreateHttpClientHandler(); + } + + try + { + using var client = new HttpClient(handler, disposeHandler: false); + if (reqOpts.Timeout.HasValue) + { + client.Timeout = TimeSpan.FromMilliseconds(reqOpts.Timeout.Value); + } + + using var request = reqOpts.CreateHttpRequestMessage(); + using HttpResponseMessage response = await client.SendAsync(request); + result = await CreateHttpResultAsync(response, binaryMode); + } + catch (HttpRequestException httpRequestException) when (httpRequestException.StatusCode.HasValue) + { + result = new HttpResult { - result.Text = sr.ReadToEnd(); + Code = (int)httpRequestException.StatusCode.Value, + RefCode = (int)httpRequestException.StatusCode.Value, + RefText = httpRequestException.Message + }; + } + catch (Exception ex) + { + StringBuilder sb = new StringBuilder(); + sb.Append($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff}] [{_userAgent}] [HTTP-{reqOpts.Method}] Error: "); + sb.AppendLine(ex.ToString()); + sb.AppendLine(); + + result = new HttpResult(); + result.RefCode = (int)HttpCode.USER_UNDEF; + result.RefText += sb.ToString(); + } + finally + { + if (!ReferenceEquals(handler, _sharedHttpClientHandler)) + { + handler.Dispose(); } } - wResp.Close(); return result; } public HttpResult SendRequest(HttpRequestOptions reqOpts, Boolean binaryMode = false) { HttpResult result; - HttpWebRequest wReq = null; + HttpClientHandler handler; + if (reqOpts.CanUseSharedHttpClientHandler()) + { + _sharedHttpClientHandler ??= new HttpClientHandler(); + handler = _sharedHttpClientHandler; + } + else + { + handler = reqOpts.CreateHttpClientHandler(); + } try { - wReq = reqOpts.CreateHttpWebRequest(); - HttpWebResponse wResp = wReq.GetResponse() as HttpWebResponse; + using var client = new HttpClient(handler); + if (reqOpts.Timeout.HasValue) + { + client.Timeout = TimeSpan.FromMilliseconds(reqOpts.Timeout.Value); + } - result = CreateHttpResult(wResp, binaryMode); + using var request = reqOpts.CreateHttpRequestMessage(); + using HttpResponseMessage response = client.Send(request); + + result = CreateHttpResult(response, binaryMode); } - catch (WebException wex) + catch (HttpRequestException httpRequestException) when (httpRequestException.StatusCode.HasValue) { - HttpWebResponse xResp = wex.Response as HttpWebResponse; - result = CreateHttpResult(xResp); + result = new HttpResult + { + Code = (int) httpRequestException.StatusCode.Value, + RefCode = (int) httpRequestException.StatusCode.Value, + RefText = httpRequestException.Message + }; } catch (Exception ex) { StringBuilder sb = new StringBuilder(); - sb.AppendFormat( - "[{0}] [{1}] [HTTP-{2}] Error: ", - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), - userAgent, - reqOpts.Method - ); + sb.Append($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff}] [{_userAgent}] [HTTP-{reqOpts.Method}] Error: "); Exception e = ex; while (e != null) { @@ -173,16 +262,9 @@ public HttpResult SendRequest(HttpRequestOptions reqOpts, Boolean binaryMode = f sb.AppendLine(); result = CreateHttpResult(null); - result.RefCode = (int)HttpCode.USER_UNDEF; + result.RefCode = (int) HttpCode.USER_UNDEF; result.RefText += sb.ToString(); } - finally - { - if (wReq != null) - { - wReq.Abort(); - } - } return result; } @@ -194,7 +276,8 @@ public HttpResult SendRequest(HttpRequestOptions reqOpts, List midd return SendRequest(reqOpts, binaryMode); } - List reversedMiddlewares = new List(middlewares); + List reversedMiddlewares = new List(middlewares.Count); + reversedMiddlewares.AddRange(middlewares); reversedMiddlewares.Reverse(); DNextSend composedHandle = reversedMiddlewares.Aggregate( req => SendRequest(req, binaryMode), @@ -229,8 +312,8 @@ public HttpResult Get(string url, StringDictionary headers, Auth auth, bool bina { headers["Content-Type"] = ContentType.WWW_FORM_URLENC; } - - addAuthHeaders(ref headers, auth); + + AddAuthHeaders(ref headers, auth); string token = auth.CreateManageTokenV2("GET", url, headers); return Get(url, headers, token, binaryMode); @@ -293,7 +376,7 @@ public HttpResult Post(string url, StringDictionary headers, Auth auth, bool bin headers["Content-Type"] = ContentType.WWW_FORM_URLENC; } - addAuthHeaders(ref headers, auth); + AddAuthHeaders(ref headers, auth); string token = auth.CreateManageTokenV2("POST", url, headers); return Post(url, headers, token, binaryMode); @@ -441,8 +524,8 @@ public HttpResult PostForm(string url, StringDictionary headers, string data, Au { headers["Content-Type"] = ContentType.WWW_FORM_URLENC; } - - addAuthHeaders(ref headers, auth); + + AddAuthHeaders(ref headers, auth); string token = auth.CreateManageTokenV2("POST", url, headers, data); return PostForm(url, headers, Encoding.UTF8.GetBytes(data), token, binaryMode); @@ -495,13 +578,13 @@ public HttpResult PostForm(string url, StringDictionary headers, byte[] data, st /// 令牌(凭证)[可选->设置为null] /// 是否以二进制模式读取响应内容(默认:否,即表示以文本方式读取) /// HTTP-POST的响应结果 - public HttpResult PostMultipart(string url, byte[] data, string boundary, string token, bool binaryMode = false) + public HttpResult PostMultipart(string url, byte[]? data, string boundary, string? token, bool binaryMode = false) { HttpRequestOptions reqOpts = CreateHttpRequestOptions("POST", url, null, token); reqOpts.Headers.Add( "Content-Type", - string.Format("{0}; boundary={1}", ContentType.MULTIPART_FORM_DATA, boundary) + $"{ContentType.MULTIPART_FORM_DATA}; boundary={boundary}" ); if (data != null) @@ -521,7 +604,7 @@ public HttpResult PostMultipart(string url, byte[] data, string boundary, string /// 上传设置的headers /// 是否以二进制模式读取响应内容(默认:否,即表示以文本方式读取) /// HTTP-PUT的响应结果 - public HttpResult PutDataWithHeaders(string url, byte[] data, Dictionary headers, bool binaryMode = false) + public HttpResult PutDataWithHeaders(string url, byte[]? data, Dictionary headers, bool binaryMode = false) { // converse headers type for compaction StringDictionary headersDict = new StringDictionary(); @@ -548,7 +631,7 @@ public HttpResult PutDataWithHeaders(string url, byte[] data, Dictionary /// 即将被HTTP请求封装函数返回的HttpResult变量 /// 正在被读取的HTTP响应 - private void getHeaders(ref HttpResult hr, HttpWebResponse resp) + private void GetHeaders(HttpResult hr, HttpResponseMessage? resp) { if (resp != null) { @@ -557,42 +640,41 @@ private void getHeaders(ref HttpResult hr, HttpWebResponse resp) hr.RefInfo = new Dictionary(); } - hr.RefInfo.Add("ProtocolVersion", resp.ProtocolVersion.ToString()); + hr.RefInfo.Add("ProtocolVersion", resp.Version.ToString()); - if (!string.IsNullOrEmpty(resp.CharacterSet)) + if (resp.Content?.Headers?.ContentType?.CharSet is string characterSet && !string.IsNullOrEmpty(characterSet)) { - hr.RefInfo.Add("Characterset", resp.CharacterSet); + hr.RefInfo.Add("Characterset", characterSet); } - if (!string.IsNullOrEmpty(resp.ContentEncoding)) + if (resp.Content?.Headers?.ContentEncoding != null) { - hr.RefInfo.Add("ContentEncoding", resp.ContentEncoding); + hr.RefInfo.Add("ContentEncoding", string.Join(",", resp.Content.Headers.ContentEncoding)); } - if (!string.IsNullOrEmpty(resp.ContentType)) + if (resp.Content?.Headers?.ContentType != null) { - hr.RefInfo.Add("ContentType", resp.ContentType); + hr.RefInfo.Add("ContentType", resp.Content.Headers.ContentType.ToString()); } - hr.RefInfo.Add("ContentLength", resp.ContentLength.ToString()); + hr.RefInfo.Add("ContentLength", (resp.Content?.Headers?.ContentLength ?? 0).ToString()); - var headers = resp.Headers; - if (headers != null && headers.Count > 0) + foreach (var header in resp.Headers) { - if (hr.RefInfo == null) - { - hr.RefInfo = new Dictionary(); - } - foreach (var key in headers.AllKeys) + hr.RefInfo[header.Key] = string.Join(",", header.Value); + } + + if (resp.Content?.Headers != null) + { + foreach (var header in resp.Content.Headers) { - hr.RefInfo.Add(key, headers[key]); + hr.RefInfo[header.Key] = string.Join(",", header.Value); } } } } - - private void addAuthHeaders(ref StringDictionary headers, Auth auth) + private void AddAuthHeaders(ref StringDictionary headers, Auth auth) { string xQiniuDate = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); string xQiniuDateDisableEnv = Environment.GetEnvironmentVariable("DISABLE_QINIU_TIMESTAMP_SIGNATURE"); @@ -615,5 +697,10 @@ private void addAuthHeaders(ref StringDictionary headers, Auth auth) headers["X-Qiniu-Date"] = xQiniuDate; } } + + public void Dispose() + { + _sharedHttpClientHandler?.Dispose(); + } } } \ No newline at end of file diff --git a/src/Qiniu/Http/HttpRequestOptions.cs b/src/Qiniu/Http/HttpRequestOptions.cs index abbbbc2b..cd9c2ebd 100644 --- a/src/Qiniu/Http/HttpRequestOptions.cs +++ b/src/Qiniu/Http/HttpRequestOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.IO; +using System.Net.Http; using System.Net; using System.Net.Cache; using System.Net.Security; @@ -18,272 +19,189 @@ public class HttpRequestOptions public bool? AllowWriteStreamBuffering { get; set; } public AuthenticationLevel? AuthenticationLevel { get; set; } public DecompressionMethods? AutomaticDecompression { get; set; } - public RequestCachePolicy CachePolicy { get; set; } - public X509CertificateCollection ClientCertificates { get; set; } - public string ConnectionGroupName { get; set; } - public HttpContinueDelegate ContinueDelegate { get; set; } + public RequestCachePolicy? CachePolicy { get; set; } + public X509CertificateCollection? ClientCertificates { get; set; } + public string? ConnectionGroupName { get; set; } + public HttpContinueDelegate? ContinueDelegate { get; set; } public int? ContinueTimeout { get; set; } - public CookieContainer CookieContainer { get; set; } - public ICredentials Credentials { get; set; } + public CookieContainer? CookieContainer { get; set; } + public ICredentials? Credentials { get; set; } public TokenImpersonationLevel? ImpersonationLevel { get; set; } public bool? KeepAlive { get; set; } public int? MaximumAutomaticRedirections { get; set; } public int? MaximumResponseHeadersLength { get; set; } - public string MediaType { get; set; } - public string Method { get; set; } + public string? MediaType { get; set; } + public string? Method { get; set; } public bool? Pipelined { get; set; } public bool? PreAuthenticate { get; set; } - public IWebProxy Proxy { get; set; } + public IWebProxy? Proxy { get; set; } public int? ReadWriteTimeout { get; set; } public bool? SendChunked { get; set; } - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + public RemoteCertificateValidationCallback? ServerCertificateValidationCallback { get; set; } public int? Timeout { get; set; } public bool? UnsafeAuthenticatedConnectionSharing { get; set; } public bool? UseDefaultCredentials { get; set; } + public HttpContent? RequestContent { get; set; } // Custom Options - public string Url; - public StringDictionary Headers; - public Stream RequestStream; - public byte[] RequestData; + public string? Url { get; set; } + public StringDictionary? Headers { get; set; } + public Stream? RequestStream { get; set; } + public byte[]? RequestData { get; set; } public HttpRequestOptions() { Headers = new StringDictionary(); } - public HttpWebRequest CreateHttpWebRequest() + public HttpRequestMessage CreateHttpRequestMessage() { - HttpWebRequest wReq = WebRequest.Create(Url) as HttpWebRequest; - if (wReq == null) + if (string.IsNullOrWhiteSpace(Url)) { - StringBuilder msg = new StringBuilder(); - msg.AppendFormat("Failed to create HttpWebRequest with URL \"{0}\".", Url); - throw new InvalidOperationException(msg.ToString()); + throw new InvalidOperationException("Failed to create HttpRequestMessage because URL is empty."); } - SetProperties(wReq); - SetHeaders(wReq); - wReq.ServicePoint.Expect100Continue = false; - SetBody(wReq); - - return wReq; - } - - private void SetProperties(HttpWebRequest wReq) - { - if (AllowAutoRedirect.HasValue) - { - wReq.AllowAutoRedirect = AllowAutoRedirect.Value; - } - - if (AllowReadStreamBuffering.HasValue) + if (Method == null) { - wReq.AllowReadStreamBuffering = AllowReadStreamBuffering.Value; + throw new InvalidOperationException("Failed to create HttpRequestMessage because HTTP method is empty."); } - if (AllowWriteStreamBuffering.HasValue) - { - wReq.AllowWriteStreamBuffering = AllowWriteStreamBuffering.Value; - } - - if (AuthenticationLevel.HasValue) - { - wReq.AuthenticationLevel = AuthenticationLevel.Value; - } - - if (AutomaticDecompression.HasValue) - { - wReq.AutomaticDecompression = AutomaticDecompression.Value; - } + var message = new HttpRequestMessage(new HttpMethod(Method), Url); + SetHeaders(message); + SetBody(message); + return message; + } - if (CachePolicy != null) + internal bool CanUseSharedHttpClientHandler() + { + if (AllowAutoRedirect is true) { - wReq.CachePolicy = CachePolicy; + return false; } - if (ClientCertificates != null) + if (AutomaticDecompression != null && AutomaticDecompression.Value != DecompressionMethods.None) { - wReq.ClientCertificates = ClientCertificates; + return false; } - if (ConnectionGroupName != null) + if (ClientCertificates is not null + || CookieContainer is not null + || Credentials is not null) { - wReq.ConnectionGroupName = ConnectionGroupName; + return false; } - if (ContinueDelegate != null) + if (PreAuthenticate is true) { - wReq.ContinueDelegate = ContinueDelegate; + return false; } - if (ContinueTimeout.HasValue) + if (Proxy is not null) { - wReq.ContinueTimeout = ContinueTimeout.Value; + return false; } - if (CookieContainer != null) + if (UseDefaultCredentials is true) { - wReq.CookieContainer = CookieContainer; + return false; } - if (Credentials != null) + if (ServerCertificateValidationCallback is not null) { - wReq.Credentials = Credentials; + return false; } - if (ImpersonationLevel.HasValue) - { - wReq.ImpersonationLevel = ImpersonationLevel.Value; - } + return true; + } - if (KeepAlive.HasValue) - { - wReq.KeepAlive = KeepAlive.Value; - } + public HttpClientHandler CreateHttpClientHandler() + { + var handler = new HttpClientHandler(); - if (MaximumAutomaticRedirections.HasValue) + if (AllowAutoRedirect.HasValue) { - wReq.MaximumAutomaticRedirections = MaximumAutomaticRedirections.Value; + handler.AllowAutoRedirect = AllowAutoRedirect.Value; } - if (MaximumResponseHeadersLength.HasValue) + if (AutomaticDecompression.HasValue) { - wReq.MaximumResponseHeadersLength = MaximumResponseHeadersLength.Value; + handler.AutomaticDecompression = AutomaticDecompression.Value; } - if (MediaType != null) + if (ClientCertificates != null) { - wReq.MediaType = MediaType; + handler.ClientCertificates.AddRange(ClientCertificates); } - if (Method != null) + if (CookieContainer != null) { - wReq.Method = Method; + handler.CookieContainer = CookieContainer; } - if (Pipelined.HasValue) + if (Credentials != null) { - wReq.Pipelined = Pipelined.Value; + handler.Credentials = Credentials; } if (PreAuthenticate.HasValue) { - wReq.PreAuthenticate = PreAuthenticate.Value; + handler.PreAuthenticate = PreAuthenticate.Value; } if (Proxy != null) { - wReq.Proxy = Proxy; + handler.Proxy = Proxy; } - if (ReadWriteTimeout.HasValue) - { - wReq.ReadWriteTimeout = ReadWriteTimeout.Value; - } - - if (SendChunked.HasValue) + if (UseDefaultCredentials.HasValue) { - wReq.SendChunked = SendChunked.Value; + handler.UseDefaultCredentials = UseDefaultCredentials.Value; } if (ServerCertificateValidationCallback != null) { - wReq.ServerCertificateValidationCallback = ServerCertificateValidationCallback; + handler.ServerCertificateCustomValidationCallback = (message, certificate, chain, errors) => + ServerCertificateValidationCallback(message?.RequestUri?.Host ?? string.Empty, certificate, chain, errors); } - if (Timeout.HasValue) - { - wReq.Timeout = Timeout.Value; - } - - if (UnsafeAuthenticatedConnectionSharing.HasValue) - { - wReq.UnsafeAuthenticatedConnectionSharing = UnsafeAuthenticatedConnectionSharing.Value; - } - - if (UseDefaultCredentials.HasValue) - { - wReq.UseDefaultCredentials = UseDefaultCredentials.Value; - } + return handler; } - private void SetHeaders(HttpWebRequest wReq) + private void SetHeaders(HttpRequestMessage request) { - if (Headers == null || wReq == null) + if (Headers == null) { return; } foreach (string fieldName in Headers.Keys) { - string fieldVal = Headers[fieldName]; - if (WebHeaderCollection.IsRestricted(fieldName)) + string? fieldVal = Headers[fieldName]; + if (!request.Headers.TryAddWithoutValidation(fieldName, fieldVal)) { - switch (fieldName) + if (request.Content == null) { - case "accept": - wReq.Accept = fieldVal; - break; - // should be set by KeepAlive property - // case "connection": - // wReq.Connection = fieldVal; - // break; - case "content-type": - wReq.ContentType = fieldVal; - break; - case "date": - wReq.Date = DateTime.Parse(fieldVal); - break; - case "expect": - wReq.Expect = fieldVal; - break; - case "host": - wReq.Host = fieldVal; - break; - case "if-modified-since": - wReq.IfModifiedSince = DateTime.Parse(fieldVal); - break; - case "referer": - wReq.Referer = fieldVal; - break; - case "transfer-encoding": - wReq.TransferEncoding = fieldVal; - break; - case "user-agent": - wReq.UserAgent = fieldVal; - break; + request.Content = new ByteArrayContent(Array.Empty()); } - } - else - { - wReq.Headers.Add(fieldName, fieldVal); + + request.Content.Headers.TryAddWithoutValidation(fieldName, fieldVal); } } } - private void SetBody(HttpWebRequest wReq) + private void SetBody(HttpRequestMessage request) { - if (RequestData != null) + if (RequestContent != null) { - wReq.ContentLength = RequestData.Length; - wReq.AllowWriteStreamBuffering = true; - using (Stream sReq = wReq.GetRequestStream()) - { - sReq.Write(RequestData, 0, RequestData.Length); - sReq.Flush(); - } - - return; + request.Content = RequestContent; } - - if (RequestStream != null) + else if (RequestData != null) { - wReq.ContentLength = RequestStream.Length; - using (Stream sReq = wReq.GetRequestStream()) - { - RequestStream.CopyTo(sReq); - } + request.Content = new ByteArrayContent(RequestData); + } + else if (RequestStream != null) + { + request.Content = new StreamContent(RequestStream); } } } diff --git a/src/Qiniu/Http/HttpResult.cs b/src/Qiniu/Http/HttpResult.cs index 4d08fd35..081c0259 100644 --- a/src/Qiniu/Http/HttpResult.cs +++ b/src/Qiniu/Http/HttpResult.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; @@ -9,7 +10,7 @@ namespace Qiniu.Http /// public class HttpResult { - private static readonly IReadOnlyList NotRetryableHttpCodes = new List + private static readonly IReadOnlyList _notRetryableHttpCodes = new List(19) { (int)HttpCode.INVALID_ARGUMENT, (int)HttpCode.INVALID_FILE, @@ -41,12 +42,12 @@ public class HttpResult /// /// 消息或错误文本 /// - public string Text { get; set; } + public string? Text { get; set; } /// /// 消息或错误(二进制格式) /// - public byte[] Data { get; set; } + public byte[]? Data { get; set; } /// /// 参考代码(用户自定义) @@ -61,7 +62,7 @@ public class HttpResult /// /// 参考信息(从返回消息WebResponse的头部获取) /// - public Dictionary RefInfo { get; set; } + public Dictionary? RefInfo { get; set; } /// /// 初始化(所有成员默认值,需要后续赋值) @@ -73,7 +74,8 @@ public HttpResult() Data = null; RefCode = (int)HttpCode.USER_UNDEF; - RefInfo = null; + RefText = string.Empty; + RefInfo = new Dictionary(); } /// @@ -82,11 +84,13 @@ public HttpResult() /// 要复制其内容的来源 public void Shadow(HttpResult hr) { + ArgumentNullException.ThrowIfNull(hr); + this.Code = hr.Code; this.Text = hr.Text; this.Data = hr.Data; this.RefCode = hr.RefCode; - this.RefText += hr.RefText; + this.RefText += hr.RefText ?? string.Empty; this.RefInfo = hr.RefInfo; } @@ -172,7 +176,7 @@ public bool NeedRetry() { return false; } - if (NotRetryableHttpCodes.Contains(Code)) + if (_notRetryableHttpCodes.Contains(Code)) { return false; } diff --git a/src/Qiniu/Qiniu.csproj b/src/Qiniu/Qiniu.csproj index be3f39c8..75dfa7de 100644 --- a/src/Qiniu/Qiniu.csproj +++ b/src/Qiniu/Qiniu.csproj @@ -1,16 +1,10 @@  - Release - AnyCPU - {2F5B0328-DE8B-4B53-A500-3077E340A51B} Library - Properties - Qiniu - Qiniu - netstandard2.0 - 512 - + net9.0;net10.0 + enable + true false @@ -18,26 +12,10 @@ QiniuCSharpSDK.snk - - - - - - - - - Qiniu 8.7.0 Rong Zhou, Qiniu SDK Shanghai Qiniu Information Technology Co., Ltd. Qiniu Resource (Cloud) Storage SDK for C# - diff --git a/src/Qiniu/QiniuCSharpSDK.cs b/src/Qiniu/QiniuCSharpSDK.cs index 34a43fe0..78f90255 100644 --- a/src/Qiniu/QiniuCSharpSDK.cs +++ b/src/Qiniu/QiniuCSharpSDK.cs @@ -1,4 +1,6 @@ -/// +namespace Qiniu; + +/// /// Qiniu (Cloud) C# SDK for .NET Framework 2.0+/Core/UWP /// Modules in this SDK: /// "Storage" 存储相关功能,上传,下载,数据处理,资源管理 @@ -26,7 +28,7 @@ public class QiniuCSharpSDK public const string RTFX = "NET45"; #elif Net46 public const string RTFX = "NET46"; -#elif NetCore +#elif NetCore || NETCOREAPP public const string RTFX = "NETCore"; #elif WINDOWS_UWP public const string RTFX = "UWP"; diff --git a/src/Qiniu/Storage/BatchInfo.cs b/src/Qiniu/Storage/BatchInfo.cs index 33b13661..1a9a7c35 100644 --- a/src/Qiniu/Storage/BatchInfo.cs +++ b/src/Qiniu/Storage/BatchInfo.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Qiniu.Storage { /// @@ -9,13 +9,15 @@ public class BatchInfo /// /// 状态码 /// - [JsonProperty("code",NullValueHandling=NullValueHandling.Ignore)] + [JsonPropertyName("code")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int Code { get; set; } /// /// 消息 /// - [JsonProperty("data",NullValueHandling=NullValueHandling.Ignore)] + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public BatchData Data { get; set; } } @@ -27,31 +29,36 @@ public class BatchData /// /// 处理遇到的错误信息 /// - [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Error { get; set; } /// /// 文件hash(ETAG) /// - [JsonProperty("hash", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("hash")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Hash { get; set; } /// /// 文件大小(字节) /// - [JsonProperty("fsize", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("fsize")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long Fsize { get; set; } /// /// 文件MIME类型 /// - [JsonProperty("mimeType", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("mimeType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string MimeType { get; set; } /// /// 上传时间 /// - [JsonProperty("putTime", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("putTime")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long PutTime { get; set; } /// @@ -62,7 +69,8 @@ public class BatchData /// 3 深度归档存储 /// 4 归档直读存储 /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int FileType { get; set; } /// @@ -71,7 +79,8 @@ public class BatchData /// 2 已解冻 /// 0 如果是归档/深度归档,但处于冻结,后端不返回此字段,因此默认值为 0。请勿依赖 0 判断冻结状态 /// - [JsonProperty("restoreStatus", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("restoreStatus")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int RestoreStatus { get; set; } /// @@ -79,7 +88,8 @@ public class BatchData /// 0 启用,非禁用后端不返回此字段,因此默认值为 0。 /// 1 禁用 /// - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("status")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int Status { get; set; } /// @@ -87,7 +97,8 @@ public class BatchData /// 服务端不确保一定返回此字段,详见: /// https://developer.qiniu.com/kodo/1308/stat#:~:text=%E8%AF%A5%E5%AD%97%E6%AE%B5%E3%80%82-,md5,-%E5%90%A6 /// - [JsonProperty("md5", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("md5")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Md5 { get; set; } /// @@ -95,7 +106,8 @@ public class BatchData /// 文件在设置过期时间后才会返回该字段。 /// 历史文件过期仍会自动删除,但不会返回该字段,重新设置文件过期时间可使历史文件返回该字段。 /// - [JsonProperty("expiration", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("expiration")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int Expiration { get; set; } /// @@ -103,7 +115,8 @@ public class BatchData /// 文件在设置低频存储转换时间后才会返回该字段。 /// 历史文件到期仍会自动转换,但不会返回该字段,重新设置文件转换时间可使历史文件返回该字段。 /// - [JsonProperty("TransitionToIA", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("TransitionToIA")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToIa { get; set; } /// @@ -111,7 +124,8 @@ public class BatchData /// 文件在设置归档直读存储转换时间后才会返回该字段。 /// 历史文件到期仍会自动转换,但不会返回该字段,重新设置文件转换时间可使历史文件返回该字段。 /// - [JsonProperty("transitionToArchiveIR", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("transitionToArchiveIR")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToArchiveIr { get; set; } /// @@ -119,7 +133,8 @@ public class BatchData /// 文件在设置归档存储转换时间后才会返回该字段。 /// 历史文件到期仍会自动转换,但不会返回该字段,重新设置文件转换时间可使历史文件返回该字段。 /// - [JsonProperty("transitionToARCHIVE", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("transitionToARCHIVE")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToArchive { get; set; } /// @@ -127,7 +142,8 @@ public class BatchData /// 文件在设置深度归档存储转换时间后才会返回该字段。 /// 历史文件到期仍会自动转换,但不会返回该字段,重新设置文件转换时间可使历史文件返回该字段。 /// - [JsonProperty("transitionToDeepArchive", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("transitionToDeepArchive")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToDeepArchive { get; set; } } } diff --git a/src/Qiniu/Storage/BatchResult.cs b/src/Qiniu/Storage/BatchResult.cs index 678b9b4c..0cd9c530 100644 --- a/src/Qiniu/Storage/BatchResult.cs +++ b/src/Qiniu/Storage/BatchResult.cs @@ -1,7 +1,7 @@ using System.Text; using System.Collections.Generic; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.Storage { @@ -21,7 +21,7 @@ public string Error if (Code != (int)HttpCode.OK && Code != (int)HttpCode.PARTLY_OK) { - Dictionary ret = JsonConvert.DeserializeObject>(Text); + Dictionary ret = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.DictionaryStringString); if (ret.ContainsKey("error")) { ex = ret["error"]; @@ -42,7 +42,7 @@ public List Result if ((Code == (int)HttpCode.OK || Code == (int)HttpCode.PARTLY_OK) && (!string.IsNullOrEmpty(Text))) { - info = JsonConvert.DeserializeObject>(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.ListBatchInfo); } return info; } diff --git a/src/Qiniu/Storage/BucketResult.cs b/src/Qiniu/Storage/BucketResult.cs index 0fb4aaf6..bcd119a2 100644 --- a/src/Qiniu/Storage/BucketResult.cs +++ b/src/Qiniu/Storage/BucketResult.cs @@ -1,6 +1,6 @@ using System.Text; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.Storage { @@ -20,7 +20,7 @@ public BucketInfo Result if (Code == (int)HttpCode.OK && !string.IsNullOrEmpty(Text)) { - info= JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.BucketInfo); } return info; diff --git a/src/Qiniu/Storage/BucketsResult.cs b/src/Qiniu/Storage/BucketsResult.cs index 61a61ef0..6b5f3135 100644 --- a/src/Qiniu/Storage/BucketsResult.cs +++ b/src/Qiniu/Storage/BucketsResult.cs @@ -1,7 +1,7 @@ using System.Text; using System.Collections.Generic; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.Storage { @@ -20,7 +20,7 @@ public List Result List buckets = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - buckets = JsonConvert.DeserializeObject>(Text); + buckets = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.ListString); } return buckets; } diff --git a/src/Qiniu/Storage/Config.cs b/src/Qiniu/Storage/Config.cs index 949f4e9a..09ad3204 100644 --- a/src/Qiniu/Storage/Config.cs +++ b/src/Qiniu/Storage/Config.cs @@ -1,5 +1,6 @@  using System.Collections.Generic; +using System.Linq; namespace Qiniu.Storage { @@ -14,67 +15,69 @@ public class Config /// /// 默认空间管理域名 /// - public static string DefaultUcHost = "uc.qiniuapi.com"; + public const string DefaultUcHost = "uc.qiniuapi.com"; /// /// 默认查询区域域名 /// - public static string DefaultQueryRegionHost = "uc.qiniuapi.com"; + public const string DefaultQueryRegionHost = "uc.qiniuapi.com"; /// /// 默认备用查询区域域名 /// - public static List DefaultBackupQueryRegionHosts = new List + public static readonly IReadOnlyList DefaultBackupQueryRegionHosts = new List { "kodo-config.qiniuapi.com", "uc.qbox.me" }; - + /// /// 默认高级资源管理域名 /// - public static string DefaultRsHost = "rs.qiniu.com"; + public const string DefaultRsHost = "rs.qiniu.com"; /// /// 默认数据处理域名 /// - public static string DefaultApiHost = "api.qiniuapi.com"; + public const string DefaultApiHost = "api.qiniuapi.com"; /// /// 默认数据处理域名 /// - public static string DefaultIoHost = "iovip.qiniuio.com"; + public const string DefaultIoHost = "iovip.qiniuio.com"; /// /// 默认数据处理域名 /// - public static string DefaultRsfHost = "rsf.qiniu.com"; + public const string DefaultRsfHost = "rsf.qiniu.com"; + /// /// 空间所在的区域(Zone) /// - public Zone Zone = null; + public Zone? Zone { get; set; } = null; + /// /// 是否采用https域名 /// - public bool UseHttps = false; + public bool UseHttps { get; set; } = false; /// /// 是否采用CDN加速域名,对上传有效 /// - public bool UseCdnDomains = false; + public bool UseCdnDomains { get; set; } = false; /// /// 分片上传时,片的大小,默认为4MB,以提高上传效率 /// - public ChunkUnit ChunkSize = ChunkUnit.U4096K; + public ChunkUnit ChunkSize { get; set; } = ChunkUnit.U4096K; /// /// 分片上传的阈值,超过该大小采用分片上传的方式 /// - public int PutThreshold =ResumeChunk.GetChunkSize(ChunkUnit.U1024K) * 10; + public int PutThreshold { get; set; } = ResumeChunk.GetChunkSize(ChunkUnit.U1024K) * 10; /// /// 重试请求次数 /// /// 默认值应与 一致 public int MaxRetryTimes { set; get; } = 3; - + private string _ucHost = DefaultUcHost; private string _queryRegionHost = DefaultQueryRegionHost; - private List _backupQueryRegionHosts = DefaultBackupQueryRegionHosts; + private List _backupQueryRegionHosts = DefaultBackupQueryRegionHosts.ToList(); public void SetUcHost(string val) { @@ -120,7 +123,7 @@ public List BackupQueryRegionHosts() public string RsHost(string ak, string bucket) { string scheme = UseHttps ? "https://" : "http://"; - Zone z = this.Zone; + Zone? z = this.Zone; if (z == null) { z = ZoneHelper.QueryZone(ak, bucket, QueryRegionHost(), BackupQueryRegionHosts()); @@ -137,7 +140,7 @@ public string RsHost(string ak, string bucket) public string RsfHost(string ak, string bucket) { string scheme = UseHttps ? "https://" : "http://"; - Zone z = this.Zone; + Zone? z = this.Zone; if (z == null) { z = ZoneHelper.QueryZone(ak, bucket, QueryRegionHost(), BackupQueryRegionHosts()); @@ -154,7 +157,7 @@ public string RsfHost(string ak, string bucket) public string ApiHost(string ak, string bucket) { string scheme = UseHttps ? "https://" : "http://"; - Zone z = this.Zone; + Zone? z = this.Zone; if (z == null) { z = ZoneHelper.QueryZone(ak, bucket, QueryRegionHost(), BackupQueryRegionHosts()); @@ -171,7 +174,7 @@ public string ApiHost(string ak, string bucket) public string IovipHost(string ak, string bucket) { string scheme = UseHttps ? "https://" : "http://"; - Zone z = this.Zone; + Zone? z = this.Zone; if (z == null) { z = ZoneHelper.QueryZone(ak, bucket, QueryRegionHost(), BackupQueryRegionHosts()); @@ -188,7 +191,7 @@ public string IovipHost(string ak, string bucket) public string UpHost(string ak, string bucket) { string scheme = UseHttps ? "https://" : "http://"; - Zone z = this.Zone; + Zone? z = this.Zone; if (z == null) { z = ZoneHelper.QueryZone(ak, bucket, QueryRegionHost(), BackupQueryRegionHosts()); diff --git a/src/Qiniu/Storage/DomainsResult.cs b/src/Qiniu/Storage/DomainsResult.cs index 1ccb721f..6cdde377 100644 --- a/src/Qiniu/Storage/DomainsResult.cs +++ b/src/Qiniu/Storage/DomainsResult.cs @@ -1,7 +1,7 @@ -using System.Text; +using System.Text; using System.Collections.Generic; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.Storage { @@ -20,7 +20,7 @@ public List Result List domains = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - domains=JsonConvert.DeserializeObject>(Text); + domains = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.ListString); } return domains; } diff --git a/src/Qiniu/Storage/DownloadManager.cs b/src/Qiniu/Storage/DownloadManager.cs index d036aba1..44c1f8ac 100644 --- a/src/Qiniu/Storage/DownloadManager.cs +++ b/src/Qiniu/Storage/DownloadManager.cs @@ -22,20 +22,21 @@ public class DownloadManager /// 已授权的下载链接 public static string CreatePrivateUrl(Mac mac, string domain, string fileName, int expireInSeconds = 3600) { + ArgumentNullException.ThrowIfNull(mac); long deadline = UnixTimestamp.GetUnixTimestamp(expireInSeconds); string publicUrl = CreatePublishUrl(domain, fileName); StringBuilder sb = new StringBuilder(publicUrl); if (publicUrl.Contains("?")) { - sb.AppendFormat("&e={0}", deadline); + sb.Append($"&e={deadline}"); } else { - sb.AppendFormat("?e={0}", deadline); + sb.Append($"?e={deadline}"); } string token = Auth.CreateDownloadToken(mac, sb.ToString()); - sb.AppendFormat("&token={0}", token); + sb.Append($"&token={token}"); return sb.ToString(); } @@ -48,7 +49,9 @@ public static string CreatePrivateUrl(Mac mac, string domain, string fileName, i /// 公开空间文件下载链接 public static string CreatePublishUrl(string domain, string fileName) { - return string.Format("{0}/{1}", domain, Uri.EscapeUriString(fileName)); + ArgumentException.ThrowIfNullOrWhiteSpace(domain); + ArgumentException.ThrowIfNullOrWhiteSpace(fileName); + return $"{domain}/{Uri.EscapeUriString(fileName)}"; } /// @@ -73,19 +76,17 @@ public static HttpResult Download(string url, string saveasFile) fs.Write(result.Data, 0, result.Data.Length); fs.Flush(); } - result.RefText += string.Format("[{0}] [Download] Success: (Remote file) ==> \"{1}\"\n", - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), saveasFile); + result.RefText += $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff}] [Download] Success: (Remote file) ==> \"{saveasFile}\"\n"; } else { - result.RefText += string.Format("[{0}] [Download] Error: code = {1}\n", - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), result.Code); + result.RefText += $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff}] [Download] Error: code = {result.Code}\n"; } } catch (Exception ex) { StringBuilder sb = new StringBuilder(); - sb.AppendFormat("[{0}] [Download] Error: ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff")); + sb.Append($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.ffff}] [Download] Error: "); Exception e = ex; while (e != null) { diff --git a/src/Qiniu/Storage/FetchInfo.cs b/src/Qiniu/Storage/FetchInfo.cs index 24673654..b55a4489 100644 --- a/src/Qiniu/Storage/FetchInfo.cs +++ b/src/Qiniu/Storage/FetchInfo.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Qiniu.Storage { /// @@ -9,25 +9,25 @@ public class FetchInfo /// /// 文件名 /// - [JsonProperty("key")] + [JsonPropertyName("key")] public string Key { set; get; } /// /// 文件大小(字节) /// - [JsonProperty("fsize")] + [JsonPropertyName("fsize")] public long Fsize { set; get; } /// /// 文件hash(ETAG) /// - [JsonProperty("hash")] + [JsonPropertyName("hash")] public string Hash { set; get; } /// /// 文件MIME类型 /// - [JsonProperty("mimeType")] + [JsonPropertyName("mimeType")] public string MimeType { set; get; } } } diff --git a/src/Qiniu/Storage/FetchResult.cs b/src/Qiniu/Storage/FetchResult.cs index f7d0dc60..cfa6cec1 100644 --- a/src/Qiniu/Storage/FetchResult.cs +++ b/src/Qiniu/Storage/FetchResult.cs @@ -1,6 +1,6 @@ using Qiniu.Http; -using Newtonsoft.Json; using System.Text; +using Qiniu.Util; namespace Qiniu.Storage { /// @@ -18,7 +18,7 @@ public FetchInfo Result FetchInfo info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info = JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.FetchInfo); } return info; } diff --git a/src/Qiniu/Storage/FileInfo.cs b/src/Qiniu/Storage/FileInfo.cs index ac54d798..35e74174 100644 --- a/src/Qiniu/Storage/FileInfo.cs +++ b/src/Qiniu/Storage/FileInfo.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Qiniu.Storage { @@ -10,25 +10,25 @@ public class FileInfo /// /// 文件大小(字节) /// - [JsonProperty("fsize")] + [JsonPropertyName("fsize")] public long Fsize { set; get; } /// /// 文件hash(ETAG) /// - [JsonProperty("hash")] + [JsonPropertyName("hash")] public string Hash { set; get; } /// /// 文件MIME类型 /// - [JsonProperty("mimeType")] + [JsonPropertyName("mimeType")] public string MimeType { set; get; } /// /// 文件上传时间 /// - [JsonProperty("putTime")] + [JsonPropertyName("putTime")] public long PutTime { set; get; } /// @@ -39,7 +39,7 @@ public class FileInfo /// 3 深度归档存储 /// 4 归档直读存储 /// - [JsonProperty("type")] + [JsonPropertyName("type")] public int FileType { get; set; } /// @@ -48,7 +48,8 @@ public class FileInfo /// 2 已解冻 /// 0 如果是归档/深度归档,但处于冻结,后端不返回此字段,因此默认值为 0。请勿依赖 0 判断冻结状态 /// - [JsonProperty("restoreStatus", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("restoreStatus")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int RestoreStatus { get; set; } /// @@ -56,7 +57,8 @@ public class FileInfo /// 0 启用,非禁用后端不返回此字段,因此默认值为 0。 /// 1 禁用 /// - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("status")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int Status { get; set; } /// @@ -64,7 +66,8 @@ public class FileInfo /// 服务端不确保一定返回此字段,详见: /// https://developer.qiniu.com/kodo/1308/stat#:~:text=%E8%AF%A5%E5%AD%97%E6%AE%B5%E3%80%82-,md5,-%E5%90%A6 /// - [JsonProperty("md5", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("md5")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Md5 { get; set; } /// @@ -72,7 +75,8 @@ public class FileInfo /// 文件在设置过期时间后才会返回该字段。 /// 历史文件过期仍会自动删除,但不会返回该字段,重新设置文件过期时间可使历史文件返回该字段。 /// - [JsonProperty("expiration", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("expiration")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int Expiration { get; set; } /// @@ -80,7 +84,8 @@ public class FileInfo /// 文件在设置过期时间后才会返回该字段。 /// 历史文件过期仍会自动删除,但不会返回该字段,重新设置文件过期时间可使历史文件返回该字段。 /// - [JsonProperty("transitionToIA", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("transitionToIA")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToIa { get; set; } /// @@ -88,7 +93,8 @@ public class FileInfo /// 文件在设置过期时间后才会返回该字段。 /// 历史文件过期仍会自动删除,但不会返回该字段,重新设置文件过期时间可使历史文件返回该字段。 /// - [JsonProperty("transitionToArchiveIR", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("transitionToArchiveIR")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToArchiveIr { get; set; } /// @@ -96,7 +102,8 @@ public class FileInfo /// 文件在设置过期时间后才会返回该字段。 /// 历史文件过期仍会自动删除,但不会返回该字段,重新设置文件过期时间可使历史文件返回该字段。 /// - [JsonProperty("transitionToARCHIVE", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("transitionToARCHIVE")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToArchive { get; set; } /// @@ -104,7 +111,8 @@ public class FileInfo /// 文件在设置过期时间后才会返回该字段。 /// 历史文件过期仍会自动删除,但不会返回该字段,重新设置文件过期时间可使历史文件返回该字段。 /// - [JsonProperty("transitionToDeepArchive", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("transitionToDeepArchive")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int TransitionToDeepArchive { get; set; } } } \ No newline at end of file diff --git a/src/Qiniu/Storage/FormUploader.cs b/src/Qiniu/Storage/FormUploader.cs index 3a78c2df..04281924 100644 --- a/src/Qiniu/Storage/FormUploader.cs +++ b/src/Qiniu/Storage/FormUploader.cs @@ -1,8 +1,16 @@ -using System; +using Qiniu.Http; +using Qiniu.Util; + +using System; +using System.Globalization; using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text; -using Qiniu.Http; -using Qiniu.Util; +using System.Threading; +using System.Threading.Tasks; + +using HttpRequestOptions = Qiniu.Http.HttpRequestOptions; namespace Qiniu.Storage { @@ -15,10 +23,10 @@ namespace Qiniu.Storage /// 上传策略 http://developer.qiniu.com/article/developer/security/upload-token.html /// 上传凭证 http://developer.qiniu.com/article/developer/security/put-policy.html /// - public class FormUploader + public class FormUploader : IDisposable { - private Config config; - private HttpManager httpManager; + private readonly Config _config; + private readonly HttpManager _httpManager; /// /// 初始化 @@ -26,8 +34,8 @@ public class FormUploader /// 表单上传的配置信息 public FormUploader(Config config) { - this.config = config; - this.httpManager = new HttpManager(); + this._config = config; + this._httpManager = new HttpManager(); } /// @@ -38,12 +46,12 @@ public FormUploader(Config config) /// 上传凭证 /// 上传可选设置 /// 上传文件后的返回结果 - public HttpResult UploadFile(string localFile, string key, string token, PutExtra extra) + public async Task UploadFile(string localFile, string key, string token, PutExtra? extra) { try { - FileStream fs = new FileStream(localFile, FileMode.Open); - return this.UploadStream(fs, key, token, extra); + await using FileStream fs = new FileStream(localFile, FileMode.Open, FileAccess.Read, FileShare.Read); + return await this.UploadStreamAsync(fs, key, token, extra); } catch (Exception ex) { @@ -53,7 +61,6 @@ public HttpResult UploadFile(string localFile, string key, string token, PutExtr } } - /// /// 上传数据 /// @@ -62,10 +69,10 @@ public HttpResult UploadFile(string localFile, string key, string token, PutExtr /// 上传凭证 /// 上传可选设置 /// 上传数据后的返回结果 - public HttpResult UploadData(byte[] data, string key, string token, PutExtra extra) + public async Task UploadDataAsync(byte[] data, string key, string token, PutExtra extra) { - MemoryStream stream = new MemoryStream(data); - return this.UploadStream(stream, key, token, extra); + using MemoryStream stream = new MemoryStream(data); + return await this.UploadStreamAsync(stream, key, token, extra); } /// @@ -74,16 +81,17 @@ public HttpResult UploadData(byte[] data, string key, string token, PutExtra ext /// (确定长度的)数据流 /// 要保存的key /// 上传凭证 - /// 上传可选设置 + /// 上传可选设置 /// 上传数据流后的返回结果 - public HttpResult UploadStream(Stream stream, string key, string token, PutExtra putExtra) + public async Task UploadStreamAsync(Stream stream, string? key, string token, PutExtra? putExtra) { if (putExtra == null) { putExtra = new PutExtra(); - putExtra.MaxRetryTimes = config.MaxRetryTimes; + putExtra.MaxRetryTimes = _config.MaxRetryTimes; } - if (string.IsNullOrEmpty(putExtra.MimeType )) { + if (string.IsNullOrEmpty(putExtra.MimeType)) + { putExtra.MimeType = "application/octet-stream"; } if (putExtra.ProgressHandler == null) @@ -94,138 +102,114 @@ public HttpResult UploadStream(Stream stream, string key, string token, PutExtra { putExtra.UploadController = DefaultUploadController; } - string fname = key; - if (string.IsNullOrEmpty(key)) + string? fileName = key; + if (string.IsNullOrEmpty(fileName)) { - fname = "fname_temp"; + fileName = "fname_temp"; } HttpResult result = new HttpResult(); - using (stream) + try { - try - { - string boundary = HttpManager.CreateFormDataBoundary(); - StringBuilder bodyBuilder = new StringBuilder(); - bodyBuilder.AppendLine("--" + boundary); + var startPosition = stream.Position; + Stream uploadStream = await ReadonlyWrapperStream.CreateWrapperStreamAsync(stream); + var uploadedBytes = uploadStream.Length; - if (key != null) - { - //write key when it is not null - bodyBuilder.AppendLine("Content-Disposition: form-data; name=\"key\""); - bodyBuilder.AppendLine(); - bodyBuilder.AppendLine(key); - bodyBuilder.AppendLine("--" + boundary); - } + string boundary = HttpManager.CreateFormDataBoundary(); + var multipartFormDataContent = new MultipartFormDataContent(boundary); + + StringBuilder bodyBuilder = new StringBuilder(); + bodyBuilder.AppendLine("--" + boundary); - //write token - bodyBuilder.AppendLine("Content-Disposition: form-data; name=\"token\""); - bodyBuilder.AppendLine(); - bodyBuilder.AppendLine(token); - bodyBuilder.AppendLine("--" + boundary); + if (key != null) + { + //write key when it is not null + multipartFormDataContent.Add(new StringContent(key), "key"); + } - //write extra params - if (putExtra.Params != null && putExtra.Params.Count > 0) + //write token + multipartFormDataContent.Add(new StringContent(token), "token"); + + //write extra params + if (putExtra.Params != null && putExtra.Params.Count > 0) + { + foreach (var p in putExtra.Params) { - foreach (var p in putExtra.Params) + if (p.Key.StartsWith("x:")) { - if (p.Key.StartsWith("x:")) - { - bodyBuilder.AppendFormat("Content-Disposition: form-data; name=\"{0}\"", p.Key); - bodyBuilder.AppendLine(); - bodyBuilder.AppendLine(); - bodyBuilder.AppendLine(p.Value); - bodyBuilder.AppendLine("--" + boundary); - } + multipartFormDataContent.Add(new StringContent(p.Value), p.Key); } } + } - //prepare data buffer - int bufferSize = 1024 * 1024; - byte[] buffer = new byte[bufferSize]; - int bytesRead = 0; - putExtra.ProgressHandler(0, stream.Length); - MemoryStream dataMS = new MemoryStream(); - while ((bytesRead = stream.Read(buffer, 0, bufferSize)) != 0) - { - dataMS.Write(buffer, 0, bytesRead); - } + //prepare data buffer + putExtra.ProgressHandler(0, uploadedBytes); - //write crc32 - uint crc32 = CRC32.CheckSumBytes(dataMS.ToArray()); - //write key when it is not null - bodyBuilder.AppendLine("Content-Disposition: form-data; name=\"crc32\""); - bodyBuilder.AppendLine(); - bodyBuilder.AppendLine(crc32.ToString()); - bodyBuilder.AppendLine("--" + boundary); - - //write fname - bodyBuilder.AppendFormat("Content-Disposition: form-data; name=\"file\"; filename=\"{0}\"", fname); - bodyBuilder.AppendLine(); - - //write mime type - bodyBuilder.AppendFormat("Content-Type: {0}", putExtra.MimeType); - bodyBuilder.AppendLine(); - bodyBuilder.AppendLine(); - - //write file data - StringBuilder bodyEnd = new StringBuilder(); - bodyEnd.AppendLine(); - bodyEnd.AppendLine("--" + boundary + "--"); - - byte[] partData1 = Encoding.UTF8.GetBytes(bodyBuilder.ToString()); - byte[] partData2 = dataMS.ToArray(); - byte[] partData3 = Encoding.UTF8.GetBytes(bodyEnd.ToString()); - - MemoryStream ms = new MemoryStream(); - ms.Write(partData1, 0, partData1.Length); - ms.Write(partData2, 0, partData2.Length); - ms.Write(partData3, 0, partData3.Length); - - putExtra.ProgressHandler(stream.Length / 5, stream.Length); - result = PostFormWithRetry(token, ms.ToArray(), boundary, putExtra); - putExtra.ProgressHandler(stream.Length, stream.Length); - if (result.Code == (int)HttpCode.OK) - { - result.RefText += string.Format("[{0}] [FormUpload] Uploaded: #STREAM# ==> \"{1}\"\n", - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), key); - } - else - { - result.RefText += string.Format("[{0}] [FormUpload] Failed: code = {1}, text = {2}\n", - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), result.Code, result.Text); - } + //write crc32 + uint crc32 = await CRC32.CheckSumBytes(uploadStream); + uploadStream.Position = startPosition; + + multipartFormDataContent.Add(new StringContent(crc32.ToString(CultureInfo.InvariantCulture)), "crc32"); + + //write fname + multipartFormDataContent.Add(new StreamContent(uploadStream) + { + Headers = { ContentType = new MediaTypeHeaderValue(putExtra.MimeType) } + }, "file", fileName); + + putExtra.ProgressHandler(uploadedBytes / 5, uploadedBytes); - //close memory stream - ms.Close(); - dataMS.Close(); + string? ak = UpToken.GetAccessKeyFromUpToken(token); + string? bucket = UpToken.GetBucketFromUpToken(token); + if (ak == null || bucket == null) + { + return HttpResult.InvalidToken; } - catch (Exception ex) + + string uploadHost = this._config.UpHost(ak, bucket); + HttpRequestOptions reqOpts = _httpManager.CreateHttpRequestOptions("POST", uploadHost, null, token); + reqOpts.RequestContent = multipartFormDataContent; + result = await _httpManager.SendRequestAsync(reqOpts); + + putExtra.ProgressHandler(uploadedBytes, uploadedBytes); + if (result.Code == (int) HttpCode.OK) { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("[{0}] [FormUpload] Error: ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff")); - Exception e = ex; - while (e != null) - { - sb.Append(e.Message + " "); - e = e.InnerException; - } - sb.AppendLine(); + result.RefText += string.Format("[{0}] [FormUpload] Uploaded: #STREAM# ==> \"{1}\"\n", + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), key); + } + else + { + result.RefText += string.Format("[{0}] [FormUpload] Failed: code = {1}, text = {2}\n", + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), result.Code, result.Text); + } + } + catch (Exception ex) + { + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("[{0}] [FormUpload] Error: ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff")); + // 不要自己循环去获取内部异常,而是直接使用 ToString 方法输出,这样还能获取正确的堆栈信息 + //Exception e = ex; + //while (e != null) + //{ + // sb.Append(e.Message + " "); + // e = e.InnerException; + //} + sb.AppendLine(ex.ToString()); + sb.AppendLine(); - if (ex is QiniuException) - { - QiniuException qex = (QiniuException)ex; - result.Code = qex.HttpResult.Code; - result.RefCode = qex.HttpResult.Code; - result.Text = qex.HttpResult.Text; - result.RefText += sb.ToString(); - } - else - { - result.RefCode = (int)HttpCode.USER_UNDEF; - result.RefText += sb.ToString(); - } + if (ex is QiniuException) + { + QiniuException qex = (QiniuException) ex; + result.Code = qex.HttpResult.Code; + result.RefCode = qex.HttpResult.Code; + result.Text = qex.HttpResult.Text; + result.RefText += sb.ToString(); + } + else + { + result.RefCode = (int) HttpCode.USER_UNDEF; + result.RefText += sb.ToString(); } } @@ -261,15 +245,15 @@ public static UploadControllerAction DefaultUploadController() private HttpResult PostFormWithRetry(string token, byte[] data, string boundary, PutExtra putExtra) { //get upload host - string ak = UpToken.GetAccessKeyFromUpToken(token); - string bucket = UpToken.GetBucketFromUpToken(token); + string? ak = UpToken.GetAccessKeyFromUpToken(token); + string? bucket = UpToken.GetBucketFromUpToken(token); if (ak == null || bucket == null) { return HttpResult.InvalidToken; } - string uploadHost = this.config.UpHost(ak, bucket); - HttpResult result = httpManager.PostMultipart(uploadHost, data, boundary, null); + string uploadHost = this._config.UpHost(ak, bucket); + HttpResult result = _httpManager.PostMultipart(uploadHost, data, boundary, null); int retryTimes = 0; while ( @@ -277,11 +261,129 @@ private HttpResult PostFormWithRetry(string token, byte[] data, string boundary, UploadUtil.ShouldRetry(result.Code, result.RefCode) ) { - result = httpManager.PostMultipart(uploadHost, data, boundary, null); + result = _httpManager.PostMultipart(uploadHost, data, boundary, null); retryTimes += 1; } return result; } + + public void Dispose() + { + _httpManager.Dispose(); + } + } + + class ReadonlyWrapperStream : Stream + { + public static async ValueTask CreateWrapperStreamAsync(Stream inputStream) + { + if (inputStream.CanSeek) + { + var wrapperStream = new ReadonlyWrapperStream(inputStream, leaveOpen: true); + return wrapperStream; + } + else + { + var memoryStream = new MemoryStream(); + await inputStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + var wrapperStream = new ReadonlyWrapperStream(memoryStream, leaveOpen: false); + return wrapperStream; + } + } + + private ReadonlyWrapperStream(Stream innerStream, bool leaveOpen) + { + _innerStream = innerStream; + _leaveOpen = leaveOpen; + } + + private readonly Stream _innerStream; + + private readonly bool _leaveOpen; + public override void Flush() + { + _innerStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _innerStream.Read(buffer, offset, count); + } + + public override int Read(Span buffer) + { + return _innerStream.Read(buffer); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = new CancellationToken()) + { + return _innerStream.ReadAsync(buffer, cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanWrite => false; + + public override long Length => _innerStream.Length; + + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + + public override void CopyTo(Stream destination, int bufferSize) + { + _innerStream.CopyTo(destination, bufferSize); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + protected override void Dispose(bool disposing) + { + if (!_leaveOpen) + { + _innerStream.Dispose(); + } + + base.Dispose(disposing); + } + + public override async ValueTask DisposeAsync() + { + if (!_leaveOpen) + { + await _innerStream.DisposeAsync(); + } + + await base.DisposeAsync(); + } } } \ No newline at end of file diff --git a/src/Qiniu/Storage/ListInfo.cs b/src/Qiniu/Storage/ListInfo.cs index b008b681..be97b5ba 100644 --- a/src/Qiniu/Storage/ListInfo.cs +++ b/src/Qiniu/Storage/ListInfo.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Qiniu.Storage { /// @@ -32,19 +32,22 @@ public class ListInfo /// /// marker标记 /// - [JsonProperty("marker", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("marker")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Marker { get; set; } /// /// 文件列表 /// - [JsonProperty("items", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("items")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List Items { get; set; } /// /// 公共前缀 /// - [JsonProperty("commonPrefixes", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("commonPrefixes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List CommonPrefixes { get; set; } } } diff --git a/src/Qiniu/Storage/ListItem.cs b/src/Qiniu/Storage/ListItem.cs index 16f8471b..9c66e203 100644 --- a/src/Qiniu/Storage/ListItem.cs +++ b/src/Qiniu/Storage/ListItem.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Qiniu.Storage { /// @@ -9,31 +9,31 @@ public class ListItem /// /// 文件名 /// - [JsonProperty("key")] + [JsonPropertyName("key")] public string Key { get; set; } /// /// 文件hash(ETAG) /// - [JsonProperty("hash")] + [JsonPropertyName("hash")] public string Hash { get; set; } /// /// 文件大小(字节) /// - [JsonProperty("fsize")] + [JsonPropertyName("fsize")] public long Fsize { get; set; } /// /// 文件MIME类型 /// - [JsonProperty("mimeType")] + [JsonPropertyName("mimeType")] public string MimeType { get; set; } /// /// 上传时间 /// - [JsonProperty("putTime")] + [JsonPropertyName("putTime")] public long PutTime { get; set; } /// @@ -44,14 +44,15 @@ public class ListItem /// 3 深度归档存储 /// 4 归档直读存储 /// - [JsonProperty("type")] + [JsonPropertyName("type")] public int FileType { get; set; } /// /// 资源内容的唯一属主标识 /// 详见上传策略:https://developer.qiniu.com/kodo/1206/put-policy /// - [JsonProperty("endUser", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("endUser")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string EndUser { get; set; } /// @@ -59,7 +60,7 @@ public class ListItem /// 0 启用 /// 1 禁用 /// - [JsonProperty("status")] + [JsonPropertyName("status")] public int Status { get; set; } /// @@ -67,7 +68,8 @@ public class ListItem /// 服务端不确保一定返回此字段,详见: /// https://developer.qiniu.com/kodo/1284/list#:~:text=%E3%80%82%0A%0A%E7%B1%BB%E5%9E%8B%EF%BC%9A%E6%95%B0%E5%AD%97-,md5,-%E5%90%A6 /// - [JsonProperty("md5", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("md5")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Md5 { get; set; } } } diff --git a/src/Qiniu/Storage/ListResult.cs b/src/Qiniu/Storage/ListResult.cs index eb4f9aa3..ef84bec3 100644 --- a/src/Qiniu/Storage/ListResult.cs +++ b/src/Qiniu/Storage/ListResult.cs @@ -1,6 +1,6 @@ using System.Text; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.Storage { @@ -19,7 +19,7 @@ public ListInfo Result ListInfo info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info= JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.ListInfo); } return info; } diff --git a/src/Qiniu/Storage/PfopInfo.cs b/src/Qiniu/Storage/PfopInfo.cs index 57457f9e..03745222 100644 --- a/src/Qiniu/Storage/PfopInfo.cs +++ b/src/Qiniu/Storage/PfopInfo.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Qiniu.Storage { @@ -10,57 +10,59 @@ public class PfopInfo /// /// 任务ID /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id; /// /// 任务类型,为 1 代表为闲时任务 /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? Type; /// /// 任务创建时间 /// - [JsonProperty("creationDate", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("creationDate")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CreationDate; /// /// 任务结果状态码 /// - [JsonProperty("code")] + [JsonPropertyName("code")] public int Code; /// /// 任务结果状态描述 /// - [JsonProperty("desc")] + [JsonPropertyName("desc")] public string Desc; /// /// 待处理的数据文件 /// - [JsonProperty("inputKey")] + [JsonPropertyName("inputKey")] public string InputKey; /// /// 待处理文件所在空间 /// - [JsonProperty("inputBucket")] + [JsonPropertyName("inputBucket")] public string InputBucket; /// /// 数据处理队列 /// - [JsonProperty("pipeline")] + [JsonPropertyName("pipeline")] public string Pipeline; /// /// 任务的Reqid /// - [JsonProperty("reqid")] + [JsonPropertyName("reqid")] public string Reqid; /// /// 任务来源 /// - [JsonProperty("taskFrom")] + [JsonPropertyName("taskFrom")] public string TaskFrom; /// /// 数据处理的命令集合 /// - [JsonProperty("items")] + [JsonPropertyName("items")] public PfopItems[] Items; } @@ -72,42 +74,47 @@ public class PfopItems /// /// 命令 /// - [JsonProperty("cmd")] + [JsonPropertyName("cmd")] public string Cmd; /// /// 命令执行结果状态码 /// - [JsonProperty("code")] + [JsonPropertyName("code")] public string Code; /// /// 命令执行结果描述 /// - [JsonProperty("desc")] + [JsonPropertyName("desc")] public string Desc; /// /// 命令执行错误 /// - [JsonProperty("Error", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("Error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Error; /// /// VSample命令的生成文件名列表 /// - [JsonProperty("keys", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("keys")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[] Keys; /// /// 命令生成的文件名 /// - [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("key")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Key; /// /// 命令生成的文件内容hash /// - [JsonProperty("hash", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("hash")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Hash; /// /// 该命令是否返回了上一次相同命令生成的结果 /// - [JsonProperty("returnOld", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("returnOld")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? ReturnOld; } } diff --git a/src/Qiniu/Storage/PfopResult.cs b/src/Qiniu/Storage/PfopResult.cs index 34d48eb9..f3988807 100644 --- a/src/Qiniu/Storage/PfopResult.cs +++ b/src/Qiniu/Storage/PfopResult.cs @@ -1,7 +1,7 @@ using System.Text; using System.Collections.Generic; -using Newtonsoft.Json; using Qiniu.Http; +using Qiniu.Util; namespace Qiniu.Storage { @@ -21,7 +21,7 @@ public string PersistentId if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - Dictionary ret= JsonConvert.DeserializeObject>(Text); + Dictionary ret = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.DictionaryStringString); if (ret.ContainsKey("persistentId")) { pid = ret["persistentId"]; diff --git a/src/Qiniu/Storage/PrefopResult.cs b/src/Qiniu/Storage/PrefopResult.cs index 2b5a733a..7422029b 100644 --- a/src/Qiniu/Storage/PrefopResult.cs +++ b/src/Qiniu/Storage/PrefopResult.cs @@ -1,6 +1,6 @@ -using Newtonsoft.Json; -using Qiniu.Http; +using Qiniu.Http; using System.Text; +using Qiniu.Util; namespace Qiniu.Storage { /// @@ -19,7 +19,7 @@ public PfopInfo Result if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info= JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.PfopInfo); } return info; } @@ -37,7 +37,7 @@ public override string ToString() if (this.Result!=null) { - sb.AppendFormat("result: {0}\n", JsonConvert.SerializeObject(this.Result)); + sb.AppendFormat("result: {0}\n", QiniuJson.Serialize(this.Result, QiniuJson.SerializerContext.PfopInfo)); } else { diff --git a/src/Qiniu/Storage/PutExtra.cs b/src/Qiniu/Storage/PutExtra.cs index db5ed4f5..3ce6976b 100644 --- a/src/Qiniu/Storage/PutExtra.cs +++ b/src/Qiniu/Storage/PutExtra.cs @@ -14,7 +14,7 @@ public class PutExtra /// /// 上传可选参数字典,参数名次以 x: 开头 /// - public Dictionary Params; + public Dictionary? Params { set; get; } /// /// 指定文件的MimeType /// @@ -22,11 +22,11 @@ public class PutExtra /// /// 设置文件上传进度处理器 /// - public UploadProgressHandler ProgressHandler { set; get; } + public UploadProgressHandler? ProgressHandler { set; get; } /// /// 设置文件上传的状态控制器 /// - public UploadController UploadController { set; get; } + public UploadController? UploadController { set; get; } /// /// 最大重试次数 diff --git a/src/Qiniu/Storage/PutPolicy.cs b/src/Qiniu/Storage/PutPolicy.cs index 537cab2f..6add93e2 100644 --- a/src/Qiniu/Storage/PutPolicy.cs +++ b/src/Qiniu/Storage/PutPolicy.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Qiniu.Util; namespace Qiniu.Storage { @@ -11,153 +12,176 @@ public class PutPolicy /// /// [必需]bucket或者bucket:key /// - [JsonProperty("scope")] + [JsonPropertyName("scope")] public string Scope { get; set; } /// /// [可选]若为 1,表示允许用户上传以 scope 的 keyPrefix 为前缀的文件。 /// - [JsonProperty("isPrefixalScope", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("isPrefixalScope")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? isPrefixalScope { get; set; } /// /// [必需]上传策略失效时刻,请使用SetExpire来设置它 /// - [JsonProperty("deadline")] + [JsonPropertyName("deadline")] public long Deadline { get; private set; } /// /// [可选]"仅新增"模式 /// - [JsonProperty("insertOnly", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("insertOnly")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? InsertOnly { get; set; } /// /// [可选]saveKey 的优先级设置。为 true 时,saveKey不能为空,会忽略客户端指定的key,强制使用saveKey进行文件命名。 /// 默认为 false /// - [JsonProperty("forceSaveKey", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("forceSaveKey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? ForceSaveKey { get; set; } /// /// [可选]保存文件的key /// - [JsonProperty("saveKey", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("saveKey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string SaveKey { get; set; } /// /// [可选]终端用户 /// - [JsonProperty("endUser", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("endUser")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string EndUser { get; set; } /// /// [可选]返回URL /// - [JsonProperty("returnUrl", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("returnUrl")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string ReturnUrl { get; set; } /// /// [可选]返回内容 /// - [JsonProperty("returnBody", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("returnBody")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string ReturnBody { get; set; } /// /// [可选]回调URL /// - [JsonProperty("callbackUrl", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("callbackUrl")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CallbackUrl { get; set; } /// /// [可选]回调内容 /// - [JsonProperty("callbackBody", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("callbackBody")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CallbackBody { get; set; } /// /// [可选]回调内容类型 /// - [JsonProperty("callbackBodyType", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("callbackBodyType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CallbackBodyType { get; set; } /// /// [可选]回调host /// - [JsonProperty("callbackHost", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("callbackHost")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CallbackHost { get; set; } /// /// [可选]回调fetchkey /// - [JsonProperty("callbackFetchKey", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("callbackFetchKey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? CallbackFetchKey { get; set; } /// /// [可选]上传预转持久化,与 PersistentWorkflowTemplateId 二选一 /// - [JsonProperty("persistentOps", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("persistentOps")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string PersistentOps { get; set; } /// /// [可选]持久化结果通知 /// - [JsonProperty("persistentNotifyUrl", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("persistentNotifyUrl")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string PersistentNotifyUrl { get; set; } /// /// [可选]私有队列 /// - [JsonProperty("persistentPipeline", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("persistentPipeline")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string PersistentPipeline { get; set; } /// /// [可选]持久化任务类型,为 1 时开启闲时任务 /// - [JsonProperty("persistentType", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("persistentType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? PersistentType { get; set; } /// /// [可选]任务模版,与 PersistentOps 二选一 /// - [JsonProperty("persistentWorkflowTemplateID", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("persistentWorkflowTemplateID")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string PersistentWorkflowTemplateId { get; set; } /// /// [可选]上传文件大小限制:最小值,单位Byte /// - [JsonProperty("fsizeMin", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("fsizeMin")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long? FsizeMin { get; set; } /// /// [可选]上传文件大小限制:最大值,单位Byte /// - [JsonProperty("fsizeLimit", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("fsizeLimit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long? FsizeLimit { get; set; } /// /// [可选]上传时是否自动检测MIME /// - [JsonProperty("detectMime", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("detectMime")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? DetectMime { get; set; } /// /// [可选]上传文件MIME限制 /// - [JsonProperty("mimeLimit", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("mimeLimit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string MimeLimit { get; set; } /// /// [可选]文件上传后多少天后自动删除 /// - [JsonProperty("deleteAfterDays", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("deleteAfterDays")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? DeleteAfterDays { get; set; } /// /// [可选]文件的存储类型,默认为普通存储,设置为:0 标准存储(默认),1 低频存储,2 归档存储,3 深度归档存储 /// - [JsonProperty("fileType", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("fileType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? FileType { get; set; } /// @@ -185,7 +209,7 @@ public string ToJsonString() //默认一个小时有效期 this.SetExpires(3600); } - return JsonConvert.SerializeObject(this); + return QiniuJson.Serialize(this, QiniuJson.SerializerContext.PutPolicy); } } diff --git a/src/Qiniu/Storage/ResumableUploader.cs b/src/Qiniu/Storage/ResumableUploader.cs index 1812b6cb..c5f1a044 100644 --- a/src/Qiniu/Storage/ResumableUploader.cs +++ b/src/Qiniu/Storage/ResumableUploader.cs @@ -5,7 +5,6 @@ using System.Threading; using Qiniu.Util; using Qiniu.Http; -using Newtonsoft.Json; namespace Qiniu.Storage { @@ -20,10 +19,10 @@ namespace Qiniu.Storage /// public class ResumableUploader { - private Config config; + private readonly Config _config; // HTTP请求管理器(GET/POST等) - private HttpManager httpManager; + private readonly HttpManager _httpManager; /// /// 初始化 @@ -33,13 +32,13 @@ public ResumableUploader(Config config) { if (config == null) { - this.config = new Config(); + _config = new Config(); } else { - this.config = config; + _config = config; } - this.httpManager = new HttpManager(); + _httpManager = new HttpManager(); } @@ -53,10 +52,13 @@ public ResumableUploader(Config config) /// 上传文件后的返回结果 public HttpResult UploadFile(string localFile, string key, string token, PutExtra putExtra) { + ArgumentException.ThrowIfNullOrWhiteSpace(localFile); + ArgumentException.ThrowIfNullOrWhiteSpace(token); + try { - FileStream fs = new FileStream(localFile, FileMode.Open); - return this.UploadStream(fs, key, token, putExtra); + using FileStream fs = new FileStream(localFile, FileMode.Open); + return UploadStream(fs, key, token, putExtra); } catch (Exception ex) { @@ -84,7 +86,7 @@ public HttpResult UploadStream(Stream stream, string key, string upToken, PutExt if (putExtra == null) { putExtra = new PutExtra(); - putExtra.MaxRetryTimes = config.MaxRetryTimes; + putExtra.MaxRetryTimes = _config.MaxRetryTimes; } if (putExtra.ProgressHandler == null) { @@ -344,7 +346,7 @@ private HttpResult UploadStreamV2(Stream stream, string key, string upToken, Put if (resumeInfo == null || UnixTimestamp.IsContextExpired(resumeInfo.ExpiredAt)) { HttpResult res = initReq(encodedObjectName, upToken); - Dictionary responseBody = JsonConvert.DeserializeObject>(res.Text); + Dictionary responseBody = QiniuJson.Deserialize(res.Text, QiniuJson.SerializerContext.DictionaryStringString); if (res.Code != 200) { return res; @@ -695,7 +697,7 @@ private HttpResult MakeBlock(object resumeBlockerObj) return result; } - string uploadHost = this.config.UpHost(ak, bucket); + string uploadHost = _config.UpHost(ak, bucket); string url = ""; if (putExtra.Version == "v1") { @@ -715,7 +717,7 @@ private HttpResult MakeBlock(object resumeBlockerObj) byte[] data = ms.ToArray(); if (putExtra.Version == "v1") { - result = httpManager.PostData(url, data, upTokenStr); + result = _httpManager.PostData(url, data, upTokenStr); } else if (putExtra.Version == "v2") { @@ -724,7 +726,7 @@ private HttpResult MakeBlock(object resumeBlockerObj) // data to md5 string md5 = LabMD5.GenerateMD5(blockBuffer); headers.Add("Content-MD5", md5); - result = httpManager.PutDataWithHeaders(url, data, headers); + result = _httpManager.PutDataWithHeaders(url, data, headers); } else { throw new Exception("Invalid Version, only supports v1 / v2"); } @@ -734,7 +736,7 @@ private HttpResult MakeBlock(object resumeBlockerObj) { if (putExtra.Version == "v1") { - ResumeContext rc = JsonConvert.DeserializeObject(result.Text); + ResumeContext rc = QiniuJson.Deserialize(result.Text, QiniuJson.SerializerContext.ResumeContext); if (rc.Crc32 > 0) { @@ -769,7 +771,7 @@ private HttpResult MakeBlock(object resumeBlockerObj) } else if (putExtra.Version == "v2") { - Dictionary rc = JsonConvert.DeserializeObject>(result.Text); + Dictionary rc = QiniuJson.Deserialize(result.Text, QiniuJson.SerializerContext.DictionaryStringString); string md5 = LabMD5.GenerateMD5(blockBuffer); if (md5 != rc["md5"]) { @@ -893,13 +895,13 @@ private HttpResult MakeFile(string fileName, long size, string key, string upTok return HttpResult.InvalidToken; } - string uploadHost = this.config.UpHost(ak, bucket); + string uploadHost = _config.UpHost(ak, bucket); string url = string.Format("{0}/mkfile/{1}{2}{3}{4}{5}", uploadHost, size, mimeTypeStr, fnameStr, keyStr, paramStr); string body = string.Join(",", contexts); string upTokenStr = string.Format("UpToken {0}", upToken); - result = httpManager.PostText(url, body, upTokenStr); + result = _httpManager.PostText(url, body, upTokenStr); } catch (Exception ex) { @@ -950,10 +952,10 @@ private HttpResult initReq(string encodedObjectName, string upToken) return HttpResult.InvalidToken; } - string uploadHost = this.config.UpHost(ak, bucket); + string uploadHost = _config.UpHost(ak, bucket); string url = string.Format("{0}/buckets/{1}/objects/{2}/uploads", uploadHost, bucket, encodedObjectName); string upTokenStr = string.Format("UpToken {0}", upToken); - result = httpManager.PostText(url, null, upTokenStr); + result = _httpManager.PostText(url, null, upTokenStr); } catch (Exception ex) { @@ -1019,7 +1021,7 @@ private HttpResult completeParts(string fileName, ResumeInfo resumeInfo, string return HttpResult.InvalidToken; } - string uploadHost = this.config.UpHost(ak, bucket); + string uploadHost = _config.UpHost(ak, bucket); string upTokenStr = string.Format("UpToken {0}", upToken); Dictionary body = new Dictionary(); @@ -1028,8 +1030,8 @@ private HttpResult completeParts(string fileName, ResumeInfo resumeInfo, string body.Add("customVars", putExtra.Params); body.Add("parts", resumeInfo.Etags); string url = string.Format("{0}/buckets/{1}/objects/{2}/uploads/{3}", uploadHost, bucket, encodedObjectName, resumeInfo.UploadId); - string bodyStr = JsonConvert.SerializeObject(body); - result = httpManager.PostJson(url, bodyStr, upTokenStr); + string bodyStr = QiniuJson.Serialize(body, QiniuJson.SerializerContext.DictionaryStringObject); + result = _httpManager.PostJson(url, bodyStr, upTokenStr); } catch (Exception ex) { diff --git a/src/Qiniu/Storage/ResumeContext.cs b/src/Qiniu/Storage/ResumeContext.cs index 3b7a020c..1c90bfad 100644 --- a/src/Qiniu/Storage/ResumeContext.cs +++ b/src/Qiniu/Storage/ResumeContext.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Qiniu.Storage { @@ -11,49 +11,49 @@ public class ResumeContext /// /// 上下文信息 /// - [JsonProperty("ctx")] + [JsonPropertyName("ctx")] public string Ctx { get; set; } /// /// 校验和 /// - [JsonProperty("checksum")] + [JsonPropertyName("checksum")] public string Checksum { get; set; } /// /// crc32校验值 /// - [JsonProperty("crc32")] + [JsonPropertyName("crc32")] public uint Crc32 { get; set; } /// /// 文件偏移位置 /// - [JsonProperty("offset")] + [JsonPropertyName("offset")] public long Offset { get; set; } /// /// 上传目的host /// - [JsonProperty("host")] + [JsonPropertyName("host")] public string Host { get; set; } /// /// ctx失效时刻 /// - [JsonProperty("expired_at")] + [JsonPropertyName("expired_at")] public long ExpiredAt { get; set; } /// /// 新版分片上传上下文etag /// - [JsonProperty("etag")] + [JsonPropertyName("etag")] public Dictionary Etag { get; set; } /// /// 新版分片上传md5校验值 /// - [JsonProperty("md5")] + [JsonPropertyName("md5")] public string Md5 { get; set; } } } diff --git a/src/Qiniu/Storage/ResumeHelper.cs b/src/Qiniu/Storage/ResumeHelper.cs index aa4a8997..eabe2da5 100644 --- a/src/Qiniu/Storage/ResumeHelper.cs +++ b/src/Qiniu/Storage/ResumeHelper.cs @@ -1,7 +1,7 @@ using System; using System.IO; +using System.Text.Json; using Qiniu.Util; -using Newtonsoft.Json; namespace Qiniu.Storage { @@ -21,7 +21,7 @@ public static string GetDefaultRecordKey(string localFile, string key) { string tempDir = System.IO.Path.GetTempPath(); System.IO.FileInfo fileInfo = new System.IO.FileInfo(localFile); - string uniqueKey = string.Format("{0}:{1}:{2}", localFile, key, fileInfo.LastWriteTime.ToFileTime()); + string uniqueKey = $"{localFile}:{key}:{fileInfo.LastWriteTime.ToFileTime()}"; return Path.Combine(tempDir, "QiniuResume_" + Hashing.CalcMD5X(uniqueKey)); } @@ -30,27 +30,36 @@ public static string GetDefaultRecordKey(string localFile, string key) /// /// 断点记录文件 /// 断点信息 - public static ResumeInfo Load(string recordFile) + public static ResumeInfo? Load(string recordFile) { - ResumeInfo resumeInfo = null; + if (string.IsNullOrWhiteSpace(recordFile)) + { + throw new ArgumentException("recordFile cannot be null or empty.", nameof(recordFile)); + } try { - using (FileStream fs = new FileStream(recordFile, FileMode.Open)) + using (FileStream fs = new FileStream(recordFile, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (StreamReader sr = new StreamReader(fs)) { string jsonStr = sr.ReadToEnd(); - resumeInfo=JsonConvert.DeserializeObject(jsonStr); + return QiniuJson.Deserialize(jsonStr, QiniuJson.SerializerContext.ResumeInfo); } } } - catch (Exception) + catch (FileNotFoundException) { - resumeInfo = null; + return null; + } + catch (DirectoryNotFoundException) + { + return null; + } + catch (JsonException) + { + return null; } - - return resumeInfo; } /// @@ -60,6 +69,12 @@ public static ResumeInfo Load(string recordFile) /// 断点记录文件 public static void Save(ResumeInfo resumeInfo, string recordFile) { + ArgumentNullException.ThrowIfNull(resumeInfo); + if (string.IsNullOrWhiteSpace(recordFile)) + { + throw new ArgumentException("recordFile cannot be null or empty.", nameof(recordFile)); + } + string jsonStr = resumeInfo.ToJsonStr(); using (FileStream fs = new FileStream(recordFile, FileMode.Create)) diff --git a/src/Qiniu/Storage/ResumeInfo.cs b/src/Qiniu/Storage/ResumeInfo.cs index 0a14167b..2e4753c2 100644 --- a/src/Qiniu/Storage/ResumeInfo.cs +++ b/src/Qiniu/Storage/ResumeInfo.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Qiniu.Storage { /// @@ -10,31 +10,31 @@ public class ResumeInfo /// /// 文件大小 /// - [JsonProperty("fileSize")] + [JsonPropertyName("fileSize")] public long FileSize { get; set; } /// /// 文件块总数 /// - [JsonProperty("blockCount")] + [JsonPropertyName("blockCount")] public long BlockCount { get; set; } /// /// 上下文信息列表 /// - [JsonProperty("contexts")] - public string[] Contexts { get; set; } + [JsonPropertyName("contexts")] + public string[] Contexts { get; set; } = System.Array.Empty(); /// /// 上下文信息过期列表,与 context 配合使用 /// - [JsonProperty("contextsExpiredAt")] - public long[] ContextsExpiredAt { get; set; } + [JsonPropertyName("contextsExpiredAt")] + public long[] ContextsExpiredAt { get; set; } = System.Array.Empty(); /// /// Ctx过期时间戳(单位秒) /// - [JsonProperty("expiredAt")] + [JsonPropertyName("expiredAt")] public long ExpiredAt { get; set; } /// @@ -45,24 +45,24 @@ public class ResumeInfo /// /// 新版分片上下文信息列表 /// - [JsonProperty("etags")] - public Dictionary[] Etags { get; set; } + [JsonPropertyName("etags")] + public Dictionary[] Etags { get; set; } = System.Array.Empty>(); /// /// 新版分片上传id /// - [JsonProperty("uploadId")] - public string UploadId { get; set; } + [JsonPropertyName("uploadId")] + public string UploadId { get; set; } = string.Empty; /// /// 完成上传的字节数 /// - [JsonProperty("uploaded")] + [JsonPropertyName("uploaded")] public long Uploaded { get; set; } public string ToJsonStr() { - return JsonConvert.SerializeObject(this); + return Util.QiniuJson.Serialize(this, Util.QiniuJson.SerializerContext.ResumeInfo); } } } diff --git a/src/Qiniu/Storage/StatResult.cs b/src/Qiniu/Storage/StatResult.cs index 3fe51f73..8e804e27 100644 --- a/src/Qiniu/Storage/StatResult.cs +++ b/src/Qiniu/Storage/StatResult.cs @@ -1,6 +1,6 @@ using System.Text; using Qiniu.Http; -using Newtonsoft.Json; +using Qiniu.Util; namespace Qiniu.Storage { @@ -12,14 +12,14 @@ public class StatResult : HttpResult /// /// stat信息列表 /// - public FileInfo Result + public FileInfo? Result { get { - FileInfo info = null; + FileInfo? info = null; if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) { - info = JsonConvert.DeserializeObject(Text); + info = QiniuJson.Deserialize(Text, QiniuJson.SerializerContext.FileInfo); } return info; } @@ -64,7 +64,7 @@ public override string ToString() sb.AppendFormat("ref-info:\n"); foreach (var d in RefInfo) { - sb.AppendLine(string.Format("{0}: {1}", d.Key, d.Value)); + sb.AppendLine($"{d.Key}: {d.Value}"); } } diff --git a/src/Qiniu/Storage/UploadManager.cs b/src/Qiniu/Storage/UploadManager.cs index 4aa6d6bd..f69002dc 100644 --- a/src/Qiniu/Storage/UploadManager.cs +++ b/src/Qiniu/Storage/UploadManager.cs @@ -2,6 +2,7 @@ using Qiniu.Util; using Qiniu.Http; using System.Collections.Generic; +using System.Threading.Tasks; namespace Qiniu.Storage { @@ -10,7 +11,7 @@ namespace Qiniu.Storage /// public class UploadManager { - private Config config; + private readonly Config _config; /// /// 初始化 @@ -18,7 +19,7 @@ public class UploadManager /// 文件上传的配置信息 public UploadManager(Config config) { - this.config = config; + this._config = config; } /// @@ -29,10 +30,10 @@ public UploadManager(Config config) /// 上传凭证 /// 上传可选设置 /// 上传文件后的返回结果 - public HttpResult UploadData(byte[] data, string key, string token, PutExtra extra) + public async Task UploadDataAsync(byte[] data, string key, string token, PutExtra extra) { - FormUploader formUploader = new FormUploader(this.config); - return formUploader.UploadData(data, key, token, extra); + FormUploader formUploader = new FormUploader(this._config); + return await formUploader.UploadDataAsync(data, key, token, extra); } /// @@ -44,26 +45,25 @@ public HttpResult UploadData(byte[] data, string key, string token, PutExtra ext /// 上传凭证 /// 上传可选设置 /// 上传文件后的返回结果 - public HttpResult UploadFile(string localFile, string key, string token, PutExtra extra) + public async Task UploadFile(string localFile, string key, string token, PutExtra extra) { - HttpResult result = new HttpResult(); + HttpResult result; System.IO.FileInfo fi = new System.IO.FileInfo(localFile); - if (fi.Length > this.config.PutThreshold) + if (fi.Length > this._config.PutThreshold) { - ResumableUploader resumeUploader = new ResumableUploader(config); + ResumableUploader resumeUploader = new ResumableUploader(_config); result = resumeUploader.UploadFile(localFile, key, token, extra); } else { - FormUploader formUploader = new FormUploader(config); - result = formUploader.UploadFile(localFile, key, token, extra); + FormUploader formUploader = new FormUploader(_config); + result = await formUploader.UploadFile(localFile, key, token, extra); } return result; } - /// /// 上传文件数据流,根据文件大小以及设置的阈值(用户初始化UploadManager时可指定该值)自动选择: /// 若文件大小超过设定阈值,使用ResumableUploader,否则使用FormUploader @@ -73,19 +73,19 @@ public HttpResult UploadFile(string localFile, string key, string token, PutExtr /// 上传凭证 /// 上传可选设置 /// 上传文件后的返回结果 - public HttpResult UploadStream(Stream stream, string key, string token, PutExtra extra) + public async Task UploadStream(Stream stream, string key, string token, PutExtra extra) { HttpResult result = new HttpResult(); - if (stream.Length > this.config.PutThreshold) + if (stream.Length > this._config.PutThreshold) { - ResumableUploader resumeUploader = new ResumableUploader(this.config); + ResumableUploader resumeUploader = new ResumableUploader(this._config); result = resumeUploader.UploadStream(stream, key, token, extra); } else { - FormUploader formUploader = new FormUploader(this.config); - result = formUploader.UploadStream(stream, key, token, extra); + FormUploader formUploader = new FormUploader(this._config); + result = await formUploader.UploadStreamAsync(stream, key, token, extra); } return result; diff --git a/src/Qiniu/Storage/ZoneHelper.cs b/src/Qiniu/Storage/ZoneHelper.cs index 198b84e3..b1cb2ef3 100644 --- a/src/Qiniu/Storage/ZoneHelper.cs +++ b/src/Qiniu/Storage/ZoneHelper.cs @@ -1,8 +1,9 @@ using System; using System.Text; using System.Collections.Generic; +using System.Linq; using Qiniu.Http; -using Newtonsoft.Json; +using Qiniu.Util; namespace Qiniu.Storage { @@ -17,8 +18,8 @@ internal class ZoneCacheValue /// public class ZoneHelper { - private static Dictionary zoneCache = new Dictionary(); - private static object rwLock = new object(); + private static readonly Dictionary _zoneCache = new Dictionary(16); + private static readonly object _rwLock = new object(); /// /// 从 UC 服务查询得到各个服务域名,生成 Zone 对象并返回 @@ -30,19 +31,22 @@ public class ZoneHelper public static Zone QueryZone( string accessKey, string bucket, - string ucHost = null, - List backupUcHosts = null + string? ucHost = null, + List? backupUcHosts = null ) { - ZoneCacheValue zoneCacheValue = null; - string cacheKey = string.Format("{0}:{1}", accessKey, bucket); + ArgumentException.ThrowIfNullOrWhiteSpace(accessKey); + ArgumentException.ThrowIfNullOrWhiteSpace(bucket); + + ZoneCacheValue? zoneCacheValue = null; + string cacheKey = $"{accessKey}:{bucket}"; //check from cache - lock (rwLock) + lock (_rwLock) { - if (zoneCache.ContainsKey(cacheKey)) + if (_zoneCache.ContainsKey(cacheKey)) { - zoneCacheValue = zoneCache[cacheKey]; + zoneCacheValue = _zoneCache[cacheKey]; } } @@ -57,7 +61,7 @@ public static Zone QueryZone( //query from uc api Zone zone; - HttpResult hr = null; + HttpResult? hr = null; if (String.IsNullOrEmpty(ucHost)) { ucHost = "https://" + Config.DefaultQueryRegionHost; @@ -65,16 +69,12 @@ public static Zone QueryZone( if (backupUcHosts == null) { - backupUcHosts = Config.DefaultBackupQueryRegionHosts; + backupUcHosts = Config.DefaultBackupQueryRegionHosts.ToList(); } try { - string queryUrl = string.Format("{0}/v4/query?ak={1}&bucket={2}", - ucHost, - accessKey, - bucket - ); + string queryUrl = $"{ucHost}/v4/query?ak={accessKey}&bucket={bucket}"; HttpManager httpManager = new HttpManager(); List middlewares = new List { @@ -88,24 +88,29 @@ public static Zone QueryZone( throw new Exception("code: " + hr.Code + ", text: " + hr.Text + ", ref-text:" + hr.RefText); } - ZoneInfo zInfo = JsonConvert.DeserializeObject(hr.Text); + ZoneInfo zInfo = QiniuJson.Deserialize(hr.Text, QiniuJson.SerializerContext.ZoneInfo); if (zInfo == null) { throw new Exception("JSON Deserialize failed: " + hr.Text); } - if (zInfo.Hosts.Length == 0) + if (zInfo.Hosts == null || zInfo.Hosts.Length == 0) { throw new Exception("There are no hosts available: " + hr.Text); } ZoneHost zHost = zInfo.Hosts[0]; + if (zHost.Up?.Domains == null || zHost.Up.Domains.Length == 0) + { + throw new Exception("There are no upload hosts available: " + hr.Text); + } + zone = new Zone(); zone.SrcUpHosts = zHost.Up.Domains; zone.CdnUpHosts = zHost.Up.Domains; - if (!string.IsNullOrEmpty(zHost.Io.Domains[0])) + if (zHost.Io?.Domains != null && zHost.Io.Domains.Length > 0 && !string.IsNullOrEmpty(zHost.Io.Domains[0])) { zone.IovipHost = zHost.Io.Domains[0]; } @@ -114,7 +119,7 @@ public static Zone QueryZone( zone.IovipHost = Config.DefaultIoHost; } - if (!string.IsNullOrEmpty(zHost.Api.Domains[0])) + if (zHost.Api?.Domains != null && zHost.Api.Domains.Length > 0 && !string.IsNullOrEmpty(zHost.Api.Domains[0])) { zone.ApiHost = zHost.Api.Domains[0]; } @@ -123,7 +128,7 @@ public static Zone QueryZone( zone.ApiHost = Config.DefaultApiHost; } - if (!string.IsNullOrEmpty(zHost.Rs.Domains[0])) + if (zHost.Rs?.Domains != null && zHost.Rs.Domains.Length > 0 && !string.IsNullOrEmpty(zHost.Rs.Domains[0])) { zone.RsHost = zHost.Rs.Domains[0]; } @@ -132,7 +137,7 @@ public static Zone QueryZone( zone.RsHost = Config.DefaultRsHost; } - if (!string.IsNullOrEmpty(zHost.Rsf.Domains[0])) + if (zHost.Rsf?.Domains != null && zHost.Rsf.Domains.Length > 0 && !string.IsNullOrEmpty(zHost.Rsf.Domains[0])) { zone.RsfHost = zHost.Rsf.Domains[0]; } @@ -141,13 +146,13 @@ public static Zone QueryZone( zone.RsfHost = Config.DefaultRsfHost; } - lock (rwLock) + lock (_rwLock) { zoneCacheValue = new ZoneCacheValue(); TimeSpan ttl = TimeSpan.FromSeconds(zHost.Ttl); zoneCacheValue.Deadline = DateTime.Now.Add(ttl); zoneCacheValue.Zone = zone; - zoneCache[cacheKey] = zoneCacheValue; + _zoneCache[cacheKey] = zoneCacheValue; } } catch (Exception ex) diff --git a/src/Qiniu/Storage/ZoneInfo.cs b/src/Qiniu/Storage/ZoneInfo.cs index 6d6b1036..6d880b28 100644 --- a/src/Qiniu/Storage/ZoneInfo.cs +++ b/src/Qiniu/Storage/ZoneInfo.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Qiniu.Storage { @@ -7,7 +7,7 @@ namespace Qiniu.Storage /// internal class ZoneInfo { - [JsonProperty("hosts")] + [JsonPropertyName("hosts")] public ZoneHost[] Hosts { get; set; } } @@ -16,45 +16,47 @@ internal class ZoneHost /// /// 过期时间,单位:秒 /// - [JsonProperty("ttl")] + [JsonPropertyName("ttl")] public int Ttl { get; set; } - [JsonProperty("region")] + [JsonPropertyName("region")] public string Region { get; set; } - [JsonProperty("io")] + [JsonPropertyName("io")] public ServiceDomains Io { get; set; } - [JsonProperty("io_src")] + [JsonPropertyName("io_src")] public ServiceDomains IoSrc { get; set; } - [JsonProperty("up")] + [JsonPropertyName("up")] public ServiceDomains Up { get; set; } - [JsonProperty("api")] + [JsonPropertyName("api")] public ServiceDomains Api { get; set; } - [JsonProperty("rs")] + [JsonPropertyName("rs")] public ServiceDomains Rs { get; set; } - [JsonProperty("rsf")] + [JsonPropertyName("rsf")] public ServiceDomains Rsf { get; set; } - [JsonProperty("s3")] + [JsonPropertyName("s3")] public ServiceDomains S3 { get; set; } - [JsonProperty("uc")] + [JsonPropertyName("uc")] public ServiceDomains Uc { get; set; } internal class ServiceDomains { - [JsonProperty("domains")] + [JsonPropertyName("domains")] public string[] Domains { get; set; } - [JsonProperty("old", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("old")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[] Old { get; set; } - [JsonProperty("region_alias", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("region_alias")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string RegionAlias { get; set; } } } diff --git a/src/Qiniu/Util/Auth.cs b/src/Qiniu/Util/Auth.cs index bf6c94ab..53d02f72 100644 --- a/src/Qiniu/Util/Auth.cs +++ b/src/Qiniu/Util/Auth.cs @@ -39,7 +39,7 @@ public Auth(Mac mac, AuthOptions authOptions = null) /// 请求的URL /// 请求的主体内容 /// 生成的管理凭证 - public string CreateManageToken(string url,byte[] body) + public string CreateManageToken(string url,byte[]? body) { return string.Format("QBox {0}", signature.SignRequest(url, body)); } diff --git a/src/Qiniu/Util/CRC32.cs b/src/Qiniu/Util/CRC32.cs index d488265e..f1baaf64 100644 --- a/src/Qiniu/Util/CRC32.cs +++ b/src/Qiniu/Util/CRC32.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.IO; +using System.Threading.Tasks; namespace Qiniu.Util { @@ -49,7 +51,7 @@ private static uint[] makeTable(uint poly) uint[] table = new uint[256]; for (int i = 0; i < 256; i++) { - uint crc = (uint)i; + uint crc = (uint) i; for (int j = 0; j < 8; j++) { if ((crc & 1) == 1) @@ -76,7 +78,7 @@ public static uint Update(UInt32 crc, UInt32[] table, byte[] p, int offset, int crc = ~crc; for (int i = 0; i < count; i++) { - crc = table[((byte)crc) ^ p[offset + i]] ^ (crc >> 8); + crc = table[((byte) crc) ^ p[offset + i]] ^ (crc >> 8); } return ~crc; } @@ -116,7 +118,7 @@ public static uint checkSumFile(string filePath) { CRC32 crc = new CRC32(); int bufferLen = 32 * 1024; - using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { byte[] buffer = new byte[bufferLen]; while (true) @@ -129,5 +131,31 @@ public static uint checkSumFile(string filePath) } return crc.Sum(); } + + public static async Task CheckSumBytes(Stream stream) + { + var buffer = ArrayPool.Shared.Rent(1024); + + CRC32 crc = new CRC32(); + + try + { + while (true) + { + var readCount = await stream.ReadAsync(buffer.AsMemory()); + if (readCount == 0) + { + break; + } + + crc.Write(buffer, 0, readCount); + } + return crc.Sum(); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } } } \ No newline at end of file diff --git a/src/Qiniu/Util/ETag.cs b/src/Qiniu/Util/ETag.cs index 2b57f959..5fd35167 100644 --- a/src/Qiniu/Util/ETag.cs +++ b/src/Qiniu/Util/ETag.cs @@ -9,10 +9,10 @@ namespace Qiniu.Util public class ETag { // 块大小(固定为4MB) - private const int BLOCK_SIZE = 4 * 1024 * 1024; + private const int BlockSize = 4 * 1024 * 1024; // 计算时以20B为单位 - private static int BLOCK_SHA1_SIZE = 20; + private const int BlockSha1Size = 20; /// /// 计算文件hash(ETAG) @@ -21,51 +21,50 @@ public class ETag /// 文件hash public static string CalcHash(string filePath) { + ArgumentException.ThrowIfNullOrWhiteSpace(filePath); + string qetag = ""; - try + using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { - using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + long fileLength = stream.Length; + byte[] buffer = new byte[BlockSize]; + byte[] finalBuffer = new byte[BlockSha1Size + 1]; + if (fileLength <= BlockSize) + { + int readByteCount = stream.Read(buffer, 0, BlockSize); + byte[] readBuffer = new byte[readByteCount]; + Array.Copy(buffer, readBuffer, readByteCount); + + byte[] sha1Buffer = Hashing.CalcSHA1(readBuffer); + + finalBuffer[0] = 0x16; + Array.Copy(sha1Buffer, 0, finalBuffer, 1, sha1Buffer.Length); + } + else { - long fileLength = stream.Length; - byte[] buffer = new byte[BLOCK_SIZE]; - byte[] finalBuffer = new byte[BLOCK_SHA1_SIZE + 1]; - if (fileLength <= BLOCK_SIZE) + long blockCount = (fileLength % BlockSize == 0) ? (fileLength / BlockSize) : (fileLength / BlockSize + 1); + byte[] sha1AllBuffer = new byte[BlockSha1Size * blockCount]; + + for (int i = 0; i < blockCount; i++) { - int readByteCount = stream.Read(buffer, 0, BLOCK_SIZE); + int readByteCount = stream.Read(buffer, 0, BlockSize); byte[] readBuffer = new byte[readByteCount]; Array.Copy(buffer, readBuffer, readByteCount); byte[] sha1Buffer = Hashing.CalcSHA1(readBuffer); - - finalBuffer[0] = 0x16; - Array.Copy(sha1Buffer, 0, finalBuffer, 1, sha1Buffer.Length); + Array.Copy(sha1Buffer, 0, sha1AllBuffer, i * BlockSha1Size, sha1Buffer.Length); } - else - { - long blockCount = (fileLength % BLOCK_SIZE == 0) ? (fileLength / BLOCK_SIZE) : (fileLength / BLOCK_SIZE + 1); - byte[] sha1AllBuffer = new byte[BLOCK_SHA1_SIZE * blockCount]; - - for (int i = 0; i < blockCount; i++) - { - int readByteCount = stream.Read(buffer, 0, BLOCK_SIZE); - byte[] readBuffer = new byte[readByteCount]; - Array.Copy(buffer, readBuffer, readByteCount); - byte[] sha1Buffer = Hashing.CalcSHA1(readBuffer); - Array.Copy(sha1Buffer, 0, sha1AllBuffer, i * BLOCK_SHA1_SIZE, sha1Buffer.Length); - } + byte[] sha1AllBufferSha1 = Hashing.CalcSHA1(sha1AllBuffer); - byte[] sha1AllBufferSha1 = Hashing.CalcSHA1(sha1AllBuffer); + finalBuffer[0] = 0x96; + Array.Copy(sha1AllBufferSha1, 0, finalBuffer, 1, sha1AllBufferSha1.Length); - finalBuffer[0] = 0x96; - Array.Copy(sha1AllBufferSha1, 0, finalBuffer, 1, sha1AllBufferSha1.Length); - - } - qetag = Base64.UrlSafeBase64Encode(finalBuffer); } + qetag = Base64.UrlSafeBase64Encode(finalBuffer); } - catch (Exception) { } + return qetag; } diff --git a/src/Qiniu/Util/Mac.cs b/src/Qiniu/Util/Mac.cs index 69c59ac2..23ad39c7 100644 --- a/src/Qiniu/Util/Mac.cs +++ b/src/Qiniu/Util/Mac.cs @@ -1,4 +1,6 @@ -namespace Qiniu.Util +using System; + +namespace Qiniu.Util { /// /// 账户访问控制(密钥) @@ -8,12 +10,12 @@ public class Mac /// /// 密钥-AccessKey /// - public string AccessKey { set; get; } + public string AccessKey { init; get; } /// /// 密钥-SecretKey /// - public string SecretKey { set; get; } + public string SecretKey { init; get; } /// /// 初始化密钥AK/SK @@ -22,8 +24,11 @@ public class Mac /// SecretKey public Mac(string accessKey, string secretKey) { - this.AccessKey = accessKey; - this.SecretKey = secretKey; + ArgumentNullException.ThrowIfNull(accessKey); + ArgumentNullException.ThrowIfNull(secretKey); + + AccessKey = accessKey; + SecretKey = secretKey; } } } \ No newline at end of file diff --git a/src/Qiniu/Util/QETag.cs b/src/Qiniu/Util/QETag.cs index 9bd8f1b8..e7e8ab03 100644 --- a/src/Qiniu/Util/QETag.cs +++ b/src/Qiniu/Util/QETag.cs @@ -1,5 +1,4 @@ using System; -using System.IO; namespace Qiniu.Util { @@ -8,66 +7,25 @@ namespace Qiniu.Util /// public class QETag { - // 块大小(固定为4MB) - private const int BLOCK_SIZE = 4 * 1024 * 1024; - - // 计算时以20B为单位 - private static int BLOCK_SHA1_SIZE = 20; - /// /// 计算文件hash(ETAG) /// /// /// 文件hash + [Obsolete("Use CalcHash instead.")] public static string calcHash(string filePath) { - string qetag = ""; - - try - { - using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) - { - long fileLength = stream.Length; - byte[] buffer = new byte[BLOCK_SIZE]; - byte[] finalBuffer = new byte[BLOCK_SHA1_SIZE + 1]; - if (fileLength <= BLOCK_SIZE) - { - int readByteCount = stream.Read(buffer, 0, BLOCK_SIZE); - byte[] readBuffer = new byte[readByteCount]; - Array.Copy(buffer, readBuffer, readByteCount); - - byte[] sha1Buffer = Hashing.CalcSHA1(readBuffer); - - finalBuffer[0] = 0x16; - Array.Copy(sha1Buffer, 0, finalBuffer, 1, sha1Buffer.Length); - } - else - { - long blockCount = (fileLength % BLOCK_SIZE == 0) ? (fileLength / BLOCK_SIZE) : (fileLength / BLOCK_SIZE + 1); - byte[] sha1AllBuffer = new byte[BLOCK_SHA1_SIZE * blockCount]; - - for (int i = 0; i < blockCount; i++) - { - int readByteCount = stream.Read(buffer, 0, BLOCK_SIZE); - byte[] readBuffer = new byte[readByteCount]; - Array.Copy(buffer, readBuffer, readByteCount); - - byte[] sha1Buffer = Hashing.CalcSHA1(readBuffer); - Array.Copy(sha1Buffer, 0, sha1AllBuffer, i * BLOCK_SHA1_SIZE, sha1Buffer.Length); - } - - byte[] sha1AllBufferSha1 = Hashing.CalcSHA1(sha1AllBuffer); - - finalBuffer[0] = 0x96; - Array.Copy(sha1AllBufferSha1, 0, finalBuffer, 1, sha1AllBufferSha1.Length); - - } - qetag = Base64.UrlSafeBase64Encode(finalBuffer); - } - } - catch (Exception) { } + return CalcHash(filePath); + } - return qetag; + /// + /// 计算文件hash(ETAG) + /// + /// 文件路径 + /// 文件hash + public static string CalcHash(string filePath) + { + return ETag.CalcHash(filePath); } } } \ No newline at end of file diff --git a/src/Qiniu/Util/QiniuJson.cs b/src/Qiniu/Util/QiniuJson.cs new file mode 100644 index 00000000..798624d5 --- /dev/null +++ b/src/Qiniu/Util/QiniuJson.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Qiniu.CDN; +using Qiniu.Storage; + +namespace Qiniu.Util +{ + internal static class QiniuJson + { + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + + internal static readonly QiniuJsonSerializerContext SerializerContext = new(SerializerOptions); + + public static T? Deserialize(string? json, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(typeInfo); + + if (string.IsNullOrWhiteSpace(json)) + { + return default; + } + + return JsonSerializer.Deserialize(json, typeInfo); + } + + public static string Serialize(T value, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(typeInfo); + return JsonSerializer.Serialize(value, typeInfo); + } + } + + [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(ResumeInfo))] + [JsonSerializable(typeof(ResumeContext))] + [JsonSerializable(typeof(ZoneInfo))] + [JsonSerializable(typeof(PutPolicy))] + [JsonSerializable(typeof(FileInfo))] + [JsonSerializable(typeof(BandwidthInfo))] + [JsonSerializable(typeof(BandwidthRequest))] + [JsonSerializable(typeof(FluxInfo))] + [JsonSerializable(typeof(FluxRequest))] + [JsonSerializable(typeof(LogListInfo))] + [JsonSerializable(typeof(LogListRequest))] + [JsonSerializable(typeof(PrefetchInfo))] + [JsonSerializable(typeof(PrefetchRequest))] + [JsonSerializable(typeof(RefreshInfo))] + [JsonSerializable(typeof(RefreshRequest))] + [JsonSerializable(typeof(BatchInfo))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(BucketInfo))] + [JsonSerializable(typeof(FetchInfo))] + [JsonSerializable(typeof(ListInfo))] + [JsonSerializable(typeof(PfopInfo))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary[]))] + internal partial class QiniuJsonSerializerContext : JsonSerializerContext + { + } +} diff --git a/src/Qiniu/Util/Signature.cs b/src/Qiniu/Util/Signature.cs index 915cb651..d2335051 100644 --- a/src/Qiniu/Util/Signature.cs +++ b/src/Qiniu/Util/Signature.cs @@ -20,7 +20,7 @@ namespace Qiniu.Util /// public class Signature { - private Mac mac; + private readonly Mac _mac; /// /// 初始化 @@ -28,20 +28,20 @@ public class Signature /// 账号(密钥) public Signature(Mac mac) { - this.mac = mac; + this._mac = mac; } - private string encodedSign(byte[] data) + private string EncodedSign(byte[] data) { - HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(mac.SecretKey)); + HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_mac.SecretKey)); byte[] digest = hmac.ComputeHash(data); return Base64.UrlSafeBase64Encode(digest); } - private string encodedSign(string str) + private string EncodedSign(string str) { byte[] data = Encoding.UTF8.GetBytes(str); - return encodedSign(data); + return EncodedSign(data); } /// @@ -51,7 +51,7 @@ private string encodedSign(string str) /// public string Sign(byte[] data) { - return string.Format("{0}:{1}", mac.AccessKey, encodedSign(data)); + return $"{_mac.AccessKey}:{EncodedSign(data)}"; } /// @@ -72,8 +72,8 @@ public string Sign(string str) /// public string SignWithData(byte[] data) { - string sstr = Base64.UrlSafeBase64Encode(data); - return string.Format("{0}:{1}:{2}", mac.AccessKey, encodedSign(sstr), sstr); + string safeString = Base64.UrlSafeBase64Encode(data); + return $"{_mac.AccessKey}:{EncodedSign(safeString)}:{safeString}"; } /// @@ -93,7 +93,7 @@ public string SignWithData(string str) /// 请求目标的URL /// 请求的主体数据 /// - public string SignRequest(string url, byte[] body) + public string SignRequest(string url, byte[]? body) { Uri u = new Uri(url); string pathAndQuery = u.PathAndQuery; @@ -107,10 +107,10 @@ public string SignRequest(string url, byte[] body) { buffer.Write(body, 0, body.Length); } - HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(mac.SecretKey)); + HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_mac.SecretKey)); byte[] digest = hmac.ComputeHash(buffer.ToArray()); string digestBase64 = Base64.UrlSafeBase64Encode(digest); - return string.Format("{0}:{1}", mac.AccessKey, digestBase64); + return $"{_mac.AccessKey}:{digestBase64}"; } } @@ -134,7 +134,7 @@ public string SignRequest(string url, string body) /// 请求的 Header,支持非规范化的字段名,内部自动转换,例如:CONTENT-TYPE -> Content-Type /// 请求的主体数据,要求 UTF-8 编码 /// 签名结果,但不包括 Qiniu 这一开头,例如:"access_key:token" - public string SignRequestV2(string method, string url, StringDictionary headers, string body) + public string SignRequestV2(string method, string url, StringDictionary? headers, string body) { Dictionary canonicalHeaders = new Dictionary(); @@ -181,11 +181,11 @@ public string SignRequestV2(string method, string url, StringDictionary headers, } // calculate sign - HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(mac.SecretKey)); + HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_mac.SecretKey)); byte[] digest = hmac.ComputeHash(Encoding.UTF8.GetBytes(strToSignBuilder.ToString())); string digestBase64 = Base64.UrlSafeBase64Encode(digest); - return string.Format("{0}:{1}", mac.AccessKey, digestBase64); + return string.Format("{0}:{1}", _mac.AccessKey, digestBase64); } /// @@ -205,10 +205,10 @@ public bool VerifyRequest( string method, string url, StringDictionary headers, - string body = null + string? body = null ) { - byte[] bodyBytes = null; + byte[]? bodyBytes = null; if (!string.IsNullOrEmpty(body)) { bodyBytes = Encoding.UTF8.GetBytes(body); } @@ -224,7 +224,7 @@ public bool VerifyRequest( string method, string url, StringDictionary headers, - byte[] body = null + byte[]? body = null ) { if (!headers.ContainsKey("Authorization")) @@ -232,7 +232,12 @@ public bool VerifyRequest( return false; } - string authString = headers["Authorization"]; + string? authString = headers["Authorization"]; + if (authString is null) + { + return false; + } + if (authString.StartsWith("QBox ")) { return authString == "QBox " + SignRequest(url, body); diff --git a/src/Qiniu/Util/UpToken.cs b/src/Qiniu/Util/UpToken.cs index afecdc8f..ae8d7949 100644 --- a/src/Qiniu/Util/UpToken.cs +++ b/src/Qiniu/Util/UpToken.cs @@ -1,6 +1,6 @@ using System; using System.Text; -using Newtonsoft.Json; +using System.Text.Json; using Qiniu.Storage; namespace Qiniu.Util { @@ -14,9 +14,14 @@ public class UpToken /// /// 上传凭证 /// AccessKey - public static string GetAccessKeyFromUpToken(string upToken) + public static string? GetAccessKeyFromUpToken(string? upToken) { - string accessKey = null; + if (string.IsNullOrWhiteSpace(upToken)) + { + return null; + } + + string? accessKey = null; string[] items = upToken.Split(':'); if (items.Length == 3) { @@ -30,9 +35,14 @@ public static string GetAccessKeyFromUpToken(string upToken) /// /// 上传凭证 /// Bucket - public static string GetBucketFromUpToken(string upToken) + public static string? GetBucketFromUpToken(string? upToken) { - string bucket = null; + if (string.IsNullOrWhiteSpace(upToken)) + { + return null; + } + + string? bucket = null; string[] items = upToken.Split(':'); if (items.Length == 3) { @@ -40,16 +50,26 @@ public static string GetBucketFromUpToken(string upToken) try { string policyStr = Encoding.UTF8.GetString(Base64.UrlsafeBase64Decode(encodedPolicy)); - PutPolicy putPolicy = JsonConvert.DeserializeObject(policyStr); + PutPolicy putPolicy = QiniuJson.Deserialize(policyStr, QiniuJson.SerializerContext.PutPolicy); + if (putPolicy == null || string.IsNullOrWhiteSpace(putPolicy.Scope)) + { + return null; + } + string scope = putPolicy.Scope; string[] scopeItems = scope.Split(':'); if (scopeItems.Length >= 1) { bucket = scopeItems[0]; } - }catch(Exception) + } + catch (FormatException) { - + return null; + } + catch (JsonException) + { + return null; } } return bucket; diff --git a/src/Qiniu/packages.config b/src/Qiniu/packages.config deleted file mode 100644 index e382e85c..00000000 --- a/src/Qiniu/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/QiniuTests/Http/HttpRequestOptions.cs b/src/QiniuTests/Http/HttpRequestOptions.cs index 9e75f3fc..c0a42137 100644 --- a/src/QiniuTests/Http/HttpRequestOptions.cs +++ b/src/QiniuTests/Http/HttpRequestOptions.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Specialized; -using System.Net; +using System.Linq; using NUnit.Framework; using Qiniu.Http; @@ -14,15 +14,14 @@ public void SetUrlTest() { HttpRequestOptions reqOpts = new HttpRequestOptions(); reqOpts.Url = "https://qiniu.com/index.html"; + reqOpts.Method = "GET"; - HttpWebRequest wReq = reqOpts.CreateHttpWebRequest(); - Assert.AreEqual("https://qiniu.com/index.html", wReq.Address.ToString()); - wReq.Abort(); + var request = reqOpts.CreateHttpRequestMessage(); + Assert.AreEqual("https://qiniu.com/index.html", request.RequestUri?.ToString()); reqOpts.Url = "https://www.qiniu.com/index.html"; - wReq = reqOpts.CreateHttpWebRequest(); - Assert.AreEqual("https://www.qiniu.com/index.html", wReq.Address.ToString()); - wReq.Abort(); + request = reqOpts.CreateHttpRequestMessage(); + Assert.AreEqual("https://www.qiniu.com/index.html", request.RequestUri?.ToString()); } [Test] @@ -59,35 +58,13 @@ public void SetPropertiesTest() reqOpts.UnsafeAuthenticatedConnectionSharing = true; // default false reqOpts.UseDefaultCredentials = true; // default false - HttpWebRequest wReq = reqOpts.CreateHttpWebRequest(); - Assert.AreEqual(false, wReq.AllowAutoRedirect); // default true - Assert.AreEqual(true, wReq.AllowReadStreamBuffering); // default false - Assert.AreEqual(false, wReq.AllowWriteStreamBuffering); // default true - // Assert.AreEqual(, wReq.AutomaticDecompression); // default Null - // Assert(, wReq.CachePolicy); // default Null - // Assert(, wReq.ClientCertificates); // default System.Security.Cryptography.X509Certificates.X509CertificateCollection - Assert.AreEqual("qngroup", wReq.ConnectionGroupName); // default "" - // Assert(, wReq.ContinueDelegate); // default Null - Assert.AreEqual(360, wReq.ContinueTimeout); // default 350 - // Assert(, wReq.CookieContainer); // default Null - // Assert(, wReq.Credentials); // default Null - // Assert(, wReq.ImpersonationLevel); // default Null - Assert.AreEqual(false, wReq.KeepAlive); // default true - Assert.AreEqual(10, wReq.MaximumAutomaticRedirections); // default 50 - Assert.AreEqual(32, wReq.MaximumResponseHeadersLength); // default 64 - Assert.AreEqual("video/mp4", wReq.MediaType); // default "" - Assert.AreEqual("POST", wReq.Method); // default "GET" - Assert.AreEqual(false, wReq.Pipelined); // default true - Assert.AreEqual(true, wReq.PreAuthenticate); // default false - // Assert(, wReq.Proxy); // default System.Net.SystemWebProxy; - Assert.AreEqual(200000, wReq.ReadWriteTimeout); // default 300000 - Assert.AreEqual(true, wReq.SendChunked); // default false - // Assert(, wReq.ServerCertificateValidationCallback); // default Null - Assert.AreEqual(50000, wReq.Timeout); // default 100000 - Assert.AreEqual(true, wReq.UnsafeAuthenticatedConnectionSharing); // default false - Assert.AreEqual(true, wReq.UseDefaultCredentials); // default false - - wReq.Abort(); + var handler = reqOpts.CreateHttpClientHandler(); + var request = reqOpts.CreateHttpRequestMessage(); + + Assert.AreEqual(false, handler.AllowAutoRedirect); + Assert.AreEqual(true, handler.PreAuthenticate); + Assert.AreEqual(true, handler.UseDefaultCredentials); + Assert.AreEqual("POST", request.Method.Method); } [Test] @@ -113,19 +90,13 @@ public void SetHeadersTest() // TransferEncoding requires the SendChunked property to be set to true reqOpts.SendChunked = true; - HttpWebRequest wReq = reqOpts.CreateHttpWebRequest(); - Assert.AreEqual("text/plain", wReq.Accept); - Assert.AreEqual("text/plain", wReq.ContentType); - Assert.AreEqual(DateTime.Parse("Wed, 03 Aug 2011 04:00:00 GMT"), wReq.Date); - Assert.AreEqual("200-ok", wReq.Expect); - Assert.AreEqual("qiniu.com", wReq.Host); - Assert.AreEqual(DateTime.Parse("Wed, 03 Aug 2011 04:00:00 GMT"), wReq.IfModifiedSince); - Assert.AreEqual("https://qiniu.com/", wReq.Referer); - Assert.AreEqual("gzip", wReq.TransferEncoding); - Assert.AreEqual("qn-csharp-sdk", wReq.UserAgent); - Assert.AreEqual("qn", wReq.Headers["X-Qiniu-A"]); + reqOpts.Method = "GET"; + var request = reqOpts.CreateHttpRequestMessage(); - wReq.Abort(); + Assert.That(request.Headers.Accept.Any(h => h.MediaType == "text/plain")); + Assert.AreEqual("200-ok", request.Headers.GetValues("expect").FirstOrDefault()); + Assert.AreEqual("qn-csharp-sdk", string.Join("", request.Headers.UserAgent.Select(u => u.ToString()))); + Assert.AreEqual("qn", request.Headers.GetValues("X-Qiniu-A").FirstOrDefault()); } } } \ No newline at end of file diff --git a/src/QiniuTests/QiniuTests.csproj b/src/QiniuTests/QiniuTests.csproj index 66b2adaa..6ab19b09 100644 --- a/src/QiniuTests/QiniuTests.csproj +++ b/src/QiniuTests/QiniuTests.csproj @@ -1,42 +1,12 @@  - Debug - AnyCPU - {E8CB1665-53F7-46A5-9AFD-B85AD08262D0} Library - Properties - QiniuTests - QiniuTests - netcoreapp2.0 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - + net9.0 + - - - - - - - - - - - - - {2F5B0328-DE8B-4B53-A500-3077E340A51B} - Qiniu - - - - + @@ -47,29 +17,4 @@ - - - - - False - - - False - - - False - - - False - - - - - \ No newline at end of file diff --git a/src/QiniuTests/Storage/FormUploaderTests.cs b/src/QiniuTests/Storage/FormUploaderTests.cs index da0824f6..0f000eee 100644 --- a/src/QiniuTests/Storage/FormUploaderTests.cs +++ b/src/QiniuTests/Storage/FormUploaderTests.cs @@ -2,7 +2,9 @@ using Qiniu.Http; using System; using System.Collections.Generic; +using System.IO; using System.Text; +using System.Threading.Tasks; using Newtonsoft.Json; using Qiniu.Util; using Qiniu.Tests; @@ -13,21 +15,17 @@ namespace Qiniu.Storage.Tests public class FormUploaderTests : TestEnv { [Test] - public void UploadFileTest() + public async Task UploadFileTest() { Mac mac = new Mac(AccessKey, SecretKey); - Random rand = new Random(); - string key = string.Format("UploadFileTest_{0}.dat", rand.Next()); + string key = $"UploadFileTest_{Random.Shared.Next()}.dat"; string tempPath = System.IO.Path.GetTempPath(); - int rnd = new Random().Next(1, 100000); - string filePath = tempPath + "resumeFile" + rnd.ToString(); - char[] testBody = new char[4 * 1024 * 1024]; - System.IO.FileStream stream = new System.IO.FileStream(filePath, System.IO.FileMode.Create); - System.IO.StreamWriter sw = new System.IO.StreamWriter(stream, System.Text.Encoding.Default); - sw.Write(testBody); - sw.Close(); - stream.Close(); + var filePath = Path.Join(tempPath, $"resumeFile_{Path.GetRandomFileName()}"); + + var testBody = new byte[8 * 1024 * 1024]; + Random.Shared.NextBytes(testBody); + await File.WriteAllBytesAsync(filePath, testBody); PutPolicy putPolicy = new PutPolicy(); putPolicy.Scope = Bucket + ":" + key; @@ -40,14 +38,14 @@ public void UploadFileTest() config.UseCdnDomains = true; config.ChunkSize = ChunkUnit.U512K; FormUploader target = new FormUploader(config); - HttpResult result = target.UploadFile(filePath, key, token, null); + HttpResult result = await target.UploadFile(filePath, key, token, null); Console.WriteLine("form upload result: " + result.ToString()); Assert.AreEqual((int)HttpCode.OK, result.Code); System.IO.File.Delete(filePath); } [Test] - public void UploadFileV2Test() + public async Task UploadFileV2Test() { Mac mac = new Mac(AccessKey, SecretKey); Random rand = new Random(); @@ -77,14 +75,14 @@ public void UploadFileV2Test() PutExtra extra = new PutExtra(); extra.Version = "v2"; extra.PartSize = 4 * 1024 * 1024; - HttpResult result = target.UploadFile(filePath, key, token, extra); + HttpResult result = await target.UploadFile(filePath, key, token, extra); Console.WriteLine("form upload result: " + result.ToString()); Assert.AreEqual((int)HttpCode.OK, result.Code); System.IO.File.Delete(filePath); } [TestCaseSource(typeof(OperationManagerTests), nameof(OperationManagerTests.PfopOptionsTestCases))] - public void UploadFileWithPersistOptionsTest(int type, string workflowId) + public async Task UploadFileWithPersistOptionsTest(int type, string workflowId) { Mac mac = new Mac(AccessKey, SecretKey); string bucketName = Bucket; @@ -134,7 +132,7 @@ public void UploadFileWithPersistOptionsTest(int type, string workflowId) config.UseCdnDomains = true; string token = Auth.CreateUploadToken(mac, putPolicy.ToJsonString()); FormUploader uploader = new FormUploader(config); - HttpResult result = uploader.UploadFile(filePath, key, token, null); + HttpResult result = await uploader.UploadFile(filePath, key, token, null); Console.WriteLine("form upload result: " + result.ToString()); Assert.AreEqual((int)HttpCode.OK, result.Code); System.IO.File.Delete(filePath); diff --git a/src/QiniuTests/TestEnv.cs b/src/QiniuTests/TestEnv.cs index aa3c8e00..56faa898 100644 --- a/src/QiniuTests/TestEnv.cs +++ b/src/QiniuTests/TestEnv.cs @@ -1,17 +1,23 @@ -namespace Qiniu.Tests +using NUnit.Framework; + +namespace Qiniu.Tests { public class TestEnv { - public string AccessKey; - public string SecretKey; - public string Bucket; - public string Domain; + public string AccessKey { get; } + public string SecretKey { get; } + public string Bucket { get; } + public string Domain { get; } public TestEnv() { this.AccessKey = System.Environment.GetEnvironmentVariable("QINIU_ACCESS_KEY"); this.SecretKey = System.Environment.GetEnvironmentVariable("QINIU_SECRET_KEY"); this.Bucket = System.Environment.GetEnvironmentVariable("QINIU_TEST_BUCKET"); - this.Domain = System.Environment.GetEnvironmentVariable("QINIU_TEST_DOMAIN"); } + this.Domain = System.Environment.GetEnvironmentVariable("QINIU_TEST_DOMAIN"); + + Assert.IsFalse(string.IsNullOrEmpty(AccessKey), "单元测试必须先配置好环境变量"); + Assert.IsFalse(string.IsNullOrEmpty(SecretKey), "单元测试必须先配置好环境变量"); + } } } \ No newline at end of file diff --git a/src/QiniuTests/packages.config b/src/QiniuTests/packages.config deleted file mode 100644 index f1240129..00000000 --- a/src/QiniuTests/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file