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)