- tweak: remove dependency from Nebula.Shared
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ obj/
|
|||||||
riderModule.iml
|
riderModule.iml
|
||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
release/
|
release/
|
||||||
|
publish/
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Nebula.Shared;
|
|
||||||
|
|
||||||
namespace Nebula.UpdateResolver;
|
namespace Nebula.UpdateResolver;
|
||||||
|
|
||||||
@@ -15,15 +13,11 @@ public partial class App : Application
|
|||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
|
||||||
services.AddServices();
|
|
||||||
services.AddTransient<MainWindow>();
|
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = serviceProvider.GetService<MainWindow>();
|
desktop.MainWindow = new MainWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
|||||||
16
Nebula.UpdateResolver/Configuration/ConVar.cs
Normal file
16
Nebula.UpdateResolver/Configuration/ConVar.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver.Configuration;
|
||||||
|
|
||||||
|
public class ConVar<T>
|
||||||
|
{
|
||||||
|
public ConVar(string name, T? defaultValue = default)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
DefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public Type Type => typeof(T);
|
||||||
|
public T? DefaultValue { get; }
|
||||||
|
}
|
||||||
14
Nebula.UpdateResolver/Configuration/ConVarBuilder.cs
Normal file
14
Nebula.UpdateResolver/Configuration/ConVarBuilder.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver.Configuration;
|
||||||
|
|
||||||
|
public static class ConVarBuilder
|
||||||
|
{
|
||||||
|
public static ConVar<T> Build<T>(string name, T? defaultValue = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
|
||||||
|
|
||||||
|
return new ConVar<T>(name, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver.Configuration;
|
||||||
|
|
||||||
|
public static class ConfigurationStandalone
|
||||||
|
{
|
||||||
|
private static FileApi _fileApi = new FileApi(Path.Join(MainWindow.RootPath, "config"));
|
||||||
|
|
||||||
|
public static T? GetConfigValue<T>(ConVar<T> conVar)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(conVar);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_fileApi.TryOpen(GetFileName(conVar), out var stream))
|
||||||
|
using (stream)
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<T>(stream);
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Successfully loaded config: {conVar.Name}");
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error loading config for {conVar.Name}: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Using default value for config: {conVar.Name}");
|
||||||
|
return conVar.DefaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetConfigValue<T>(ConVar<T> conVar,
|
||||||
|
[NotNullWhen(true)] out T? value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(conVar);
|
||||||
|
value = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_fileApi.TryOpen(GetFileName(conVar), out var stream))
|
||||||
|
using (stream)
|
||||||
|
{
|
||||||
|
var obj = JsonSerializer.Deserialize<T>(stream);
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Successfully loaded config: {conVar.Name}");
|
||||||
|
value = obj;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error loading config for {conVar.Name}: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Using default value for config: {conVar.Name}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetConfigValue<T>(ConVar<T> conVar, T value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(conVar);
|
||||||
|
if (value == null) throw new ArgumentNullException(nameof(value));
|
||||||
|
|
||||||
|
if (!conVar.Type.IsInstanceOfType(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var serializedData = JsonSerializer.Serialize(value);
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using var writer = new StreamWriter(stream);
|
||||||
|
writer.Write(serializedData);
|
||||||
|
writer.Flush();
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
_fileApi.Save(GetFileName(conVar), stream);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFileName<T>(ConVar<T> conVar)
|
||||||
|
{
|
||||||
|
return $"{conVar.Name}.json";
|
||||||
|
}
|
||||||
|
}
|
||||||
88
Nebula.UpdateResolver/FileApi.cs
Normal file
88
Nebula.UpdateResolver/FileApi.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver;
|
||||||
|
|
||||||
|
public sealed class FileApi
|
||||||
|
{
|
||||||
|
public string RootPath;
|
||||||
|
|
||||||
|
public FileApi(string rootPath)
|
||||||
|
{
|
||||||
|
RootPath = rootPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryOpen(string path,[NotNullWhen(true)] out Stream? stream)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Join(RootPath, path);
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
stream = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Save(string path, Stream input)
|
||||||
|
{
|
||||||
|
var currPath = Path.Join(RootPath, path);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dirInfo = new DirectoryInfo(Path.GetDirectoryName(currPath) ?? throw new InvalidOperationException());
|
||||||
|
if (!dirInfo.Exists) dirInfo.Create();
|
||||||
|
|
||||||
|
using var stream = new FileStream(currPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
input.CopyTo(stream);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(string path)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Join(RootPath, path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
File.Delete(fullPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Log exception if necessary
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Has(string path)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Join(RootPath, path);
|
||||||
|
return File.Exists(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetAllFiles(){
|
||||||
|
|
||||||
|
if(!Directory.Exists(RootPath)) return [];
|
||||||
|
return Directory.EnumerateFiles(RootPath, "*.*", SearchOption.AllDirectories).Select(p=>p.Replace(RootPath,"").Substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> AllFiles => GetAllFiles();
|
||||||
|
}
|
||||||
@@ -7,75 +7,77 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Nebula.Shared.FileApis;
|
using Nebula.UpdateResolver.Configuration;
|
||||||
using Nebula.Shared.FileApis.Interfaces;
|
using Nebula.UpdateResolver.Rest;
|
||||||
using Nebula.Shared.Models;
|
|
||||||
using Nebula.Shared.Services;
|
|
||||||
using Tmds.DBus.Protocol;
|
|
||||||
|
|
||||||
namespace Nebula.UpdateResolver;
|
namespace Nebula.UpdateResolver;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private readonly ConfigurationService _configurationService;
|
public static readonly string RootPath = Path.Join(Environment.GetFolderPath(
|
||||||
private readonly RestService _restService;
|
Environment.SpecialFolder.ApplicationData), "Datum");
|
||||||
|
|
||||||
private readonly HttpClient _httpClient = new HttpClient();
|
private readonly HttpClient _httpClient = new HttpClient();
|
||||||
public FileApi FileApi { get; set; }
|
public readonly FileApi FileApi = new FileApi(Path.Join(RootPath,"app"));
|
||||||
|
|
||||||
public MainWindow(FileService fileService, ConfigurationService configurationService, RestService restService)
|
public MainWindow()
|
||||||
{
|
{
|
||||||
_configurationService = configurationService;
|
|
||||||
_restService = restService;
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
FileApi = (FileApi)fileService.CreateFileApi("app");
|
|
||||||
|
|
||||||
Start();
|
Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Start()
|
private async Task Start()
|
||||||
{
|
{
|
||||||
var info = await EnsureFiles();
|
try
|
||||||
Log("Downloading files...");
|
|
||||||
|
|
||||||
foreach (var file in info.ToDelete)
|
|
||||||
{
|
{
|
||||||
Log("Deleting " + file.Path);
|
var info = await EnsureFiles();
|
||||||
FileApi.Remove(file.Path);
|
Log("Downloading files...");
|
||||||
}
|
|
||||||
|
|
||||||
var loadedManifest = info.FilesExist;
|
foreach (var file in info.ToDelete)
|
||||||
Save(loadedManifest);
|
{
|
||||||
|
Log("Deleting " + file.Path);
|
||||||
|
FileApi.Remove(file.Path);
|
||||||
|
}
|
||||||
|
|
||||||
var count = info.ToDownload.Count;
|
var loadedManifest = info.FilesExist;
|
||||||
var resolved = 0;
|
|
||||||
|
|
||||||
foreach (var file in info.ToDownload)
|
|
||||||
{
|
|
||||||
using var response = await _httpClient.GetAsync(
|
|
||||||
_configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl)
|
|
||||||
+ "/" + file.Hash);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
|
||||||
FileApi.Save(file.Path, stream);
|
|
||||||
resolved++;
|
|
||||||
Log("Saving " + file.Path, (int)(resolved/(float)count*100f));
|
|
||||||
|
|
||||||
loadedManifest.Add(file);
|
|
||||||
Save(loadedManifest);
|
Save(loadedManifest);
|
||||||
}
|
|
||||||
Log("Download finished. Running launcher...");
|
|
||||||
|
|
||||||
var process = Process.Start(new ProcessStartInfo
|
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++;
|
||||||
|
Log("Saving " + file.Path, (int)(resolved / (float)count * 100f));
|
||||||
|
|
||||||
|
loadedManifest.Add(file);
|
||||||
|
Save(loadedManifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("Download finished. Running launcher...");
|
||||||
|
|
||||||
|
var process = Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "dotnet.exe",
|
||||||
|
Arguments = Path.Join(FileApi.RootPath, "Nebula.Launcher.dll"),
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
StandardOutputEncoding = Encoding.UTF8
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
FileName = "dotnet.exe",
|
Log("Error! " + e.Message);
|
||||||
Arguments = Path.Join(FileApi.RootPath,"Nebula.Launcher.dll"),
|
}
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
StandardOutputEncoding = Encoding.UTF8
|
|
||||||
});
|
|
||||||
|
|
||||||
Thread.Sleep(2000);
|
Thread.Sleep(2000);
|
||||||
|
|
||||||
@@ -85,29 +87,29 @@ public partial class MainWindow : Window
|
|||||||
private async Task<ManifestEnsureInfo> EnsureFiles()
|
private async Task<ManifestEnsureInfo> EnsureFiles()
|
||||||
{
|
{
|
||||||
Log("Ensuring launcher manifest...");
|
Log("Ensuring launcher manifest...");
|
||||||
var manifest = await _restService.GetAsync<LauncherManifest>(
|
var manifest = await RestStandalone.GetAsync<LauncherManifest>(
|
||||||
new Uri(_configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
|
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
|
||||||
|
|
||||||
var toDownload = new HashSet<LauncherManifestEntry>();
|
var toDownload = new HashSet<LauncherManifestEntry>();
|
||||||
var toDelete = new HashSet<LauncherManifestEntry>();
|
var toDelete = new HashSet<LauncherManifestEntry>();
|
||||||
var filesExist = new HashSet<LauncherManifestEntry>();
|
var filesExist = new HashSet<LauncherManifestEntry>();
|
||||||
|
|
||||||
Log("Manifest loaded!");
|
Log("Manifest loaded!");
|
||||||
if (_configurationService.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest))
|
if (ConfigurationStandalone.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest))
|
||||||
{
|
{
|
||||||
Log("Delta manifest loaded!");
|
Log("Delta manifest loaded!");
|
||||||
foreach (var file in currentManifest.Entries)
|
foreach (var file in currentManifest.Entries)
|
||||||
{
|
{
|
||||||
if (!manifest.Entries.Contains(file))
|
if (!manifest.Entries.Contains(file))
|
||||||
toDelete.Add(file);
|
toDelete.Add(EnsurePath(file));
|
||||||
else
|
else
|
||||||
filesExist.Add(file);
|
filesExist.Add(EnsurePath(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in manifest.Entries)
|
foreach (var file in manifest.Entries)
|
||||||
{
|
{
|
||||||
if(!currentManifest.Entries.Contains(file))
|
if(!currentManifest.Entries.Contains(file))
|
||||||
toDownload.Add(file);
|
toDownload.Add(EnsurePath(file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -133,8 +135,43 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void Save(HashSet<LauncherManifestEntry> entries)
|
private void Save(HashSet<LauncherManifestEntry> entries)
|
||||||
{
|
{
|
||||||
_configurationService.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries));
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record struct ManifestEnsureInfo(HashSet<LauncherManifestEntry> ToDownload, HashSet<LauncherManifestEntry> ToDelete, HashSet<LauncherManifestEntry> FilesExist);
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Nebula.UpdateResolver/ManifestEnsureInfo.cs
Normal file
5
Nebula.UpdateResolver/ManifestEnsureInfo.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver;
|
||||||
|
|
||||||
|
public record struct ManifestEnsureInfo(HashSet<LauncherManifestEntry> ToDownload, HashSet<LauncherManifestEntry> ToDelete, HashSet<LauncherManifestEntry> FilesExist);
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<!--<PublishTrimmed>true</PublishTrimmed>-->
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -22,10 +25,5 @@
|
|||||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
39
Nebula.UpdateResolver/Rest/Helper.cs
Normal file
39
Nebula.UpdateResolver/Rest/Helper.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver.Rest;
|
||||||
|
|
||||||
|
public static class Helper
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions JsonWebOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
public static void SafeOpenBrowser(string uri)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Unable to parse URI in server-provided link: {Link}", uri);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedUri.Scheme is not ("http" or "https"))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Refusing to open server-provided link {Link}, only http/https are allowed", parsedUri);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenBrowser(parsedUri.ToString());
|
||||||
|
}
|
||||||
|
public static void OpenBrowser(string url)
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<T> AsJson<T>(this HttpContent content) where T : notnull
|
||||||
|
{
|
||||||
|
var str = await content.ReadAsStringAsync();
|
||||||
|
return JsonSerializer.Deserialize<T>(str, JsonWebOptions) ??
|
||||||
|
throw new JsonException("AsJson: did not expect null response");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Nebula.UpdateResolver/Rest/RestRequestException.cs
Normal file
11
Nebula.UpdateResolver/Rest/RestRequestException.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver.Rest;
|
||||||
|
|
||||||
|
public sealed class RestRequestException(HttpContent content, HttpStatusCode statusCode) : Exception
|
||||||
|
{
|
||||||
|
public HttpStatusCode StatusCode { get; } = statusCode;
|
||||||
|
public HttpContent Content { get; } = content;
|
||||||
|
}
|
||||||
77
Nebula.UpdateResolver/Rest/RestStandalone.cs
Normal file
77
Nebula.UpdateResolver/Rest/RestStandalone.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Nebula.UpdateResolver.Rest;
|
||||||
|
|
||||||
|
public static class RestStandalone
|
||||||
|
{
|
||||||
|
private static readonly HttpClient _client = new();
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<T> GetAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
||||||
|
{
|
||||||
|
var response = await _client.GetAsync(uri, cancellationToken);
|
||||||
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<T> GetAsyncDefault<T>(Uri uri, T defaultValue, CancellationToken cancellationToken) where T : notnull
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await GetAsync<T>(uri, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<K> PostAsync<K, T>(T information, Uri uri, CancellationToken cancellationToken) where K : notnull
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(information, _serializerOptions);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
var response = await _client.PostAsync(uri, content, cancellationToken);
|
||||||
|
return await ReadResult<K>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<T> PostAsync<T>(Stream stream, Uri uri, CancellationToken cancellationToken) where T : notnull
|
||||||
|
{
|
||||||
|
using var multipartFormContent =
|
||||||
|
new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
|
||||||
|
multipartFormContent.Add(new StreamContent(stream), "formFile", "image.png");
|
||||||
|
var response = await _client.PostAsync(uri, multipartFormContent, cancellationToken);
|
||||||
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<T> DeleteAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
||||||
|
{
|
||||||
|
var response = await _client.DeleteAsync(uri, cancellationToken);
|
||||||
|
return await ReadResult<T>(response, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<T> ReadResult<T>(HttpResponseMessage response, CancellationToken cancellationToken) where T : notnull
|
||||||
|
{
|
||||||
|
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (typeof(T) == typeof(string) && content is T t)
|
||||||
|
return t;
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return await response.Content.AsJson<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RestRequestException(response.Content, response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Nebula.UpdateResolver.Configuration;
|
||||||
using Nebula.Shared.Models;
|
|
||||||
using Nebula.Shared.Services;
|
|
||||||
|
|
||||||
namespace Nebula.UpdateResolver;
|
namespace Nebula.UpdateResolver;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACancellationToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2565b9d99fdde488bc7801b84387b2cc864959cfb63212e1ff576fc9c6bb7e_003FCancellationToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConsole_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ffd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1_003FConsole_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFrozenDictionary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F89dff9063ddb01ff8125b579122b88bf4de94526490d77bcbbef7d0ee662a_003FFrozenDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFrozenDictionary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F89dff9063ddb01ff8125b579122b88bf4de94526490d77bcbbef7d0ee662a_003FFrozenDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9657cc383c70851dc2bdcf91eff27f21196844abfe552fc9c3243ff36974cd_003FHttpContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializer_002ERead_002EString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27c4858128168eda568c1334d70d5241efb9461e2a3209258a04deee5d9c367_003FJsonSerializer_002ERead_002EString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa8ceca48b7b645dd875a40ee6d28725416d08_003F1b_003F6cd78dc8_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa8ceca48b7b645dd875a40ee6d28725416d08_003F1b_003F6cd78dc8_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002EManipulation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe75a5575ba872c8ea754c015cb363850e6c661f39569712d5b74aaca67263c_003FString_002EManipulation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||||
Reference in New Issue
Block a user