diff --git a/src/Todoist.Net.Tests/RateLimitAwareRestClient.cs b/src/Todoist.Net.Tests/RateLimitAwareRestClient.cs index 523ebac..9435c37 100644 --- a/src/Todoist.Net.Tests/RateLimitAwareRestClient.cs +++ b/src/Todoist.Net.Tests/RateLimitAwareRestClient.cs @@ -80,6 +80,15 @@ public async Task PostFormAsync( .ConfigureAwait(false); } + public async Task PostFormAsync( + string resource, + MultipartFormDataContent data, + CancellationToken cancellationToken = default) + { + return await ExecuteRequest(() => _restClient.PostFormAsync(resource, data, cancellationToken)) + .ConfigureAwait(false); + } + public async Task GetRateLimitCooldown(HttpResponseMessage response) { var defaultCooldown = TimeSpan.FromSeconds(30); diff --git a/src/Todoist.Net/IAdvancedTodoistClient.cs b/src/Todoist.Net/IAdvancedTodoistClient.cs index 287a3ce..b2d094e 100644 --- a/src/Todoist.Net/IAdvancedTodoistClient.cs +++ b/src/Todoist.Net/IAdvancedTodoistClient.cs @@ -83,6 +83,24 @@ Task PostFormAsync( IEnumerable files, CancellationToken cancellationToken = default); + /// + /// Sends a POST request with form data, and handles response asynchronously. + /// + /// The result type. + /// The resource. + /// The parameters. + /// The files. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// The result. + /// + /// API exception. + Task PostFormAsync( + string resource, + ICollection> parameters, + IEnumerable files, + CancellationToken cancellationToken = default); + /// /// Sends a POST request asynchronously, and returns raw content. /// diff --git a/src/Todoist.Net/ITodoistRestClient.cs b/src/Todoist.Net/ITodoistRestClient.cs index 2920e9e..7bdeec9 100644 --- a/src/Todoist.Net/ITodoistRestClient.cs +++ b/src/Todoist.Net/ITodoistRestClient.cs @@ -45,6 +45,20 @@ Task PostFormAsync( string resource, IEnumerable> parameters, IEnumerable files, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default + ); + + /// + /// Sends a POST request with form data, and handles response asynchronously. + /// + /// The resource. + /// The form data. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The response. + Task PostFormAsync( + string resource, + MultipartFormDataContent data, + CancellationToken cancellationToken = default + ); } } diff --git a/src/Todoist.Net/Models/FormFile.cs b/src/Todoist.Net/Models/FormFile.cs new file mode 100644 index 0000000..899cb49 --- /dev/null +++ b/src/Todoist.Net/Models/FormFile.cs @@ -0,0 +1,16 @@ +namespace Todoist.Net.Models +{ + internal class FormFile + { + public FormFile(byte[] content, string filename, string mimeType = null) + { + Content = content; + Filename = filename; + MimeType = mimeType; + } + + public byte[] Content; + public string Filename; + public string MimeType; + } +} diff --git a/src/Todoist.Net/Services/UploadService.cs b/src/Todoist.Net/Services/UploadService.cs index be68d13..f73d599 100644 --- a/src/Todoist.Net/Services/UploadService.cs +++ b/src/Todoist.Net/Services/UploadService.cs @@ -1,8 +1,15 @@ using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +#if NETSTANDARD2_0 +using Microsoft.AspNetCore.StaticFiles; +#else +using System.Web; +#endif + using Todoist.Net.Models; namespace Todoist.Net.Services @@ -15,6 +22,10 @@ internal class UploadService : IUploadService { private readonly IAdvancedTodoistClient _todoistClient; + #if NETSTANDARD2_0 + private static readonly FileExtensionContentTypeProvider MimeProvider = new FileExtensionContentTypeProvider(); + #endif + internal UploadService(IAdvancedTodoistClient todoistClient) { _todoistClient = todoistClient; @@ -40,13 +51,19 @@ public Task> GetAsync(CancellationToken cancellationToken = } /// - public Task UploadAsync(string fileName, byte[] fileContent, CancellationToken cancellationToken = default) + public Task UploadAsync( + string fileName, byte[] fileContent, CancellationToken cancellationToken = default + ) { - var parameters = new List> - { - new KeyValuePair("file_name", fileName) - }; - var files = new[] { new ByteArrayContent(fileContent) }; +#if NETSTANDARD2_0 + MimeProvider.TryGetContentType(fileName, out var mimeType); +#else + var mimeType = MimeMapping.GetMimeMapping(fileName); +#endif + + var parameters = new Dictionary(); + var file = new FormFile(fileContent, fileName, mimeType); + var files = new[] { file }; return _todoistClient.PostFormAsync("uploads/add", parameters, files, cancellationToken); } diff --git a/src/Todoist.Net/Todoist.Net.csproj b/src/Todoist.Net/Todoist.Net.csproj index 5fbc95c..ada5275 100644 --- a/src/Todoist.Net/Todoist.Net.csproj +++ b/src/Todoist.Net/Todoist.Net.csproj @@ -31,6 +31,7 @@ + @@ -46,6 +47,7 @@ + diff --git a/src/Todoist.Net/TodoistClient.cs b/src/Todoist.Net/TodoistClient.cs index c563a21..6fad1be 100644 --- a/src/Todoist.Net/TodoistClient.cs +++ b/src/Todoist.Net/TodoistClient.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -298,6 +299,30 @@ Task IAdvancedTodoistClient.PostFormAsync( return ProcessFormAsync(resource, parameters, files, cancellationToken); } + /// + Task IAdvancedTodoistClient.PostFormAsync( + string resource, + ICollection> parameters, + IEnumerable files, + CancellationToken cancellationToken) + { + var data = new MultipartFormDataContent(); + + foreach (var file in files) + { + var mime = file.MimeType != null ? MediaTypeHeaderValue.Parse(file.MimeType) : null; + var content = new ByteArrayContent(file.Content) { Headers = { ContentType = mime } }; + data.Add(content, "file", file.Filename); + } + + foreach (var keyValuePair in parameters) + { + data.Add(new StringContent(keyValuePair.Value), $"\"{keyValuePair.Key}\""); + } + + return ProcessFormAsync(resource, data, cancellationToken); + } + /// async Task IAdvancedTodoistClient.GetAsync( string resource, @@ -357,6 +382,29 @@ private async Task ProcessFormAsync( return DeserializeResponse(responseContent); } + /// + /// Processes the form asynchronous. + /// + /// The type of the response. + /// The resource. + /// The form data. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// API exception. + /// The response. + private async Task ProcessFormAsync( + string resource, + MultipartFormDataContent data, + CancellationToken cancellationToken) + { + var response = await _restClient.PostFormAsync(resource, data, cancellationToken).ConfigureAwait(false); + + var responseContent = await ReadResponseAsync(response, cancellationToken) + .ConfigureAwait(false); + + return DeserializeResponse(responseContent); + } + + /// /// Processes the request asynchronous. /// diff --git a/src/Todoist.Net/TodoistRestClient.cs b/src/Todoist.Net/TodoistRestClient.cs index 9a80442..c9725af 100644 --- a/src/Todoist.Net/TodoistRestClient.cs +++ b/src/Todoist.Net/TodoistRestClient.cs @@ -63,7 +63,7 @@ public void Dispose() { if (_disposeHttpClient) { - _httpClient?.Dispose(); + _httpClient?.Dispose(); } } @@ -134,8 +134,19 @@ public async Task PostFormAsync( multipartFormDataContent.Add(file, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); } - return await _httpClient.PostAsync(resource, multipartFormDataContent, cancellationToken).ConfigureAwait(false); + return await _httpClient.PostAsync(resource, multipartFormDataContent, cancellationToken) + .ConfigureAwait(false); } } + + /// + public async Task PostFormAsync( + string resource, + MultipartFormDataContent data, + CancellationToken cancellationToken = default + ) + { + return await _httpClient.PostAsync(resource, data, cancellationToken).ConfigureAwait(false); + } } }