public async Task CompareLocalPackageToFeedPackageShouldCorrectlyInterpretFeedResponse( string feedResponseContentName, HttpStatusCode feedResponseStatusCode, PackageFeedStatus expectedResult) { var localPackagePath = TestInputs.GetFullPath(Path.Combine("Nupkgs", "test-package-a.zip")); var packageContentUrl = $"https://fakefeed.azure.com/nuget/v3/{feedResponseContentName}.nupkg"; var taskLoggingHelper = new Microsoft.Build.Utilities.TaskLoggingHelper(new StubTask()); var retryHandler = new MockRetryHandler(); var response = new HttpResponseMessage(feedResponseStatusCode); if (!string.IsNullOrEmpty(feedResponseContentName)) { var content = TestInputs.ReadAllBytes(Path.Combine("Nupkgs", $"{feedResponseContentName}.zip")); response.Content = new ByteArrayContent(content); } ; var httpClient = FakeHttpClient.WithResponses(response); var result = await GeneralUtils.CompareLocalPackageToFeedPackage( localPackagePath, packageContentUrl, httpClient, taskLoggingHelper, retryHandler); result.Should().Be(expectedResult); }
/// <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); }