2025-05-01 19:01:59 +03:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2025-05-01 21:03:55 +03:00
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.IO;
|
2025-05-01 19:01:59 +03:00
|
|
|
using System.Net.Http;
|
2025-05-01 21:03:55 +03:00
|
|
|
using System.Text;
|
2025-05-01 19:01:59 +03:00
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Avalonia.Controls;
|
2025-05-02 10:56:19 +03:00
|
|
|
using Nebula.UpdateResolver.Configuration;
|
|
|
|
|
using Nebula.UpdateResolver.Rest;
|
2025-05-01 19:01:59 +03:00
|
|
|
|
|
|
|
|
namespace Nebula.UpdateResolver;
|
|
|
|
|
|
|
|
|
|
public partial class MainWindow : Window
|
|
|
|
|
{
|
2025-05-02 10:56:19 +03:00
|
|
|
public static readonly string RootPath = Path.Join(Environment.GetFolderPath(
|
|
|
|
|
Environment.SpecialFolder.ApplicationData), "Datum");
|
|
|
|
|
|
2025-05-01 19:01:59 +03:00
|
|
|
private readonly HttpClient _httpClient = new HttpClient();
|
2025-05-02 10:56:19 +03:00
|
|
|
public readonly FileApi FileApi = new FileApi(Path.Join(RootPath,"app"));
|
2025-05-06 11:19:19 +03:00
|
|
|
private string LogStr = "";
|
2025-05-02 10:56:19 +03:00
|
|
|
public MainWindow()
|
2025-05-01 19:01:59 +03:00
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.OnLog += (message, percentage) =>
|
|
|
|
|
{
|
|
|
|
|
ProgressLabel.Content = message;
|
|
|
|
|
if (percentage == 0)
|
|
|
|
|
PercentLabel.Content = "";
|
|
|
|
|
else
|
|
|
|
|
PercentLabel.Content = percentage + "%";
|
|
|
|
|
|
|
|
|
|
var messageOut =
|
|
|
|
|
$"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {PercentLabel.Content}";
|
|
|
|
|
Console.WriteLine(messageOut);
|
|
|
|
|
LogStr += messageOut + "\n";
|
|
|
|
|
};
|
2025-10-04 19:59:23 +03:00
|
|
|
LogStandalone.Log("Starting up");
|
2025-09-08 21:26:12 +03:00
|
|
|
if (!Design.IsDesignMode)
|
2025-10-04 19:59:23 +03:00
|
|
|
_ = Start();
|
2025-09-08 21:26:12 +03:00
|
|
|
else
|
|
|
|
|
LogStandalone.Log("Debug information", 51);
|
2025-05-01 19:01:59 +03:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 21:03:55 +03:00
|
|
|
private async Task Start()
|
2025-05-01 19:01:59 +03:00
|
|
|
{
|
2025-05-02 10:56:19 +03:00
|
|
|
try
|
2025-05-01 19:01:59 +03:00
|
|
|
{
|
2025-05-02 10:56:19 +03:00
|
|
|
var info = await EnsureFiles();
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Downloading files...");
|
2025-05-01 21:03:55 +03:00
|
|
|
|
2025-05-02 10:56:19 +03:00
|
|
|
foreach (var file in info.ToDelete)
|
|
|
|
|
{
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Deleting " + file.Path);
|
2025-05-02 10:56:19 +03:00
|
|
|
FileApi.Remove(file.Path);
|
|
|
|
|
}
|
2025-05-01 21:03:55 +03:00
|
|
|
|
2025-05-02 10:56:19 +03:00
|
|
|
var loadedManifest = info.FilesExist;
|
2025-05-01 21:03:55 +03:00
|
|
|
Save(loadedManifest);
|
2025-05-02 10:56:19 +03:00
|
|
|
|
|
|
|
|
var count = info.ToDownload.Count;
|
|
|
|
|
var resolved = 0;
|
|
|
|
|
|
|
|
|
|
foreach (var file in info.ToDownload)
|
|
|
|
|
{
|
|
|
|
|
using var response = await _httpClient.GetAsync(
|
|
|
|
|
ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)
|
|
|
|
|
+ "/" + file.Hash);
|
|
|
|
|
|
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
|
|
|
|
FileApi.Save(file.Path, stream);
|
|
|
|
|
resolved++;
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Saving " + file.Path, (int)(resolved / (float)count * 100f));
|
2025-05-02 10:56:19 +03:00
|
|
|
|
|
|
|
|
loadedManifest.Add(file);
|
|
|
|
|
Save(loadedManifest);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Download finished. Running launcher...");
|
2025-05-02 10:56:19 +03:00
|
|
|
|
2025-06-15 13:48:56 +03:00
|
|
|
await DotnetStandalone.Run(Path.Join(FileApi.RootPath, "Nebula.Launcher.dll"));
|
2025-05-01 19:01:59 +03:00
|
|
|
}
|
2025-05-06 11:19:19 +03:00
|
|
|
catch(HttpRequestException e){
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.LogError(e);
|
|
|
|
|
LogStandalone.Log("Network connection error...");
|
2025-05-06 11:19:19 +03:00
|
|
|
var logPath = Path.Join(RootPath,"updateResloverError.txt");
|
2025-06-15 13:48:56 +03:00
|
|
|
await File.WriteAllTextAsync(logPath, LogStr);
|
2025-05-06 11:19:19 +03:00
|
|
|
Process.Start(new ProcessStartInfo(){
|
|
|
|
|
FileName = "notepad",
|
|
|
|
|
Arguments = logPath
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-05-02 10:56:19 +03:00
|
|
|
catch (Exception e)
|
2025-05-01 21:03:55 +03:00
|
|
|
{
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.LogError(e);
|
2025-05-06 11:19:19 +03:00
|
|
|
var logPath = Path.Join(RootPath,"updateResloverError.txt");
|
2025-06-15 13:48:56 +03:00
|
|
|
await File.WriteAllTextAsync(logPath, LogStr);
|
2025-05-06 11:19:19 +03:00
|
|
|
Process.Start(new ProcessStartInfo(){
|
|
|
|
|
FileName = "notepad",
|
|
|
|
|
Arguments = logPath
|
|
|
|
|
});
|
2025-05-02 10:56:19 +03:00
|
|
|
}
|
2025-05-01 21:03:55 +03:00
|
|
|
|
2025-05-06 11:19:19 +03:00
|
|
|
Thread.Sleep(4000);
|
2025-05-01 21:03:55 +03:00
|
|
|
|
|
|
|
|
Environment.Exit(0);
|
2025-05-01 19:01:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<ManifestEnsureInfo> EnsureFiles()
|
|
|
|
|
{
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Ensuring launcher manifest...");
|
2025-05-02 10:56:19 +03:00
|
|
|
var manifest = await RestStandalone.GetAsync<LauncherManifest>(
|
|
|
|
|
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
|
2025-05-01 21:03:55 +03:00
|
|
|
|
2025-05-01 19:01:59 +03:00
|
|
|
var toDownload = new HashSet<LauncherManifestEntry>();
|
|
|
|
|
var toDelete = new HashSet<LauncherManifestEntry>();
|
2025-05-01 21:03:55 +03:00
|
|
|
var filesExist = new HashSet<LauncherManifestEntry>();
|
2025-05-01 19:01:59 +03:00
|
|
|
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Manifest loaded!");
|
2025-05-02 10:56:19 +03:00
|
|
|
if (ConfigurationStandalone.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest))
|
2025-05-01 19:01:59 +03:00
|
|
|
{
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Delta manifest loaded!");
|
2025-05-01 19:01:59 +03:00
|
|
|
foreach (var file in currentManifest.Entries)
|
|
|
|
|
{
|
2025-05-01 21:03:55 +03:00
|
|
|
if (!manifest.Entries.Contains(file))
|
2025-05-02 10:56:19 +03:00
|
|
|
toDelete.Add(EnsurePath(file));
|
2025-05-01 21:03:55 +03:00
|
|
|
else
|
2025-05-02 10:56:19 +03:00
|
|
|
filesExist.Add(EnsurePath(file));
|
2025-05-01 19:01:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var file in manifest.Entries)
|
|
|
|
|
{
|
|
|
|
|
if(!currentManifest.Entries.Contains(file))
|
2025-05-02 10:56:19 +03:00
|
|
|
toDownload.Add(EnsurePath(file));
|
2025-05-01 19:01:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
toDownload = manifest.Entries;
|
|
|
|
|
}
|
2025-05-01 21:03:55 +03:00
|
|
|
|
2025-06-15 13:48:56 +03:00
|
|
|
LogStandalone.Log("Saving launcher manifest...");
|
2025-05-01 21:03:55 +03:00
|
|
|
|
|
|
|
|
return new ManifestEnsureInfo(toDownload, toDelete, filesExist);
|
|
|
|
|
}
|
2025-06-15 13:48:56 +03:00
|
|
|
|
2025-05-01 21:03:55 +03:00
|
|
|
|
|
|
|
|
private void Save(HashSet<LauncherManifestEntry> entries)
|
|
|
|
|
{
|
2025-05-02 10:56:19 +03:00
|
|
|
ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private LauncherManifestEntry EnsurePath(LauncherManifestEntry entry)
|
|
|
|
|
{
|
|
|
|
|
if(!PathValidator.IsSafePath(FileApi.RootPath, entry.Path))
|
|
|
|
|
throw new ArgumentException("Path contains invalid characters. Manifest hash: " + entry.Hash);
|
|
|
|
|
|
|
|
|
|
return entry;
|
2025-05-01 19:01:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-02 10:56:19 +03:00
|
|
|
|
|
|
|
|
public static class PathValidator
|
|
|
|
|
{
|
|
|
|
|
public static bool IsSafePath(string baseDirectory, string relativePath)
|
|
|
|
|
{
|
|
|
|
|
if (Path.IsPathRooted(relativePath))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
var fullBase = Path.GetFullPath(baseDirectory);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var combinedPath = Path.Combine(fullBase, relativePath);
|
|
|
|
|
var fullPath = Path.GetFullPath(combinedPath);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!fullPath.StartsWith(fullBase, StringComparison.Ordinal))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (File.Exists(fullPath) || Directory.Exists(fullPath))
|
|
|
|
|
{
|
|
|
|
|
FileInfo fileInfo = new FileInfo(fullPath);
|
|
|
|
|
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|