Skip to content

Commit 11c5cb2

Browse files
Fix X-Unique-Upload-Id header duplicate value
* Improve stability * Cleanup code
1 parent 6d1827a commit 11c5cb2

File tree

6 files changed

+60
-148
lines changed

6 files changed

+60
-148
lines changed

CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ public void TestAgentPlatformHeaders()
488488
var request = new HttpRequestMessage { RequestUri = new Uri("https://dummy.com") };
489489
m_api.UserPlatform = "Test/1.0";
490490

491-
m_api.PrepareRequestBodyAsync(
491+
m_api.PrepareRequestAsync(
492492
request,
493493
HttpMethod.GET,
494494
new SortedDictionary<string, object>()).GetAwaiter().GetResult();

CloudinaryDotNet/Actions/AssetsUpload/BasicRawUploadParams.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ public class BasicRawUploadParams : BaseParams
3838
/// <summary>
3939
/// Gets the 'raw' type of file you are uploading.
4040
/// </summary>
41-
public virtual ResourceType ResourceType
42-
{
43-
get { return Actions.ResourceType.Raw; }
44-
}
41+
public virtual ResourceType ResourceType => ResourceType.Raw;
4542

4643
/// <summary>
4744
/// Gets or sets file name to override an original file name.

CloudinaryDotNet/ApiShared.Internal.cs

Lines changed: 41 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Threading;
1414
using System.Threading.Tasks;
1515
using CloudinaryDotNet.Actions;
16+
using CloudinaryDotNet.Core;
1617
using Newtonsoft.Json;
1718
using Newtonsoft.Json.Converters;
1819
using Newtonsoft.Json.Linq;
@@ -32,10 +33,8 @@ public partial class ApiShared : ISignProvider
3233
internal static async Task<T> ParseAsync<T>(HttpResponseMessage response)
3334
where T : BaseResult
3435
{
35-
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
36-
{
37-
return CreateResult<T>(response, stream);
38-
}
36+
using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
37+
return CreateResult<T>(response, stream);
3938
}
4039

4140
/// <summary>
@@ -47,10 +46,8 @@ internal static async Task<T> ParseAsync<T>(HttpResponseMessage response)
4746
internal static T Parse<T>(HttpResponseMessage response)
4847
where T : BaseResult
4948
{
50-
using (var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
51-
{
52-
return CreateResult<T>(response, stream);
53-
}
49+
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
50+
return CreateResult<T>(response, stream);
5451
}
5552

5653
/// <summary>
@@ -128,14 +125,15 @@ internal virtual T CallApi<T>(HttpMethod method, string url, BaseParams paramete
128125
/// <param name="extraHeaders">(Optional) Headers to add to the request.</param>
129126
/// <param name="cancellationToken">(Optional) Cancellation token.</param>
130127
/// <returns>Prepared HTTP request.</returns>
131-
internal async Task<HttpRequestMessage> PrepareRequestBodyAsync(
128+
internal async Task<HttpRequestMessage> PrepareRequestAsync(
132129
HttpRequestMessage request,
133130
HttpMethod method,
134131
SortedDictionary<string, object> parameters,
135132
Dictionary<string, string> extraHeaders = null,
136133
CancellationToken? cancellationToken = null)
137134
{
138-
PrePrepareRequestBody(request, method, extraHeaders);
135+
SetHttpMethod(method, request);
136+
SetRequestHeaders(request, extraHeaders);
139137

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

145143
SetChunkedEncoding(request);
146144

147-
await PrepareRequestContentAsync(request, parameters, extraHeaders, cancellationToken)
145+
await SetRequestContentAsync(request, parameters, extraHeaders, cancellationToken)
148146
.ConfigureAwait(false);
149147

150148
return request;
151149
}
152150

153-
/// <summary>
154-
/// Prepares request body to be sent on custom call to Cloudinary API.
155-
/// </summary>
156-
/// <param name="request">HTTP request to alter.</param>
157-
/// <param name="method">HTTP method of call.</param>
158-
/// <param name="parameters">Dictionary of call parameters.</param>
159-
/// <param name="extraHeaders">(Optional) Headers to add to the request.</param>
160-
/// <returns>Prepared HTTP request.</returns>
161-
internal HttpRequestMessage PrepareRequestBody(
162-
HttpRequestMessage request,
163-
HttpMethod method,
164-
SortedDictionary<string, object> parameters,
165-
Dictionary<string, string> extraHeaders = null)
166-
{
167-
PrePrepareRequestBody(request, method, extraHeaders);
168-
169-
if (ShouldPrepareContent(method, parameters))
170-
{
171-
SetChunkedEncoding(request);
172-
173-
PrepareRequestContent(request, parameters, extraHeaders);
174-
}
175-
176-
return request;
177-
}
178-
179151
/// <summary>
180152
/// Extends Cloudinary upload parameters with additional attributes.
181153
/// </summary>
@@ -303,8 +275,8 @@ private static void UpdateResultFromResponse<T>(HttpResponseMessage response, T
303275
return;
304276
}
305277

306-
response?.Headers
307-
.Where(_ => _.Key.StartsWith("X-FeatureRateLimit", StringComparison.OrdinalIgnoreCase))
278+
response.Headers
279+
.Where(p => p.Key.StartsWith("X-FeatureRateLimit", StringComparison.OrdinalIgnoreCase))
308280
.ToList()
309281
.ForEach(header =>
310282
{
@@ -330,9 +302,9 @@ private static void UpdateResultFromResponse<T>(HttpResponseMessage response, T
330302
}
331303

332304
private static bool ShouldPrepareContent(HttpMethod method, object parameters) =>
333-
(method == HttpMethod.POST || method == HttpMethod.PUT) && parameters != null;
305+
method is HttpMethod.POST or HttpMethod.PUT && parameters != null;
334306

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

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

358-
private static StringContent CreateStringContent(SortedDictionary<string, object> parameters) =>
359-
new StringContent(ParamsToJson(parameters), Encoding.UTF8, Constants.CONTENT_TYPE_APPLICATION_JSON);
330+
private static void SetChunkContent(ChunkData chunk, MultipartFormDataContent content)
331+
{
332+
content.Headers.TryAddWithoutValidation("Content-Range", $"bytes {chunk.StartByte}-{chunk.EndByte}/{chunk.TotalBytes}");
333+
}
334+
335+
private static StringContent CreateJsonContent(SortedDictionary<string, object> parameters) =>
336+
new (ParamsToJson(parameters), Encoding.UTF8, Constants.CONTENT_TYPE_APPLICATION_JSON);
360337

361-
private static bool IsStringContent(Dictionary<string, string> extraHeaders) =>
338+
private static bool IsJsonContent(IReadOnlyDictionary<string, string> extraHeaders) =>
362339
extraHeaders != null &&
363340
extraHeaders.TryGetValue(Constants.HEADER_CONTENT_TYPE, out var value) &&
364341
value == Constants.CONTENT_TYPE_APPLICATION_JSON;
365342

366-
private static void SetHeadersAndContent(HttpRequestMessage request, Dictionary<string, string> extraHeaders, HttpContent content)
367-
{
368-
if (extraHeaders != null)
369-
{
370-
foreach (var header in extraHeaders)
371-
{
372-
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
373-
}
374-
}
375-
376-
request.Content = content;
377-
}
378-
379343
private static void SetHttpMethod(HttpMethod method, HttpRequestMessage req)
380344
{
381345
switch (method)
@@ -414,13 +378,14 @@ private static async Task<HttpContent> CreateMultipartContentAsync(
414378
case FileDescription file:
415379
{
416380
Stream stream;
417-
if (IsContentRange(extraHeaders))
381+
382+
if (IsChunkedUpload(extraHeaders))
418383
{
419384
var chunk = await file.GetNextChunkAsync(cancellationToken).ConfigureAwait(false);
420385

421-
stream = chunk.Chunk;
386+
SetChunkContent(chunk, content);
422387

423-
extraHeaders!["Content-Range"] = $"bytes {chunk.StartByte}-{chunk.EndByte}/{chunk.TotalBytes}";
388+
stream = chunk.Chunk;
424389
}
425390
else
426391
{
@@ -450,54 +415,6 @@ private static async Task<HttpContent> CreateMultipartContentAsync(
450415
return content;
451416
}
452417

453-
private static HttpContent CreateMultipartContent(
454-
SortedDictionary<string, object> parameters,
455-
Dictionary<string, string> extraHeaders = null)
456-
{
457-
var content = new MultipartFormDataContent(HTTP_BOUNDARY);
458-
foreach (var param in parameters.Where(param => param.Value != null))
459-
{
460-
switch (param.Value)
461-
{
462-
case FileDescription { IsRemote: true } file:
463-
SetContentForRemoteFile(param.Key, file, content);
464-
break;
465-
case FileDescription file:
466-
{
467-
var stream = file.GetFileStream();
468-
469-
if (IsContentRange(extraHeaders))
470-
{
471-
var chunk = file.GetNextChunkAsync().GetAwaiter().GetResult();
472-
473-
stream = chunk.Chunk;
474-
475-
extraHeaders!["Content-Range"] = $"bytes {chunk.StartByte}-{chunk.EndByte}/{chunk.TotalBytes}";
476-
}
477-
478-
SetStreamContent(param.Key, file, stream, content);
479-
break;
480-
}
481-
482-
case IEnumerable<string> value:
483-
{
484-
foreach (var item in value)
485-
{
486-
content.Add(new StringContent(item), string.Format(CultureInfo.InvariantCulture, "\"{0}\"", string.Concat(param.Key, "[]")));
487-
}
488-
489-
break;
490-
}
491-
492-
default:
493-
content.Add(new StringContent(param.Value.ToString()), string.Format(CultureInfo.InvariantCulture, "\"{0}\"", param.Key));
494-
break;
495-
}
496-
}
497-
498-
return content;
499-
}
500-
501418
private CancellationToken GetDefaultCancellationToken() =>
502419
Timeout > 0
503420
? new CancellationTokenSource(Timeout).Token
@@ -518,13 +435,10 @@ private AuthenticationHeaderValue GetAuthorizationHeaderValue()
518435
: new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(GetApiCredentials())));
519436
}
520437

521-
private void PrePrepareRequestBody(
438+
private void SetRequestHeaders(
522439
HttpRequestMessage request,
523-
HttpMethod method,
524-
Dictionary<string, string> extraHeaders)
440+
Dictionary<string, string> headers)
525441
{
526-
SetHttpMethod(method, request);
527-
528442
// Add platform information to the USER_AGENT header
529443
// This is intended for platform information and not individual applications!
530444
var userPlatform = string.IsNullOrEmpty(UserPlatform)
@@ -534,48 +448,36 @@ private void PrePrepareRequestBody(
534448

535449
request.Headers.Authorization = GetAuthorizationHeaderValue();
536450

537-
if (extraHeaders != null)
451+
if (headers == null)
538452
{
539-
if (extraHeaders.ContainsKey("Accept"))
540-
{
541-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(extraHeaders["Accept"]));
542-
extraHeaders.Remove("Accept");
543-
}
453+
return;
454+
}
544455

545-
foreach (var header in extraHeaders)
456+
foreach (var header in headers)
457+
{
458+
if (header.Key == "Accept")
546459
{
547-
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
460+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(headers["Accept"]));
461+
continue;
548462
}
463+
464+
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
549465
}
550466
}
551467

552-
private async Task PrepareRequestContentAsync(
468+
private async Task SetRequestContentAsync(
553469
HttpRequestMessage request,
554470
SortedDictionary<string, object> parameters,
555471
Dictionary<string, string> extraHeaders = null,
556472
CancellationToken? cancellationToken = null)
557473
{
558474
HandleUnsignedParameters(parameters);
559475

560-
var content = IsStringContent(extraHeaders)
561-
? CreateStringContent(parameters)
476+
var content = IsJsonContent(extraHeaders)
477+
? CreateJsonContent(parameters)
562478
: await CreateMultipartContentAsync(parameters, extraHeaders, cancellationToken).ConfigureAwait(false);
563479

564-
SetHeadersAndContent(request, extraHeaders, content);
565-
}
566-
567-
private void PrepareRequestContent(
568-
HttpRequestMessage request,
569-
SortedDictionary<string, object> parameters,
570-
Dictionary<string, string> extraHeaders = null)
571-
{
572-
HandleUnsignedParameters(parameters);
573-
574-
var content = IsStringContent(extraHeaders)
575-
? CreateStringContent(parameters)
576-
: CreateMultipartContent(parameters, extraHeaders);
577-
578-
SetHeadersAndContent(request, extraHeaders, content);
480+
request.Content = content;
579481
}
580482
}
581483
}

CloudinaryDotNet/ApiShared.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public partial class ApiShared : ISignProvider
148148
protected string m_apiAddr = "https://" + ADDR_API;
149149

150150
private readonly Func<string, HttpRequestMessage> requestBuilder =
151-
(url) => new HttpRequestMessage { RequestUri = new Uri(url) };
151+
url => new HttpRequestMessage { RequestUri = new Uri(url) };
152152

153153
/// <summary>
154154
/// Initializes a new instance of the <see cref="ApiShared"/> class.
@@ -576,7 +576,7 @@ public async Task<HttpResponseMessage> CallAsync(
576576
CancellationToken? cancellationToken = null)
577577
{
578578
using var request =
579-
await PrepareRequestBodyAsync(
579+
await PrepareRequestAsync(
580580
requestBuilder(PrepareRequestUrl(method, url, parameters)),
581581
method,
582582
parameters,

CloudinaryDotNet/Cloudinary.UploadApi.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public partial class Cloudinary
3737
/// </summary>
3838
protected const int DEFAULT_CONCURRENT_UPLOADS = 1;
3939

40+
private readonly object chunkLock = new ();
41+
4042
/// <summary>
4143
/// Uploads an image file to Cloudinary asynchronously.
4244
/// </summary>
@@ -402,7 +404,13 @@ public async Task<T> UploadChunkAsync<T>(
402404
if (string.IsNullOrEmpty(parameters.UniqueUploadId))
403405
{
404406
// The first chunk
405-
parameters.UniqueUploadId = Utils.RandomPublicId();
407+
lock (chunkLock)
408+
{
409+
if (string.IsNullOrEmpty(parameters.UniqueUploadId))
410+
{
411+
parameters.UniqueUploadId = Utils.RandomPublicId();
412+
}
413+
}
406414
}
407415

408416
// Mark upload as chunked in order to set appropriate content range header.

CloudinaryDotNet/FileDescription.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public class FileDescription : IDisposable
4646

4747
private readonly Mutex mutex = new ();
4848

49+
private readonly object chunkLock = new ();
50+
4951
private Stream fileStream;
5052

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

249254
CurrPos = startByte + chunkSize;
250255

0 commit comments

Comments
 (0)