Skip to content

Commit d5e3de8

Browse files
committed
Various improvements
1 parent 1b085a6 commit d5e3de8

9 files changed

Lines changed: 289 additions & 109 deletions

CloudConvert.API/CloudConvert.API.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
<ItemGroup>
2020
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" />
21+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
22+
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
2123
<PackageReference Include="System.Net.Http" Version="4.3.4" />
2224
</ItemGroup>
2325

CloudConvert.API/CloudConvertAPI.cs

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
using System.Net.Http;
66
using System.Security.Cryptography;
77
using System.Text;
8-
using System.Threading.Tasks;
98
using System.Text.Json;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using CloudConvert.API.Models;
1012
using CloudConvert.API.Models.JobModels;
1113
using CloudConvert.API.Models.TaskModels;
12-
using CloudConvert.API.Models;
13-
using System.Threading;
1414

1515
namespace CloudConvert.API
1616
{
@@ -45,17 +45,17 @@ public class CloudConvertAPI : ICloudConvertAPI
4545

4646
readonly RestHelper _restHelper;
4747
readonly string _api_key = "Bearer ";
48-
const string sandboxUrlApi = "https://api.sandbox.cloudconvert.com/v2";
49-
const string publicUrlApi = "https://api.cloudconvert.com/v2";
50-
const string sandboxUrlSyncApi = "https://sync.api.sandbox.cloudconvert.com/v2";
51-
const string publicUrlSyncApi = "https://sync.api.cloudconvert.com/v2";
48+
private const string SandboxUrlApi = "https://api.sandbox.cloudconvert.com/v2";
49+
private const string PublicUrlApi = "https://api.cloudconvert.com/v2";
50+
private const string SandboxUrlSyncApi = "https://sync.api.sandbox.cloudconvert.com/v2";
51+
private const string PublicUrlSyncApi = "https://sync.api.cloudconvert.com/v2";
5252
static readonly char[] base64Padding = { '=' };
5353

5454
internal CloudConvertAPI(RestHelper restHelper, string api_key, bool isSandbox = false)
5555
{
56-
_apiUrl = isSandbox ? sandboxUrlApi : publicUrlApi;
57-
_apiSyncUrl = isSandbox ? sandboxUrlSyncApi : publicUrlSyncApi;
58-
_api_key += api_key;
56+
_apiUrl = isSandbox ? SandboxUrlApi : PublicUrlApi;
57+
_apiSyncUrl = isSandbox ? SandboxUrlSyncApi : PublicUrlSyncApi;
58+
_api_key = $"Bearer {api_key}";
5959
_restHelper = restHelper;
6060
}
6161

@@ -67,15 +67,15 @@ public CloudConvertAPI(string api_key, bool isSandbox = false)
6767
public CloudConvertAPI(string url, string api_key)
6868
{
6969
_apiUrl = url;
70-
_api_key += api_key;
70+
_api_key = $"Bearer {api_key}";
7171
_restHelper = new RestHelper();
7272
}
7373

7474
private HttpRequestMessage GetRequest(string endpoint, HttpMethod method, object model = null)
7575
{
7676
var request = new HttpRequestMessage { RequestUri = new Uri(endpoint), Method = method };
7777

78-
if (model != null)
78+
if (model is not null)
7979
{
8080
var content = new StringContent(JsonSerializer.Serialize(model, DefaultJsonSerializerOptions.SerializerOptions), Encoding.UTF8, "application/json");
8181
request.Content = content;
@@ -93,7 +93,7 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth
9393
var content = new MultipartFormDataContent();
9494
var request = new HttpRequestMessage { RequestUri = new Uri(endpoint), Method = method, };
9595

96-
if (parameters != null)
96+
if (parameters is not null)
9797
{
9898
foreach (var param in parameters)
9999
{
@@ -119,7 +119,7 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth
119119
/// <returns>
120120
/// The list of jobs. You can find details about the job model response in the documentation about the show jobs endpoint.
121121
/// </returns>
122-
public Task<ListResponse<JobResponse>> GetAllJobsAsync(JobListFilter jobFilter, CancellationToken cancellationToken = default)
122+
public Task<ListResponse<JobResponse>> GetAllJobsAsync(JobListFilter jobFilter, CancellationToken cancellationToken = default)
123123
=> _restHelper.RequestAsync<ListResponse<JobResponse>>(GetRequest($"{_apiUrl}/jobs?filter[status]={jobFilter.Status}&filter[tag]={jobFilter.Tag}&include={jobFilter.Include}&per_page={jobFilter.PerPage}&page={jobFilter.Page}", HttpMethod.Get), cancellationToken);
124124

125125
/// <summary>
@@ -130,7 +130,7 @@ public Task<ListResponse<JobResponse>> GetAllJobsAsync(JobListFilter jobFilter,
130130
/// <returns>
131131
/// The created job. You can find details about the job model response in the documentation about the show jobs endpoint.
132132
/// </returns>
133-
public Task<Response<JobResponse>> CreateJobAsync(JobCreateRequest model, CancellationToken cancellationToken = default)
133+
public Task<Response<JobResponse>> CreateJobAsync(JobCreateRequest model, CancellationToken cancellationToken = default)
134134
=> _restHelper.RequestAsync<Response<JobResponse>>(GetRequest($"{_apiUrl}/jobs", HttpMethod.Post, model), cancellationToken);
135135

136136
/// <summary>
@@ -195,7 +195,7 @@ public Task<ListResponse<TaskResponse>> GetAllTasksAsync(TaskListFilter taskFilt
195195
/// <returns>
196196
/// The created task. You can find details about the task model response in the documentation about the show tasks endpoint.
197197
/// </returns>
198-
public Task<Response<TaskResponse>> CreateTaskAsync<T>(string operation, T model, CancellationToken cancellationToken = default)
198+
public Task<Response<TaskResponse>> CreateTaskAsync<T>(string operation, T model, CancellationToken cancellationToken = default)
199199
=> _restHelper.RequestAsync<Response<TaskResponse>>(GetRequest($"{_apiUrl}/{operation}", HttpMethod.Post, model), cancellationToken);
200200

201201
/// <summary>
@@ -205,7 +205,7 @@ public Task<Response<TaskResponse>> CreateTaskAsync<T>(string operation, T model
205205
/// <param name="include"></param>
206206
/// <param name="cancellationToken"></param>
207207
/// <returns></returns>
208-
public Task<Response<TaskResponse>> GetTaskAsync(string id, string include = null, CancellationToken cancellationToken = default)
208+
public Task<Response<TaskResponse>> GetTaskAsync(string id, string include = null, CancellationToken cancellationToken = default)
209209
=> _restHelper.RequestAsync<Response<TaskResponse>>(GetRequest($"{_apiUrl}/tasks/{id}?include={include}", HttpMethod.Get), cancellationToken);
210210

211211
/// <summary>
@@ -222,7 +222,7 @@ public Task<Response<TaskResponse>> GetTaskAsync(string id, string include = nul
222222
/// <returns>
223223
/// The finished or failed task. You can find details about the task model response in the documentation about the show tasks endpoint.
224224
/// </returns>
225-
public Task<Response<TaskResponse>> WaitTaskAsync(string id, CancellationToken cancellationToken = default)
225+
public Task<Response<TaskResponse>> WaitTaskAsync(string id, CancellationToken cancellationToken = default)
226226
=> _restHelper.RequestAsync<Response<TaskResponse>>(GetRequest($"{_apiSyncUrl}/tasks/{id}", HttpMethod.Get), cancellationToken);
227227

228228
/// <summary>
@@ -234,34 +234,38 @@ public Task<Response<TaskResponse>> WaitTaskAsync(string id, CancellationToken c
234234
/// <returns>
235235
/// An empty response with HTTP Code 204.
236236
/// </returns>
237-
public Task DeleteTaskAsync(string id, CancellationToken cancellationToken = default)
237+
public Task DeleteTaskAsync(string id, CancellationToken cancellationToken = default)
238238
=> _restHelper.RequestAsync<object>(GetRequest($"{_apiUrl}/tasks/{id}", HttpMethod.Delete), cancellationToken);
239239

240240
#endregion
241241

242-
public Task<string> UploadAsync(string url, byte[] file, string fileName, object parameters, CancellationToken cancellationToken)
242+
public Task<string> UploadAsync(string url, byte[] file, string fileName, object parameters, CancellationToken cancellationToken)
243243
=> _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new ByteArrayContent(file), fileName, GetParameters(parameters)), cancellationToken);
244244

245245
public Task<string> UploadAsync(string url, Stream stream, string fileName, object parameters, CancellationToken cancellationToken = default)
246246
=> _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new StreamContent(stream), fileName, GetParameters(parameters)), cancellationToken);
247247

248248
public string CreateSignedUrl(string baseUrl, string signingSecret, JobCreateRequest job, string cacheKey = null)
249249
{
250-
string url = baseUrl;
251-
string jobJson = JsonSerializer.Serialize(job, DefaultJsonSerializerOptions.SerializerOptions);
252-
string base64Job = System.Convert.ToBase64String(Encoding.ASCII.GetBytes(jobJson)).TrimEnd(base64Padding).Replace('+', '-').Replace('/', '_');
250+
var jobJson = JsonSerializer.Serialize(job, DefaultJsonSerializerOptions.SerializerOptions);
251+
var base64Job = Convert.ToBase64String(Encoding.ASCII.GetBytes(jobJson))
252+
.TrimEnd(base64Padding)
253+
.Replace('+', '-')
254+
.Replace('/', '_');
253255

254-
url += "?job=" + base64Job;
256+
var builder = new StringBuilder(baseUrl)
257+
.Append("?job=")
258+
.Append(base64Job);
255259

256-
if(cacheKey != null) {
257-
url += "&cache_key=" + cacheKey;
260+
if (cacheKey is not null)
261+
{
262+
builder.Append("&cache_key=").Append(cacheKey);
258263
}
259264

260-
string signature = HashHMAC(signingSecret, url);
265+
var urlWithoutSignature = builder.ToString();
266+
var signature = HashHMAC(signingSecret, urlWithoutSignature);
261267

262-
url += "&s=" + signature;
263-
264-
return url;
268+
return builder.Append("&s=").Append(signature).ToString();
265269
}
266270

267271
public bool ValidateWebhookSignatures(string payloadString, string signature, string signingSecret)
@@ -271,11 +275,13 @@ public bool ValidateWebhookSignatures(string payloadString, string signature, st
271275
return hashHMAC == signature;
272276
}
273277

274-
private string HashHMAC(string key, string message)
278+
private static string HashHMAC(string key, string message)
275279
{
276-
byte[] hash = new HMACSHA256(Encoding.UTF8.GetBytes(key)).ComputeHash(new UTF8Encoding().GetBytes(message));
280+
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
281+
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
277282
return BitConverter.ToString(hash).Replace("-", "").ToLower();
278283
}
284+
279285
private Dictionary<string, string> GetParameters(object parameters)
280286
{
281287
var dictionaryParameters = new Dictionary<string, string>();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace CloudConvert.API.Extensions
2+
{
3+
/// <summary>
4+
/// Configuration options for the CloudConvert API client.
5+
/// </summary>
6+
public class CloudConvertOptions
7+
{
8+
/// <summary>
9+
/// The CloudConvert API key used to authenticate requests.
10+
/// </summary>
11+
public string ApiKey { get; set; }
12+
13+
/// <summary>
14+
/// Whether to use the CloudConvert sandbox environment.
15+
/// Use <c>true</c> during development and testing to avoid consuming real credits.
16+
/// Defaults to <c>false</c>.
17+
/// </summary>
18+
public bool IsSandbox { get; set; } = false;
19+
}
20+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Threading;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace CloudConvert.API.Extensions
7+
{
8+
/// <summary>
9+
/// Extension methods for registering CloudConvert API services with the dependency injection container.
10+
/// </summary>
11+
public static class ServiceCollectionExtensions
12+
{
13+
/// <summary>
14+
/// Registers the CloudConvert API client and its dependencies with the dependency injection container.
15+
/// </summary>
16+
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
17+
/// <param name="configure">A delegate to configure the <see cref="CloudConvertOptions"/>.</param>
18+
/// <returns>The <see cref="IServiceCollection"/> so that calls can be chained.</returns>
19+
/// <exception cref="ArgumentException">Thrown when <see cref="CloudConvertOptions.ApiKey"/> is not provided.</exception>
20+
/// <remarks>
21+
/// Register the CloudConvert API client in your DI container:
22+
/// <code>
23+
/// services.AddCloudConvertAPI(options =>
24+
/// {
25+
/// options.ApiKey = "your_api_key";
26+
/// options.IsSandbox = false;
27+
/// });
28+
/// </code>
29+
/// </remarks>
30+
public static IServiceCollection AddCloudConvertAPI(
31+
this IServiceCollection services,
32+
Action<CloudConvertOptions> configure)
33+
{
34+
var options = new CloudConvertOptions();
35+
configure(options);
36+
37+
if (string.IsNullOrWhiteSpace(options.ApiKey))
38+
{
39+
throw new ArgumentException("ApiKey is required", nameof(configure));
40+
}
41+
42+
services.AddHttpClient<ICloudConvertAPI, CloudConvertAPI>(client =>
43+
{
44+
client.Timeout = Timeout.InfiniteTimeSpan;
45+
})
46+
.ConfigurePrimaryHttpMessageHandler(() => new WebApiHandler());
47+
48+
services.AddSingleton<ICloudConvertAPI>(sp =>
49+
{
50+
var httpClient = sp.GetRequiredService<IHttpClientFactory>()
51+
.CreateClient(nameof(CloudConvertAPI));
52+
53+
return new CloudConvertAPI(new RestHelper(httpClient), options.ApiKey, options.IsSandbox);
54+
});
55+
56+
return services;
57+
}
58+
}
59+
}

CloudConvert.API/Extensions/StringExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static string TrimLengthWithEllipsis([NotNull] this string str, int maxLe
1515
return str;
1616
}
1717

18-
return str.Substring(0, maxLength) + "...";
18+
return string.Concat(str.AsSpan(0, maxLength), "...");
1919
}
2020

2121
public static string GetEnumDescription<TEnum>(this TEnum source) where TEnum : struct, Enum
@@ -39,5 +39,5 @@ public static string GetEnumDescription<TEnum>(this TEnum source) where TEnum :
3939
return attributes[0].Description;
4040
}
4141

42-
}
42+
}
4343
}

CloudConvert.API/RestHelper.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class RestHelper
1111

1212
internal RestHelper()
1313
{
14-
_httpClient = new HttpClient(new WebApiHandler(true));
14+
_httpClient = new HttpClient(new WebApiHandler());
1515
_httpClient.Timeout = System.TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite);
1616
}
1717

@@ -20,7 +20,7 @@ internal RestHelper(HttpClient httpClient)
2020
_httpClient = httpClient;
2121
}
2222

23-
public async Task<T> RequestAsync<T>(HttpRequestMessage request, CancellationToken cancellationToken)
23+
internal async Task<T> RequestAsync<T>(HttpRequestMessage request, CancellationToken cancellationToken)
2424
{
2525
var response = await _httpClient.SendAsync(request, cancellationToken);
2626
var responseRaw = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -29,13 +29,13 @@ public async Task<T> RequestAsync<T>(HttpRequestMessage request, CancellationTok
2929
// System.Text.Json throws when trying to deserialize an empty string
3030
if (string.IsNullOrWhiteSpace(responseRaw) || response.StatusCode == System.Net.HttpStatusCode.NoContent)
3131
{
32-
return default(T);
32+
return default;
3333
}
3434

3535
return JsonSerializer.Deserialize<T>(responseRaw, DefaultJsonSerializerOptions.SerializerOptions);
3636
}
3737

38-
public async Task<string> RequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
38+
internal async Task<string> RequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
3939
{
4040
var response = await _httpClient.SendAsync(request, cancellationToken);
4141
return await response.Content.ReadAsStringAsync(cancellationToken);

CloudConvert.API/WebApiHandler.cs

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,14 @@
66

77
namespace CloudConvert.API
88
{
9-
internal sealed class WebApiHandler : HttpClientHandler
9+
internal sealed class WebApiHandler() : HttpClientHandler
1010
{
11-
12-
private readonly bool _loggingEnabled;
13-
14-
public WebApiHandler(bool loggingEnabled)
15-
{
16-
_loggingEnabled = loggingEnabled;
17-
}
18-
1911
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
2012
{
21-
22-
bool writeLog = _loggingEnabled;
23-
2413
try
2514
{
26-
if (writeLog)
27-
{
28-
var requestString = request.Content != null ? await request.Content.ReadAsStringAsync(cancellationToken) : string.Empty;
29-
}
30-
3115
var response = await base.SendAsync(request, cancellationToken);
3216

33-
if (writeLog)
34-
{
35-
string responseString = (await response.Content.ReadAsStringAsync(cancellationToken)).TrimLengthWithEllipsis(20000);
36-
}
37-
3817
if ((int)response.StatusCode >= 400)
3918
{
4019
throw new WebApiException((await response.Content.ReadAsStringAsync(cancellationToken)).TrimLengthWithEllipsis(20000));

0 commit comments

Comments
 (0)