| File: Registry\DefaultBlobUploadOperations.cs | Web Access |
| Project: src\sdk\src\Containers\Microsoft.NET.Build.Containers\Microsoft.NET.Build.Containers.csproj (Microsoft.NET.Build.Containers) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; using System.Net.Http.Headers; using Microsoft.Extensions.Logging; using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; internal class DefaultBlobUploadOperations : IBlobUploadOperations { private readonly Uri _baseUri; private readonly HttpClient _client; private readonly ILogger _logger; internal DefaultBlobUploadOperations(Uri baseUri, HttpClient client, ILogger logger) { _baseUri = baseUri; _client = client; _logger = logger; } public async Task CompleteAsync(Uri uploadUri, string digest, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // PUT with digest to finalize UriBuilder builder = new(uploadUri.IsAbsoluteUri ? uploadUri : new Uri(_baseUri, uploadUri)); builder.Query += $"&digest={Uri.EscapeDataString(digest)}"; Uri putUri = builder.Uri; HttpResponseMessage finalizeResponse = await _client.PutAsync(putUri, null, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (finalizeResponse.StatusCode != HttpStatusCode.Created) { await finalizeResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); string errorMessage = Resource.FormatString(nameof(Strings.BlobUploadFailed), $"PUT {putUri}", finalizeResponse.StatusCode); throw new ApplicationException(errorMessage); } } public async Task<HttpResponseMessage> GetStatusAsync(Uri uploadUri, CancellationToken cancellationToken) { return await _client.GetAsync(uploadUri.IsAbsoluteUri ? uploadUri : new Uri(_baseUri, uploadUri), cancellationToken).ConfigureAwait(false); } public async Task<StartUploadInformation> StartAsync(string repositoryName, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Uri startUploadUri = new(_baseUri, $"/v2/{repositoryName}/blobs/uploads/"); HttpResponseMessage pushResponse = await _client.PostAsync(startUploadUri, content: null, cancellationToken).ConfigureAwait(false); if (pushResponse.StatusCode != HttpStatusCode.Accepted) { await pushResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); string errorMessage = Resource.FormatString(nameof(Strings.BlobUploadFailed), $"POST {startUploadUri}", pushResponse.StatusCode); throw new ApplicationException(errorMessage); } cancellationToken.ThrowIfCancellationRequested(); Uri location = pushResponse.GetNextLocation(); return new(location); } public async Task<bool> TryMountAsync(string destinationRepository, string sourceRepository, string digest, CancellationToken cancellationToken) { // Blob wasn't there; can we tell the server to get it from the base image? HttpResponseMessage pushResponse = await _client.PostAsync(new Uri(_baseUri, $"/v2/{destinationRepository}/blobs/uploads/?mount={digest}&from={sourceRepository}"), content: null, cancellationToken).ConfigureAwait(false); return pushResponse.StatusCode == HttpStatusCode.Created; } public async Task<FinalizeUploadInformation> UploadAtomicallyAsync(Uri uploadUri, Stream content, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); StreamContent httpContent = new(content); httpContent.Headers.ContentLength = content.Length; Uri nextUploadUri = await PatchAsync(uploadUri, httpContent, cancellationToken).ConfigureAwait(false); return new(nextUploadUri); } public async Task<NextChunkUploadInformation> UploadChunkAsync(Uri uploadUri, HttpContent content, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Uri nextUploadUri = await PatchAsync(uploadUri, content, cancellationToken).ConfigureAwait(false); return new(nextUploadUri); } private HttpRequestMessage GetPatchHttpRequest(Uri uploadUri, HttpContent httpContent) { Uri finalUri = uploadUri.IsAbsoluteUri ? uploadUri : new Uri(_baseUri, uploadUri); HttpRequestMessage patchMessage = new(HttpMethod.Patch, finalUri) { Content = httpContent }; patchMessage.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); return patchMessage; } private async Task<Uri> PatchAsync(Uri uploadUri, HttpContent content, CancellationToken cancellationToken) { _logger.LogTrace("Uploading {0} bytes of content at {1}", content.Headers.ContentLength, uploadUri); HttpRequestMessage patchMessage = GetPatchHttpRequest(uploadUri, content); using HttpResponseMessage patchResponse = await _client.SendAsync(patchMessage, cancellationToken).ConfigureAwait(false); _logger.LogTrace("Received status code '{0}' from upload.", patchResponse.StatusCode); // Fail the upload if the response code is not Accepted (202) or if uploading to Amazon ECR which returns back Created (201). if (!(patchResponse.StatusCode == HttpStatusCode.Accepted || (uploadUri.IsAmazonECRRegistry() && patchResponse.StatusCode == HttpStatusCode.Created))) { await patchResponse.LogHttpResponseAsync(_logger, cancellationToken).ConfigureAwait(false); string errorMessage = Resource.FormatString(nameof(Strings.BlobUploadFailed), $"PATCH {uploadUri}", patchResponse.StatusCode); throw new ApplicationException(errorMessage); } return patchResponse.GetNextLocation(); } }