Skip to content

Commit

Permalink
Fix X-Unique-Upload-Id header duplicate value
Browse files Browse the repository at this point in the history
* Improve stability
* Cleanup code
  • Loading branch information
const-cloudinary committed Mar 16, 2024
1 parent 6d1827a commit 11c5cb2
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 148 deletions.
2 changes: 1 addition & 1 deletion CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ public void TestAgentPlatformHeaders()
var request = new HttpRequestMessage { RequestUri = new Uri("https://dummy.com") };
m_api.UserPlatform = "Test/1.0";

m_api.PrepareRequestBodyAsync(
m_api.PrepareRequestAsync(
request,
HttpMethod.GET,
new SortedDictionary<string, object>()).GetAwaiter().GetResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ public class BasicRawUploadParams : BaseParams
/// <summary>
/// Gets the 'raw' type of file you are uploading.
/// </summary>
public virtual ResourceType ResourceType
{
get { return Actions.ResourceType.Raw; }
}
public virtual ResourceType ResourceType => ResourceType.Raw;

/// <summary>
/// Gets or sets file name to override an original file name.
Expand Down
180 changes: 41 additions & 139 deletions CloudinaryDotNet/ApiShared.Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Threading;
using System.Threading.Tasks;
using CloudinaryDotNet.Actions;
using CloudinaryDotNet.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
Expand All @@ -32,10 +33,8 @@ public partial class ApiShared : ISignProvider
internal static async Task<T> ParseAsync<T>(HttpResponseMessage response)
where T : BaseResult
{
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
return CreateResult<T>(response, stream);
}
using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return CreateResult<T>(response, stream);
}

/// <summary>
Expand All @@ -47,10 +46,8 @@ internal static async Task<T> ParseAsync<T>(HttpResponseMessage response)
internal static T Parse<T>(HttpResponseMessage response)
where T : BaseResult
{
using (var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
{
return CreateResult<T>(response, stream);
}
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
return CreateResult<T>(response, stream);
}

/// <summary>
Expand Down Expand Up @@ -128,14 +125,15 @@ internal virtual T CallApi<T>(HttpMethod method, string url, BaseParams paramete
/// <param name="extraHeaders">(Optional) Headers to add to the request.</param>
/// <param name="cancellationToken">(Optional) Cancellation token.</param>
/// <returns>Prepared HTTP request.</returns>
internal async Task<HttpRequestMessage> PrepareRequestBodyAsync(
internal async Task<HttpRequestMessage> PrepareRequestAsync(
HttpRequestMessage request,
HttpMethod method,
SortedDictionary<string, object> parameters,
Dictionary<string, string> extraHeaders = null,
CancellationToken? cancellationToken = null)
{
PrePrepareRequestBody(request, method, extraHeaders);
SetHttpMethod(method, request);
SetRequestHeaders(request, extraHeaders);

if (!ShouldPrepareContent(method, parameters))
{
Expand All @@ -144,38 +142,12 @@ internal async Task<HttpRequestMessage> PrepareRequestBodyAsync(

SetChunkedEncoding(request);

await PrepareRequestContentAsync(request, parameters, extraHeaders, cancellationToken)
await SetRequestContentAsync(request, parameters, extraHeaders, cancellationToken)
.ConfigureAwait(false);

return request;
}

/// <summary>
/// Prepares request body to be sent on custom call to Cloudinary API.
/// </summary>
/// <param name="request">HTTP request to alter.</param>
/// <param name="method">HTTP method of call.</param>
/// <param name="parameters">Dictionary of call parameters.</param>
/// <param name="extraHeaders">(Optional) Headers to add to the request.</param>
/// <returns>Prepared HTTP request.</returns>
internal HttpRequestMessage PrepareRequestBody(
HttpRequestMessage request,
HttpMethod method,
SortedDictionary<string, object> parameters,
Dictionary<string, string> extraHeaders = null)
{
PrePrepareRequestBody(request, method, extraHeaders);

if (ShouldPrepareContent(method, parameters))
{
SetChunkedEncoding(request);

PrepareRequestContent(request, parameters, extraHeaders);
}

return request;
}

/// <summary>
/// Extends Cloudinary upload parameters with additional attributes.
/// </summary>
Expand Down Expand Up @@ -303,8 +275,8 @@ private static void UpdateResultFromResponse<T>(HttpResponseMessage response, T
return;
}

response?.Headers
.Where(_ => _.Key.StartsWith("X-FeatureRateLimit", StringComparison.OrdinalIgnoreCase))
response.Headers
.Where(p => p.Key.StartsWith("X-FeatureRateLimit", StringComparison.OrdinalIgnoreCase))
.ToList()
.ForEach(header =>
{
Expand All @@ -330,9 +302,9 @@ private static void UpdateResultFromResponse<T>(HttpResponseMessage response, T
}

private static bool ShouldPrepareContent(HttpMethod method, object parameters) =>
(method == HttpMethod.POST || method == HttpMethod.PUT) && parameters != null;
method is HttpMethod.POST or HttpMethod.PUT && parameters != null;

private static bool IsContentRange(Dictionary<string, string> extraHeaders) =>
private static bool IsChunkedUpload(Dictionary<string, string> extraHeaders) =>
extraHeaders != null && extraHeaders.ContainsKey("X-Unique-Upload-Id");

private static void SetStreamContent(string fieldName, FileDescription file, Stream stream, MultipartFormDataContent content)
Expand All @@ -355,27 +327,19 @@ private static void SetContentForRemoteFile(string fieldName, FileDescription fi
content.Add(strContent);
}

private static StringContent CreateStringContent(SortedDictionary<string, object> parameters) =>
new StringContent(ParamsToJson(parameters), Encoding.UTF8, Constants.CONTENT_TYPE_APPLICATION_JSON);
private static void SetChunkContent(ChunkData chunk, MultipartFormDataContent content)
{
content.Headers.TryAddWithoutValidation("Content-Range", $"bytes {chunk.StartByte}-{chunk.EndByte}/{chunk.TotalBytes}");
}

private static StringContent CreateJsonContent(SortedDictionary<string, object> parameters) =>
new (ParamsToJson(parameters), Encoding.UTF8, Constants.CONTENT_TYPE_APPLICATION_JSON);

private static bool IsStringContent(Dictionary<string, string> extraHeaders) =>
private static bool IsJsonContent(IReadOnlyDictionary<string, string> extraHeaders) =>
extraHeaders != null &&
extraHeaders.TryGetValue(Constants.HEADER_CONTENT_TYPE, out var value) &&
value == Constants.CONTENT_TYPE_APPLICATION_JSON;

private static void SetHeadersAndContent(HttpRequestMessage request, Dictionary<string, string> extraHeaders, HttpContent content)
{
if (extraHeaders != null)
{
foreach (var header in extraHeaders)
{
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}

request.Content = content;
}

private static void SetHttpMethod(HttpMethod method, HttpRequestMessage req)
{
switch (method)
Expand Down Expand Up @@ -414,13 +378,14 @@ private static async Task<HttpContent> CreateMultipartContentAsync(
case FileDescription file:
{
Stream stream;
if (IsContentRange(extraHeaders))

if (IsChunkedUpload(extraHeaders))
{
var chunk = await file.GetNextChunkAsync(cancellationToken).ConfigureAwait(false);

stream = chunk.Chunk;
SetChunkContent(chunk, content);

extraHeaders!["Content-Range"] = $"bytes {chunk.StartByte}-{chunk.EndByte}/{chunk.TotalBytes}";
stream = chunk.Chunk;
}
else
{
Expand Down Expand Up @@ -450,54 +415,6 @@ private static async Task<HttpContent> CreateMultipartContentAsync(
return content;
}

private static HttpContent CreateMultipartContent(
SortedDictionary<string, object> parameters,
Dictionary<string, string> extraHeaders = null)
{
var content = new MultipartFormDataContent(HTTP_BOUNDARY);
foreach (var param in parameters.Where(param => param.Value != null))
{
switch (param.Value)
{
case FileDescription { IsRemote: true } file:
SetContentForRemoteFile(param.Key, file, content);
break;
case FileDescription file:
{
var stream = file.GetFileStream();

if (IsContentRange(extraHeaders))
{
var chunk = file.GetNextChunkAsync().GetAwaiter().GetResult();

stream = chunk.Chunk;

extraHeaders!["Content-Range"] = $"bytes {chunk.StartByte}-{chunk.EndByte}/{chunk.TotalBytes}";
}

SetStreamContent(param.Key, file, stream, content);
break;
}

case IEnumerable<string> value:
{
foreach (var item in value)
{
content.Add(new StringContent(item), string.Format(CultureInfo.InvariantCulture, "\"{0}\"", string.Concat(param.Key, "[]")));
}

break;
}

default:
content.Add(new StringContent(param.Value.ToString()), string.Format(CultureInfo.InvariantCulture, "\"{0}\"", param.Key));
break;
}
}

return content;
}

private CancellationToken GetDefaultCancellationToken() =>
Timeout > 0
? new CancellationTokenSource(Timeout).Token
Expand All @@ -518,13 +435,10 @@ private AuthenticationHeaderValue GetAuthorizationHeaderValue()
: new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(GetApiCredentials())));
}

private void PrePrepareRequestBody(
private void SetRequestHeaders(
HttpRequestMessage request,
HttpMethod method,
Dictionary<string, string> extraHeaders)
Dictionary<string, string> headers)
{
SetHttpMethod(method, request);

// Add platform information to the USER_AGENT header
// This is intended for platform information and not individual applications!
var userPlatform = string.IsNullOrEmpty(UserPlatform)
Expand All @@ -534,48 +448,36 @@ private void PrePrepareRequestBody(

request.Headers.Authorization = GetAuthorizationHeaderValue();

if (extraHeaders != null)
if (headers == null)
{
if (extraHeaders.ContainsKey("Accept"))
{
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(extraHeaders["Accept"]));
extraHeaders.Remove("Accept");
}
return;
}

foreach (var header in extraHeaders)
foreach (var header in headers)
{
if (header.Key == "Accept")
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(headers["Accept"]));
continue;
}

request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}

private async Task PrepareRequestContentAsync(
private async Task SetRequestContentAsync(
HttpRequestMessage request,
SortedDictionary<string, object> parameters,
Dictionary<string, string> extraHeaders = null,
CancellationToken? cancellationToken = null)
{
HandleUnsignedParameters(parameters);

var content = IsStringContent(extraHeaders)
? CreateStringContent(parameters)
var content = IsJsonContent(extraHeaders)
? CreateJsonContent(parameters)
: await CreateMultipartContentAsync(parameters, extraHeaders, cancellationToken).ConfigureAwait(false);

SetHeadersAndContent(request, extraHeaders, content);
}

private void PrepareRequestContent(
HttpRequestMessage request,
SortedDictionary<string, object> parameters,
Dictionary<string, string> extraHeaders = null)
{
HandleUnsignedParameters(parameters);

var content = IsStringContent(extraHeaders)
? CreateStringContent(parameters)
: CreateMultipartContent(parameters, extraHeaders);

SetHeadersAndContent(request, extraHeaders, content);
request.Content = content;
}
}
}
4 changes: 2 additions & 2 deletions CloudinaryDotNet/ApiShared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public partial class ApiShared : ISignProvider
protected string m_apiAddr = "https://" + ADDR_API;

private readonly Func<string, HttpRequestMessage> requestBuilder =
(url) => new HttpRequestMessage { RequestUri = new Uri(url) };
url => new HttpRequestMessage { RequestUri = new Uri(url) };

/// <summary>
/// Initializes a new instance of the <see cref="ApiShared"/> class.
Expand Down Expand Up @@ -576,7 +576,7 @@ public async Task<HttpResponseMessage> CallAsync(
CancellationToken? cancellationToken = null)
{
using var request =
await PrepareRequestBodyAsync(
await PrepareRequestAsync(
requestBuilder(PrepareRequestUrl(method, url, parameters)),
method,
parameters,
Expand Down
10 changes: 9 additions & 1 deletion CloudinaryDotNet/Cloudinary.UploadApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public partial class Cloudinary
/// </summary>
protected const int DEFAULT_CONCURRENT_UPLOADS = 1;

private readonly object chunkLock = new ();

/// <summary>
/// Uploads an image file to Cloudinary asynchronously.
/// </summary>
Expand Down Expand Up @@ -402,7 +404,13 @@ public async Task<T> UploadChunkAsync<T>(
if (string.IsNullOrEmpty(parameters.UniqueUploadId))
{
// The first chunk
parameters.UniqueUploadId = Utils.RandomPublicId();
lock (chunkLock)
{
if (string.IsNullOrEmpty(parameters.UniqueUploadId))
{
parameters.UniqueUploadId = Utils.RandomPublicId();
}
}
}

// Mark upload as chunked in order to set appropriate content range header.
Expand Down
7 changes: 6 additions & 1 deletion CloudinaryDotNet/FileDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class FileDescription : IDisposable

private readonly Mutex mutex = new ();

private readonly object chunkLock = new ();

private Stream fileStream;

private string filePath;
Expand Down Expand Up @@ -244,7 +246,10 @@ public void AddChunks(List<Stream> chunkStreams)
/// <param name="last"> Indicates whether the chunk represents the last chunk of a large file.</param>
public void AddChunk(Stream chunkStream, long startByte, long chunkSize, bool last = false)
{
chunks ??= new BlockingCollection<ChunkData>();
lock (chunkLock)
{
chunks ??= new BlockingCollection<ChunkData>();
}

CurrPos = startByte + chunkSize;

Expand Down

0 comments on commit 11c5cb2

Please sign in to comment.