224 lines
No EOL
9.6 KiB
C#
224 lines
No EOL
9.6 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
|
|
namespace MossyUpdater.Services
|
|
{
|
|
public class DownloadService : IDownloadService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly ILogger _logger;
|
|
private const int MaxRetries = 3;
|
|
private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(2);
|
|
|
|
public DownloadService(HttpClient httpClient, ILogger logger)
|
|
{
|
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public async Task<DownloadResult> DownloadFileAsync(string url, string destinationPath, IProgress<DownloadProgress>? progress = null, CancellationToken cancellationToken = default)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var result = new DownloadResult();
|
|
|
|
await _logger.LogInfoAsync($"Starting download from {url} to {destinationPath}");
|
|
|
|
for (int attempt = 1; attempt <= MaxRetries; attempt++)
|
|
{
|
|
try
|
|
{
|
|
if (attempt > 1)
|
|
{
|
|
progress?.Report(new DownloadProgress { Status = $"Retrying download (attempt {attempt}/{MaxRetries})..." });
|
|
await _logger.LogInfoAsync($"Retry attempt {attempt}/{MaxRetries}");
|
|
await Task.Delay(RetryDelay, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
progress?.Report(new DownloadProgress { Status = "Starting download..." });
|
|
}
|
|
|
|
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var totalBytes = response.Content.Headers.ContentLength;
|
|
progress?.Report(new DownloadProgress { TotalBytes = totalBytes, Status = "Downloading..." });
|
|
|
|
var directory = Path.GetDirectoryName(destinationPath);
|
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 8192, useAsync: true);
|
|
|
|
var buffer = new byte[8192];
|
|
long totalBytesRead = 0;
|
|
int bytesRead;
|
|
|
|
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
|
|
{
|
|
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
|
|
totalBytesRead += bytesRead;
|
|
|
|
progress?.Report(new DownloadProgress
|
|
{
|
|
BytesReceived = totalBytesRead,
|
|
TotalBytes = totalBytes,
|
|
Status = $"Downloaded {FormatBytes(totalBytesRead)}" + (totalBytes.HasValue ? $" of {FormatBytes(totalBytes.Value)}" : "")
|
|
});
|
|
}
|
|
|
|
result.Success = true;
|
|
result.BytesDownloaded = totalBytesRead;
|
|
progress?.Report(new DownloadProgress
|
|
{
|
|
BytesReceived = totalBytesRead,
|
|
TotalBytes = totalBytes,
|
|
Status = "Download completed successfully"
|
|
});
|
|
|
|
await _logger.LogInfoAsync($"Download completed successfully. Downloaded {FormatBytes(totalBytesRead)} in {stopwatch.Elapsed:mm\\:ss}");
|
|
break;
|
|
}
|
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException && attempt < MaxRetries)
|
|
{
|
|
await _logger.LogWarningAsync($"Timeout on attempt {attempt}: {ex.Message}");
|
|
continue;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
result.ErrorMessage = "Download was cancelled";
|
|
progress?.Report(new DownloadProgress { Status = "Download cancelled" });
|
|
await _logger.LogInfoAsync("Download was cancelled by user");
|
|
|
|
if (File.Exists(destinationPath))
|
|
{
|
|
try { File.Delete(destinationPath); } catch { }
|
|
}
|
|
break;
|
|
}
|
|
catch (HttpRequestException ex) when (IsRetryableHttpError(ex) && attempt < MaxRetries)
|
|
{
|
|
await _logger.LogWarningAsync($"Retryable error on attempt {attempt}: {ex.Message}");
|
|
continue;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.ErrorMessage = GetFriendlyErrorMessage(ex);
|
|
progress?.Report(new DownloadProgress { Status = $"Error: {result.ErrorMessage}" });
|
|
await _logger.LogErrorAsync($"Download failed on attempt {attempt}", ex);
|
|
|
|
if (File.Exists(destinationPath))
|
|
{
|
|
try { File.Delete(destinationPath); } catch { }
|
|
}
|
|
|
|
if (attempt >= MaxRetries)
|
|
break;
|
|
}
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
result.Duration = stopwatch.Elapsed;
|
|
return result;
|
|
}
|
|
|
|
public async Task<FileUpdateInfo> CheckForUpdatesAsync(string url, string localFilePath, CancellationToken cancellationToken = default)
|
|
{
|
|
var updateInfo = new FileUpdateInfo();
|
|
|
|
try
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Head, url);
|
|
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
updateInfo.RemoteLastModified = response.Content.Headers.LastModified?.DateTime;
|
|
updateInfo.RemoteSize = response.Content.Headers.ContentLength;
|
|
|
|
if (File.Exists(localFilePath))
|
|
{
|
|
var localFileInfo = new FileInfo(localFilePath);
|
|
updateInfo.LocalLastModified = localFileInfo.LastWriteTime;
|
|
updateInfo.LocalSize = localFileInfo.Length;
|
|
|
|
updateInfo.UpdateAvailable =
|
|
updateInfo.RemoteLastModified.HasValue &&
|
|
updateInfo.RemoteLastModified > updateInfo.LocalLastModified ||
|
|
(updateInfo.RemoteSize.HasValue && updateInfo.RemoteSize != updateInfo.LocalSize);
|
|
}
|
|
else
|
|
{
|
|
updateInfo.UpdateAvailable = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
updateInfo.ErrorMessage = $"Failed to check for updates: {response.StatusCode}";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
updateInfo.ErrorMessage = $"Error checking for updates: {ex.Message}";
|
|
}
|
|
|
|
return updateInfo;
|
|
}
|
|
|
|
public void UnblockFile(string filePath)
|
|
{
|
|
try
|
|
{
|
|
var zoneIdentifier = filePath + ":Zone.Identifier";
|
|
if (File.Exists(zoneIdentifier))
|
|
{
|
|
File.Delete(zoneIdentifier);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
private static string FormatBytes(long bytes)
|
|
{
|
|
string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
|
|
int counter = 0;
|
|
decimal number = bytes;
|
|
|
|
while (Math.Round(number / 1024) >= 1)
|
|
{
|
|
number /= 1024;
|
|
counter++;
|
|
}
|
|
|
|
return $"{number:n1} {suffixes[counter]}";
|
|
}
|
|
|
|
private static bool IsRetryableHttpError(HttpRequestException ex)
|
|
{
|
|
return ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) ||
|
|
ex.Message.Contains("connection", StringComparison.OrdinalIgnoreCase) ||
|
|
ex.Message.Contains("network", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static string GetFriendlyErrorMessage(Exception ex)
|
|
{
|
|
return ex switch
|
|
{
|
|
HttpRequestException httpEx when httpEx.Message.Contains("404") => "File not found on server",
|
|
HttpRequestException httpEx when httpEx.Message.Contains("403") => "Access denied to file",
|
|
HttpRequestException httpEx when httpEx.Message.Contains("timeout") => "Download timed out",
|
|
UnauthorizedAccessException => "Access denied to destination folder",
|
|
DirectoryNotFoundException => "Destination folder does not exist",
|
|
IOException => "File operation failed",
|
|
_ => ex.Message
|
|
};
|
|
}
|
|
}
|
|
} |