using System.Diagnostics; using System.IO.Compression; using System.Net.Http.Json; using System.Reflection; using System.Text.Json; using Windows.Media.Protection.PlayReady; namespace BlueberryUpdater { internal class Program { static string appName = "Blueberry"; static string updaterName = appName + "Updater"; static string installPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), appName); static void Main(string[] args) { if (Directory.Exists(installPath) && File.Exists(Path.Combine(installPath, appName + ".exe"))) Uninstall(); else Install(); } private static void Uninstall() { Console.WriteLine("Would you like to uninstall Blueberry? [y/N]"); var key = Console.ReadLine(); if (key == null ||key.ToLower() != "y") return; var appdata = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), appName); Console.WriteLine("Removing Blueberry..."); var dirs = Directory.GetDirectories(installPath); var files = Directory.GetFiles(installPath); var total = dirs.Length + files.Length; var i = 0; foreach (var dir in dirs) { i++; Directory.Delete(dir, true); DrawProgressBar((int)((double)i / total * 100), dir.Split('\\').Last()); } foreach (var file in files) { i++; if (file.Split('\\').Last() == updaterName + ".exe") continue; File.Delete(file); i++; DrawProgressBar((int)((double)i / total * 100), file.Split('\\').Last()); } Directory.Delete(appdata, true); DrawProgressBar(100, "Done"); Console.WriteLine(); Console.WriteLine("Uninstall Complete!"); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); SelfDelete(); } private static void Install() { try { var client = new HttpClient(); client.DefaultRequestHeaders.UserAgent.ParseAdd("BlueberryUpdater"); string releaseUrl = "https://git.technopunk.space/api/v1/repos/tomi/Blueberry/releases/latest"; // 1. Fetch JSON var root = client.GetFromJsonAsync(releaseUrl).ConfigureAwait(false).GetAwaiter().GetResult(); // 2. Find URL for "payload.zip" string downloadUrl = root.GetProperty("assets") .EnumerateArray() .First(x => x.GetProperty("name").GetString() == "payload.zip") .GetProperty("browser_download_url") .GetString() ?? throw new NullReferenceException(); Console.WriteLine("Downloading Blueberry..."); var stream = DownloadFileWithProgressAsync(client, downloadUrl).GetAwaiter().GetResult(); Console.WriteLine($"Installing to {installPath}..."); // 1. Clean existing install if necessary if (Directory.Exists(installPath)) { Directory.Delete(installPath, true); } Directory.CreateDirectory(installPath); ExtractWithProgress(stream); MoveUpdater(); Console.WriteLine(); Console.WriteLine("Installation Complete!"); // Optional: Create Shortcut logic here } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"Error: {ex.Message}"); } Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } public static void SelfDelete() { string exePath = Process.GetCurrentProcess().MainModule.FileName; string directoryToDelete = Path.GetDirectoryName(exePath); string args = $"/C timeout /t 2 /nobreak > Nul & del \"{exePath}\" & rmdir /q \"{directoryToDelete}\""; ProcessStartInfo psi = new ProcessStartInfo { FileName = "cmd.exe", Arguments = args, WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, UseShellExecute = false }; Process.Start(psi); Environment.Exit(0); } public static void MoveUpdater() { string currentExe = Process.GetCurrentProcess().MainModule.FileName; var updaterPath = Path.Combine(installPath, updaterName + ".exe"); Directory.CreateDirectory(Path.GetDirectoryName(updaterPath)); File.Copy(currentExe, updaterPath, overwrite: true); } static void DrawProgressBar(int percent, string filename) { // Move cursor to start of line Console.CursorLeft = 0; // Limit filename length for clean display string shortName = filename.Length > 20 ? filename.Substring(0, 17) + "..." : filename.PadRight(20); Console.Write("["); int width = Console.WindowWidth - 21; // Width of the bar int progress = (int)((percent / 100.0) * width); // Draw filled part Console.Write(new string('#', progress)); // Draw empty part Console.Write(new string('-', width - progress)); Console.Write($"] {percent}% {shortName}"); } static async Task DownloadFileWithProgressAsync(HttpClient client, string url) { // 1. Get headers only first to check size using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); var totalBytes = response.Content.Headers.ContentLength ?? -1L; var canReportProgress = totalBytes != -1; using var contentStream = await response.Content.ReadAsStreamAsync(); using var memoryStream = new MemoryStream(); var buffer = new byte[8192]; long totalRead = 0; int bytesRead; while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await memoryStream.WriteAsync(buffer, 0, bytesRead); totalRead += bytesRead; if (canReportProgress) DrawProgressBar((int)((double)totalRead / totalBytes * 100), "Downloading..."); } DrawProgressBar(100, "Done"); return memoryStream; } static void ExtractWithProgress(MemoryStream stream) { using (ZipArchive archive = new(stream)) { int totalEntries = archive.Entries.Count; int currentEntry = 0; foreach (ZipArchiveEntry entry in archive.Entries) { currentEntry++; // Calculate percentage int percent = (int)((double)currentEntry / totalEntries * 100); // Draw Progress Bar DrawProgressBar(percent, entry.Name); // Create the full path string destinationPath = Path.GetFullPath(Path.Combine(installPath, entry.FullName)); // Security check: prevent ZipSlip (writing outside target folder) if (!destinationPath.StartsWith(installPath, StringComparison.OrdinalIgnoreCase)) continue; // Handle folders vs files if (string.IsNullOrEmpty(entry.Name)) // It's a directory { Directory.CreateDirectory(destinationPath); } else // It's a file { // Ensure the directory exists (zipped files might not list their dir first) Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); entry.ExtractToFile(destinationPath, overwrite: true); } } } DrawProgressBar(100, "Done"); } } }