DownloadFile.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Net;
  7. using System.Net.Http;
  8. using System.Threading.Tasks;
  9. using Microsoft.Build.Framework;
  10. using Microsoft.Build.Utilities;
  11. namespace RepoTasks;
  12. public class DownloadFile : Microsoft.Build.Utilities.Task
  13. {
  14. [Required]
  15. public string Uri { get; set; }
  16. /// <summary>
  17. /// If this field is set and the task fail to download the file from `Uri`, with a NotFound
  18. /// status, it will try to download the file from `PrivateUri`.
  19. /// </summary>
  20. public string PrivateUri { get; set; }
  21. /// <summary>
  22. /// Suffix for the private URI in base64 form (for SAS compatibility)
  23. /// </summary>
  24. public string PrivateUriSuffix { get; set; }
  25. public int MaxRetries { get; set; } = 5;
  26. [Required]
  27. public string DestinationPath { get; set; }
  28. public bool Overwrite { get; set; }
  29. public override bool Execute()
  30. {
  31. return ExecuteAsync().GetAwaiter().GetResult();
  32. }
  33. private async System.Threading.Tasks.Task<bool> ExecuteAsync()
  34. {
  35. string destinationDir = Path.GetDirectoryName(DestinationPath);
  36. if (!Directory.Exists(destinationDir))
  37. {
  38. Directory.CreateDirectory(destinationDir);
  39. }
  40. if (File.Exists(DestinationPath) && !Overwrite)
  41. {
  42. return true;
  43. }
  44. const string FileUriProtocol = "file://";
  45. if (Uri.StartsWith(FileUriProtocol, StringComparison.Ordinal))
  46. {
  47. var filePath = Uri.Substring(FileUriProtocol.Length);
  48. Log.LogMessage($"Copying '{filePath}' to '{DestinationPath}'");
  49. File.Copy(filePath, DestinationPath);
  50. return true;
  51. }
  52. List<string> errorMessages = new List<string>();
  53. bool? downloadStatus = await DownloadWithRetriesAsync(Uri, DestinationPath, errorMessages);
  54. if (downloadStatus == false && !string.IsNullOrEmpty(PrivateUri))
  55. {
  56. string uriSuffix = "";
  57. if (!string.IsNullOrEmpty(PrivateUriSuffix))
  58. {
  59. var uriSuffixBytes = System.Convert.FromBase64String(PrivateUriSuffix);
  60. uriSuffix = System.Text.Encoding.UTF8.GetString(uriSuffixBytes);
  61. }
  62. downloadStatus = await DownloadWithRetriesAsync($"{PrivateUri}{uriSuffix}", DestinationPath, errorMessages);
  63. }
  64. if (downloadStatus != true)
  65. {
  66. foreach (var error in errorMessages)
  67. {
  68. Log.LogError(error);
  69. }
  70. }
  71. return downloadStatus == true;
  72. }
  73. /// <summary>
  74. /// Attempt to download file from `source` with retries when response error is different of FileNotFound and Success.
  75. /// </summary>
  76. /// <param name="source">URL to the file to be downloaded.</param>
  77. /// <param name="target">Local path where to put the downloaded file.</param>
  78. /// <returns>true: Download Succeeded. false: Download failed with 404. null: Download failed but is retriable.</returns>
  79. private async Task<bool?> DownloadWithRetriesAsync(string source, string target, List<string> errorMessages)
  80. {
  81. Random rng = new Random();
  82. Log.LogMessage(MessageImportance.High, $"Attempting download '{source}' to '{target}'");
  83. using (var httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(7.5) })
  84. {
  85. for (int retryNumber = 0; retryNumber < MaxRetries; retryNumber++)
  86. {
  87. try
  88. {
  89. var httpResponse = await httpClient.GetAsync(source);
  90. Log.LogMessage(MessageImportance.High, $"{source} -> {httpResponse.StatusCode}");
  91. // The Azure Storage REST API returns '400 - Bad Request' in some cases
  92. // where the resource is not found on the storage.
  93. // https://docs.microsoft.com/en-us/rest/api/storageservices/common-rest-api-error-codes
  94. if (httpResponse.StatusCode == HttpStatusCode.NotFound ||
  95. httpResponse.ReasonPhrase.IndexOf("The requested URI does not represent any resource on the server.", StringComparison.OrdinalIgnoreCase) == 0)
  96. {
  97. errorMessages.Add($"Problems downloading file from '{source}'. Does the resource exist on the storage? {httpResponse.StatusCode} : {httpResponse.ReasonPhrase}");
  98. return false;
  99. }
  100. httpResponse.EnsureSuccessStatusCode();
  101. using (var outStream = File.Create(target))
  102. {
  103. await httpResponse.Content.CopyToAsync(outStream);
  104. }
  105. Log.LogMessage(MessageImportance.High, $"returning true {source} -> {httpResponse.StatusCode}");
  106. return true;
  107. }
  108. catch (Exception e)
  109. {
  110. Log.LogMessage(MessageImportance.High, $"returning error in {source} ");
  111. errorMessages.Add($"Problems downloading file from '{source}'. {e.Message} {e.StackTrace}");
  112. File.Delete(target);
  113. }
  114. await System.Threading.Tasks.Task.Delay(rng.Next(1000, 10000));
  115. }
  116. }
  117. Log.LogMessage(MessageImportance.High, $"giving up {source} ");
  118. errorMessages.Add($"Giving up downloading the file from '{source}' after {MaxRetries} retries.");
  119. return null;
  120. }
  121. }