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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Services/FileLogger.cs
Normal file
52
Services/FileLogger.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.IO;
|
||||
|
||||
namespace MossyUpdater.Services
|
||||
{
|
||||
public class FileLogger : ILogger
|
||||
{
|
||||
private readonly string _logPath;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public FileLogger()
|
||||
{
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var appFolder = Path.Combine(appDataPath, "MossyUpdater");
|
||||
Directory.CreateDirectory(appFolder);
|
||||
_logPath = Path.Combine(appFolder, "log.txt");
|
||||
}
|
||||
|
||||
public async Task LogInfoAsync(string message)
|
||||
{
|
||||
await WriteLogAsync(LogLevel.Info, message);
|
||||
}
|
||||
|
||||
public async Task LogWarningAsync(string message)
|
||||
{
|
||||
await WriteLogAsync(LogLevel.Warning, message);
|
||||
}
|
||||
|
||||
public async Task LogErrorAsync(string message, Exception? exception = null)
|
||||
{
|
||||
var fullMessage = exception != null ? $"{message} - Exception: {exception}" : message;
|
||||
await WriteLogAsync(LogLevel.Error, fullMessage);
|
||||
}
|
||||
|
||||
private async Task WriteLogAsync(LogLevel level, string message)
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
var logEntry = $"[{timestamp}] [{level}] {message}{Environment.NewLine}";
|
||||
await File.AppendAllTextAsync(_logPath, logEntry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Services/IDownloadService.cs
Normal file
37
Services/IDownloadService.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface IDownloadService
|
||||
{
|
||||
Task<DownloadResult> DownloadFileAsync(string url, string destinationPath, IProgress<DownloadProgress>? progress = null, CancellationToken cancellationToken = default);
|
||||
Task<FileUpdateInfo> CheckForUpdatesAsync(string url, string localFilePath, CancellationToken cancellationToken = default);
|
||||
void UnblockFile(string filePath);
|
||||
}
|
||||
|
||||
public class DownloadResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public long BytesDownloaded { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
}
|
||||
|
||||
public class DownloadProgress
|
||||
{
|
||||
public long BytesReceived { get; set; }
|
||||
public long? TotalBytes { get; set; }
|
||||
public double ProgressPercentage => TotalBytes.HasValue && TotalBytes > 0
|
||||
? (double)BytesReceived / TotalBytes.Value * 100
|
||||
: 0;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class FileUpdateInfo
|
||||
{
|
||||
public bool UpdateAvailable { get; set; }
|
||||
public DateTime? RemoteLastModified { get; set; }
|
||||
public DateTime? LocalLastModified { get; set; }
|
||||
public long? RemoteSize { get; set; }
|
||||
public long? LocalSize { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
}
|
||||
16
Services/ILogger.cs
Normal file
16
Services/ILogger.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
Task LogInfoAsync(string message);
|
||||
Task LogWarningAsync(string message);
|
||||
Task LogErrorAsync(string message, Exception? exception = null);
|
||||
}
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
||||
9
Services/IQuotesService.cs
Normal file
9
Services/IQuotesService.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface IQuotesService
|
||||
{
|
||||
string GetRandomQuote();
|
||||
string GetNextQuote();
|
||||
IEnumerable<string> GetAllQuotes();
|
||||
}
|
||||
}
|
||||
19
Services/ISettingsService.cs
Normal file
19
Services/ISettingsService.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<AppSettings> LoadSettingsAsync();
|
||||
Task SaveSettingsAsync(AppSettings settings);
|
||||
}
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public string LastUsedFolder { get; set; } = string.Empty;
|
||||
public string DefaultUrl { get; set; } = "https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/MosswartMassacre/raw/branch/spawn-detection/MosswartMassacre/bin/Release/MosswartMassacre.dll";
|
||||
public string DefaultFilename { get; set; } = "MosswartMassacre.dll";
|
||||
public double WindowWidth { get; set; } = 550;
|
||||
public double WindowHeight { get; set; } = 450;
|
||||
public bool CheckForUpdatesOnStartup { get; set; } = true;
|
||||
public int DownloadTimeoutMinutes { get; set; } = 10;
|
||||
}
|
||||
}
|
||||
80
Services/QuotesService.cs
Normal file
80
Services/QuotesService.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public class QuotesService : IQuotesService
|
||||
{
|
||||
private readonly string[] _quotes = new[]
|
||||
{
|
||||
"I don't run late. I glide fashionably through time.",
|
||||
"Time is a flat circle—and I'm still trying to find the starting point.",
|
||||
"Being on time is like spotting a unicorn: rare, beautiful, and definitely not me.",
|
||||
"Time zones are a social construct. I operate on instinct.",
|
||||
"Every time I try to be early, time responds with traffic.",
|
||||
"Time is an illusion. My ETA is performance art.",
|
||||
"I treat deadlines like speed limits: suggestions.",
|
||||
"I don't set alarms. I set vague intentions.",
|
||||
"If you want me to be on time, lie about when it starts.",
|
||||
"Why stress about time when you can just embrace chaos?",
|
||||
"My to-do list is a historical document.",
|
||||
"Time and I broke up. It was toxic.",
|
||||
"I'm not late. I'm temporally creative.",
|
||||
"'In five minutes' means something different in my language.",
|
||||
"I don't track time. I let it track me—poorly.",
|
||||
"Time management is easy. Just avoid doing anything.",
|
||||
"Time is fleeting. So is my attention span.",
|
||||
"I live in the moment. Just never the right one.",
|
||||
"Time doesn't control me. It just heavily inconveniences me.",
|
||||
"I'm a time optimist: always wrong, always hopeful.",
|
||||
"Punctuality is for people who don't trust spontaneity.",
|
||||
"My life is a series of missed trains and strong coffee.",
|
||||
"If time is a river, I'm definitely upstream without a paddle.",
|
||||
"I don't watch the clock. I avoid eye contact with it.",
|
||||
"'ASAP' means 'as soon as procrastination ends.'",
|
||||
"Early is suspicious. On time is impressive. Late is expected.",
|
||||
"Time management? I prefer time improvisation.",
|
||||
"I like to keep time on its toes. Mostly by ignoring it.",
|
||||
"Alarms are like plot twists. I didn't see it coming, and I still ignore it.",
|
||||
"If being late was a sport, I'd already have missed the medal ceremony.",
|
||||
"I don't lose track of time. I just pretend it doesn't exist.",
|
||||
"Watches are decorative lies.",
|
||||
"Time flies. I miss every flight.",
|
||||
"My planner is just a coloring book for stress.",
|
||||
"I'm not in a hurry. I'm in denial.",
|
||||
"I live in the now, just usually a little bit after everyone else.",
|
||||
"My calendar and I are estranged, but we're working on it.",
|
||||
"The early bird catches the worm. I order food later.",
|
||||
"Time moves fast. I move slower.",
|
||||
"I was going to be on time, but then I remembered who I am.",
|
||||
"I see '9:00 AM' and read it as 'guideline.'",
|
||||
"Every plan is a maybe with extra steps.",
|
||||
"I don't have time blindness—I just have time indifference.",
|
||||
"I'm not late. Reality is early.",
|
||||
"Time and I aren't speaking after what happened last Monday.",
|
||||
"Being on time is impressive. Being consistently late is a brand.",
|
||||
"I'm in sync with the universe—just in a different dimension.",
|
||||
"My sense of time is like my sock drawer: chaotic and mostly missing.",
|
||||
"Some people chase time. I let it wander off.",
|
||||
"I'm not running late. I'm setting the tone for a relaxed experience."
|
||||
};
|
||||
|
||||
private int _currentIndex = 0;
|
||||
private readonly Random _random = new();
|
||||
|
||||
public string GetRandomQuote()
|
||||
{
|
||||
var index = _random.Next(_quotes.Length);
|
||||
return $"\"{_quotes[index]}\" - Time According to Alex";
|
||||
}
|
||||
|
||||
public string GetNextQuote()
|
||||
{
|
||||
var quote = $"\"{_quotes[_currentIndex]}\" - Time According to Alex";
|
||||
_currentIndex = (_currentIndex + 1) % _quotes.Length;
|
||||
return quote;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAllQuotes()
|
||||
{
|
||||
return _quotes.Select(q => $"\"{q}\" - Time According to Alex");
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Services/SettingsService.cs
Normal file
50
Services/SettingsService.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MossyUpdater.Services
|
||||
{
|
||||
public class SettingsService : ISettingsService
|
||||
{
|
||||
private readonly string _settingsPath;
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var appFolder = Path.Combine(appDataPath, "MossyUpdater");
|
||||
Directory.CreateDirectory(appFolder);
|
||||
_settingsPath = Path.Combine(appFolder, "settings.json");
|
||||
}
|
||||
|
||||
public async Task<AppSettings> LoadSettingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_settingsPath))
|
||||
return new AppSettings();
|
||||
|
||||
var json = await File.ReadAllTextAsync(_settingsPath);
|
||||
var settings = JsonSerializer.Deserialize<AppSettings>(json);
|
||||
return settings ?? new AppSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new AppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveSettingsAsync(AppSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
await File.WriteAllTextAsync(_settingsPath, json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue