diff --git a/CloudConvert.API/CloudConvert.API.csproj b/CloudConvert.API/CloudConvert.API.csproj
index d986f43..42c798e 100644
--- a/CloudConvert.API/CloudConvert.API.csproj
+++ b/CloudConvert.API/CloudConvert.API.csproj
@@ -11,7 +11,11 @@
true
true
-
+
+
+
+
+
diff --git a/CloudConvert.API/CloudConvertAPI.cs b/CloudConvert.API/CloudConvertAPI.cs
index 5ee96c4..8d8780d 100644
--- a/CloudConvert.API/CloudConvertAPI.cs
+++ b/CloudConvert.API/CloudConvertAPI.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
@@ -33,6 +34,7 @@ public interface ICloudConvertAPI
#endregion
Task UploadAsync(string url, byte[] file, string fileName, object parameters);
+ Task UploadAsync(string url, Stream file, string fileName, object parameters);
bool ValidateWebhookSignatures(string payloadString, string signature, string signingSecret);
string CreateSignedUrl(string baseUrl, string signingSecret, JobCreateRequest job, string cacheKey = null);
}
@@ -50,13 +52,17 @@ public class CloudConvertAPI : ICloudConvertAPI
const string publicUrlSyncApi = "https://sync.api.cloudconvert.com/v2";
static readonly char[] base64Padding = { '=' };
-
- public CloudConvertAPI(string api_key, bool isSandbox = false)
+ internal CloudConvertAPI(RestHelper restHelper, string api_key, bool isSandbox = false)
{
_apiUrl = isSandbox ? sandboxUrlApi : publicUrlApi;
_apiSyncUrl = isSandbox ? sandboxUrlSyncApi : publicUrlSyncApi;
_api_key += api_key;
- _restHelper = new RestHelper();
+ _restHelper = restHelper;
+ }
+
+ public CloudConvertAPI(string api_key, bool isSandbox = false)
+ : this(new RestHelper(), api_key, isSandbox)
+ {
}
public CloudConvertAPI(string url, string api_key)
@@ -82,7 +88,7 @@ private HttpRequestMessage GetRequest(string endpoint, HttpMethod method, object
return request;
}
- private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMethod method, byte[] file, string fileName, Dictionary parameters = null)
+ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMethod method, HttpContent fileContent, string fileName, Dictionary parameters = null)
{
var content = new MultipartFormDataContent();
var request = new HttpRequestMessage { RequestUri = new Uri(endpoint), Method = method, };
@@ -95,7 +101,6 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth
}
}
- var fileContent = new ByteArrayContent(file);
fileContent.Headers.Add("Content-Disposition", $"form-data; name=\"file\"; filename=\"{ new string(Encoding.UTF8.GetBytes(fileName).Select(b => (char)b).ToArray())}\"");
content.Add(fileContent);
@@ -214,7 +219,9 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth
#endregion
- public Task UploadAsync(string url, byte[] file, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest($"{url}", HttpMethod.Post, file, fileName, GetParameters(parameters)));
+ public Task UploadAsync(string url, byte[] file, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new ByteArrayContent(file), fileName, GetParameters(parameters)));
+
+ public Task UploadAsync(string url, Stream stream, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new StreamContent(stream), fileName, GetParameters(parameters)));
public string CreateSignedUrl(string baseUrl, string signingSecret, JobCreateRequest job, string cacheKey = null)
{
diff --git a/CloudConvert.API/RestHelper.cs b/CloudConvert.API/RestHelper.cs
index 64ebf6e..36108e3 100644
--- a/CloudConvert.API/RestHelper.cs
+++ b/CloudConvert.API/RestHelper.cs
@@ -14,6 +14,11 @@ internal RestHelper()
_httpClient.Timeout = System.TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite);
}
+ internal RestHelper(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
public async Task RequestAsync(HttpRequestMessage request)
{
var response = await _httpClient.SendAsync(request);
diff --git a/CloudConvert.Test/Extensions/MockExtensions.cs b/CloudConvert.Test/Extensions/MockExtensions.cs
new file mode 100644
index 0000000..4a0b31c
--- /dev/null
+++ b/CloudConvert.Test/Extensions/MockExtensions.cs
@@ -0,0 +1,37 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Moq.Language.Flow;
+using Moq.Protected;
+
+namespace CloudConvert.Test.Extensions
+{
+ public static class MockExtensions
+ {
+ public static IReturnsResult MockResponse(this Mock mock, string endpoint, string fileName)
+ {
+ return mock.Protected()
+ .Setup>("SendAsync",
+ ItExpr.Is(message => message.RequestUri.AbsolutePath.EndsWith(endpoint)),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Responses", fileName)))
+ });
+ }
+
+ public static void VerifyRequest(this Mock mock, string endpoint, Times times)
+ {
+ mock.Protected()
+ .Verify("SendAsync",
+ times,
+ ItExpr.Is(message => message.RequestUri.AbsolutePath.EndsWith(endpoint)),
+ ItExpr.IsAny());
+ }
+ }
+}
diff --git a/CloudConvert.Test/IntegrationTests.cs b/CloudConvert.Test/IntegrationTests.cs
index 9c60f01..b647f62 100644
--- a/CloudConvert.Test/IntegrationTests.cs
+++ b/CloudConvert.Test/IntegrationTests.cs
@@ -27,8 +27,9 @@ public void Setup()
_cloudConvertAPI = new CloudConvertAPI(apiKey, true);
}
- [Test]
- public async Task CreateJob()
+ [TestCase("stream")]
+ [TestCase("bytes")]
+ public async Task CreateJob(string streamingMethod)
{
var job = await _cloudConvertAPI.CreateJobAsync(new JobCreateRequest
{
@@ -48,10 +49,25 @@ public async Task CreateJob()
Assert.IsNotNull(uploadTask);
var path = AppDomain.CurrentDomain.BaseDirectory + @"TestFiles/input.pdf";
- byte[] file = File.ReadAllBytes(path);
string fileName = "input.pdf";
- var result = await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), file, fileName, uploadTask.Result.Form.Parameters);
+ string result;
+
+ switch (streamingMethod)
+ {
+ case "bytes":
+ {
+ byte[] file = File.ReadAllBytes(path);
+ result = await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), file, fileName, uploadTask.Result.Form.Parameters);
+ break;
+ }
+ case "stream":
+ {
+ using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ result = await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), stream, fileName, uploadTask.Result.Form.Parameters);
+ break;
+ }
+ }
job = await _cloudConvertAPI.WaitJobAsync(job.Data.Id);
@@ -69,8 +85,9 @@ public async Task CreateJob()
using (var client = new WebClient()) client.DownloadFile(fileExport.Url, fileExport.Filename);
}
- [Test]
- public async Task CreateTask()
+ [TestCase("stream")]
+ [TestCase("bytes")]
+ public async Task CreateTask(string streamingMethod)
{
// import
@@ -79,10 +96,23 @@ public async Task CreateTask()
var importTask = await _cloudConvertAPI.CreateTaskAsync(ImportUploadCreateRequest.Operation, reqImport);
var path = AppDomain.CurrentDomain.BaseDirectory + @"TestFiles/input.pdf";
- byte[] file = File.ReadAllBytes(path);
string fileName = "input.pdf";
- await _cloudConvertAPI.UploadAsync(importTask.Data.Result.Form.Url.ToString(), file, fileName, importTask.Data.Result.Form.Parameters);
+ switch (streamingMethod)
+ {
+ case "bytes":
+ {
+ byte[] file = File.ReadAllBytes(path);
+ await _cloudConvertAPI.UploadAsync(importTask.Data.Result.Form.Url.ToString(), file, fileName, importTask.Data.Result.Form.Parameters);
+ break;
+ }
+ case "stream":
+ {
+ using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ await _cloudConvertAPI.UploadAsync(importTask.Data.Result.Form.Url.ToString(), stream, fileName, importTask.Data.Result.Form.Parameters);
+ break;
+ }
+ }
importTask = await _cloudConvertAPI.WaitTaskAsync(importTask.Data.Id);
diff --git a/CloudConvert.Test/TestTasks.cs b/CloudConvert.Test/TestTasks.cs
index 96a8004..c50854a 100644
--- a/CloudConvert.Test/TestTasks.cs
+++ b/CloudConvert.Test/TestTasks.cs
@@ -1,5 +1,8 @@
using System;
using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
using System.Threading.Tasks;
using CloudConvert.API;
using CloudConvert.API.Models;
@@ -7,7 +10,9 @@
using CloudConvert.API.Models.ImportOperations;
using CloudConvert.API.Models.TaskModels;
using CloudConvert.API.Models.TaskOperations;
+using CloudConvert.Test.Extensions;
using Moq;
+using Moq.Protected;
using Newtonsoft.Json;
using NUnit.Framework;
@@ -15,8 +20,6 @@ namespace CloudConvert.Test
{
public class TestTasks
{
- readonly Mock _cloudConvertAPI = new Mock();
-
[Test]
public async Task GetAllTasks()
{
@@ -26,10 +29,12 @@ public async Task GetAllTasks()
var path = @"Responses/tasks.json";
string json = File.ReadAllText(path);
- _cloudConvertAPI.Setup(cc => cc.GetAllTasksAsync(filter))
+
+ var cloudConvertApi = new Mock();
+ cloudConvertApi.Setup(cc => cc.GetAllTasksAsync(filter))
.ReturnsAsync(JsonConvert.DeserializeObject>(json));
- var tasks = await _cloudConvertAPI.Object.GetAllTasksAsync(filter);
+ var tasks = await cloudConvertApi.Object.GetAllTasksAsync(filter);
Assert.IsNotNull(tasks);
Assert.IsTrue(tasks.Data.Count >= 0);
@@ -64,10 +69,12 @@ public async Task CreateTask()
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/task_created.json";
string json = File.ReadAllText(path);
- _cloudConvertAPI.Setup(cc => cc.CreateTaskAsync(ConvertCreateRequest.Operation, req))
+
+ var cloudConvertApi = new Mock();
+ cloudConvertApi.Setup(cc => cc.CreateTaskAsync(ConvertCreateRequest.Operation, req))
.ReturnsAsync(JsonConvert.DeserializeObject>(json));
- var task = await _cloudConvertAPI.Object.CreateTaskAsync(ConvertCreateRequest.Operation, req);
+ var task = await cloudConvertApi.Object.CreateTaskAsync(ConvertCreateRequest.Operation, req);
Assert.IsNotNull(task);
Assert.IsTrue(task.Data.Status == API.Models.Enums.TaskStatus.waiting);
@@ -80,6 +87,8 @@ public async Task GetTask()
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/task.json";
string json = File.ReadAllText(path);
+
+ var _cloudConvertAPI = new Mock();
_cloudConvertAPI.Setup(cc => cc.GetTaskAsync(id, null))
.ReturnsAsync(JsonConvert.DeserializeObject>(json));
@@ -96,10 +105,12 @@ public async Task WaitTask()
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/task.json";
string json = File.ReadAllText(path);
- _cloudConvertAPI.Setup(cc => cc.WaitTaskAsync(id))
+
+ var cloudConvertApi = new Mock();
+ cloudConvertApi.Setup(cc => cc.WaitTaskAsync(id))
.ReturnsAsync(JsonConvert.DeserializeObject>(json));
- var task = await _cloudConvertAPI.Object.WaitTaskAsync(id);
+ var task = await cloudConvertApi.Object.WaitTaskAsync(id);
Assert.IsNotNull(task);
Assert.IsTrue(task.Data.Operation == "convert");
@@ -111,22 +122,24 @@ public async Task DeleteTask()
{
string id = "9de1a620-952c-4482-9d44-681ae28d72a1";
- _cloudConvertAPI.Setup(cc => cc.DeleteTaskAsync(id));
+ var cloudConvertApi = new Mock();
+ cloudConvertApi.Setup(cc => cc.DeleteTaskAsync(id));
- await _cloudConvertAPI.Object.DeleteTaskAsync("c8a8da46-3758-45bf-b983-2510e3170acb");
+ await cloudConvertApi.Object.DeleteTaskAsync("c8a8da46-3758-45bf-b983-2510e3170acb");
}
[Test]
public async Task Upload()
{
+ var cloudConvertApi = new Mock();
var req = new ImportUploadCreateRequest();
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/upload_task_created.json";
string json = File.ReadAllText(path);
- _cloudConvertAPI.Setup(cc => cc.CreateTaskAsync(ImportUploadCreateRequest.Operation, req))
+ cloudConvertApi.Setup(cc => cc.CreateTaskAsync(ImportUploadCreateRequest.Operation, req))
.ReturnsAsync(JsonConvert.DeserializeObject>(json));
- var task = await _cloudConvertAPI.Object.CreateTaskAsync(ImportUploadCreateRequest.Operation, req);
+ var task = await cloudConvertApi.Object.CreateTaskAsync(ImportUploadCreateRequest.Operation, req);
Assert.IsNotNull(task);
@@ -134,9 +147,35 @@ public async Task Upload()
byte[] file = File.ReadAllBytes(pathFile);
string fileName = "input.pdf";
- _cloudConvertAPI.Setup(cc => cc.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters));
+ cloudConvertApi.Setup(cc => cc.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters));
+
+ await cloudConvertApi.Object.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters);
+ }
+
+ [Test]
+ public async Task UploadStream()
+ {
+ var httpMessageHandlerMock = new Mock();
+ httpMessageHandlerMock.MockResponse("/import/upload", "upload_task_created.json");
+ httpMessageHandlerMock.MockResponse("/tasks", "tasks.json");
+
+ var httpClient = new HttpClient(httpMessageHandlerMock.Object);
+ var restHelper = new RestHelper(httpClient);
+ var req = new ImportUploadCreateRequest();
+ var cloudConvertApi = new CloudConvertAPI(restHelper, "API_KEY");
+
+ var task = await cloudConvertApi.CreateTaskAsync(ImportUploadCreateRequest.Operation, req);
+
+ Assert.IsNotNull(task);
+ httpMessageHandlerMock.VerifyRequest("/import/upload", Times.Once());
+
+ var streamMock = new Mock();
+ var fileName = "input.pdf";
+
+ await cloudConvertApi.UploadAsync(task.Data.Result.Form.Url.ToString(), streamMock.Object, fileName, task.Data.Result.Form.Parameters);
- await _cloudConvertAPI.Object.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters);
+ httpMessageHandlerMock.VerifyRequest("/tasks", Times.Once());
+ streamMock.Protected().Verify("Dispose", Times.Never(), args: true);
}
}
}
diff --git a/README.md b/README.md
index d54a9d9..6dd3693 100644
--- a/README.md
+++ b/README.md
@@ -66,28 +66,50 @@ using (var client = new WebClient()) client.DownloadFile(fileExport.Url, fileExp
```
## Uploading Files
+Uploads to CloudConvert are done via `import/upload` tasks (see the [docs](https://cloudconvert.com/api/v2/import#import-upload-tasks)). This SDK offers a convenient upload method.
-Uploads to CloudConvert are done via `import/upload` tasks (see the [docs](https://cloudconvert.com/api/v2/import#import-upload-tasks)). This SDK offers a convenient upload method:
+First create the upload job with `CreateJobAsync`:
```c#
var job = await _cloudConvertAPI.CreateJobAsync(new JobCreateRequest
{
- Tasks = new
- {
- upload_my_file = new ImportUploadCreateRequest()
- // ...
- }
+ Tasks = new
+ {
+ upload_my_file = new ImportUploadCreateRequest()
+ // ...
+ }
});
var uploadTask = job.Data.Tasks.FirstOrDefault(t => t.Name == "upload_my_file");
-
-var path = @"TestFiles/test.pdf";
-byte[] file = await File.ReadAllBytesAsync(path);
-string fileName = "test.pdf";
-
-await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), file, fileName, uploadTask.Result.Form.Parameters);
```
+Then upload the file the file with `UploadAsync`. This can be done two ways:
+
+1. **Upload using a Stream**
+ The file will be opened and send in chunks to CloudConvert.
+
+ > **Note**
+ > The stream will not be disposed. Make sure to dispose the stream with `stream.Dispose()` or by using the [using-statement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement) as shown in the following example.
+ ```cs
+ string path = @"TestFiles/test.pdf";
+ string fileName = "test.pdf";
+
+ using (System.IO.Stream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), stream, fileName, uploadTask.Result.Form.Parameters);
+ }
+ ```
+
+2. **Upload using a byte-array (`byte[]`)**
+ The entire file will be load into memory and send to CloudConvert.
+ ```cs
+ string path = @"TestFiles/test.pdf";
+ byte[] file = await File.ReadAllBytesAsync(path);
+ string fileName = "test.pdf";
+
+ await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), file, fileName, uploadTask.Result.Form.Parameters);
+ ```
+
## Conversion Specific Options
You can pass any custom options to the task payload via the special `Options` property:
@@ -176,4 +198,4 @@ By default, this runs the integration tests against the Sandbox API with an offi
## Resources
- [API v2 Documentation](https://cloudconvert.com/api/v2)
-- [CloudConvert Blog](https://cloudconvert.com/blog)
\ No newline at end of file
+- [CloudConvert Blog](https://cloudconvert.com/blog)