Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix file upload filenames not being set, allow setting MIME type #62

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Todoist.Net.Tests/RateLimitAwareRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ public async Task<HttpResponseMessage> PostFormAsync(
.ConfigureAwait(false);
}

public async Task<HttpResponseMessage> PostFormAsync(
string resource,
MultipartFormDataContent data,
CancellationToken cancellationToken = default)
{
return await ExecuteRequest(() => _restClient.PostFormAsync(resource, data, cancellationToken))
.ConfigureAwait(false);
}

public async Task<TimeSpan> GetRateLimitCooldown(HttpResponseMessage response)
{
var defaultCooldown = TimeSpan.FromSeconds(30);
Expand Down
18 changes: 18 additions & 0 deletions src/Todoist.Net/IAdvancedTodoistClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ Task<T> PostFormAsync<T>(
IEnumerable<ByteArrayContent> files,
CancellationToken cancellationToken = default);

/// <summary>
/// Sends a <c>POST</c> request with form data, and handles response asynchronously.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="resource">The resource.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="files">The files.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>
/// The result.
/// </returns>
/// <exception cref="HttpRequestException">API exception.</exception>
Task<T> PostFormAsync<T>(
string resource,
ICollection<KeyValuePair<string, string>> parameters,
IEnumerable<FormFile> files,
CancellationToken cancellationToken = default);

/// <summary>
/// Sends a <c>POST</c> request asynchronously, and returns raw content.
/// </summary>
Expand Down
16 changes: 15 additions & 1 deletion src/Todoist.Net/ITodoistRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ Task<HttpResponseMessage> PostFormAsync(
string resource,
IEnumerable<KeyValuePair<string, string>> parameters,
IEnumerable<ByteArrayContent> files,
CancellationToken cancellationToken = default);
CancellationToken cancellationToken = default
);

/// <summary>
/// Sends a <c>POST</c> request with form data, and handles response asynchronously.
/// </summary>
/// <param name="resource">The resource.</param>
/// <param name="data">The form data.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The response.</returns>
Task<HttpResponseMessage> PostFormAsync(
string resource,
MultipartFormDataContent data,
CancellationToken cancellationToken = default
);
}
}
16 changes: 16 additions & 0 deletions src/Todoist.Net/Models/FormFile.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
29 changes: 23 additions & 6 deletions src/Todoist.Net/Services/UploadService.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand All @@ -40,13 +51,19 @@ public Task<IEnumerable<Upload>> GetAsync(CancellationToken cancellationToken =
}

/// <inheritdoc/>
public Task<FileAttachment> UploadAsync(string fileName, byte[] fileContent, CancellationToken cancellationToken = default)
public Task<FileAttachment> UploadAsync(
string fileName, byte[] fileContent, CancellationToken cancellationToken = default
)
{
var parameters = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("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<string, string>();
var file = new FormFile(fileContent, fileName, mimeType);
var files = new[] { file };

return _todoistClient.PostFormAsync<FileAttachment>("uploads/add", parameters, files, cancellationToken);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Todoist.Net/Todoist.Net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
<Reference Include="System.Net.Http" />
<Reference Include="System.Web" />
</ItemGroup>

<ItemGroup>
Expand All @@ -46,6 +47,7 @@

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.3.0" />
</ItemGroup>

</Project>
48 changes: 48 additions & 0 deletions src/Todoist.Net/TodoistClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -298,6 +299,30 @@ Task<T> IAdvancedTodoistClient.PostFormAsync<T>(
return ProcessFormAsync<T>(resource, parameters, files, cancellationToken);
}

/// <inheritdoc/>
Task<T> IAdvancedTodoistClient.PostFormAsync<T>(
string resource,
ICollection<KeyValuePair<string, string>> parameters,
IEnumerable<FormFile> 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<T>(resource, data, cancellationToken);
}

/// <inheritdoc/>
async Task<T> IAdvancedTodoistClient.GetAsync<T>(
string resource,
Expand Down Expand Up @@ -357,6 +382,29 @@ private async Task<T> ProcessFormAsync<T>(
return DeserializeResponse<T>(responseContent);
}

/// <summary>
/// Processes the form asynchronous.
/// </summary>
/// <typeparam name="T">The type of the response.</typeparam>
/// <param name="resource">The resource.</param>
/// <param name="data">The form data.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="HttpRequestException">API exception.</exception>
/// <returns>The response.</returns>
private async Task<T> ProcessFormAsync<T>(
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<T>(responseContent);
}


/// <summary>
/// Processes the request asynchronous.
/// </summary>
Expand Down
15 changes: 13 additions & 2 deletions src/Todoist.Net/TodoistRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void Dispose()
{
if (_disposeHttpClient)
{
_httpClient?.Dispose();
_httpClient?.Dispose();
}
}

Expand Down Expand Up @@ -134,8 +134,19 @@ public async Task<HttpResponseMessage> 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);
}
}

/// <inheritdoc/>
public async Task<HttpResponseMessage> PostFormAsync(
string resource,
MultipartFormDataContent data,
CancellationToken cancellationToken = default
)
{
return await _httpClient.PostAsync(resource, data, cancellationToken).ConfigureAwait(false);
}
}
}