Skip to content

Commit 3ce9bbc

Browse files
committed
Consume URI in PUT response for GitHub-owned storage for GEI.
1 parent f9ee191 commit 3ce9bbc

File tree

2 files changed

+61
-19
lines changed

2 files changed

+61
-19
lines changed

src/Octoshift/Services/ArchiveUploader.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Linq;
55
using System.Net.Http;
66
using System.Threading.Tasks;
7-
using System.Web;
87
using Newtonsoft.Json.Linq;
98
using OctoshiftCLI.Extensions;
109

@@ -64,11 +63,8 @@ private async Task<string> UploadMultipart(Stream archiveContent, string archive
6463
{
6564
// 1. Start the upload
6665
var startHeaders = await StartUpload(uploadUrl, archiveName, archiveContent.Length);
67-
6866
var nextUrl = GetNextUrl(startHeaders);
6967

70-
var guid = HttpUtility.ParseQueryString(nextUrl.Query)["guid"];
71-
7268
// 2. Upload parts
7369
int bytesRead;
7470
var partsRead = 0;
@@ -80,9 +76,9 @@ private async Task<string> UploadMultipart(Stream archiveContent, string archive
8076
}
8177

8278
// 3. Complete the upload
83-
await CompleteUpload(nextUrl.ToString());
79+
var geiUri = await CompleteUpload(nextUrl.ToString());
8480

85-
return $"gei://archive/{guid}";
81+
return geiUri.ToString();
8682
}
8783
catch (Exception ex)
8884
{
@@ -132,12 +128,16 @@ private async Task<Uri> UploadPart(byte[] body, int bytesRead, string nextUrl, i
132128
}
133129
}
134130

135-
private async Task CompleteUpload(string lastUrl)
131+
private async Task<Uri> CompleteUpload(string lastUrl)
136132
{
137133
try
138134
{
139-
await _retryPolicy.Retry(async () => await _client.PutAsync(lastUrl, ""));
135+
var response = await _retryPolicy.Retry(async () => await _client.PutAsync(lastUrl, ""));
136+
var responseData = JObject.Parse(response);
137+
140138
_log.LogInformation("Finished uploading archive");
139+
140+
return new Uri((string)responseData["uri"]);
141141
}
142142
catch (Exception ex)
143143
{

src/OctoshiftCLI.Tests/Octoshift/Services/ArchiveUploadersTests.cs

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using FluentAssertions;
77
using Moq;
8+
using Newtonsoft.Json.Linq;
89
using OctoshiftCLI.Extensions;
910
using OctoshiftCLI.Services;
1011
using Xunit;
@@ -50,9 +51,20 @@ public async Task Upload_Should_Upload_All_Chunks_When_Stream_Exceeds_Limit()
5051
const string archiveName = "test-archive";
5152
const string baseUrl = "https://uploads.github.com";
5253
const string guid = "c9dbd27b-f190-4fe4-979f-d0b7c9b0fcb3";
54+
const string geiUri = $"gei://archive/{guid}";
5355

5456
var startUploadBody = new { content_type = "application/octet-stream", name = archiveName, size = contentSize };
5557

58+
var completeUploadResponse = new JObject
59+
{
60+
["guid"] = guid,
61+
["node_id"] = "global-relay-id",
62+
["name"] = archiveName,
63+
["size"] = largeContent.Length,
64+
["uri"] = geiUri,
65+
["created_at"] = "2025-06-23T17:13:02.818-07:00"
66+
};
67+
5668
const string initialUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads";
5769
const string firstUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=1&guid={guid}";
5870
const string secondUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=2&guid={guid}";
@@ -81,13 +93,13 @@ public async Task Upload_Should_Upload_All_Chunks_When_Stream_Exceeds_Limit()
8193
// Mocking the final PUT request to complete the multipart upload
8294
_githubClientMock
8395
.Setup(m => m.PutAsync($"{baseUrl}{lastUrl}", "", null))
84-
.ReturnsAsync(string.Empty);
96+
.ReturnsAsync(completeUploadResponse.ToString());
8597

8698
// act
8799
var result = await _archiveUploader.Upload(archiveContent, archiveName, orgDatabaseId);
88100

89101
// assert
90-
result.Should().Be($"gei://archive/{guid}");
102+
result.Should().Be(geiUri);
91103

92104
_githubClientMock.Verify(m => m.PostWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Once);
93105
_githubClientMock.Verify(m => m.PatchWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Exactly(3));
@@ -107,10 +119,20 @@ public async Task Upload_Should_Retry_Failed_Upload_Part_Patch_Requests()
107119
const string archiveName = "test-archive";
108120
const string baseUrl = "https://uploads.github.com";
109121
const string guid = "c9dbd27b-f190-4fe4-979f-d0b7c9b0fcb3";
110-
const string expectedResult = $"gei://archive/{guid}";
122+
const string geiUri = $"gei://archive/{guid}";
111123

112124
var startUploadBody = new { content_type = "application/octet-stream", name = archiveName, size = largeContent.Length };
113125

126+
var completeUploadResponse = new JObject
127+
{
128+
["guid"] = guid,
129+
["node_id"] = "global-relay-id",
130+
["name"] = archiveName,
131+
["size"] = largeContent.Length,
132+
["uri"] = geiUri,
133+
["created_at"] = "2025-06-23T17:13:02.818-07:00"
134+
};
135+
114136
const string initialUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads";
115137
const string firstUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=1&guid={guid}";
116138
const string secondUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=2&guid={guid}";
@@ -137,13 +159,13 @@ public async Task Upload_Should_Retry_Failed_Upload_Part_Patch_Requests()
137159
// Mocking the final PUT request to complete the multipart upload
138160
_githubClientMock
139161
.Setup(m => m.PutAsync($"{baseUrl}{lastUrl}", "", null))
140-
.ReturnsAsync(string.Empty);
162+
.ReturnsAsync(completeUploadResponse.ToString());
141163

142164
// act
143165
var result = await _archiveUploader.Upload(archiveContent, archiveName, orgDatabaseId);
144166

145167
// assert
146-
Assert.Equal(expectedResult, result);
168+
Assert.Equal(geiUri, result);
147169

148170
_githubClientMock.Verify(m => m.PostWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Once);
149171
_githubClientMock.Verify(m => m.PatchWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Exactly(4)); // 2 retries + 2 success
@@ -163,10 +185,20 @@ public async Task Upload_Should_Retry_Failed_Start_Upload_Post_Request()
163185
const string archiveName = "test-archive";
164186
const string baseUrl = "https://uploads.github.com";
165187
const string guid = "c9dbd27b-f190-4fe4-979f-d0b7c9b0fcb3";
166-
const string expectedResult = $"gei://archive/{guid}";
188+
const string geiUri = $"gei://archive/{guid}";
167189

168190
var startUploadBody = new { content_type = "application/octet-stream", name = archiveName, size = largeContent.Length };
169191

192+
var completeUploadResponse = new JObject
193+
{
194+
["guid"] = guid,
195+
["node_id"] = "global-relay-id",
196+
["name"] = archiveName,
197+
["size"] = largeContent.Length,
198+
["uri"] = geiUri,
199+
["created_at"] = "2025-06-23T17:13:02.818-07:00"
200+
};
201+
170202
const string initialUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads";
171203
const string firstUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=1&guid={guid}";
172204
const string secondUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=2&guid={guid}";
@@ -193,13 +225,13 @@ public async Task Upload_Should_Retry_Failed_Start_Upload_Post_Request()
193225
// Mocking the final PUT request to complete the multipart upload
194226
_githubClientMock
195227
.Setup(m => m.PutAsync($"{baseUrl}{lastUrl}", "", null))
196-
.ReturnsAsync(string.Empty);
228+
.ReturnsAsync(completeUploadResponse.ToString());
197229

198230
// act
199231
var result = await _archiveUploader.Upload(archiveContent, archiveName, orgDatabaseId);
200232

201233
// assert
202-
Assert.Equal(expectedResult, result);
234+
Assert.Equal(geiUri, result);
203235

204236
_githubClientMock.Verify(m => m.PostWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Exactly(3)); // 2 retries + 1 success
205237
_githubClientMock.Verify(m => m.PatchWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Exactly(2));
@@ -219,10 +251,20 @@ public async Task Upload_Should_Retry_Failed_Complete_Upload_Put_Request()
219251
const string archiveName = "test-archive";
220252
const string baseUrl = "https://uploads.github.com";
221253
const string guid = "c9dbd27b-f190-4fe4-979f-d0b7c9b0fcb3";
222-
const string expectedResult = $"gei://archive/{guid}";
254+
const string geiUri = $"gei://archive/{guid}";
223255

224256
var startUploadBody = new { content_type = "application/octet-stream", name = archiveName, size = largeContent.Length };
225257

258+
var completeUploadResponse = new JObject
259+
{
260+
["guid"] = guid,
261+
["node_id"] = "global-relay-id",
262+
["name"] = archiveName,
263+
["size"] = largeContent.Length,
264+
["uri"] = geiUri,
265+
["created_at"] = "2025-06-23T17:13:02.818-07:00"
266+
};
267+
226268
const string initialUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads";
227269
const string firstUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=1&guid={guid}";
228270
const string secondUploadUrl = $"/organizations/{orgDatabaseId}/gei/archive/blobs/uploads?part_number=2&guid={guid}";
@@ -249,13 +291,13 @@ public async Task Upload_Should_Retry_Failed_Complete_Upload_Put_Request()
249291
.SetupSequence(m => m.PutAsync($"{baseUrl}{lastUrl}", "", null))
250292
.ThrowsAsync(new TimeoutException("The operation was canceled."))
251293
.ThrowsAsync(new TimeoutException("The operation was canceled."))
252-
.ReturnsAsync(string.Empty);
294+
.ReturnsAsync(completeUploadResponse.ToString());
253295

254296
// act
255297
var result = await _archiveUploader.Upload(archiveContent, archiveName, orgDatabaseId);
256298

257299
// assert
258-
Assert.Equal(expectedResult, result);
300+
Assert.Equal(geiUri, result);
259301

260302
_githubClientMock.Verify(m => m.PostWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Once);
261303
_githubClientMock.Verify(m => m.PatchWithFullResponseAsync(It.IsAny<string>(), It.IsAny<object>(), null), Times.Exactly(2));

0 commit comments

Comments
 (0)