Skip to content

Commit aa503c7

Browse files
committed
feat: add upload temp file api
1 parent ec4b7b2 commit aa503c7

18 files changed

+387
-14
lines changed

src/Cnblogs.DashScope.Core/DashScopeClientCore.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,67 @@ public async Task<SpeechSynthesizerSocketSession> CreateSpeechSynthesizerSocketS
287287
return new SpeechSynthesizerSocketSession(socket, modelId);
288288
}
289289

290+
/// <inheritdoc />
291+
public Task<DashScopeTemporaryUploadPolicy?> GetTemporaryUploadPolicyAsync(
292+
string modelId,
293+
CancellationToken cancellationToken = default)
294+
{
295+
var request = BuildRequest(HttpMethod.Get, ApiLinks.Uploads + $"?action=getPolicy&model={modelId}");
296+
return SendAsync<DashScopeTemporaryUploadPolicy>(request, cancellationToken);
297+
}
298+
299+
/// <inheritdoc />
300+
public async Task<string> UploadTemporaryFileAsync(
301+
string modelId,
302+
Stream fileStream,
303+
string filename,
304+
CancellationToken cancellationToken = default)
305+
{
306+
var policy = await GetTemporaryUploadPolicyAsync(modelId, cancellationToken);
307+
if (policy is null)
308+
{
309+
throw new DashScopeException(
310+
"/api/v1/upload",
311+
200,
312+
null,
313+
"GET /api/v1/upload returns empty response, check your connection");
314+
}
315+
316+
return await UploadTemporaryFileAsync(fileStream, filename, policy);
317+
}
318+
319+
/// <inheritdoc />
320+
public async Task<string> UploadTemporaryFileAsync(
321+
Stream fileStream,
322+
string filename,
323+
DashScopeTemporaryUploadPolicy policy)
324+
{
325+
var filenameStartsWithSlash = filename.StartsWith('/');
326+
var key = filenameStartsWithSlash
327+
? $"{policy.Data.UploadDir}{filename}"
328+
: $"{policy.Data.UploadDir}/{filename}";
329+
330+
var form = new MultipartFormDataContent();
331+
form.Add(new StringContent(policy.Data.OssAccessKeyId), "OSSAccessKeyId");
332+
form.Add(new StringContent(policy.Data.Policy), "policy");
333+
form.Add(new StringContent(policy.Data.Signature), "Signature");
334+
form.Add(new StringContent(key), "key");
335+
form.Add(new StringContent(policy.Data.XOssObjectAcl), "x-oss-object-acl");
336+
form.Add(new StringContent(policy.Data.XOssForbidOverwrite), "x-oss-forbid-overwrite");
337+
form.Add(new StreamContent(fileStream), "file");
338+
var response = await _httpClient.PostAsync(policy.Data.UploadHost, form);
339+
if (response.IsSuccessStatusCode)
340+
{
341+
return $"oss://{key}";
342+
}
343+
344+
throw new DashScopeException(
345+
policy.Data.UploadHost,
346+
(int)response.StatusCode,
347+
null,
348+
await response.Content.ReadAsStringAsync());
349+
}
350+
290351
private static HttpRequestMessage BuildSseRequest<TPayload>(HttpMethod method, string url, TPayload payload)
291352
where TPayload : class
292353
{
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Cnblogs.DashScope.Core;
2+
3+
/// <summary>
4+
/// Represents one response of get upload policy api call.
5+
/// </summary>
6+
/// <param name="RequestId">Unique id for current request.</param>
7+
/// <param name="Data">The grant data.</param>
8+
public record DashScopeTemporaryUploadPolicy(string RequestId, DashScopeTemporaryUploadPolicyData Data);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace Cnblogs.DashScope.Core;
2+
3+
/// <summary>
4+
/// Represent data of oss temp file upload grant.
5+
/// </summary>
6+
/// <param name="Policy">Upload policy.</param>
7+
/// <param name="Signature">Upload signature.</param>
8+
/// <param name="UploadDir">Directory that granted to upload.</param>
9+
/// <param name="UploadHost">Hostname that upload to.</param>
10+
/// <param name="ExpireInSeconds">Grant's expiration.</param>
11+
/// <param name="MaxFileSizeMb">Maximum size of file.</param>
12+
/// <param name="CapacityLimitMb">Total upload limit of account.</param>
13+
/// <param name="OssAccessKeyId">Key used to upload.</param>
14+
/// <param name="XOssObjectAcl">Access of the uploaded file.</param>
15+
/// <param name="XOssForbidOverwrite">Can file be overwritten by another file with same name.</param>
16+
public record DashScopeTemporaryUploadPolicyData(
17+
string Policy,
18+
string Signature,
19+
string UploadDir,
20+
string UploadHost,
21+
int ExpireInSeconds,
22+
int MaxFileSizeMb,
23+
int CapacityLimitMb,
24+
string OssAccessKeyId,
25+
string XOssObjectAcl,
26+
string XOssForbidOverwrite);

src/Cnblogs.DashScope.Core/IDashScopeClient.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,42 @@ public Task<DashScopeDeleteFileResult> DeleteFileAsync(
257257
public Task<SpeechSynthesizerSocketSession> CreateSpeechSynthesizerSocketSessionAsync(
258258
string modelId,
259259
CancellationToken cancellationToken = default);
260+
261+
/// <summary>
262+
/// Get a temporary upload grant for <see cref="modelId"/> to access.
263+
/// </summary>
264+
/// <param name="modelId">The name of the model.</param>
265+
/// <param name="cancellationToken"></param>
266+
/// <returns></returns>
267+
public Task<DashScopeTemporaryUploadPolicy?> GetTemporaryUploadPolicyAsync(
268+
string modelId,
269+
CancellationToken cancellationToken = default);
270+
271+
/// <summary>
272+
/// Upload file that granted.
273+
/// </summary>
274+
/// <param name="modelId">The model's id that can access the file.</param>
275+
/// <param name="fileStream">The file data.</param>
276+
/// <param name="filename">The name of the file.</param>
277+
/// <param name="cancellationToken"></param>
278+
/// <returns>Oss url of the file.</returns>
279+
/// <exception cref="DashScopeException">Throws if response code is not 200.</exception>
280+
public Task<string> UploadTemporaryFileAsync(
281+
string modelId,
282+
Stream fileStream,
283+
string filename,
284+
CancellationToken cancellationToken = default);
285+
286+
/// <summary>
287+
/// Upload file that granted.
288+
/// </summary>
289+
/// <param name="fileStream">The file data.</param>
290+
/// <param name="filename"></param>
291+
/// <param name="policy">The grant info.</param>
292+
/// <returns></returns>
293+
/// <exception cref="DashScopeException">Throws if response code is not 200.</exception>
294+
public Task<string> UploadTemporaryFileAsync(
295+
Stream fileStream,
296+
string filename,
297+
DashScopeTemporaryUploadPolicy policy);
260298
}

src/Cnblogs.DashScope.Core/Internals/ApiLinks.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ internal static class ApiLinks
99
public const string ImageGeneration = "services/aigc/image-generation/generation";
1010
public const string BackgroundGeneration = "services/aigc/background-generation/generation/";
1111
public const string Tasks = "tasks/";
12+
public const string Uploads = "uploads/";
1213
public const string Tokenizer = "tokenizer";
1314
public const string Files = "/compatible-mode/v1/files";
1415
public static string Application(string applicationId) => $"apps/{applicationId}/completion";
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Net;
2+
using Cnblogs.DashScope.Core;
3+
using Cnblogs.DashScope.Tests.Shared.Utils;
4+
using NSubstitute;
5+
6+
namespace Cnblogs.DashScope.Sdk.UnitTests;
7+
8+
public class UploadSerializationTests
9+
{
10+
[Fact]
11+
public async Task Upload_GetPolicy_SuccessAsync()
12+
{
13+
// Arrange
14+
const bool sse = false;
15+
var testCase = Snapshots.Upload.GetPolicyNoSse;
16+
var (client, handler) = await Sut.GetTestClientAsync(sse, testCase);
17+
18+
// Act
19+
var task = await client.GetTemporaryUploadPolicyAsync("qwen-vl-plus");
20+
21+
// Assert
22+
handler.Received().MockSend(
23+
Arg.Is<HttpRequestMessage>(r => r.RequestUri!.PathAndQuery.Contains("model=qwen-vl-plus")),
24+
Arg.Any<CancellationToken>());
25+
Assert.Equivalent(testCase.ResponseModel, task);
26+
}
27+
28+
[Fact]
29+
public async Task Upload_SubmitFileForm_SuccessAsync()
30+
{
31+
// Arrange
32+
var file = Snapshots.File.TestImage;
33+
var policy = Snapshots.Upload.GetPolicyNoSse.ResponseModel;
34+
var testCase = Snapshots.Upload.UploadTemporaryFileNoSse;
35+
var (client, handler) = await Sut.GetTestClientAsync(new HttpResponseMessage(HttpStatusCode.NoContent));
36+
37+
// Act
38+
var uri = await client.UploadTemporaryFileAsync(file.OpenRead(), file.Name, policy);
39+
var expectedRequestForm = await testCase.GetRequestFormAsync(false);
40+
41+
// Assert
42+
handler.Received().MockSend(
43+
Arg.Is<HttpRequestMessage>(r
44+
=> r.RequestUri == new Uri(policy.Data.UploadHost)
45+
&& Checkers.CheckFormContent(r, expectedRequestForm)),
46+
Arg.Any<CancellationToken>());
47+
Assert.Equal($"oss://{policy.Data.UploadDir}/{file.Name}", uri);
48+
}
49+
50+
[Fact]
51+
public async Task Upload_GetOssLinkDirectly_SuccessAsync()
52+
{
53+
// Arrange
54+
const bool sse = false;
55+
var file = Snapshots.File.TestImage;
56+
var policyCase = Snapshots.Upload.GetPolicyNoSse;
57+
var testCase = Snapshots.Upload.UploadTemporaryFileNoSse;
58+
var (client, handler) = await Sut.GetTestClientAsync(sse, policyCase);
59+
60+
// Act
61+
var uri = await client.UploadTemporaryFileAsync("qwen-vl-plus", file.OpenRead(), file.Name);
62+
var expectedRequestForm = await testCase.GetRequestFormAsync(false);
63+
64+
// Assert
65+
handler.Received().MockSend(
66+
Arg.Is<HttpRequestMessage>(r
67+
=> r.RequestUri == new Uri(policyCase.ResponseModel.Data.UploadHost)
68+
&& Checkers.CheckFormContent(r, expectedRequestForm)),
69+
Arg.Any<CancellationToken>());
70+
Assert.Equal($"oss://{policyCase.ResponseModel.Data.UploadDir}/{file.Name}", uri);
71+
}
72+
73+
[Fact]
74+
public async Task Upload_GetPolicyFailed_ThrowsAsync()
75+
{
76+
// Arrange
77+
const bool sse = false;
78+
var file = Snapshots.File.TestImage;
79+
var policyCase = Snapshots.Upload.GetPolicyNoSse;
80+
var testCase = Snapshots.Upload.UploadTemporaryFileNoSse;
81+
var (client, handler) = await Sut.GetTestClientAsync(
82+
new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("null") });
83+
84+
// Act
85+
var act = async () => await client.UploadTemporaryFileAsync("qwen-vl-plus", file.OpenRead(), file.Name);
86+
87+
// Assert
88+
await Assert.ThrowsAsync<DashScopeException>(act);
89+
}
90+
}

test/Cnblogs.DashScope.Tests.Shared/Cnblogs.DashScope.Tests.Shared.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
1112
<PackageReference Include="NSubstitute" Version="5.3.0"/>
1213
<PackageReference Include="System.Linq.Async" Version="6.0.3" />
1314
<PackageReference Include="JsonSchema.Net.Generation" Version="4.6.0" />
@@ -22,6 +23,9 @@
2223
<None Update="RawHttpData\*.*">
2324
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
2425
</None>
26+
<None Update="RawHttpData\Lenna.jpg">
27+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
28+
</None>
2529
</ItemGroup>
2630

2731
</Project>
84 KB
Loading

test/Cnblogs.DashScope.Tests.Shared/RawHttpData/get-task-unknown-nosse.request.header.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ Cache-Control: no-cache
44
Host: dashscope.aliyuncs.com
55
Accept-Encoding: gzip, deflate, br
66
Connection: keep-alive
7+
\
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
GET /api/v1/uploads?action=getPolicy&model=qwen-vl-plus HTTP/1.1
2+
Accept: */*
3+
Cache-Control: no-cache
4+
Host: dashscope.aliyuncs.com
5+
Accept-Encoding: gzip, deflate, br
6+
Connection: keep-alive

0 commit comments

Comments
 (0)