Skip to content

Commit ebaf87b

Browse files
Add support for UploadChunk and parallel uploads in Upload Api
1 parent b2153d0 commit ebaf87b

18 files changed

+1124
-350
lines changed

CloudinaryDotNet.IntegrationTests/IntegrationTestBase.cs

+29-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ private void SaveTestResources(Assembly assembly)
180180
SaveEmbeddedToDisk(assembly, TEST_PDF, m_testPdfPath);
181181
}
182182

183-
private void SaveEmbeddedToDisk(Assembly assembly, string sourcePath, string destPath)
183+
private static void SaveEmbeddedToDisk(Assembly assembly, string sourcePath, string destPath)
184184
{
185185
try
186186
{
@@ -206,6 +206,34 @@ private void SaveEmbeddedToDisk(Assembly assembly, string sourcePath, string des
206206
}
207207
}
208208

209+
210+
protected List<string> SplitFile(string sourceFile, int chunkSize, string suffix = "")
211+
{
212+
var chunks = new List<string>();
213+
214+
var baseName = Path.GetFileNameWithoutExtension(sourceFile);
215+
var extension = Path.GetExtension(sourceFile);
216+
var buffer = new byte[chunkSize];
217+
using (var fs = new FileStream(sourceFile, FileMode.Open, FileAccess.Read))
218+
{
219+
int read;
220+
var currChunkNum = 0;
221+
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
222+
{
223+
var path = $"{Path.GetDirectoryName(sourceFile)}/{baseName}.{currChunkNum}{suffix}{extension}.tmp";
224+
using (var outputFile = new FileStream(path, FileMode.Create, FileAccess.Write))
225+
{
226+
outputFile.Write(buffer, 0, read);
227+
}
228+
229+
chunks.Add(path);
230+
currChunkNum++;
231+
}
232+
}
233+
234+
return chunks;
235+
}
236+
209237
/// <summary>
210238
/// A convenient method for uploading an image before testing.
211239
/// </summary>

CloudinaryDotNet.IntegrationTests/UploadApi/UploadMethodsTest.cs

+173-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
@@ -7,6 +8,7 @@
78
using System.Net;
89
using System.Threading.Tasks;
910
using CloudinaryDotNet.Actions;
11+
using CloudinaryDotNet.Core;
1012
using Newtonsoft.Json;
1113
using Newtonsoft.Json.Linq;
1214
using NUnit.Framework;
@@ -260,7 +262,7 @@ public void TestModerationManual()
260262
Assert.AreEqual(MODERATION_MANUAL, uploadResult.Moderation[0].Kind);
261263
Assert.AreEqual(ModerationStatus.Pending, uploadResult.Moderation[0].Status);
262264

263-
var getResult = m_cloudinary.GetResource(uploadResult.PublicId);
265+
var getResult = m_cloudinary.GetResourceByAssetId(uploadResult.AssetId);
264266

265267
Assert.NotNull(getResult);
266268
Assert.NotNull(getResult.Moderation, getResult.Error?.Message);
@@ -561,7 +563,7 @@ public NonSeekableStream(byte[] buffer) : base(buffer) { }
561563
[Test, RetryWithDelay]
562564
public void TestUploadLargeNonSeekableStream()
563565
{
564-
byte[] bytes = File.ReadAllBytes(m_testLargeImagePath);
566+
var bytes = File.ReadAllBytes(m_testLargeImagePath);
565567
const string streamed = "stream_non_seekable";
566568

567569
using (var memoryStream = new NonSeekableStream(bytes))
@@ -593,15 +595,15 @@ public void TestUploadLargeRawFiles()
593595
}
594596

595597
[Test, RetryWithDelay]
596-
public async Task TestUploadLargeRawFilesAsync()
598+
public async Task TestUploadLargeRawFilesAsyncInParallel()
597599
{
598600
// support asynchronous uploading large raw files
599601
var largeFilePath = m_testLargeImagePath;
600602
int largeFileLength = (int)new FileInfo(largeFilePath).Length;
601603

602604
var uploadParams = GetUploadLargeRawParams(largeFilePath);
603605

604-
var result = await m_cloudinary.UploadLargeAsync(uploadParams, TEST_CHUNK_SIZE);
606+
var result = await m_cloudinary.UploadLargeAsync<RawUploadResult>(uploadParams, TEST_CHUNK_SIZE, 2);
605607

606608
AssertUploadLarge(result, largeFileLength);
607609
}
@@ -617,6 +619,7 @@ private RawUploadParams GetUploadLargeRawParams(string path)
617619

618620
private void AssertUploadLarge(RawUploadResult result, int fileLength)
619621
{
622+
Assert.NotNull(result);
620623
Assert.AreEqual(fileLength, result.Bytes, result.Error?.Message);
621624
}
622625

@@ -655,6 +658,172 @@ public async Task TestUploadLargeAutoFilesAsync()
655658

656659
Assert.AreEqual("image", result.ResourceType);
657660
}
661+
662+
[Test, RetryWithDelay]
663+
public void TestUploadChunkSingleStream()
664+
{
665+
var largeFilePath = m_testLargeImagePath;
666+
var largeFileLength = (int)new FileInfo(largeFilePath).Length;
667+
668+
ImageUploadResult result = null;
669+
670+
using (var currChunk = new MemoryStream())
671+
{
672+
var uploadParams = new ImageUploadParams()
673+
{
674+
File = new FileDescription($"ImageFromChunks_{GetTaggedRandomValue()}", currChunk),
675+
Tags = m_apiTag
676+
};
677+
678+
var buffer = new byte[TEST_CHUNK_SIZE];
679+
680+
using (var source = File.Open(largeFilePath, FileMode.Open))
681+
{
682+
int read;
683+
while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
684+
{
685+
currChunk.Seek(0, SeekOrigin.End);
686+
currChunk.Write(buffer, 0, read);
687+
688+
// Need to specify whether the chunk is the last one in order to finish the upload.
689+
uploadParams.File.LastChunk = read != TEST_CHUNK_SIZE;
690+
691+
result = m_cloudinary.UploadChunk(uploadParams);
692+
}
693+
}
694+
}
695+
696+
AssertUploadLarge(result, largeFileLength);
697+
Assert.AreEqual("image", result?.ResourceType);
698+
}
699+
700+
[Test, RetryWithDelay]
701+
public async Task TestUploadChunkMultipleStreamsCustomOffsetAsync()
702+
{
703+
var largeFilePath = m_testLargeImagePath;
704+
var largeFileLength = (int)new FileInfo(largeFilePath).Length;
705+
706+
ImageUploadResult result = null;
707+
708+
var uploadParams = new ImageUploadParams()
709+
{
710+
// File path will be ignored, since we use streams.
711+
File = new FileDescription($"ImageFromMultipleChunks_{GetTaggedRandomValue()}", true),
712+
Tags = m_apiTag
713+
};
714+
715+
var buffer = new byte[TEST_CHUNK_SIZE];
716+
717+
using (var source = File.Open(largeFilePath, FileMode.Open))
718+
{
719+
int read;
720+
while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
721+
{
722+
var currChunk = new MemoryStream(buffer);
723+
// Set current chunk
724+
uploadParams.File.AddChunk(currChunk, source.Position - read, read, read != TEST_CHUNK_SIZE);
725+
726+
result = await m_cloudinary.UploadChunkAsync(uploadParams);
727+
}
728+
}
729+
730+
AssertUploadLarge(result, largeFileLength);
731+
Assert.AreEqual("image", result?.ResourceType);
732+
}
733+
734+
[Test, RetryWithDelay]
735+
public void TestUploadChunkMultipleFileParts()
736+
{
737+
var largeFilePath = m_testLargeImagePath;
738+
var largeFileLength = (int)new FileInfo(largeFilePath).Length;
739+
740+
ImageUploadResult result = null;
741+
742+
var fileChunks = SplitFile(largeFilePath, TEST_CHUNK_SIZE, "multiple");
743+
744+
var uploadParams = new ImageUploadParams()
745+
{
746+
File = new FileDescription($"ImageFromFileChunks_{GetTaggedRandomValue()}", true),
747+
Tags = m_apiTag,
748+
};
749+
try
750+
{
751+
foreach (var chunk in fileChunks)
752+
{
753+
// Set file path of the current chunk.
754+
uploadParams.File.AddChunk(chunk, fileChunks.IndexOf(chunk) == fileChunks.Count - 1);
755+
// Need to specify whether the chunk is the last one in order to finish the upload.
756+
757+
result = m_cloudinary.UploadChunk(uploadParams);
758+
}
759+
}
760+
finally
761+
{
762+
uploadParams.File.Dispose();
763+
foreach (var chunk in fileChunks)
764+
{
765+
try
766+
{
767+
File.Delete(chunk);
768+
}
769+
catch (IOException)
770+
{
771+
// nothing to do
772+
}
773+
}
774+
}
775+
776+
AssertUploadLarge(result, largeFileLength);
777+
Assert.AreEqual("image", result?.ResourceType);
778+
}
779+
780+
[Test, RetryWithDelay]
781+
public void TestUploadChunkMultipleFilePartsInParallel()
782+
{
783+
var largeFilePath = m_testLargeImagePath;
784+
var largeFileLength = (int)new FileInfo(largeFilePath).Length;
785+
786+
var fileChunks = SplitFile(largeFilePath, TEST_CHUNK_SIZE, "multiple_parallel");
787+
788+
var uploadParams = new RawUploadParams()
789+
{
790+
File = new FileDescription($"ImageFromFileChunks_{GetTaggedRandomValue()}", true),
791+
Tags = m_apiTag
792+
};
793+
794+
var resultCollection = new ConcurrentBag<RawUploadResult>();
795+
796+
uploadParams.File.AddChunks(fileChunks);
797+
798+
try
799+
{
800+
Parallel.For(0, fileChunks.Count, new ParallelOptions { MaxDegreeOfParallelism = 2 },chunkNum =>
801+
{
802+
resultCollection.Add(m_cloudinary.UploadChunk(uploadParams));
803+
});
804+
}
805+
finally
806+
{
807+
uploadParams.File.Dispose();
808+
foreach (var chunk in fileChunks)
809+
{
810+
try
811+
{
812+
File.Delete(chunk);
813+
}
814+
catch (IOException)
815+
{
816+
// nothing to do
817+
}
818+
}
819+
}
820+
821+
var uploadResult = resultCollection.FirstOrDefault(r => r.AssetId != null);
822+
823+
AssertUploadLarge(uploadResult, largeFileLength);
824+
Assert.AreEqual("raw", uploadResult?.ResourceType);
825+
}
826+
658827
/// <summary>
659828
/// Test access control rules
660829
/// </summary>

CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -485,13 +485,13 @@ public void TestExcludeEmptyTransformation()
485485
[Test]
486486
public void TestAgentPlatformHeaders()
487487
{
488-
var request = new HttpRequestMessage { RequestUri = new Uri("http://dummy.com") };
488+
var request = new HttpRequestMessage { RequestUri = new Uri("https://dummy.com") };
489489
m_api.UserPlatform = "Test/1.0";
490490

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

496496
//Can't test the result, so we just verify the UserAgent parameter is sent to the server
497497
StringAssert.AreEqualIgnoringCase($"{m_api.UserPlatform} {ApiShared.USER_AGENT}",

CloudinaryDotNet/Actions/AssetsUpload/BasicRawUploadParams.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public virtual ResourceType ResourceType
5858
/// </summary>
5959
public string Signature { get; set; }
6060

61+
/// <summary>
62+
/// Gets or sets unique upload ID.
63+
/// </summary>
64+
public string UniqueUploadId { get; set; }
65+
6166
/// <summary>
6267
/// Validate object model.
6368
/// </summary>
@@ -68,7 +73,7 @@ public override void Check()
6873
throw new ArgumentException("File must be specified in UploadParams!");
6974
}
7075

71-
if (!File.IsRemote && File.Stream == null && string.IsNullOrEmpty(File.FilePath))
76+
if (!File.Chunked && !File.IsRemote && File.Stream == null && string.IsNullOrEmpty(File.FilePath))
7277
{
7378
throw new ArgumentException("File is not ready!");
7479
}

CloudinaryDotNet/Actions/AssetsUpload/ExplicitResult.cs

-6
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ public class ExplicitResult : RawUploadResult
2525
/// </summary>
2626
public List<ResponsiveBreakpointList> ResponsiveBreakpoints { get; set; }
2727

28-
/// <summary>
29-
/// Gets or sets a status that is returned when passing 'Async' argument set to 'true' to the server.
30-
/// </summary>
31-
[DataMember(Name = "status")]
32-
public string Status { get; set; }
33-
3428
/// <summary>
3529
/// Gets or sets any requested information from executing one of the Cloudinary Add-ons on the media asset.
3630
/// </summary>

CloudinaryDotNet/Actions/AssetsUpload/UploadResult.cs

+6
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ public long Length
102102
[DataMember(Name = "metadata")]
103103
public JToken MetadataFields { get; set; }
104104

105+
/// <summary>
106+
/// Gets or sets a status that is returned when passing 'Async' argument set to 'true' to the server.
107+
/// </summary>
108+
[DataMember(Name = "status")]
109+
public string Status { get; set; }
110+
105111
/// <summary>
106112
/// Gets or sets upload hook execution status.
107113
/// </summary>

CloudinaryDotNet/Actions/AssetsUpload/VideoUploadParams.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ public class VideoUploadParams : ImageUploadParams
88
/// <summary>
99
/// Gets get the type of video asset for upload.
1010
/// </summary>
11-
public override ResourceType ResourceType
12-
{
13-
get { return Actions.ResourceType.Video; }
14-
}
11+
public override ResourceType ResourceType => ResourceType.Video;
1512
}
1613
}

0 commit comments

Comments
 (0)