Skip to content

Commit 46df956

Browse files
Merge pull request #492 from notion-dotnet/479-add-support-for-complete-file-upload-endpoint
Add support for Complete file upload api endpoint
2 parents a95f947 + 339b725 commit 46df956

File tree

9 files changed

+205
-2
lines changed

9 files changed

+205
-2
lines changed

Src/Notion.Client/Api/ApiEndpoints.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public static class FileUploadsApiUrls
144144
{
145145
public static string Create() => "/v1/file_uploads";
146146
public static string Send(string fileUploadId) => $"/v1/file_uploads/{fileUploadId}/send";
147+
public static string Complete(string fileUploadId) => $"/v1/file_uploads/{fileUploadId}/complete";
147148
}
148149
}
149150
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace Notion.Client
6+
{
7+
public sealed partial class FileUploadsClient
8+
{
9+
public async Task<CompleteFileUploadResponse> CompleteAsync(
10+
CompleteFileUploadRequest completeFileUploadRequest,
11+
CancellationToken cancellationToken = default)
12+
{
13+
if (completeFileUploadRequest == null)
14+
{
15+
throw new ArgumentNullException(nameof(completeFileUploadRequest));
16+
}
17+
18+
if (string.IsNullOrEmpty(completeFileUploadRequest.FileUploadId))
19+
{
20+
throw new ArgumentException("FileUploadId cannot be null or empty.", nameof(completeFileUploadRequest.FileUploadId));
21+
}
22+
23+
var path = ApiEndpoints.FileUploadsApiUrls.Complete(completeFileUploadRequest.FileUploadId);
24+
25+
return await _restClient.PostAsync<CompleteFileUploadResponse>(
26+
path,
27+
body: null,
28+
cancellationToken: cancellationToken
29+
);
30+
}
31+
}
32+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Notion.Client
2+
{
3+
public class CompleteFileUploadRequest : ICompleteFileUploadPathParameters
4+
{
5+
public string FileUploadId { get; set; }
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Notion.Client
2+
{
3+
public interface ICompleteFileUploadPathParameters
4+
{
5+
public string FileUploadId { get; set; }
6+
}
7+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Notion.Client
2+
{
3+
public class CompleteFileUploadResponse : FileObjectResponse
4+
{
5+
}
6+
}

Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,16 @@ Task<SendFileUploadResponse> SendAsync(
2929
SendFileUploadRequest sendFileUploadRequest,
3030
CancellationToken cancellationToken = default
3131
);
32+
33+
/// <summary>
34+
/// After uploading all parts of a file (mode=multi_part), call this endpoint to complete the upload process.
35+
/// </summary>
36+
/// <param name="completeFileUploadRequest"></param>
37+
/// <param name="cancellationToken"></param>
38+
/// <returns></returns>
39+
Task<CompleteFileUploadResponse> CompleteAsync(
40+
CompleteFileUploadRequest completeFileUploadRequest,
41+
CancellationToken cancellationToken = default
42+
);
3243
}
3344
}

Src/Notion.Client/RestClient/RestClient.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,13 @@ public async Task<T> PostAsync<T>(
5353
{
5454
void AttachContent(HttpRequestMessage httpRequest)
5555
{
56-
httpRequest.Content = new StringContent(JsonConvert.SerializeObject(body, DefaultSerializerSettings),
57-
Encoding.UTF8, "application/json");
56+
if (body == null)
57+
{
58+
return;
59+
}
60+
61+
var jsonObjectString = JsonConvert.SerializeObject(body, DefaultSerializerSettings);
62+
httpRequest.Content = new StringContent(jsonObjectString, Encoding.UTF8, "application/json");
5863
}
5964

6065
var response = await SendAsync(

Test/Notion.IntegrationTests/FileUploadsClientTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,65 @@ public async Task Verify_file_upload_flow()
6161
Assert.Equal("uploaded", sendResponse.Status);
6262
}
6363
}
64+
65+
[Fact]
66+
public async Task Verify_multi_part_file_upload_flow()
67+
{
68+
// Create file upload
69+
var createRequest = new CreateFileUploadRequest
70+
{
71+
Mode = FileUploadMode.MultiPart,
72+
FileName = "notion-logo.png",
73+
NumberOfParts = 2
74+
};
75+
76+
var createResponse = await Client.FileUploads.CreateAsync(createRequest);
77+
78+
Assert.NotNull(createResponse);
79+
Assert.NotNull(createResponse.Id);
80+
Assert.Equal("notion-logo.png", createResponse.FileName);
81+
Assert.Equal("image/png", createResponse.ContentType);
82+
Assert.Equal("pending", createResponse.Status);
83+
84+
// Send file parts
85+
using (var fileStream = File.OpenRead("assets/notion-logo.png"))
86+
{
87+
var splitStreams = StreamSplitExtensions.Split(fileStream, 2);
88+
89+
foreach (var (partStream, index) in splitStreams.WithIndex())
90+
{
91+
var partSendRequest = SendFileUploadRequest.Create(
92+
createResponse.Id,
93+
new FileData
94+
{
95+
FileName = "notion-logo.png",
96+
Data = partStream,
97+
ContentType = createResponse.ContentType
98+
},
99+
100+
partNumber: (index + 1).ToString()
101+
);
102+
103+
var partSendResponse = await Client.FileUploads.SendAsync(partSendRequest);
104+
105+
Assert.NotNull(partSendResponse);
106+
Assert.Equal(createResponse.Id, partSendResponse.Id);
107+
Assert.Equal("notion-logo.png", partSendResponse.FileName);
108+
}
109+
110+
// Complete file upload
111+
var completeRequest = new CompleteFileUploadRequest
112+
{
113+
FileUploadId = createResponse.Id
114+
};
115+
116+
var completeResponse = await Client.FileUploads.CompleteAsync(completeRequest);
117+
118+
Assert.NotNull(completeResponse);
119+
Assert.Equal(createResponse.Id, completeResponse.Id);
120+
Assert.Equal("notion-logo.png", completeResponse.FileName);
121+
Assert.Equal("completed", completeResponse.Status);
122+
}
123+
}
64124
}
65125
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
5+
namespace Notion.IntegrationTests
6+
{
7+
public static class StreamSplitExtensions
8+
{
9+
public static IEnumerable<Stream> Split(Stream inputStream, int numberOfParts)
10+
{
11+
if (numberOfParts <= 0)
12+
{
13+
throw new ArgumentException("Number of parts must be greater than zero.", nameof(numberOfParts));
14+
}
15+
16+
if (inputStream == null)
17+
{
18+
throw new ArgumentNullException(nameof(inputStream));
19+
}
20+
21+
MemoryStream buffer = new();
22+
inputStream.CopyTo(buffer);
23+
24+
buffer.Position = 0;
25+
26+
long totalSize = buffer.Length;
27+
long baseSize = totalSize / numberOfParts;
28+
long remainder = totalSize % numberOfParts;
29+
30+
for (int i = 0; i < numberOfParts; i++)
31+
{
32+
long currentPartSize = i == numberOfParts - 1 ? baseSize + remainder : baseSize;
33+
34+
var partStream = new MemoryStream();
35+
CopyStream(buffer, partStream, currentPartSize);
36+
partStream.Position = 0;
37+
yield return partStream;
38+
}
39+
}
40+
41+
private static void CopyStream(Stream buffer, MemoryStream partStream, long bytesToCopy)
42+
{
43+
byte[] tempBuffer = new byte[81920]; // 80 KB buffer
44+
45+
while (bytesToCopy > 0)
46+
{
47+
int bytesToRead = (int)Math.Min(tempBuffer.Length, bytesToCopy);
48+
int bytesRead = buffer.Read(tempBuffer, 0, bytesToRead);
49+
if (bytesRead == 0)
50+
{
51+
break; // End of stream
52+
}
53+
54+
partStream.Write(tempBuffer, 0, bytesRead);
55+
bytesToCopy -= bytesRead;
56+
}
57+
}
58+
59+
// enumerate with index
60+
public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> source)
61+
{
62+
if (source == null)
63+
{
64+
throw new ArgumentNullException(nameof(source));
65+
}
66+
67+
int index = 0;
68+
foreach (var item in source)
69+
{
70+
yield return (item, index++);
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)