diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinMediaExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinMediaExtensions.cs index 7054b1b8..11625e4d 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinMediaExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinMediaExtensions.cs @@ -1,9 +1,7 @@ using System; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using System.Web; using Flurl; using Flurl.Http; @@ -60,14 +58,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work .SetQueryParam("access_token", request.AccessToken) .SetQueryParam("type", request.Type); - string boundary = "--BOUNDARY--" + DateTimeOffset.Now.Ticks.ToString("x"); - using var fileContent = new ByteArrayContent(request.FileBytes ?? Array.Empty()); - using var httpContent = new MultipartFormDataContent(boundary); - httpContent.Add(fileContent, "\"media\"", $"\"{HttpUtility.UrlEncode(request.FileName)}\""); - httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data; boundary=" + boundary); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(request.FileContentType); - fileContent.Headers.ContentLength = request.FileBytes?.Length; - + using var httpContent = Utilities.FileHttpContentBuilder.Build(fileName: request.FileName, fileBytes: request.FileBytes, fileContentType: request.FileContentType, formDataName: "media"); return await client.SendRequestAsync(flurlReq, httpContent: httpContent, cancellationToken: cancellationToken); } @@ -96,14 +87,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work .CreateRequest(request, HttpMethod.Post, "cgi-bin", "media", "uploadimg") .SetQueryParam("access_token", request.AccessToken); - string boundary = "--BOUNDARY--" + DateTimeOffset.Now.Ticks.ToString("x"); - using var fileContent = new ByteArrayContent(request.FileBytes ?? Array.Empty()); - using var httpContent = new MultipartFormDataContent(boundary); - httpContent.Add(fileContent, "\"media\"", $"\"{HttpUtility.UrlEncode(request.FileName)}\""); - httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data; boundary=" + boundary); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(request.FileContentType); - fileContent.Headers.ContentLength = request.FileBytes?.Length; - + using var httpContent = Utilities.FileHttpContentBuilder.Build(fileName: request.FileName, fileBytes: request.FileBytes, fileContentType: request.FileContentType, formDataName: "media"); return await client.SendRequestAsync(flurlReq, httpContent: httpContent, cancellationToken: cancellationToken); } @@ -156,14 +140,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work .SetQueryParam("media_type", request.Type) .SetQueryParam("attachment_type", request.AttachmentType); - string boundary = "--BOUNDARY--" + DateTimeOffset.Now.Ticks.ToString("x"); - using var fileContent = new ByteArrayContent(request.FileBytes ?? Array.Empty()); - using var httpContent = new MultipartFormDataContent(boundary); - httpContent.Add(fileContent, "\"media\"", $"\"{HttpUtility.UrlEncode(request.FileName)}\""); - httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data; boundary=" + boundary); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(request.FileContentType); - fileContent.Headers.ContentLength = request.FileBytes?.Length; - + using var httpContent = Utilities.FileHttpContentBuilder.Build(fileName: request.FileName, fileBytes: request.FileBytes, fileContentType: request.FileContentType, formDataName: "media"); return await client.SendRequestAsync(flurlReq, httpContent: httpContent, cancellationToken: cancellationToken); } diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinServiceExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinServiceExtensions.cs index c4553b88..c729f01a 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinServiceExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientExecuteCgibinServiceExtensions.cs @@ -1,9 +1,7 @@ using System; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using System.Web; using Flurl; using Flurl.Http; @@ -316,14 +314,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work .SetQueryParam("provider_access_token", request.ProviderAccessToken) .SetQueryParam("type", request.Type); - string boundary = "--BOUNDARY--" + DateTimeOffset.Now.Ticks.ToString("x"); - using var fileContent = new ByteArrayContent(request.FileBytes ?? Array.Empty()); - using var httpContent = new MultipartFormDataContent(boundary); - httpContent.Add(fileContent, "\"media\"", $"\"{HttpUtility.UrlEncode(request.FileName)}\""); - httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data; boundary=" + boundary); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(request.FileContentType); - fileContent.Headers.ContentLength = request.FileBytes?.Length; - + using var httpContent = Utilities.FileHttpContentBuilder.Build(fileName: request.FileName, fileBytes: request.FileBytes, fileContentType: request.FileContentType, formDataName: "media"); return await client.SendRequestAsync(flurlReq, httpContent: httpContent, cancellationToken: cancellationToken); } #endregion diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/Internal/FileHttpContentBuilder.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/Internal/FileHttpContentBuilder.cs new file mode 100644 index 00000000..a1c7285b --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/Internal/FileHttpContentBuilder.cs @@ -0,0 +1,45 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities +{ + internal static class FileHttpContentBuilder + { + public static MultipartFormDataContent Build(string fileName, byte[] fileBytes, string fileContentType, string formDataName) + { + return Build(fileName: fileName, fileBytes: fileBytes, fileContentType: fileContentType, formDataName: formDataName, (_) => { }); + } + + public static MultipartFormDataContent Build(string fileName, byte[] fileBytes, string fileContentType, string formDataName, Action configureFileHttpContent) + { + if (fileName == null) throw new ArgumentNullException(nameof(fileName)); + if (formDataName == null) throw new ArgumentNullException(nameof(formDataName)); + if (configureFileHttpContent == null) throw new ArgumentNullException(nameof(configureFileHttpContent)); + + fileName = fileName.Replace("\"", ""); + fileBytes = fileBytes ?? Array.Empty(); + fileContentType = string.IsNullOrEmpty(fileContentType) ? "application/octet-stream" : fileContentType; + formDataName = formDataName.Replace("\"", ""); + + // HACKED: 默认不支持 Unicode 文件名 https://github.com/dotnet/runtime/issues/22996 + byte[] bytesFileName = Encoding.UTF8.GetBytes(fileName); + char[] bytesHackedFileName = new char[bytesFileName.Length]; + Array.Copy(bytesFileName, 0, bytesHackedFileName, 0, bytesFileName.Length); + string hackedFileName = new string(bytesHackedFileName); + + ByteArrayContent fileContent = new ByteArrayContent(fileBytes); + fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse($"form-data; name=\"{formDataName}\"; filename=\"{hackedFileName}\""); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(fileContentType); + fileContent.Headers.ContentLength = fileBytes.Length; + configureFileHttpContent(fileContent); + + string boundary = "--BOUNDARY--" + DateTimeOffset.Now.Ticks.ToString("x"); + MultipartFormDataContent httpContent = new MultipartFormDataContent(boundary); + httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/form-data; boundary={boundary}"); + httpContent.Add(fileContent); + return httpContent; + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ApiExecuteCgibinMediaTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ApiExecuteCgibinMediaTests.cs new file mode 100644 index 00000000..71a098b4 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ApiExecuteCgibinMediaTests.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests +{ + public class TestCase_ApiExecuteCgibinMediaTests + { + [Fact(DisplayName = "测试用例:调用 API [POST] /cgi-bin/media/upload")] + public async Task TestExecuteCgibinMediaUpload() + { + var request = new Models.CgibinMediaUploadRequest() + { + AccessToken = TestConfigs.WechatAccessToken, + Type = "image", + FileContentType = "image/jpeg", + FileName = "测试图片.jpg", + FileBytes = Convert.FromBase64String("/9j/4AAQSkZJRgABAQEAeAB4AAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAcABwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9/KK+bv25viR458H/ABS+E+meDNb8S6YNW1LffWuk22lyRaig1DS7fZePfRuY7QR3cxc2zR3BOwRsWwp8I8Hftl/FD4k+H9G1HVvEnij4dHSbnTJ2hubXQGXxYrHSIypKpc7bS7N9MyqjW92DsH7nGwkfe++3/pP/AMktN97IbTX3X/P/AORfltqfoRRXzTr3xA8eeI7T4i6BdeJ/EXhM/DiyuFm13RrLTmvtZmuJPtGn/ZhdW09v5iWapHKDAVea6wqjZXvnw40rVtC+HuhWWv6m+ta7aafBDqOoNHHGb65WNRLMVjREXe4ZsIiqM8KBxRHVc3p+N/ytr2uvkno7ev4W/O+nezK3jX4TaB8Q9VsL7V7Frm70yNo7WVbmWFoVaa3nONjLz5lrA2eo8vjgsD554v8A+Cf/AMKvHGnaLaX2g6tHb6Bc2d1aR2PiTVLAFrQQi3WXyLhPPjQ28DeVNvRmhRmUsoNezUUdb9tfu2DpY4X4mfs4+E/i54b8RaVrFrq0dv4qntLrUZtM1u+0q7eW1aJ7eSO5tZopoGRoYyDE6E7ec5Oes8NeH4PCfh2w0u1e9lttOt47aJ7y8mvbh0RQoMk8zPLK+By8jM7HJYkkmr1FC0VkG+/T+v0R/9k=") + }; + var response = await TestClients.Instance.ExecuteCgibinMediaUploadAsync(request); + + Assert.NotNull(response.MediaId); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs index 5acc91f0..e57d0a57 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs @@ -21,6 +21,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests WechatCorpId = config.GetProperty("CorpId").GetString()!; WechatAgentId = int.Parse(config.GetProperty("AgentId").GetString())!; WechatAgentSecret = config.GetProperty("AgentSecret").GetString()!; + WechatAccessToken = config.GetProperty("AccessToken").GetString()!; WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!; WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!; @@ -34,6 +35,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests public static readonly string WechatCorpId; public static readonly int WechatAgentId; public static readonly string WechatAgentSecret; + public static readonly string WechatAccessToken; public static readonly string WorkDirectoryForSdk; public static readonly string WorkDirectoryForTest; diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json index 82a79009..47047d84 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json @@ -2,7 +2,8 @@ "TestConfig": { "CorpId": "请在此填写用于测试的企业微信 CorpId", "AgentId": "请在此填写用于测试的企业微信 AgentId", - "AgentSecret": "请在此填写用于测试的企业微信 AgentSecret" + "AgentSecret": "请在此填写用于测试的企业微信 AgentSecret", + "AccessToken": "请在此填写用于测试的微信 AccessToken" }, "WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Work\\", "WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\\"