6
6
using System . Net . Http ;
7
7
using System . Net . Http . Headers ;
8
8
using System . Net . Http . Json ;
9
+ using System . Runtime . ExceptionServices ;
9
10
using System . Text ;
10
11
using System . Text . Json . Nodes ;
11
12
using System . Xml . Linq ;
@@ -16,6 +17,7 @@ public record struct Registry(Uri BaseUri)
16
17
{
17
18
private const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json" ;
18
19
private const string DockerContainerV1 = "application/vnd.docker.container.image.v1+json" ;
20
+ private const int MaxChunkSizeBytes = 1024 * 64 ;
19
21
20
22
private string RegistryName { get ; } = BaseUri . Host ;
21
23
@@ -132,17 +134,69 @@ private readonly async Task UploadBlob(string name, string digest, Stream conten
132
134
x = new UriBuilder ( new Uri ( BaseUri , pushResponse . Headers . Location ? . OriginalString ?? "" ) ) ;
133
135
}
134
136
137
+ Uri patchUri = x . Uri ;
138
+
139
+ x . Query += $ "&digest={ Uri . EscapeDataString ( digest ) } ";
140
+
141
+ Uri putUri = x . Uri ;
142
+
143
+ // TODO: this chunking is super tiny and probably not necessary; what does the docker client do
144
+ // and can we be smarter?
145
+
146
+ byte [ ] chunkBackingStore = new byte [ MaxChunkSizeBytes ] ;
147
+
148
+ int chunkCount = 0 ;
149
+ int chunkStart = 0 ;
150
+
151
+ while ( contents . Position < contents . Length )
152
+ {
153
+ int bytesRead = await contents . ReadAsync ( chunkBackingStore ) ;
154
+
155
+ ByteArrayContent content = new ( chunkBackingStore , offset : 0 , count : bytesRead ) ;
156
+ content . Headers . ContentType = new MediaTypeHeaderValue ( "application/octet-stream" ) ;
157
+ content . Headers . ContentLength = bytesRead ;
158
+
159
+ // manual because ACR throws an error with the .NET type {"Range":"bytes 0-84521/*","Reason":"the Content-Range header format is invalid"}
160
+ // content.Headers.Add("Content-Range", $"0-{contents.Length - 1}");
161
+ Debug . Assert ( content . Headers . TryAddWithoutValidation ( "Content-Range" , $ "{ chunkStart } -{ chunkStart + bytesRead - 1 } ") ) ;
162
+
163
+ HttpResponseMessage patchResponse = await client . PatchAsync ( patchUri , content ) ;
164
+
165
+ if ( patchResponse . StatusCode != HttpStatusCode . Accepted )
166
+ {
167
+ string errorMessage = $ "Failed to upload blob to { patchUri } ; recieved { patchResponse . StatusCode } with detail { await patchResponse . Content . ReadAsStringAsync ( ) } ";
168
+ throw new ApplicationException ( errorMessage ) ;
169
+ }
170
+
171
+ if ( patchResponse . Headers . Location is { IsAbsoluteUri : true } )
172
+ {
173
+ x = new UriBuilder ( patchResponse . Headers . Location ) ;
174
+ }
175
+ else
176
+ {
177
+ // if we don't trim the BaseUri and relative Uri of slashes, you can get invalid urls.
178
+ // Uri constructor does this on our behalf.
179
+ x = new UriBuilder ( new Uri ( BaseUri , patchResponse . Headers . Location ? . OriginalString ?? "" ) ) ;
180
+ }
181
+
182
+ patchUri = x . Uri ;
183
+
184
+ chunkCount += 1 ;
185
+ chunkStart += bytesRead ;
186
+ }
187
+
188
+ // PUT with digest to finalize
135
189
x . Query += $ "&digest={ Uri . EscapeDataString ( digest ) } ";
136
190
137
- // TODO: consider chunking
138
- StreamContent content = new StreamContent ( contents ) ;
139
- content . Headers . ContentType = new MediaTypeHeaderValue ( "application/octet-stream" ) ;
140
- content . Headers . ContentLength = contents . Length ;
141
- HttpResponseMessage putResponse = await client . PutAsync ( x . Uri , content ) ;
191
+ putUri = x . Uri ;
142
192
143
- string resp = await putResponse . Content . ReadAsStringAsync ( ) ;
193
+ HttpResponseMessage finalizeResponse = await client . PutAsync ( putUri , content : null ) ;
144
194
145
- putResponse . EnsureSuccessStatusCode ( ) ;
195
+ if ( finalizeResponse . StatusCode != HttpStatusCode . Created )
196
+ {
197
+ string errorMessage = $ "Failed to finalize upload to { putUri } ; recieved { finalizeResponse . StatusCode } with detail { await finalizeResponse . Content . ReadAsStringAsync ( ) } ";
198
+ throw new ApplicationException ( errorMessage ) ;
199
+ }
146
200
}
147
201
148
202
private readonly async Task < bool > BlobAlreadyUploaded ( string name , string digest , HttpClient client )
0 commit comments