diff --git a/Photino-Boilerplate.csproj b/BrikInstaller.csproj
similarity index 85%
rename from Photino-Boilerplate.csproj
rename to BrikInstaller.csproj
index 2b7ed89..f38a49c 100644
--- a/Photino-Boilerplate.csproj
+++ b/BrikInstaller.csproj
@@ -7,6 +7,7 @@
enable
enable
false
+ src/resources/build/icon.ico
@@ -18,6 +19,11 @@
+
+
+ PreserveNewest
+
+
diff --git a/Photino-Boilerplate.sln b/BrikInstaller.sln
similarity index 86%
rename from Photino-Boilerplate.sln
rename to BrikInstaller.sln
index b092ffb..b67ed25 100644
--- a/Photino-Boilerplate.sln
+++ b/BrikInstaller.sln
@@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Photino-Boilerplate", "Photino-Boilerplate.csproj", "{5FF73963-D009-62ED-7350-FF17EC625A26}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrikInstaller", "BrikInstaller.csproj", "{5FF73963-D009-62ED-7350-FF17EC625A26}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/src/main/Constants.cs b/src/main/Constants.cs
new file mode 100644
index 0000000..6f490d6
--- /dev/null
+++ b/src/main/Constants.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Text.Json;
+using BrikInstaller.Utils;
+
+namespace BrikInstaller;
+
+public static class Constants {
+ public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ public static readonly string os = OsHelper.getOperatingSystem();
+ public static readonly string arch = OsHelper.getArchitecture();
+
+ public static readonly string env = "DEV";
+ public static readonly string __dirname = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
+ public static readonly string BaseDirectory = AppDomain.CurrentDomain.BaseDirectory;
+
+ public static class URLs {
+ public static readonly string basePoint = "https://brik.azures.fr/products";
+ }
+}
\ No newline at end of file
diff --git a/src/main/Program.cs b/src/main/Program.cs
index 2f6371f..8a27a43 100644
--- a/src/main/Program.cs
+++ b/src/main/Program.cs
@@ -1,28 +1,29 @@
-using System.Reflection;
-using System.Text.Json;
+using Photino.NET;
using BrikPackager;
-using PhotinoBoilerplate.Utils;
-using Photino.NET;
+using System.Text.Json;
+using BrikInstaller.Utils;
+using BrikInstaller.Schemas;
+using BrikInstaller.Services;
-namespace PhotinoBoilerplate;
+namespace BrikInstaller;
class Program{
-
- public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
- private static readonly string __dirname = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
-
[STAThread]
static void Main(string[] args) {
CreateMainWindow().WaitForClose();
}
public static PhotinoWindow CreateMainWindow() {
- BrikPackage bpkg = new BrikPackage(Path.Combine(__dirname, "root.dat"), 66);
+ BrikPackage bpkg = new BrikPackage(Path.Combine(Constants.__dirname, "root.dat"), 66);
List entries = bpkg.ListEntries();
+ foreach (string entry in entries) {
+ Console.WriteLine(entry);
+ }
var window = new PhotinoWindow()
- .SetTitle("Photino Boilerplate")
+ .SetTitle("Brik Installer")
.SetUseOsDefaultLocation(false)
.SetUseOsDefaultSize(false)
+ .SetChromeless(true)
.RegisterCustomSchemeHandler("http", (object sender, string scheme, string url, out string contentType) =>{
Uri uri = new Uri(url);
switch (uri.Host){
@@ -38,41 +39,112 @@ class Program{
});
window.WindowCreated += (sender, e) => {
+ string iconPath = Path.Combine(Constants.BaseDirectory, "src", "resources", "build", "icon.ico");
SetupBridge(window);
- WindowHelper.setSize(window, 900, 600);
+ WindowHelper.setSize(window, 465, 290);
+ WindowHelper.RemoveTitleBar(window);
+ WindowHelper.ForceTaskbarIcon(window, iconPath);
window.Center();
};
-
- window.Load("http://internal/index.html");
-
+ LoadPage(window, "index.html");
return window;
}
- public static void SetupBridge(PhotinoWindow window) {
+ public static void SetupBridge(PhotinoWindow window) {
window.RegisterWebMessageReceivedHandler(async (object? sender, string message) => {
try {
var json = JsonDocument.Parse(message);
string requestId = json.RootElement.GetProperty("requestId").GetString()!;
string method = json.RootElement.GetProperty("functionName").GetString()!;
var jsonPayload = json.RootElement.GetProperty("payload");
+
+ object? responsePayload = null;
var pw = (PhotinoWindow)sender!;
- object? payload = null;
-
switch (method) {
- case "hello::world":
- payload = "Hello from C#";
+ case "network::fetch":
+ responsePayload = await NetworkService.HandleFetchRequest(jsonPayload);
+ break;
+ case "check::requirements":
+ if (jsonPayload.TryGetProperty("soft", out var soft)) {
+ RequirementCheckingResponse requirementChecking = await RequirementsService.CheckRequirementsAsync(pw, soft.GetString()!);
+ if (!requirementChecking.Success) {
+ IPC.sendToRenderer(window, "requirement::failed", null!);
+ } else {
+ IPC.sendToRenderer(window, "requirement::success", null!);
+ }
+ } else {
+ responsePayload = new { success = false, error = "Soft name missing, wtf?" };
+ }
+ break;
+ case "browser::open":
+ if (jsonPayload.TryGetProperty("url", out var url)) {
+ BashUtils.OpenUrl(url.ToString());
+ } else {
+ responsePayload = new { success = false, error = "Soft name missing, wtf?" };
+ }
+ break;
+ case "installer::elevate":
+ InstallationService.ElevateAndRestart();
+ responsePayload = new { success = true };
+ break;
+ case "installer::testInstallationPath":
+ if (jsonPayload.TryGetProperty("path", out var installPath)) {
+ bool canWrite = InstallationService.CanCreateInPath(installPath.ToString());
+ if (!canWrite) {
+ InstallationService.ElevateAndRestart();
+ responsePayload = new { success = canWrite };
+ } else {
+ responsePayload = new { success = canWrite };
+ }
+ } else {
+ responsePayload = new { success = false };
+ }
+ break;
+ case "dialog::selectDirectory":
+ string[] selectedFolders = window.ShowOpenFolder();
+
+ if (selectedFolders != null && selectedFolders.Length > 0) {
+ responsePayload = new { success = true, path = selectedFolders[0] };
+ } else {
+ responsePayload = new { success = false };
+ }
+ break;
+ case "installer::defaultInstallPath":
+ if (jsonPayload.TryGetProperty("soft", out var productName)) {
+ string defaultInstallPath = InstallationService.GetDefaultInstallationPath(productName.ToString());
+ responsePayload = new { success = true, path = defaultInstallPath };
+ } else {
+ responsePayload = new { success = false };
+ }
+ break;
+ case "installer::install":
+ if (jsonPayload.TryGetProperty("soft", out var softName) && jsonPayload.TryGetProperty("path", out var softInstallPath)) {
+ try {
+ await GenericFilesService.SyncFilesAsync($"{Constants.URLs.basePoint}/{softName}/{Constants.os}-{Constants.arch}/download/", $"{Constants.URLs.basePoint}/{softName}/{Constants.os}-{Constants.arch}/manifest", softInstallPath.ToString());
+ responsePayload = new { success = true };
+ } catch (Exception) {
+ responsePayload = new { success = false };
+ }
+ }
break;
-
}
- var response = new { requestId, payload };
- window.SendWebMessage(JsonSerializer.Serialize(response, _jsonOptions));
+ var finalResponse = new { requestId, payload = responsePayload };
+ window.SendWebMessage(JsonSerializer.Serialize(finalResponse, Constants._jsonOptions));
} catch (Exception ex) {
Console.WriteLine($"Error stacktrace: {ex.StackTrace}");
Console.WriteLine($"Bridge error: {ex.Message}");
}
});
}
+
+ public static void LoadPage(PhotinoWindow window, string url) {
+ if (Constants.env == "PROD") {
+ window.Load($"http://internal/{url}");
+ } else {
+ window.Load($"src/resources/renderer/{url}");
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/helpers/Bash.cs b/src/main/helpers/Bash.cs
new file mode 100644
index 0000000..b81bd7d
--- /dev/null
+++ b/src/main/helpers/Bash.cs
@@ -0,0 +1,70 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace BrikInstaller.Utils;
+
+public static class BashUtils {
+ public static string GetCommandOutput(string command) {
+ string shell = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/bash";
+ string argPrefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "/c" : "-c";
+
+ string formattedCommand = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? command
+ : $"\"{command.Replace("\"", "\\\"")}\"";
+
+ using var proc = new Process {
+ StartInfo = new ProcessStartInfo {
+ FileName = shell,
+ Arguments = $"{argPrefix} {formattedCommand}",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ StandardOutputEncoding = System.Text.Encoding.UTF8
+ }
+ };
+
+ try {
+ proc.Start();
+ string output = proc.StandardOutput.ReadToEnd();
+ string error = proc.StandardError.ReadToEnd();
+ proc.WaitForExit();
+
+ return string.IsNullOrEmpty(output) ? error : output;
+ } catch (Exception ex) {
+ return $"Execution error : {ex.Message}";
+ }
+ }
+
+ public static string GetBashCommandOutput(string command) {
+ using var proc = new Process {
+ StartInfo = new ProcessStartInfo {
+ FileName = "/bin/bash",
+ Arguments = $"-c \"{command}\"",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ CreateNoWindow = true
+ }
+ };
+ proc.Start();
+ return proc.StandardOutput.ReadToEnd();
+ }
+
+ public static void OpenUrl(string url) {
+ try {
+ Process.Start(url);
+ }
+ catch {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+ url = url.Replace("&", "^&");
+ Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
+ } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
+ Process.Start("xdg-open", url);
+ } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ Process.Start("open", url);
+ } else {
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/helpers/Http.cs b/src/main/helpers/Http.cs
new file mode 100644
index 0000000..d783549
--- /dev/null
+++ b/src/main/helpers/Http.cs
@@ -0,0 +1,51 @@
+using System.Net.Http.Json;
+using BrikInstaller;
+
+public static class HttpHelper {
+ private static readonly HttpClient Http = new HttpClient();
+
+ public static async Task FetchAsync(string url, HttpMethod? method = null, object? body = null, Dictionary? headers = null) {
+ Console.WriteLine($"Fetching {url} using {method} method");
+ var request = new HttpRequestMessage(method ?? HttpMethod.Get, url);
+
+ if (headers != null) {
+ foreach (var header in headers) {
+ request.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }
+
+ if (body != null) {
+ if (body is HttpContent httpContent) {
+ request.Content = httpContent;
+ } else if (body is string s) {
+ request.Content = new StringContent(s, System.Text.Encoding.UTF8, "application/json");
+ } else {
+ request.Content = JsonContent.Create(body, options: Constants._jsonOptions);
+ }
+ }
+
+ request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+
+ return await Http.SendAsync(request);
+ }
+
+ public static async Task DownloadFileAsync(string url, string destinationPath) {
+ Console.WriteLine($"Downloading file from {url} to {destinationPath}");
+ string? directory = Path.GetDirectoryName(destinationPath);
+ if (!string.IsNullOrEmpty(directory)) {
+ Directory.CreateDirectory(directory);
+ }
+
+ if (File.Exists(destinationPath)) {
+ File.Delete(destinationPath);
+ }
+
+ using var response = await HttpHelper.FetchAsync(url);
+ response.EnsureSuccessStatusCode();
+
+ using var streamToReadFrom = await response.Content.ReadAsStreamAsync();
+ using var streamToWriteTo = File.Create(destinationPath);
+
+ await streamToReadFrom.CopyToAsync(streamToWriteTo);
+ }
+}
\ No newline at end of file
diff --git a/src/main/helpers/Os.cs b/src/main/helpers/Os.cs
new file mode 100644
index 0000000..fa38fbd
--- /dev/null
+++ b/src/main/helpers/Os.cs
@@ -0,0 +1,31 @@
+using System.Runtime.InteropServices;
+
+namespace BrikInstaller.Utils;
+
+public static class OsHelper {
+
+ public static string getOperatingSystem() {
+ string os = "unknown";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+ os = "win";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
+ os = "linux";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ os = "macos";
+ }
+ return os;
+ }
+
+ public static string getArchitecture() {
+ string arch = RuntimeInformation.OSArchitecture switch {
+ Architecture.X64 => "x64",
+ Architecture.X86 => "x86",
+ Architecture.Arm64 => "arm64",
+ Architecture.Arm => "arm",
+ _ => RuntimeInformation.OSArchitecture.ToString().ToLower()
+ };
+ return arch;
+ }
+}
\ No newline at end of file
diff --git a/src/main/helpers/Window.cs b/src/main/helpers/Window.cs
index 91ed8cb..9843b05 100644
--- a/src/main/helpers/Window.cs
+++ b/src/main/helpers/Window.cs
@@ -1,13 +1,39 @@
using Photino.NET;
using System.Drawing;
+using System.Runtime.InteropServices;
-namespace PhotinoBoilerplate.Utils;
+namespace BrikInstaller.Utils;
public static class WindowHelper {
private const float StandardDpi = 96.0f;
+
+ [DllImport("user32.dll")]
+ static extern int GetWindowLong(IntPtr hWnd, int nIndex);
+ [DllImport("user32.dll")]
+ static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
+ [DllImport("user32.dll")]
+ static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
+ [DllImport("user32.dll", CharSet = CharSet.Auto)]
+ static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
+ [DllImport("user32.dll", SetLastError = true)]
+ static extern IntPtr LoadImage(IntPtr hinst, string lpszName, uint uType, int cxDesired, int cyDesired, uint fuLoad);
-
+ const int GWL_STYLE = -16;
+ const int WS_CAPTION = 0x00C00000;
+ const int WS_THICKFRAME = 0x00040000;
+ const int WS_MINIMIZEBOX = 0x00020000;
+ const int WS_MAXIMIZEBOX = 0x00010000;
+ const int WS_SYSMENU = 0x00080000;
+ const uint SWP_FRAMECHANGED = 0x0020;
+ const uint SWP_NOMOVE = 0x0002;
+ const uint SWP_NOSIZE = 0x0001;
+ const uint SWP_NOZORDER = 0x0004;
+ const uint WM_SETICON = 0x0080;
+ const IntPtr ICON_SMALL = 0;
+ const IntPtr ICON_BIG = 1;
+ const uint IMAGE_ICON = 1;
+ const uint LR_LOADFROMFILE = 0x00000010;
public static Size GetScaledSize(PhotinoWindow window, int logicalWidth, int logicalHeight) {
float currentDpi = window.ScreenDpi > 0 ? window.ScreenDpi : StandardDpi;
@@ -22,8 +48,27 @@ public static class WindowHelper {
public static void setSize(PhotinoWindow window, int width, int height) {
Size targetSize = GetScaledSize(window, width, height);
window
- .SetResizable(true)
+ .SetResizable(false)
.SetSize(targetSize)
.SetMinSize(targetSize.Width, targetSize.Height);
}
+
+ public static void RemoveTitleBar(PhotinoWindow window) {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+ IntPtr hWnd = window.WindowHandle;
+ int style = GetWindowLong(hWnd, GWL_STYLE);
+ style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
+ SetWindowLong(hWnd, GWL_STYLE, style);
+ SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
+ }
+ }
+
+ public static void ForceTaskbarIcon(PhotinoWindow window, string iconPath) {
+ IntPtr hWnd = window.WindowHandle;
+ IntPtr hIcon = LoadImage(IntPtr.Zero, iconPath, IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
+ if (hIcon != IntPtr.Zero) {
+ SendMessage(hWnd, WM_SETICON, ICON_SMALL, hIcon);
+ SendMessage(hWnd, WM_SETICON, ICON_BIG, hIcon);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/schemas/GenericBrikFiles.cs b/src/main/schemas/GenericBrikFiles.cs
new file mode 100644
index 0000000..4a00f7f
--- /dev/null
+++ b/src/main/schemas/GenericBrikFiles.cs
@@ -0,0 +1,20 @@
+namespace BrikInstaller.Schemas;
+
+public class GenericArtifact {
+ public string Path { get; set; } = string.Empty;
+ public string Sha1 { get; set; } = string.Empty;
+ public long Size { get; set; }
+ public string Url { get; set; } = string.Empty;
+}
+
+public class BrikFilesIndex {
+ public List Root { get; set; } = new();
+}
+
+public class ArtifactEntry {
+ public ArtifactContainer Downloads { get; set; } = null!;
+}
+
+public class ArtifactContainer {
+ public GenericArtifact Artifact { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/main/schemas/Product.cs b/src/main/schemas/Product.cs
new file mode 100644
index 0000000..35262b4
--- /dev/null
+++ b/src/main/schemas/Product.cs
@@ -0,0 +1,10 @@
+namespace BrikInstaller.Schemas;
+
+public class ProductRequirement {
+ public string? Id { get; set; }
+ public string? Name { get; set; }
+ public string? CheckCommand { get; set; }
+ public string? ExpectedOutputPattern { get; set; }
+ public string? InstallationMode { get; set; }
+ public string? InstallationUrl { get; set; }
+}
\ No newline at end of file
diff --git a/src/main/schemas/Requirement.cs b/src/main/schemas/Requirement.cs
new file mode 100644
index 0000000..5f71628
--- /dev/null
+++ b/src/main/schemas/Requirement.cs
@@ -0,0 +1,9 @@
+namespace BrikInstaller.Schemas;
+
+public class RequirementCheckingResponse {
+ public bool Success { get; set; }
+ public string? Error { get; set; }
+ public int? Status { get; set; }
+
+ public bool? hasNoDeps { get; set; }
+}
\ No newline at end of file
diff --git a/src/main/services/Files.cs b/src/main/services/Files.cs
new file mode 100644
index 0000000..b7341d8
--- /dev/null
+++ b/src/main/services/Files.cs
@@ -0,0 +1,56 @@
+using System.Security.Cryptography;
+using System.Net.Http.Json;
+using BrikInstaller.Schemas;
+
+namespace BrikInstaller.Services;
+
+public static class GenericFilesService {
+ private static readonly HttpClient _httpClient = new();
+
+ public static async Task SyncFilesAsync(string rootEndpoint, string apiUrl, string destinationDir) {
+ try {
+ var index = await _httpClient.GetFromJsonAsync(apiUrl);
+ if (index?.Root == null) return;
+
+ var artifacts = index.Root.Select(r => r.Downloads.Artifact).ToList();
+ await ProcessDownloadsAsync(rootEndpoint, artifacts, destinationDir);
+ }
+ catch (Exception ex) {
+ Console.WriteLine($"[Sync Error] {ex.Message}");
+ }
+ }
+
+ private static async Task ProcessDownloadsAsync(string rootEndpoint, List artifacts, string baseDir) {
+ var queue = artifacts.Where(a => !IsFileValid(Path.Combine(baseDir, a.Path), a)).ToList();
+
+ if (queue.Count == 0) return;
+
+ await Parallel.ForEachAsync(queue, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (artifact, token) => {
+ string localPath = Path.Combine(baseDir, artifact.Path);
+
+ try {
+ await HttpHelper.DownloadFileAsync(rootEndpoint + artifact.Url, localPath);
+
+ if (!IsFileValid(localPath, artifact)) {
+ Console.WriteLine($"[Validation Failed] {artifact.Path}");
+ }
+ }
+ catch (Exception ex) {
+ Console.WriteLine($"[Download Error] {artifact.Path}: {ex.Message}");
+ }
+ });
+ }
+
+ public static bool IsFileValid(string filePath, GenericArtifact expected) {
+ if (!File.Exists(filePath)) return false;
+
+ var info = new FileInfo(filePath);
+ if (info.Length != expected.Size) return false;
+
+ using var stream = File.OpenRead(filePath);
+ byte[] hashBytes = SHA1.HashData(stream);
+ string hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
+
+ return hash == expected.Sha1.ToLowerInvariant();
+ }
+}
\ No newline at end of file
diff --git a/src/main/services/IPC.cs b/src/main/services/IPC.cs
new file mode 100644
index 0000000..a5ae4fb
--- /dev/null
+++ b/src/main/services/IPC.cs
@@ -0,0 +1,19 @@
+using System.Text.Json;
+using Photino.NET;
+
+namespace BrikInstaller.Services;
+
+public static class IPC {
+
+ public static void sendRequirementCheckConsoleLog(PhotinoWindow window, string log) {
+ sendToRenderer(window, "requirement::log", log);
+ }
+
+ public static void sendToRenderer(PhotinoWindow window, string requestId, object data) {
+ var logMessage = new {
+ requestId,
+ payload = new { message = data }
+ };
+ window.SendWebMessage(JsonSerializer.Serialize(logMessage, Constants._jsonOptions));
+ }
+}
\ No newline at end of file
diff --git a/src/main/services/Installation.cs b/src/main/services/Installation.cs
new file mode 100644
index 0000000..92c3065
--- /dev/null
+++ b/src/main/services/Installation.cs
@@ -0,0 +1,76 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace BrikInstaller.Services;
+
+public static class InstallationService {
+ public static bool CanCreateInPath(string folderPath) {
+ if (string.IsNullOrWhiteSpace(folderPath)) return false;
+
+ try {
+ string fullPath = Path.GetFullPath(folderPath);
+ DirectoryInfo di = new DirectoryInfo(fullPath);
+
+ while (di != null && !di.Exists) {
+ di = di.Parent!;
+ }
+
+ if (di == null) return false;
+
+ string testPath = Path.Combine(di.FullName, Guid.NewGuid().ToString());
+
+ Directory.CreateDirectory(testPath);
+ Directory.Delete(testPath);
+
+ return true;
+ } catch (UnauthorizedAccessException) {
+ return false;
+ } catch (Exception) {
+ return false;
+ }
+ }
+
+ public static void ElevateAndRestart() {
+ string exePath = Process.GetCurrentProcess().MainModule!.FileName;
+ ProcessStartInfo startInfo = new ProcessStartInfo();
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+ startInfo.FileName = exePath;
+ startInfo.Verb = "runas";
+ startInfo.UseShellExecute = true;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
+ startInfo.FileName = "pkexec";
+ startInfo.Arguments = $"\"{exePath}\"";
+ startInfo.UseShellExecute = false;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ startInfo.FileName = "osascript";
+ startInfo.Arguments = $"-e \"do shell script \\\"{exePath}\\\" with administrator privileges\"";
+ startInfo.UseShellExecute = false;
+ }
+
+ try {
+ Process.Start(startInfo);
+ Environment.Exit(0);
+ } catch (Exception ex) {
+ Console.WriteLine($"Elevation failed : {ex.Message}");
+ }
+ }
+
+ public static string GetDefaultInstallationPath(string appName) {
+ string final = "";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+ string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ final = Path.Combine(localAppData, "Programs", appName);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ final = Path.Combine(home, ".local", "share", appName.ToLower().Replace(" ", "_"));
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ final = $"/Applications/{appName}.app";
+ }
+ return final;
+ }
+}
\ No newline at end of file
diff --git a/src/main/services/Network.cs b/src/main/services/Network.cs
new file mode 100644
index 0000000..1b93996
--- /dev/null
+++ b/src/main/services/Network.cs
@@ -0,0 +1,30 @@
+using System.Text.Json;
+using BrikInstaller;
+
+public static class NetworkService {
+ public static async Task