/// <summary>
        ///     Determine whether a local package is the same as a package on an AzDO feed.
        /// </summary>
        /// <param name="localPackageFullPath"></param>
        /// <param name="packageContentUrl"></param>
        /// <param name="client"></param>
        /// <returns></returns>
        /// <remarks>
        ///     Open a stream to the local file and an http request to the package. There are a couple possibilities:
        ///     - The returned headers includes a content MD5 header, in which case we can
        ///       hash the local file and just compare those.
        ///     - No content MD5 hash, and the streams must be compared in blocks. This is a bit trickier to do efficiently,
        ///       since we do not necessarily want to read all bytes if we can help it. Thus, we should compare in blocks.  However,
        ///       the streams make no gaurantee that they will return a full block each time when read operations are performed, so we
        ///       must be sure to only compare the minimum number of bytes returned.
        /// </remarks>
        private async Task <bool> IsLocalPackageIdenticalToFeedPackage(string localPackageFullPath, string packageContentUrl, HttpClient client)
        {
            Log.LogMessage($"Getting package content from {packageContentUrl} and comparing to {localPackageFullPath}");

            try
            {
                using (Stream localFileStream = File.OpenRead(localPackageFullPath))
                    using (HttpResponseMessage response = await client.GetAsync(packageContentUrl))
                    {
                        response.EnsureSuccessStatusCode();

                        // Check the headers for content length and md5
                        bool md5HeaderAvailable    = response.Headers.TryGetValues("Content-MD5", out var md5);
                        bool lengthHeaderAvailable = response.Headers.TryGetValues("Content-Length", out var contentLength);

                        if (lengthHeaderAvailable && long.Parse(contentLength.Single()) != localFileStream.Length)
                        {
                            Log.LogMessage(MessageImportance.Low, $"Package '{localPackageFullPath}' has different length than remote package '{packageContentUrl}'.");
                            return(false);
                        }

                        if (md5HeaderAvailable)
                        {
                            var localMD5 = AzureStorageUtils.CalculateMD5(localPackageFullPath);
                            if (!localMD5.Equals(md5.Single(), StringComparison.OrdinalIgnoreCase))
                            {
                                Log.LogMessage(MessageImportance.Low, $"Package '{localPackageFullPath}' has different MD5 hash than remote package '{packageContentUrl}'.");
                            }

                            return(true);
                        }

                        const int BufferSize = 64 * 1024;

                        // Otherwise, compare the streams
                        var remoteStream = await response.Content.ReadAsStreamAsync();

                        return(await CompareStreamsAsync(localFileStream, remoteStream, BufferSize));
                    }
            }
            catch (Exception e)
            {
                // This is an error. It means we were unable to push using nuget, and then could not access to the package otherwise.
                Log.LogWarning($"Failed to determine whether an existing package on the feed has the same content: {e.Message}");
                return(false);
            }
        }
Example #2
0
        /// <summary>
        ///     Determine whether a local package is the same as a package on an AzDO feed.
        /// </summary>
        /// <param name="localPackageFullPath"></param>
        /// <param name="packageContentUrl"></param>
        /// <param name="client"></param>
        /// <returns></returns>
        /// <remarks>
        ///     Open a stream to the local file and an http request to the package. There are a couple possibilities:
        ///     - The returned headers includes a content MD5 header, in which case we can
        ///       hash the local file and just compare those.
        ///     - No content MD5 hash, and the streams must be compared in blocks. This is a bit trickier to do efficiently,
        ///       since we do not necessarily want to read all bytes if we can help it. Thus, we should compare in blocks.  However,
        ///       the streams make no gaurantee that they will return a full block each time when read operations are performed, so we
        ///       must be sure to only compare the minimum number of bytes returned.
        /// </remarks>
        public static async Task <PackageFeedStatus> CompareLocalPackageToFeedPackage(string localPackageFullPath, string packageContentUrl, HttpClient client, TaskLoggingHelper log)
        {
            log.LogMessage($"Getting package content from {packageContentUrl} and comparing to {localPackageFullPath}");

            PackageFeedStatus result = PackageFeedStatus.Unknown;

            ExponentialRetry RetryHandler = new ExponentialRetry
            {
                MaxAttempts = MaxRetries
            };

            bool success = await RetryHandler.RunAsync(async attempt =>
            {
                try
                {
                    using (Stream localFileStream = File.OpenRead(localPackageFullPath))
                        using (HttpResponseMessage response = await client.GetAsync(packageContentUrl))
                        {
                            response.EnsureSuccessStatusCode();

                            // Check the headers for content length and md5
                            bool md5HeaderAvailable    = response.Headers.TryGetValues("Content-MD5", out var md5);
                            bool lengthHeaderAvailable = response.Headers.TryGetValues("Content-Length", out var contentLength);

                            if (lengthHeaderAvailable && long.Parse(contentLength.Single()) != localFileStream.Length)
                            {
                                log.LogMessage(MessageImportance.Low, $"Package '{localPackageFullPath}' has different length than remote package '{packageContentUrl}'.");
                                result = PackageFeedStatus.ExistsAndDifferent;
                                return(true);
                            }

                            if (md5HeaderAvailable)
                            {
                                var localMD5 = AzureStorageUtils.CalculateMD5(localPackageFullPath);
                                if (!localMD5.Equals(md5.Single(), StringComparison.OrdinalIgnoreCase))
                                {
                                    log.LogMessage(MessageImportance.Low, $"Package '{localPackageFullPath}' has different MD5 hash than remote package '{packageContentUrl}'.");
                                }

                                result = PackageFeedStatus.ExistsAndDifferent;
                                return(true);
                            }

                            const int BufferSize = 64 * 1024;

                            // Otherwise, compare the streams
                            var remoteStream = await response.Content.ReadAsStreamAsync();
                            var streamsMatch = await GeneralUtils.CompareStreamsAsync(localFileStream, remoteStream, BufferSize);
                            result           = streamsMatch ? PackageFeedStatus.ExistsAndIdenticalToLocal : PackageFeedStatus.ExistsAndDifferent;
                            return(true);
                        }
                }
                // String based comparison because the status code isn't exposed in HttpRequestException
                // see here: https://github.com/dotnet/runtime/issues/23648
                catch (HttpRequestException e)
                {
                    if (e.Message.Contains("404 (Not Found)"))
                    {
                        result = PackageFeedStatus.DoesNotExist;
                        return(true);
                    }

                    // Retry this. Could be an http client timeout, 500, etc.
                    return(false);
                }
            });

            return(result);
        }