/// <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); } }
/// <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); }