Added checking for updates

This commit is contained in:
erik 2025-05-31 00:06:18 +02:00
parent cc15863e81
commit 5ec6525257
19 changed files with 1231 additions and 100 deletions

224
Services/DownloadService.cs Normal file
View 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
View 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();
}
}
}
}

View 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
View 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
}
}

View file

@ -0,0 +1,9 @@
namespace MossyUpdater.Services
{
public interface IQuotesService
{
string GetRandomQuote();
string GetNextQuote();
IEnumerable<string> GetAllQuotes();
}
}

View 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
View 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");
}
}
}

View 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
{
}
}
}
}