247 lines
8.6 KiB
C#
247 lines
8.6 KiB
C#
using System.Diagnostics;
|
|
using System.IO.Compression;
|
|
|
|
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 json = client.GetStringAsync(releaseUrl).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
json = json.Split("\"name\":\"payload.zip\",")[1];
|
|
|
|
|
|
// 2. Find URL for "payload.zip"
|
|
string downloadUrl = json.Split("\"browser_download_url\":\"")[1].Split("\"")[0];
|
|
|
|
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)
|
|
{
|
|
percent = Math.Clamp(percent, 0, 100);
|
|
|
|
// 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 - 31; // 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<MemoryStream> 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();
|
|
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");
|
|
Console.WriteLine();
|
|
return memoryStream;
|
|
}
|
|
static void ExtractWithProgress(MemoryStream stream)
|
|
{
|
|
stream.Position = 0;
|
|
using (ZipArchive archive = new(stream, ZipArchiveMode.Read))
|
|
{
|
|
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");
|
|
Console.WriteLine();
|
|
}
|
|
}
|
|
|
|
public class Dto
|
|
{
|
|
public class Item
|
|
{
|
|
public Assets[] assets { get; set; }
|
|
}
|
|
public class Assets
|
|
{
|
|
public string name { get; set; }
|
|
public string browser_download_url { get; set; }
|
|
}
|
|
}
|
|
}
|