diff options
author | Karsten Heimrich <karsten.heimrich@qt.io> | 2025-04-30 15:39:34 +0200 |
---|---|---|
committer | Karsten Heimrich <karsten.heimrich@qt.io> | 2025-05-05 11:03:20 +0000 |
commit | a31bafffadc5466f4a588aee04b98dfd4441166c (patch) | |
tree | 15b0c9513c0c15a209234150fd57dd892ff6cc91 /QtVsTools.Core/FileDownloader.cs | |
parent | 72b7d1f6cef044de76ce0edc444dab84242f9855 (diff) |
Rename QmlLanguageServerDownloader to FileDownloader
Prepares this class for more general purposes, such as downloading
development release packages, etc. Also fixed some typos in the code.
Change-Id: I0988896623f64d437259a857037c5e554a284f2b
Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
Diffstat (limited to 'QtVsTools.Core/FileDownloader.cs')
-rw-r--r-- | QtVsTools.Core/FileDownloader.cs | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/QtVsTools.Core/FileDownloader.cs b/QtVsTools.Core/FileDownloader.cs new file mode 100644 index 00000000..c3dac027 --- /dev/null +++ b/QtVsTools.Core/FileDownloader.cs @@ -0,0 +1,108 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace QtVsTools.Core +{ + internal static class FileDownloader + { + private const int MaxRetries = 3; + private const int MaxRedirects = 10; + private const int BufferSize = 81920; // 80 KB + private static readonly HttpClient HttpClient = new(); + + internal static async Task DownloadAsync(string url, string destPath, + CancellationToken token, + Func<(long CurrentBytes, long MaxBytes), Task> callback = null) + { + var downloadUrl = url; + for (var i = 0; i < MaxRedirects; ++i) { + if (string.IsNullOrEmpty(downloadUrl)) + throw new InvalidOperationException("Invalid download URL."); + + using var response = await RetryPolicyAsync(() => HttpClient.GetAsync(downloadUrl, + HttpCompletionOption.ResponseHeadersRead, token), MaxRetries); + + if (IsRedirect(response.StatusCode) && response.Headers.Location != null) { + var redirectUrl = response.Headers.Location; + if (!redirectUrl.IsAbsoluteUri) + redirectUrl = new Uri(new Uri(downloadUrl), redirectUrl); + + downloadUrl = redirectUrl.ToString(); + continue; + } + + await DownloadFileAsync(response, destPath, token, callback); + return; + } + + throw new InvalidOperationException("Too many redirects."); + } + + private static async Task DownloadFileAsync(HttpResponseMessage response, + string destPath, CancellationToken token, + Func<(long CurrentBytes, long MaxBytes), Task> callback = null) + { + if (!response.IsSuccessStatusCode) { + throw new InvalidOperationException($"Unexpected status: {response.StatusCode} " + + $"for URL: '{response.RequestMessage?.RequestUri}'."); + } + + var contentLength = response.Content.Headers.ContentLength; + long downloadedBytes = 0; + var buffer = new byte[BufferSize]; + + try { + using var fileStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, + FileShare.None, BufferSize, useAsync: true); + using var contentStream = await response.Content.ReadAsStreamAsync(); + + while (true) { + var read = await contentStream.ReadAsync(buffer, 0, buffer.Length, token); + if (read == 0) + break; + + await fileStream.WriteAsync(buffer, 0, read, token); + if (callback != null) + await callback((downloadedBytes += read, contentLength ?? -1)); + } + } catch (IOException ex) { + throw new IOException($"Error writing to file '{destPath}'.", ex); + } + } + + private static async Task<HttpResponseMessage> + RetryPolicyAsync(Func<Task<HttpResponseMessage>> operation, int maxRetries) + { + for (var retry = 0; retry < maxRetries; retry++) { + try { + return await operation(); + } catch (HttpRequestException) when (retry < maxRetries - 1) { + await Task.Delay(TimeSpan.FromSeconds(2 * (retry + 1))); // Exponential backoff + } + } + throw new HttpRequestException("Failed after maximum retry attempts."); + } + + private static bool IsRedirect(HttpStatusCode statusCode) + { + return statusCode switch + { + HttpStatusCode.Moved or HttpStatusCode.MovedPermanently => true, + HttpStatusCode.Found or HttpStatusCode.Redirect => true, + HttpStatusCode.RedirectMethod or HttpStatusCode.SeeOther => true, + HttpStatusCode.NotModified + or HttpStatusCode.UseProxy + or HttpStatusCode.Unused => true, + HttpStatusCode.RedirectKeepVerb or HttpStatusCode.TemporaryRedirect => true, + _ => false + }; + } + } +} |