Skip to content

Commit 74693ff

Browse files
authored
Merge pull request #20 from GerardSmit/feature/upload-stream
Added Stream to UploadAsync
2 parents 581056c + 73d8d91 commit 74693ff

7 files changed

Lines changed: 186 additions & 42 deletions

File tree

CloudConvert.API/CloudConvert.API.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1212
<IsPackable>true</IsPackable>
1313
</PropertyGroup>
14-
14+
15+
<ItemGroup>
16+
<InternalsVisibleTo Include="CloudConvert.Test" />
17+
</ItemGroup>
18+
1519
<ItemGroup>
1620
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" />
1721
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />

CloudConvert.API/CloudConvertAPI.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Globalization;
4+
using System.IO;
45
using System.Linq;
56
using System.Net.Http;
67
using System.Security.Cryptography;
@@ -33,6 +34,7 @@ public interface ICloudConvertAPI
3334
#endregion
3435

3536
Task<string> UploadAsync(string url, byte[] file, string fileName, object parameters);
37+
Task<string> UploadAsync(string url, Stream file, string fileName, object parameters);
3638
bool ValidateWebhookSignatures(string payloadString, string signature, string signingSecret);
3739
string CreateSignedUrl(string baseUrl, string signingSecret, JobCreateRequest job, string cacheKey = null);
3840
}
@@ -50,13 +52,17 @@ public class CloudConvertAPI : ICloudConvertAPI
5052
const string publicUrlSyncApi = "https://sync.api.cloudconvert.com/v2";
5153
static readonly char[] base64Padding = { '=' };
5254

53-
54-
public CloudConvertAPI(string api_key, bool isSandbox = false)
55+
internal CloudConvertAPI(RestHelper restHelper, string api_key, bool isSandbox = false)
5556
{
5657
_apiUrl = isSandbox ? sandboxUrlApi : publicUrlApi;
5758
_apiSyncUrl = isSandbox ? sandboxUrlSyncApi : publicUrlSyncApi;
5859
_api_key += api_key;
59-
_restHelper = new RestHelper();
60+
_restHelper = restHelper;
61+
}
62+
63+
public CloudConvertAPI(string api_key, bool isSandbox = false)
64+
: this(new RestHelper(), api_key, isSandbox)
65+
{
6066
}
6167

6268
public CloudConvertAPI(string url, string api_key)
@@ -82,7 +88,7 @@ private HttpRequestMessage GetRequest(string endpoint, HttpMethod method, object
8288
return request;
8389
}
8490

85-
private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMethod method, byte[] file, string fileName, Dictionary<string, string> parameters = null)
91+
private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMethod method, HttpContent fileContent, string fileName, Dictionary<string, string> parameters = null)
8692
{
8793
var content = new MultipartFormDataContent();
8894
var request = new HttpRequestMessage { RequestUri = new Uri(endpoint), Method = method, };
@@ -95,7 +101,6 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth
95101
}
96102
}
97103

98-
var fileContent = new ByteArrayContent(file);
99104
fileContent.Headers.Add("Content-Disposition", $"form-data; name=\"file\"; filename=\"{ new string(Encoding.UTF8.GetBytes(fileName).Select(b => (char)b).ToArray())}\"");
100105
content.Add(fileContent);
101106

@@ -214,7 +219,9 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth
214219

215220
#endregion
216221

217-
public Task<string> UploadAsync(string url, byte[] file, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest($"{url}", HttpMethod.Post, file, fileName, GetParameters(parameters)));
222+
public Task<string> UploadAsync(string url, byte[] file, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new ByteArrayContent(file), fileName, GetParameters(parameters)));
223+
224+
public Task<string> UploadAsync(string url, Stream stream, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new StreamContent(stream), fileName, GetParameters(parameters)));
218225

219226
public string CreateSignedUrl(string baseUrl, string signingSecret, JobCreateRequest job, string cacheKey = null)
220227
{

CloudConvert.API/RestHelper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ internal RestHelper()
1414
_httpClient.Timeout = System.TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite);
1515
}
1616

17+
internal RestHelper(HttpClient httpClient)
18+
{
19+
_httpClient = httpClient;
20+
}
21+
1722
public async Task<T> RequestAsync<T>(HttpRequestMessage request)
1823
{
1924
var response = await _httpClient.SendAsync(request);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.IO;
3+
using System.Net;
4+
using System.Net.Http;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Moq;
8+
using Moq.Language.Flow;
9+
using Moq.Protected;
10+
11+
namespace CloudConvert.Test.Extensions
12+
{
13+
public static class MockExtensions
14+
{
15+
public static IReturnsResult<HttpMessageHandler> MockResponse(this Mock<HttpMessageHandler> mock, string endpoint, string fileName)
16+
{
17+
return mock.Protected()
18+
.Setup<Task<HttpResponseMessage>>("SendAsync",
19+
ItExpr.Is<HttpRequestMessage>(message => message.RequestUri.AbsolutePath.EndsWith(endpoint)),
20+
ItExpr.IsAny<CancellationToken>())
21+
.ReturnsAsync(new HttpResponseMessage
22+
{
23+
StatusCode = HttpStatusCode.OK,
24+
Content = new StringContent(File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Responses", fileName)))
25+
});
26+
}
27+
28+
public static void VerifyRequest(this Mock<HttpMessageHandler> mock, string endpoint, Times times)
29+
{
30+
mock.Protected()
31+
.Verify("SendAsync",
32+
times,
33+
ItExpr.Is<HttpRequestMessage>(message => message.RequestUri.AbsolutePath.EndsWith(endpoint)),
34+
ItExpr.IsAny<CancellationToken>());
35+
}
36+
}
37+
}

CloudConvert.Test/IntegrationTests.cs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ public void Setup()
2727
_cloudConvertAPI = new CloudConvertAPI(apiKey, true);
2828
}
2929

30-
[Test]
31-
public async Task CreateJob()
30+
[TestCase("stream")]
31+
[TestCase("bytes")]
32+
public async Task CreateJob(string streamingMethod)
3233
{
3334
var job = await _cloudConvertAPI.CreateJobAsync(new JobCreateRequest
3435
{
@@ -48,10 +49,25 @@ public async Task CreateJob()
4849
Assert.IsNotNull(uploadTask);
4950

5051
var path = AppDomain.CurrentDomain.BaseDirectory + @"TestFiles/input.pdf";
51-
byte[] file = File.ReadAllBytes(path);
5252
string fileName = "input.pdf";
5353

54-
var result = await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), file, fileName, uploadTask.Result.Form.Parameters);
54+
string result;
55+
56+
switch (streamingMethod)
57+
{
58+
case "bytes":
59+
{
60+
byte[] file = File.ReadAllBytes(path);
61+
result = await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), file, fileName, uploadTask.Result.Form.Parameters);
62+
break;
63+
}
64+
case "stream":
65+
{
66+
using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
67+
result = await _cloudConvertAPI.UploadAsync(uploadTask.Result.Form.Url.ToString(), stream, fileName, uploadTask.Result.Form.Parameters);
68+
break;
69+
}
70+
}
5571

5672
job = await _cloudConvertAPI.WaitJobAsync(job.Data.Id);
5773

@@ -69,8 +85,9 @@ public async Task CreateJob()
6985
using (var client = new WebClient()) client.DownloadFile(fileExport.Url, fileExport.Filename);
7086
}
7187

72-
[Test]
73-
public async Task CreateTask()
88+
[TestCase("stream")]
89+
[TestCase("bytes")]
90+
public async Task CreateTask(string streamingMethod)
7491
{
7592
// import
7693

@@ -79,10 +96,23 @@ public async Task CreateTask()
7996
var importTask = await _cloudConvertAPI.CreateTaskAsync(ImportUploadCreateRequest.Operation, reqImport);
8097

8198
var path = AppDomain.CurrentDomain.BaseDirectory + @"TestFiles/input.pdf";
82-
byte[] file = File.ReadAllBytes(path);
8399
string fileName = "input.pdf";
84100

85-
await _cloudConvertAPI.UploadAsync(importTask.Data.Result.Form.Url.ToString(), file, fileName, importTask.Data.Result.Form.Parameters);
101+
switch (streamingMethod)
102+
{
103+
case "bytes":
104+
{
105+
byte[] file = File.ReadAllBytes(path);
106+
await _cloudConvertAPI.UploadAsync(importTask.Data.Result.Form.Url.ToString(), file, fileName, importTask.Data.Result.Form.Parameters);
107+
break;
108+
}
109+
case "stream":
110+
{
111+
using (var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
112+
await _cloudConvertAPI.UploadAsync(importTask.Data.Result.Form.Url.ToString(), stream, fileName, importTask.Data.Result.Form.Parameters);
113+
break;
114+
}
115+
}
86116

87117
importTask = await _cloudConvertAPI.WaitTaskAsync(importTask.Data.Id);
88118

CloudConvert.Test/TestTasks.cs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
using System;
22
using System.IO;
3+
using System.Net;
4+
using System.Net.Http;
5+
using System.Threading;
36
using System.Threading.Tasks;
47
using CloudConvert.API;
58
using CloudConvert.API.Models;
69
using CloudConvert.API.Models.Enums;
710
using CloudConvert.API.Models.ImportOperations;
811
using CloudConvert.API.Models.TaskModels;
912
using CloudConvert.API.Models.TaskOperations;
13+
using CloudConvert.Test.Extensions;
1014
using Moq;
15+
using Moq.Protected;
1116
using Newtonsoft.Json;
1217
using NUnit.Framework;
1318

1419
namespace CloudConvert.Test
1520
{
1621
public class TestTasks
1722
{
18-
readonly Mock<ICloudConvertAPI> _cloudConvertAPI = new Mock<ICloudConvertAPI>();
19-
2023
[Test]
2124
public async Task GetAllTasks()
2225
{
@@ -26,10 +29,12 @@ public async Task GetAllTasks()
2629

2730
var path = @"Responses/tasks.json";
2831
string json = File.ReadAllText(path);
29-
_cloudConvertAPI.Setup(cc => cc.GetAllTasksAsync(filter))
32+
33+
var cloudConvertApi = new Mock<ICloudConvertAPI>();
34+
cloudConvertApi.Setup(cc => cc.GetAllTasksAsync(filter))
3035
.ReturnsAsync(JsonConvert.DeserializeObject<ListResponse<TaskResponse>>(json));
3136

32-
var tasks = await _cloudConvertAPI.Object.GetAllTasksAsync(filter);
37+
var tasks = await cloudConvertApi.Object.GetAllTasksAsync(filter);
3338

3439
Assert.IsNotNull(tasks);
3540
Assert.IsTrue(tasks.Data.Count >= 0);
@@ -64,10 +69,12 @@ public async Task CreateTask()
6469

6570
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/task_created.json";
6671
string json = File.ReadAllText(path);
67-
_cloudConvertAPI.Setup(cc => cc.CreateTaskAsync(ConvertCreateRequest.Operation, req))
72+
73+
var cloudConvertApi = new Mock<ICloudConvertAPI>();
74+
cloudConvertApi.Setup(cc => cc.CreateTaskAsync(ConvertCreateRequest.Operation, req))
6875
.ReturnsAsync(JsonConvert.DeserializeObject<Response<TaskResponse>>(json));
6976

70-
var task = await _cloudConvertAPI.Object.CreateTaskAsync(ConvertCreateRequest.Operation, req);
77+
var task = await cloudConvertApi.Object.CreateTaskAsync(ConvertCreateRequest.Operation, req);
7178

7279
Assert.IsNotNull(task);
7380
Assert.IsTrue(task.Data.Status == API.Models.Enums.TaskStatus.waiting);
@@ -80,6 +87,8 @@ public async Task GetTask()
8087

8188
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/task.json";
8289
string json = File.ReadAllText(path);
90+
91+
var _cloudConvertAPI = new Mock<ICloudConvertAPI>();
8392
_cloudConvertAPI.Setup(cc => cc.GetTaskAsync(id, null))
8493
.ReturnsAsync(JsonConvert.DeserializeObject<Response<TaskResponse>>(json));
8594

@@ -96,10 +105,12 @@ public async Task WaitTask()
96105

97106
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/task.json";
98107
string json = File.ReadAllText(path);
99-
_cloudConvertAPI.Setup(cc => cc.WaitTaskAsync(id))
108+
109+
var cloudConvertApi = new Mock<ICloudConvertAPI>();
110+
cloudConvertApi.Setup(cc => cc.WaitTaskAsync(id))
100111
.ReturnsAsync(JsonConvert.DeserializeObject<Response<TaskResponse>>(json));
101112

102-
var task = await _cloudConvertAPI.Object.WaitTaskAsync(id);
113+
var task = await cloudConvertApi.Object.WaitTaskAsync(id);
103114

104115
Assert.IsNotNull(task);
105116
Assert.IsTrue(task.Data.Operation == "convert");
@@ -111,32 +122,60 @@ public async Task DeleteTask()
111122
{
112123
string id = "9de1a620-952c-4482-9d44-681ae28d72a1";
113124

114-
_cloudConvertAPI.Setup(cc => cc.DeleteTaskAsync(id));
125+
var cloudConvertApi = new Mock<ICloudConvertAPI>();
126+
cloudConvertApi.Setup(cc => cc.DeleteTaskAsync(id));
115127

116-
await _cloudConvertAPI.Object.DeleteTaskAsync("c8a8da46-3758-45bf-b983-2510e3170acb");
128+
await cloudConvertApi.Object.DeleteTaskAsync("c8a8da46-3758-45bf-b983-2510e3170acb");
117129
}
118130

119131
[Test]
120132
public async Task Upload()
121133
{
134+
var cloudConvertApi = new Mock<ICloudConvertAPI>();
122135
var req = new ImportUploadCreateRequest();
123136

124137
var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/upload_task_created.json";
125138
string json = File.ReadAllText(path);
126-
_cloudConvertAPI.Setup(cc => cc.CreateTaskAsync(ImportUploadCreateRequest.Operation, req))
139+
cloudConvertApi.Setup(cc => cc.CreateTaskAsync(ImportUploadCreateRequest.Operation, req))
127140
.ReturnsAsync(JsonConvert.DeserializeObject<Response<TaskResponse>>(json));
128141

129-
var task = await _cloudConvertAPI.Object.CreateTaskAsync(ImportUploadCreateRequest.Operation, req);
142+
var task = await cloudConvertApi.Object.CreateTaskAsync(ImportUploadCreateRequest.Operation, req);
130143

131144
Assert.IsNotNull(task);
132145

133146
var pathFile = AppDomain.CurrentDomain.BaseDirectory + @"TestFiles/input.pdf";
134147
byte[] file = File.ReadAllBytes(pathFile);
135148
string fileName = "input.pdf";
136149

137-
_cloudConvertAPI.Setup(cc => cc.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters));
150+
cloudConvertApi.Setup(cc => cc.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters));
151+
152+
await cloudConvertApi.Object.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters);
153+
}
154+
155+
[Test]
156+
public async Task UploadStream()
157+
{
158+
var httpMessageHandlerMock = new Mock<HttpMessageHandler>();
159+
httpMessageHandlerMock.MockResponse("/import/upload", "upload_task_created.json");
160+
httpMessageHandlerMock.MockResponse("/tasks", "tasks.json");
161+
162+
var httpClient = new HttpClient(httpMessageHandlerMock.Object);
163+
var restHelper = new RestHelper(httpClient);
164+
var req = new ImportUploadCreateRequest();
165+
var cloudConvertApi = new CloudConvertAPI(restHelper, "API_KEY");
166+
167+
var task = await cloudConvertApi.CreateTaskAsync(ImportUploadCreateRequest.Operation, req);
168+
169+
Assert.IsNotNull(task);
170+
httpMessageHandlerMock.VerifyRequest("/import/upload", Times.Once());
171+
172+
var streamMock = new Mock<Stream>();
173+
var fileName = "input.pdf";
174+
175+
await cloudConvertApi.UploadAsync(task.Data.Result.Form.Url.ToString(), streamMock.Object, fileName, task.Data.Result.Form.Parameters);
138176

139-
await _cloudConvertAPI.Object.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters);
177+
httpMessageHandlerMock.VerifyRequest("/tasks", Times.Once());
178+
streamMock.Protected().Verify("Dispose", Times.Never(), args: true);
140179
}
141180
}
142181
}

0 commit comments

Comments
 (0)