Skip to content

Commit 4db2990

Browse files
authored
Merge pull request #123 from cnblogs/support-temp-oss-upload
feat: support temp oss upload
2 parents 25d56c7 + 4b6c9b6 commit 4db2990

31 files changed

+587
-26
lines changed

sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</ItemGroup>
2424

2525
<ItemGroup>
26-
<PackageReference Include="Microsoft.Extensions.AI" Version="9.9.0" />
26+
<PackageReference Include="Microsoft.Extensions.AI" Version="9.9.1" />
2727
</ItemGroup>
2828

2929
</Project>

sample/Cnblogs.DashScope.Sample/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ async Task ChatStreamAsync()
190190

191191
async Task ChatWithImageAsync()
192192
{
193-
var image = await File.ReadAllBytesAsync("Lenna.jpg");
193+
var image = File.OpenRead("Lenna.jpg");
194+
var ossLink = await dashScopeClient.UploadTemporaryFileAsync("qvq-plus", image, "Lenna.jpg");
195+
Console.WriteLine($"Successfully uploaded temp file: {ossLink}");
194196
var response = dashScopeClient.GetMultimodalGenerationStreamAsync(
195197
new ModelRequest<MultimodalInput, IMultimodalParameters>()
196198
{
@@ -201,7 +203,7 @@ async Task ChatWithImageAsync()
201203
[
202204
MultimodalMessage.User(
203205
[
204-
MultimodalMessageContent.ImageContent(image, "image/jpeg"),
206+
MultimodalMessageContent.ImageContent(ossLink),
205207
MultimodalMessageContent.TextContent("她是谁?")
206208
])
207209
]

src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
</ItemGroup>
1212

1313
<ItemGroup>
14-
<PackageReference Include="JsonSchema.Net.Generation" Version="5.0.4" />
15-
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.0" />
14+
<PackageReference Include="JsonSchema.Net.Generation" Version="4.6.0" />
15+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.1" />
1616
</ItemGroup>
1717

1818
</Project>

src/Cnblogs.DashScope.Core/Cnblogs.DashScope.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<ItemGroup>
1515
<PackageReference Include="Microsoft.DeepDev.TokenizerLib" Version="1.3.3" />
16-
<PackageReference Include="System.Text.Json" Version="8.0.5" />
16+
<PackageReference Include="System.Text.Json" Version="8.0.6" />
1717
</ItemGroup>
1818

1919
</Project>

src/Cnblogs.DashScope.Core/DashScopeClientCore.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,75 @@ 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 key = $"{policy.Data.UploadDir}/{filename}";
326+
var form = DashScopeMultipartContent.Create();
327+
form.Add(GetFormDataStringContent(policy.Data.OssAccessKeyId, "OSSAccessKeyId"));
328+
form.Add(GetFormDataStringContent(policy.Data.Policy, "policy"));
329+
form.Add(GetFormDataStringContent(policy.Data.Signature, "Signature"));
330+
form.Add(GetFormDataStringContent(key, "key"));
331+
form.Add(GetFormDataStringContent(policy.Data.XOssObjectAcl, "x-oss-object-acl"));
332+
form.Add(GetFormDataStringContent(policy.Data.XOssForbidOverwrite, "x-oss-forbid-overwrite"));
333+
var file = new StreamContent(fileStream);
334+
file.Headers.ContentType = null;
335+
file.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"file\"; filename=\"{filename}\"");
336+
file.Headers.TryAddWithoutValidation("Content-Type", "application/octet-stream");
337+
form.Add(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+
351+
private static StringContent GetFormDataStringContent(string value, string key)
352+
{
353+
var content = new StringContent(value);
354+
content.Headers.ContentType = null;
355+
content.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"{key}\"");
356+
return content;
357+
}
358+
290359
private static HttpRequestMessage BuildSseRequest<TPayload>(HttpMethod method, string url, TPayload payload)
291360
where TPayload : class
292361
{
@@ -328,6 +397,11 @@ private static HttpRequestMessage BuildRequest<TPayload>(
328397
message.Headers.Add("X-DashScope-WorkSpace", config.WorkspaceId);
329398
}
330399

400+
if (payload is IDashScopeOssUploadConfig ossConfig && ossConfig.EnableOssResolve())
401+
{
402+
message.Headers.Add("X-DashScope-OssResourceResolve", "enable");
403+
}
404+
331405
return message;
332406
}
333407

src/Cnblogs.DashScope.Core/DashScopeClientWebSocket.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed class DashScopeClientWebSocket : IDisposable
2020
};
2121

2222
private readonly IClientWebSocket _socket;
23+
// ReSharper disable once NotAccessedField.Local
2324
private Task? _receiveTask;
2425
private TaskCompletionSource<bool> _taskStartedSignal = new();
2526
private Channel<byte>? _binaryOutput;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System.Buffers;
2+
using System.Net;
3+
using System.Text;
4+
5+
namespace Cnblogs.DashScope.Core;
6+
7+
internal class DashScopeMultipartContent : MultipartContent
8+
{
9+
private const string CrLf = "\r\n";
10+
private readonly string _boundary;
11+
12+
private DashScopeMultipartContent(string boundary)
13+
: base("form-data", boundary)
14+
{
15+
_boundary = boundary;
16+
}
17+
18+
/// <inheritdoc />
19+
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
20+
{
21+
// Write start boundary.
22+
await EncodeStringToStreamAsync(stream, "--" + _boundary + CrLf);
23+
24+
// Write each nested content.
25+
var output = new MemoryStream();
26+
var contentIndex = 0;
27+
foreach (var content in this)
28+
{
29+
output.SetLength(0);
30+
SerializeHeadersToStream(output, content, writeDivider: contentIndex != 0);
31+
output.Position = 0;
32+
await output.CopyToAsync(stream);
33+
await content.CopyToAsync(stream, context);
34+
contentIndex++;
35+
}
36+
37+
// Write footer boundary.
38+
await EncodeStringToStreamAsync(stream, CrLf + "--" + _boundary + "--" + CrLf);
39+
}
40+
41+
/// <inheritdoc />
42+
protected override bool TryComputeLength(out long length)
43+
{
44+
var success = base.TryComputeLength(out length);
45+
return success;
46+
}
47+
48+
private void SerializeHeadersToStream(Stream stream, HttpContent content, bool writeDivider)
49+
{
50+
// Add divider.
51+
if (writeDivider)
52+
{
53+
WriteToStream(stream, CrLf + "--");
54+
WriteToStream(stream, _boundary);
55+
WriteToStream(stream, CrLf);
56+
}
57+
58+
// Add headers.
59+
foreach (var headerPair in content.Headers.NonValidated)
60+
{
61+
var headerValueEncoding = HeaderEncodingSelector?.Invoke(headerPair.Key, content)
62+
?? Encoding.UTF8;
63+
64+
WriteToStream(stream, headerPair.Key);
65+
WriteToStream(stream, ": ");
66+
var delim = string.Empty;
67+
foreach (var value in headerPair.Value)
68+
{
69+
WriteToStream(stream, delim);
70+
WriteToStream(stream, value, headerValueEncoding);
71+
delim = ", ";
72+
}
73+
74+
WriteToStream(stream, CrLf);
75+
}
76+
77+
WriteToStream(stream, CrLf);
78+
}
79+
80+
private static void WriteToStream(Stream stream, string content) => WriteToStream(stream, content, Encoding.UTF8);
81+
82+
private static void WriteToStream(Stream stream, string content, Encoding encoding)
83+
{
84+
const int stackallocThreshold = 1024;
85+
86+
var maxLength = encoding.GetMaxByteCount(content.Length);
87+
88+
byte[]? rentedBuffer = null;
89+
var buffer = maxLength <= stackallocThreshold
90+
? stackalloc byte[stackallocThreshold]
91+
: (rentedBuffer = ArrayPool<byte>.Shared.Rent(maxLength));
92+
93+
try
94+
{
95+
var written = encoding.GetBytes(content, buffer);
96+
stream.Write(buffer.Slice(0, written));
97+
}
98+
finally
99+
{
100+
if (rentedBuffer != null)
101+
{
102+
ArrayPool<byte>.Shared.Return(rentedBuffer);
103+
}
104+
}
105+
}
106+
107+
private static ValueTask EncodeStringToStreamAsync(Stream stream, string input)
108+
{
109+
var buffer = Encoding.UTF8.GetBytes(input);
110+
return stream.WriteAsync(new ReadOnlyMemory<byte>(buffer));
111+
}
112+
113+
public static DashScopeMultipartContent Create()
114+
{
115+
return Create(Guid.NewGuid().ToString());
116+
}
117+
118+
internal static DashScopeMultipartContent Create(string boundary)
119+
{
120+
var content = new DashScopeMultipartContent(boundary);
121+
content.Headers.ContentType = null;
122+
content.Headers.TryAddWithoutValidation(
123+
"Content-Type",
124+
$"multipart/form-data; boundary={boundary}");
125+
return content;
126+
}
127+
}
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
}

0 commit comments

Comments
 (0)