@@ -19,23 +19,33 @@ namespace Microsoft.Dotnet.Installation.Internal;
1919/// <summary>
2020/// Handles downloading and parsing .NET release manifests to find the correct installer/archive for a given installation.
2121/// </summary>
22- internal class DotnetArchiveDownloader ( HttpClient httpClient ) : IDisposable
22+ internal class DotnetArchiveDownloader : IDisposable
2323{
2424 private const int MaxRetryCount = 3 ;
2525 private const int RetryDelayMilliseconds = 1000 ;
2626
27- private readonly HttpClient _httpClient = httpClient ?? throw new ArgumentNullException ( nameof ( httpClient ) ) ;
28- private ReleaseManifest _releaseManifest = new ( ) ;
27+ private readonly HttpClient _httpClient ;
28+ private readonly bool _shouldDisposeHttpClient ;
29+ private ReleaseManifest _releaseManifest ;
2930
3031 public DotnetArchiveDownloader ( )
31- : this ( CreateDefaultHttpClient ( ) )
32+ : this ( new ReleaseManifest ( ) )
3233 {
3334 }
3435
35- public DotnetArchiveDownloader ( ReleaseManifest releaseManifest )
36- : this ( CreateDefaultHttpClient ( ) )
36+ public DotnetArchiveDownloader ( ReleaseManifest releaseManifest , HttpClient ? httpClient = null )
3737 {
3838 _releaseManifest = releaseManifest ?? throw new ArgumentNullException ( nameof ( releaseManifest ) ) ;
39+ if ( httpClient == null )
40+ {
41+ _httpClient = CreateDefaultHttpClient ( ) ;
42+ _shouldDisposeHttpClient = true ;
43+ }
44+ else
45+ {
46+ _httpClient = httpClient ;
47+ _shouldDisposeHttpClient = false ;
48+ }
3949 }
4050
4151 /// <summary>
@@ -69,8 +79,7 @@ private static HttpClient CreateDefaultHttpClient()
6979 /// <param name="downloadUrl">The URL to download from</param>
7080 /// <param name="destinationPath">The local path to save the downloaded file</param>
7181 /// <param name="progress">Optional progress reporting</param>
72- /// <returns>True if download was successful, false otherwise</returns>
73- protected async Task < bool > DownloadArchiveAsync ( string downloadUrl , string destinationPath , IProgress < DownloadProgress > ? progress = null )
82+ async Task DownloadArchiveAsync ( string downloadUrl , string destinationPath , IProgress < DownloadProgress > ? progress = null )
7483 {
7584 // Create temp file path in same directory for atomic move when complete
7685 string tempPath = $ "{ destinationPath } .download";
@@ -82,15 +91,14 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
8291 // Ensure the directory exists
8392 Directory . CreateDirectory ( Path . GetDirectoryName ( destinationPath ) ! ) ;
8493
85- // Try to get content length for progress reporting
86- long ? totalBytes = await GetContentLengthAsync ( downloadUrl ) ;
94+ // Content length for progress reporting
95+ long ? totalBytes = null ;
8796
8897 // Make the actual download request
8998 using var response = await _httpClient . GetAsync ( downloadUrl , HttpCompletionOption . ResponseHeadersRead ) ;
9099 response . EnsureSuccessStatusCode ( ) ;
91100
92- // Get the total bytes if we didn't get it before
93- if ( ! totalBytes . HasValue && response . Content . Headers . ContentLength . HasValue )
101+ if ( response . Content . Headers . ContentLength . HasValue )
94102 {
95103 totalBytes = response . Content . Headers . ContentLength . Value ;
96104 }
@@ -102,7 +110,7 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
102110 long bytesRead = 0 ;
103111 int read ;
104112
105- var lastProgressReport = DateTime . UtcNow ;
113+ var lastProgressReport = DateTime . MinValue ;
106114
107115 while ( ( read = await contentStream . ReadAsync ( buffer ) ) > 0 )
108116 {
@@ -133,9 +141,20 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
133141 }
134142 File . Move ( tempPath , destinationPath ) ;
135143
136- return true ;
144+ return ;
137145 }
138146 catch ( Exception )
147+ {
148+ if ( attempt < MaxRetryCount )
149+ {
150+ await Task . Delay ( RetryDelayMilliseconds * attempt ) ; // Linear backoff
151+ }
152+ else
153+ {
154+ throw ;
155+ }
156+ }
157+ finally
139158 {
140159 // Delete the partial download if it exists
141160 try
@@ -150,35 +169,9 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
150169 // Ignore cleanup errors
151170 }
152171
153- if ( attempt < MaxRetryCount )
154- {
155- await Task . Delay ( RetryDelayMilliseconds * attempt ) ; // Exponential backoff
156- }
157- else
158- {
159- return false ;
160- }
161172 }
162173 }
163174
164- return false ;
165- }
166-
167- /// <summary>
168- /// Gets the content length of a resource.
169- /// </summary>
170- private async Task < long ? > GetContentLengthAsync ( string url )
171- {
172- try
173- {
174- using var headRequest = new HttpRequestMessage ( HttpMethod . Head , url ) ;
175- using var headResponse = await _httpClient . SendAsync ( headRequest ) ;
176- return headResponse . Content . Headers . ContentLength ;
177- }
178- catch
179- {
180- return null ;
181- }
182175 }
183176
184177 /// <summary>
@@ -187,10 +180,9 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
187180 /// <param name="downloadUrl">The URL to download from</param>
188181 /// <param name="destinationPath">The local path to save the downloaded file</param>
189182 /// <param name="progress">Optional progress reporting</param>
190- /// <returns>True if download was successful, false otherwise</returns>
191- protected bool DownloadArchive ( string downloadUrl , string destinationPath , IProgress < DownloadProgress > ? progress = null )
183+ void DownloadArchive ( string downloadUrl , string destinationPath , IProgress < DownloadProgress > ? progress = null )
192184 {
193- return DownloadArchiveAsync ( downloadUrl , destinationPath , progress ) . GetAwaiter ( ) . GetResult ( ) ;
185+ DownloadArchiveAsync ( downloadUrl , destinationPath , progress ) . GetAwaiter ( ) . GetResult ( ) ;
194186 }
195187
196188 /// <summary>
@@ -200,23 +192,24 @@ protected bool DownloadArchive(string downloadUrl, string destinationPath, IProg
200192 /// <param name="destinationPath">The local path to save the downloaded file</param>
201193 /// <param name="progress">Optional progress reporting</param>
202194 /// <returns>True if download and verification were successful, false otherwise</returns>
203- public bool DownloadArchiveWithVerification ( DotnetInstallRequest installRequest , ReleaseVersion resolvedVersion , string destinationPath , IProgress < DownloadProgress > ? progress = null )
195+ public void DownloadArchiveWithVerification ( DotnetInstallRequest installRequest , ReleaseVersion resolvedVersion , string destinationPath , IProgress < DownloadProgress > ? progress = null )
204196 {
205197 var targetFile = _releaseManifest . FindReleaseFile ( installRequest , resolvedVersion ) ;
206198 string ? downloadUrl = targetFile ? . Address . ToString ( ) ;
207199 string ? expectedHash = targetFile ? . Hash . ToString ( ) ;
208200
209- if ( string . IsNullOrEmpty ( expectedHash ) || string . IsNullOrEmpty ( downloadUrl ) )
201+ if ( string . IsNullOrEmpty ( expectedHash ) )
210202 {
211- return false ;
203+ throw new ArgumentException ( $ " { nameof ( expectedHash ) } cannot be null or empty" ) ;
212204 }
213-
214- if ( ! DownloadArchive ( downloadUrl , destinationPath , progress ) )
205+ if ( string . IsNullOrEmpty ( downloadUrl ) )
215206 {
216- return false ;
207+ throw new ArgumentException ( $ " { nameof ( downloadUrl ) } cannot be null or empty" ) ;
217208 }
218209
219- return VerifyFileHash ( destinationPath , expectedHash ) ;
210+ DownloadArchive ( downloadUrl , destinationPath , progress ) ;
211+
212+ VerifyFileHash ( destinationPath , expectedHash ) ;
220213 }
221214
222215
@@ -240,20 +233,25 @@ public static string ComputeFileHash(string filePath)
240233 /// </summary>
241234 /// <param name="filePath">Path to the file to verify</param>
242235 /// <param name="expectedHash">Expected hash value</param>
243- /// <returns>True if the hash matches, false otherwise</returns>
244- public static bool VerifyFileHash ( string filePath , string expectedHash )
236+ public static void VerifyFileHash ( string filePath , string expectedHash )
245237 {
246238 if ( string . IsNullOrEmpty ( expectedHash ) )
247239 {
248- return false ;
240+ throw new ArgumentException ( "Expected hash cannot be null or empty" , nameof ( expectedHash ) ) ;
249241 }
250242
251243 string actualHash = ComputeFileHash ( filePath ) ;
252- return string . Equals ( actualHash , expectedHash , StringComparison . OrdinalIgnoreCase ) ;
244+ if ( ! string . Equals ( actualHash , expectedHash , StringComparison . OrdinalIgnoreCase ) )
245+ {
246+ throw new Exception ( $ "File hash mismatch. Expected: { expectedHash } , Actual: { actualHash } ") ;
247+ }
253248 }
254249
255250 public void Dispose ( )
256251 {
257- _httpClient ? . Dispose ( ) ;
252+ if ( _shouldDisposeHttpClient )
253+ {
254+ _httpClient ? . Dispose ( ) ;
255+ }
258256 }
259257}
0 commit comments