diff --git a/Wabbajack.Launcher/App.axaml.cs b/Wabbajack.Launcher/App.axaml.cs index f1458a91..33cbb515 100644 --- a/Wabbajack.Launcher/App.axaml.cs +++ b/Wabbajack.Launcher/App.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; using Wabbajack.Launcher.ViewModels; using Wabbajack.Launcher.Views; @@ -15,10 +16,11 @@ public class App : Application public override void OnFrameworkInitializationCompleted() { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) desktop.MainWindow = new MainWindow { - DataContext = new MainWindowViewModel() + DataContext = Program.Services.GetRequiredService() }; base.OnFrameworkInitializationCompleted(); diff --git a/Wabbajack.Launcher/Models/LegacyNexusApiKey.cs b/Wabbajack.Launcher/Models/LegacyNexusApiKey.cs new file mode 100644 index 00000000..b4b12749 --- /dev/null +++ b/Wabbajack.Launcher/Models/LegacyNexusApiKey.cs @@ -0,0 +1,40 @@ +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Text.Json; +using Wabbajack.DTOs.Logins; +using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; + +namespace Wabbajack.Launcher.Models; + +public class LegacyNexusApiKey : ITokenProvider +{ + private AbsolutePath TokenPath => KnownFolders.WabbajackAppLocal.Combine("nexusapikey"); + public async ValueTask Get() + { + var data = await TokenPath.ReadAllBytesAsync(); + var decoded = ProtectedData.Unprotect(data, Encoding.UTF8.GetBytes("nexusapikey"), DataProtectionScope.LocalMachine); + var apiKey = JsonSerializer.Deserialize(decoded)!; + return new NexusApiState() + { + ApiKey = apiKey + }; + } + + public ValueTask SetToken(NexusApiState val) + { + throw new System.NotImplementedException(); + } + + public ValueTask Delete() + { + throw new System.NotImplementedException(); + } + + public bool HaveToken() + { + return TokenPath.FileExists(); + } +} \ No newline at end of file diff --git a/Wabbajack.Launcher/Program.cs b/Wabbajack.Launcher/Program.cs index a127dd3d..d56c897c 100644 --- a/Wabbajack.Launcher/Program.cs +++ b/Wabbajack.Launcher/Program.cs @@ -1,5 +1,22 @@ -using Avalonia; +using System; +using System.Net.Http; +using System.Runtime.InteropServices; +using Avalonia; using Avalonia.ReactiveUI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Wabbajack.Downloaders.Http; +using Wabbajack.DTOs; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.DTOs.Logins; +using Wabbajack.Launcher.Models; +using Wabbajack.Launcher.ViewModels; +using Wabbajack.Networking.Http; +using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.Networking.NexusApi; +using Wabbajack.Paths; +using Wabbajack.RateLimiter; namespace Wabbajack.Launcher; @@ -11,9 +28,40 @@ internal class Program // yet and stuff might break. public static void Main(string[] args) { + var host = Host.CreateDefaultBuilder(Array.Empty()) + .ConfigureLogging(c => { c.ClearProviders(); }) + .ConfigureServices((host, services) => + { + services.AddNexusApi(); + services.AddDTOConverters(); + services.AddDTOSerializer(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton, LegacyNexusApiKey>(); + services.AddSingleton(); + services.AddAllSingleton>(s => new Resource("Web Requests", 4)); + services.AddAllSingleton(); + + var version = + $"{ThisAssembly.Git.SemVer.Major}.{ThisAssembly.Git.SemVer.Major}.{ThisAssembly.Git.SemVer.Patch}{ThisAssembly.Git.SemVer.DashLabel}"; + services.AddSingleton(s => new ApplicationInfo + { + ApplicationSlug = "Wabbajack", + ApplicationName = Environment.ProcessPath?.ToAbsolutePath().FileName.ToString() ?? "Wabbajack", + ApplicationSha = ThisAssembly.Git.Sha, + Platform = RuntimeInformation.ProcessArchitecture.ToString(), + OperatingSystemDescription = RuntimeInformation.OSDescription, + RuntimeIdentifier = RuntimeInformation.RuntimeIdentifier, + OSVersion = Environment.OSVersion.VersionString, + Version = version + }); + }).Build(); + Services = host.Services; + BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); } + public static IServiceProvider Services { get; set; } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/Wabbajack.Launcher/ViewModels/MainWindowViewModel.cs b/Wabbajack.Launcher/ViewModels/MainWindowViewModel.cs index 1b1eb55e..aba03450 100644 --- a/Wabbajack.Launcher/ViewModels/MainWindowViewModel.cs +++ b/Wabbajack.Launcher/ViewModels/MainWindowViewModel.cs @@ -7,8 +7,14 @@ using System.Linq; using System.Net; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using ReactiveUI.Fody.Helpers; +using Wabbajack.Compression.Zip; +using Wabbajack.Downloaders.Http; +using Wabbajack.DTOs; +using Wabbajack.DTOs.DownloadStates; +using Wabbajack.Networking.NexusApi; using Wabbajack.Paths; using Wabbajack.Paths.IO; @@ -19,12 +25,16 @@ public class MainWindowViewModel : ViewModelBase private readonly WebClient _client = new(); private readonly List _errors = new(); - private Release _version; + private (Version Version, long Size, Func> Uri) _version; public Uri GITHUB_REPO = new("https://api.github.com/repos/wabbajack-tools/wabbajack/releases"); + private readonly NexusApi _nexusApi; + private readonly HttpDownloader _downloader; - public MainWindowViewModel() + public MainWindowViewModel(NexusApi nexusApi, HttpDownloader downloader) { + _nexusApi = nexusApi; Status = "Checking for new versions"; + _downloader = downloader; var tsk = CheckForUpdates(); } @@ -37,13 +47,15 @@ public class MainWindowViewModel : ViewModelBase try { - var releases = await GetReleases(); - _version = releases.OrderByDescending(r => + var nexusRelease = await GetNexusReleases(CancellationToken.None); + if (nexusRelease != default) + _version = nexusRelease; + else { - if (r.Tag.Split(".").Length == 4 && Version.TryParse(r.Tag, out var v)) - return v; - return new Version(0, 0, 0, 0); - }).FirstOrDefault(); + _version = await GetGithubRelease(CancellationToken.None); + } + + } catch (Exception ex) { @@ -51,32 +63,28 @@ public class MainWindowViewModel : ViewModelBase await FinishAndExit(); } - if (_version == null) + if (_version == default) { - _errors.Add("Unable to parse Github releases"); + _errors.Add("Unable to find releases"); await FinishAndExit(); } Status = "Looking for Updates"; - var base_folder = Path.Combine(Directory.GetCurrentDirectory(), _version.Tag); + var baseFolder = KnownFolders.CurrentDirectory.Combine(_version.Version.ToString()); - if (File.Exists(Path.Combine(base_folder, "Wabbajack.exe"))) await FinishAndExit(); + if (baseFolder.Combine("Wabbajack.exe").FileExists()) await FinishAndExit(); - var asset = _version.Assets.FirstOrDefault(a => a.Name == _version.Tag + ".zip"); - if (asset == null) - { - _errors.Add("No zip file for release " + _version.Tag); - await FinishAndExit(); - } + Status = $"Getting download Uri for {_version.Version}"; + var uri = await _version.Uri(); var wc = new WebClient(); wc.DownloadProgressChanged += UpdateProgress; - Status = $"Downloading {_version.Tag} ..."; + Status = $"Downloading {_version.Version} ..."; byte[] data; try { - data = await wc.DownloadDataTaskAsync(asset.BrowserDownloadUrl); + data = await wc.DownloadDataTaskAsync(uri); } catch (Exception ex) { @@ -84,7 +92,7 @@ public class MainWindowViewModel : ViewModelBase // Something went wrong so fallback to original URL try { - data = await wc.DownloadDataTaskAsync(asset.BrowserDownloadUrl); + data = await wc.DownloadDataTaskAsync(uri); } catch (Exception ex2) { @@ -96,21 +104,19 @@ public class MainWindowViewModel : ViewModelBase try { - using (var zip = new ZipArchive(new MemoryStream(data), ZipArchiveMode.Read)) + using var zip = new ZipArchive(new MemoryStream(data), ZipArchiveMode.Read); + foreach (var entry in zip.Entries) { - foreach (var entry in zip.Entries) - { - Status = $"Extracting: {entry.Name}"; - var outPath = Path.Combine(base_folder, entry.FullName); - if (!Directory.Exists(Path.GetDirectoryName(outPath))) - Directory.CreateDirectory(Path.GetDirectoryName(outPath)); + Status = $"Extracting: {entry.Name}"; + var outPath = baseFolder.Combine(entry.FullName.ToRelativePath()); + if (!outPath.Parent.DirectoryExists()) + outPath.Parent.CreateDirectory(); - if (entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\")) - continue; - await using var o = entry.Open(); - await using var of = File.Create(outPath); - await o.CopyToAsync(of); - } + if (entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\")) + continue; + await using var o = entry.Open(); + await using var of = outPath.Open(FileMode.Create, FileAccess.Write, FileShare.None); + await o.CopyToAsync(of); } } catch (Exception ex) @@ -175,16 +181,64 @@ public class MainWindowViewModel : ViewModelBase private void UpdateProgress(object sender, DownloadProgressChangedEventArgs e) { - Status = $"Downloading {_version.Tag} ({e.ProgressPercentage}%)..."; + Status = $"Downloading {_version.Version} ({e.ProgressPercentage}%)..."; } - private async Task GetReleases() + private async Task<(Version Version, long Size, Func> Uri)> GetGithubRelease(CancellationToken token) + { + var releases = await GetGithubReleases(); + + + var version = releases.Select(r => + { + if (r.Tag.Split(".").Length == 4 && Version.TryParse(r.Tag, out var v)) + return (v, r); + return (new Version(0, 0, 0, 0), r); + }) + .OrderByDescending(r => r.Item1) + .FirstOrDefault(); + + var asset = version.r.Assets.FirstOrDefault(a => a.Name == version.Item1 + ".zip"); + if (asset == null) + { + Status = $"Error, no asset found for Github Release {version.r}"; + return default; + } + + return (version.Item1, asset.Size, async () => asset!.BrowserDownloadUrl); + } + + private async Task GetGithubReleases() { Status = "Checking GitHub Repository"; var data = await _client.DownloadStringTaskAsync(GITHUB_REPO); Status = "Parsing Response"; return JsonSerializer.Deserialize(data)!; } + + private async Task<(Version Version, long Size, Func> uri)> GetNexusReleases(CancellationToken token) + { + Status = "Checking Nexus for updates"; + if (!await _nexusApi.IsPremium(token)) + return default; + + var data = await _nexusApi.ModFiles("site", 403, token); + Status = "Parsing Response"; + //return JsonSerializer.Deserialize(data)!; + + var found = data.info.Files.Where(f => f.CategoryId == 5) + .Where(f => f.Name.EndsWith(".zip")) + .Select(f => Version.TryParse(f.Name[..^4], out var version) ? (version, f.SizeInBytes ?? f.Size, f.FileId) : default) + .FirstOrDefault(f => f != default); + if (found == default) return default; + + return (found.version, found.Item2, async () => + { + var link = await _nexusApi.DownloadLink("site", 403, found.FileId, token); + + return link.info.First().URI; + }); + } private class Release @@ -200,5 +254,8 @@ public class MainWindowViewModel : ViewModelBase public Uri BrowserDownloadUrl { get; set; } [JsonPropertyName("name")] public string Name { get; set; } + + + [JsonPropertyName("size")] public long Size { get; set; } } } \ No newline at end of file diff --git a/Wabbajack.Launcher/Wabbajack.Launcher.csproj b/Wabbajack.Launcher/Wabbajack.Launcher.csproj index 6080a8c4..4978293b 100644 --- a/Wabbajack.Launcher/Wabbajack.Launcher.csproj +++ b/Wabbajack.Launcher/Wabbajack.Launcher.csproj @@ -7,17 +7,24 @@ Wabbajack - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + +