File: Resources\PackageStagingResourceV3.cs
Web Access
Project: NuGet.Protocol.csproj (NuGet.Protocol)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Protocol.Core.Types;

namespace NuGet.Protocol
{
    /// <summary>
    /// Provides operations for uploading packages to a V3 package staging service.
    /// </summary>
    public sealed class PackageStagingResourceV3 : INuGetResource
    {
        private readonly Uri _endpoint;
        private readonly HttpSource _httpSource;

        internal PackageStagingResourceV3(Uri endpoint, HttpSource httpSource)
        {
            _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
            _httpSource = httpSource ?? throw new ArgumentNullException(nameof(httpSource));
        }

        /// <summary>
        /// Gets the exact package staging endpoint advertised by the service index.
        /// </summary>
        public Uri SourceUri => _endpoint;

        /// <summary>
        /// Uploads a package to the staging service.
        /// </summary>
        public Task PushPackageAsync(
            string packagePath,
            string? apiKey,
            string? groupId,
            TimeSpan requestTimeout,
            bool allowInsecureConnections,
            ILogger logger,
            CancellationToken cancellationToken)
        {
            return PushAsync(
                packagePath: packagePath,
                apiKey: apiKey,
                groupId: groupId,
                requestTimeout: requestTimeout,
                route: "package",
                formFieldName: "package",
                allowInsecureConnections: allowInsecureConnections,
                logger: logger,
                cancellationToken: cancellationToken);
        }

        /// <summary>
        /// Uploads a symbol package to the staging service.
        /// </summary>
        public Task PushSymbolsAsync(
            string packagePath,
            string? apiKey,
            string? groupId,
            TimeSpan requestTimeout,
            ILogger logger,
            CancellationToken cancellationToken)
        {
            return PushSymbolsAsync(
                packagePath: packagePath,
                apiKey: apiKey,
                groupId: groupId,
                requestTimeout: requestTimeout,
                allowInsecureConnections: false,
                logger: logger,
                cancellationToken: cancellationToken);
        }

        /// <summary>
        /// Uploads a symbol package to the staging service.
        /// </summary>
        public Task PushSymbolsAsync(
            string packagePath,
            string? apiKey,
            string? groupId,
            TimeSpan requestTimeout,
            bool allowInsecureConnections,
            ILogger logger,
            CancellationToken cancellationToken)
        {
            return PushAsync(
                packagePath: packagePath,
                apiKey: apiKey,
                groupId: groupId,
                requestTimeout: requestTimeout,
                route: "symbols",
                formFieldName: "symbols",
                allowInsecureConnections: allowInsecureConnections,
                logger: logger,
                cancellationToken: cancellationToken);
        }

        private async Task PushAsync(
            string packagePath,
            string? apiKey,
            string? groupId,
            TimeSpan requestTimeout,
            string route,
            string formFieldName,
            bool allowInsecureConnections,
            ILogger logger,
            CancellationToken cancellationToken)
        {
            var requestUriBuilder = new UriBuilder(_endpoint)
            {
                Path = _endpoint.AbsolutePath.TrimEnd('/') + "/" + route,
            };
            Uri requestUri = requestUriBuilder.Uri;
            if (requestUri.Scheme == Uri.UriSchemeHttp && !allowInsecureConnections)
            {
                throw new FatalProtocolException(string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.Error_Insecure_HTTP,
                    _httpSource.PackageSource,
                    requestUri));
            }

            var request = new HttpSourceRequest(
                () => CreateRequest(requestUri, packagePath, apiKey, groupId, formFieldName, logger))
            {
                RequestTimeout = requestTimeout,
            };

            await _httpSource.ProcessResponseAsync(
                request,
                response =>
                {
                    response.EnsureSuccessStatusCode();

                    return TaskResult.Zero;
                },
                logger,
                cancellationToken);
        }

        private static HttpRequestMessage CreateRequest(
            Uri requestUri,
            string packagePath,
            string? apiKey,
            string? groupId,
            string formFieldName,
            ILogger logger)
        {
            var request = HttpRequestMessageFactory.Create(
                HttpMethod.Put,
                requestUri,
                new HttpRequestMessageConfiguration(logger, promptOn403: string.IsNullOrEmpty(apiKey)));
            var content = new MultipartFormDataContent();
            var fileStream = new FileStream(packagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            var fileContent = new StreamContent(fileStream);
            fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
            content.Add(fileContent, formFieldName, Path.GetFileName(packagePath));

            if (groupId is not null)
            {
                content.Add(new StringContent(groupId), "groupId");
            }

            request.Content = content;
            request.Headers.TransferEncodingChunked = true;

            if (!string.IsNullOrEmpty(apiKey))
            {
                request.Headers.Add(ProtocolConstants.ApiKeyHeader, apiKey);
            }

            return request;
        }
    }
}