Added checking for updates
This commit is contained in:
parent
cc15863e81
commit
5ec6525257
19 changed files with 1231 additions and 100 deletions
224
Services/DownloadService.cs
Normal file
224
Services/DownloadService.cs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue