Update the launcher to try the Nexus for updates before Github

This commit is contained in:
Timothy Baldridge 2022-06-01 15:45:55 -06:00
parent 43778f41ea
commit 9e4c272c1c
5 changed files with 193 additions and 39 deletions

View File

@ -1,6 +1,7 @@
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 Wabbajack.Launcher.ViewModels; using Wabbajack.Launcher.ViewModels;
using Wabbajack.Launcher.Views; using Wabbajack.Launcher.Views;
@ -15,10 +16,11 @@ public class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
{ {
DataContext = new MainWindowViewModel() DataContext = Program.Services.GetRequiredService<MainWindowViewModel>()
}; };
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();

View File

@ -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<NexusApiState>
{
private AbsolutePath TokenPath => KnownFolders.WabbajackAppLocal.Combine("nexusapikey");
public async ValueTask<NexusApiState?> Get()
{
var data = await TokenPath.ReadAllBytesAsync();
var decoded = ProtectedData.Unprotect(data, Encoding.UTF8.GetBytes("nexusapikey"), DataProtectionScope.LocalMachine);
var apiKey = JsonSerializer.Deserialize<string>(decoded)!;
return new NexusApiState()
{
ApiKey = apiKey
};
}
public ValueTask SetToken(NexusApiState val)
{
throw new System.NotImplementedException();
}
public ValueTask<bool> Delete()
{
throw new System.NotImplementedException();
}
public bool HaveToken()
{
return TokenPath.FileExists();
}
}

View File

@ -1,5 +1,22 @@
using Avalonia; using System;
using System.Net.Http;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.ReactiveUI; 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; namespace Wabbajack.Launcher;
@ -11,9 +28,40 @@ internal class Program
// yet and stuff might break. // yet and stuff might break.
public static void Main(string[] args) public static void Main(string[] args)
{ {
var host = Host.CreateDefaultBuilder(Array.Empty<string>())
.ConfigureLogging(c => { c.ClearProviders(); })
.ConfigureServices((host, services) =>
{
services.AddNexusApi();
services.AddDTOConverters();
services.AddDTOSerializer();
services.AddSingleton<MainWindowViewModel>();
services.AddSingleton<HttpClient>();
services.AddSingleton<ITokenProvider<NexusApiState>, LegacyNexusApiKey>();
services.AddSingleton<HttpDownloader>();
services.AddAllSingleton<IResource, IResource<HttpClient>>(s => new Resource<HttpClient>("Web Requests", 4));
services.AddAllSingleton<IHttpDownloader, SingleThreadedDownloader>();
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() BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args); .StartWithClassicDesktopLifetime(args);
} }
public static IServiceProvider Services { get; set; }
// Avalonia configuration, don't remove; also used by visual designer. // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()

View File

@ -7,8 +7,14 @@ using System.Linq;
using System.Net; using System.Net;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI.Fody.Helpers; 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;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
@ -19,12 +25,16 @@ public class MainWindowViewModel : ViewModelBase
private readonly WebClient _client = new(); private readonly WebClient _client = new();
private readonly List<string> _errors = new(); private readonly List<string> _errors = new();
private Release _version; private (Version Version, long Size, Func<Task<Uri>> Uri) _version;
public Uri GITHUB_REPO = new("https://api.github.com/repos/wabbajack-tools/wabbajack/releases"); 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"; Status = "Checking for new versions";
_downloader = downloader;
var tsk = CheckForUpdates(); var tsk = CheckForUpdates();
} }
@ -37,13 +47,15 @@ public class MainWindowViewModel : ViewModelBase
try try
{ {
var releases = await GetReleases(); var nexusRelease = await GetNexusReleases(CancellationToken.None);
_version = releases.OrderByDescending(r => if (nexusRelease != default)
_version = nexusRelease;
else
{ {
if (r.Tag.Split(".").Length == 4 && Version.TryParse(r.Tag, out var v)) _version = await GetGithubRelease(CancellationToken.None);
return v; }
return new Version(0, 0, 0, 0);
}).FirstOrDefault();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -51,32 +63,28 @@ public class MainWindowViewModel : ViewModelBase
await FinishAndExit(); await FinishAndExit();
} }
if (_version == null) if (_version == default)
{ {
_errors.Add("Unable to parse Github releases"); _errors.Add("Unable to find releases");
await FinishAndExit(); await FinishAndExit();
} }
Status = "Looking for Updates"; 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"); Status = $"Getting download Uri for {_version.Version}";
if (asset == null) var uri = await _version.Uri();
{
_errors.Add("No zip file for release " + _version.Tag);
await FinishAndExit();
}
var wc = new WebClient(); var wc = new WebClient();
wc.DownloadProgressChanged += UpdateProgress; wc.DownloadProgressChanged += UpdateProgress;
Status = $"Downloading {_version.Tag} ..."; Status = $"Downloading {_version.Version} ...";
byte[] data; byte[] data;
try try
{ {
data = await wc.DownloadDataTaskAsync(asset.BrowserDownloadUrl); data = await wc.DownloadDataTaskAsync(uri);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -84,7 +92,7 @@ public class MainWindowViewModel : ViewModelBase
// Something went wrong so fallback to original URL // Something went wrong so fallback to original URL
try try
{ {
data = await wc.DownloadDataTaskAsync(asset.BrowserDownloadUrl); data = await wc.DownloadDataTaskAsync(uri);
} }
catch (Exception ex2) catch (Exception ex2)
{ {
@ -96,21 +104,19 @@ public class MainWindowViewModel : ViewModelBase
try 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 = baseFolder.Combine(entry.FullName.ToRelativePath());
Status = $"Extracting: {entry.Name}"; if (!outPath.Parent.DirectoryExists())
var outPath = Path.Combine(base_folder, entry.FullName); outPath.Parent.CreateDirectory();
if (!Directory.Exists(Path.GetDirectoryName(outPath)))
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
if (entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\")) if (entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\"))
continue; continue;
await using var o = entry.Open(); await using var o = entry.Open();
await using var of = File.Create(outPath); await using var of = outPath.Open(FileMode.Create, FileAccess.Write, FileShare.None);
await o.CopyToAsync(of); await o.CopyToAsync(of);
}
} }
} }
catch (Exception ex) catch (Exception ex)
@ -175,10 +181,34 @@ public class MainWindowViewModel : ViewModelBase
private void UpdateProgress(object sender, DownloadProgressChangedEventArgs e) private void UpdateProgress(object sender, DownloadProgressChangedEventArgs e)
{ {
Status = $"Downloading {_version.Tag} ({e.ProgressPercentage}%)..."; Status = $"Downloading {_version.Version} ({e.ProgressPercentage}%)...";
} }
private async Task<Release[]> GetReleases() private async Task<(Version Version, long Size, Func<Task<Uri>> 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<Release[]> GetGithubReleases()
{ {
Status = "Checking GitHub Repository"; Status = "Checking GitHub Repository";
var data = await _client.DownloadStringTaskAsync(GITHUB_REPO); var data = await _client.DownloadStringTaskAsync(GITHUB_REPO);
@ -186,6 +216,30 @@ public class MainWindowViewModel : ViewModelBase
return JsonSerializer.Deserialize<Release[]>(data)!; return JsonSerializer.Deserialize<Release[]>(data)!;
} }
private async Task<(Version Version, long Size, Func<Task<Uri>> 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<Release[]>(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 private class Release
{ {
@ -200,5 +254,8 @@ public class MainWindowViewModel : ViewModelBase
public Uri BrowserDownloadUrl { get; set; } public Uri BrowserDownloadUrl { get; set; }
[JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("name")] public string Name { get; set; }
[JsonPropertyName("size")] public long Size { get; set; }
} }
} }

View File

@ -7,17 +7,24 @@
<AssemblyName>Wabbajack</AssemblyName> <AssemblyName>Wabbajack</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GitInfo" Version="2.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Avalonia" Version="0.10.14" /> <PackageReference Include="Avalonia" Version="0.10.14" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.14" /> <PackageReference Include="Avalonia.Desktop" Version="0.10.14" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.14" /> <PackageReference Include="Avalonia.Diagnostics" Version="0.10.14" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.14" /> <PackageReference Include="Avalonia.ReactiveUI" Version="0.10.14" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="ReactiveUI.Fody" Version="18.0.10" /> <PackageReference Include="ReactiveUI.Fody" Version="18.0.10" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wabbajack.Downloaders.Http\Wabbajack.Downloaders.Http.csproj" />
<ProjectReference Include="..\Wabbajack.Downloaders.Nexus\Wabbajack.Downloaders.Nexus.csproj" />
<ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" /> <ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" />
<ProjectReference Include="..\Wabbajack.Paths\Wabbajack.Paths.csproj" /> <ProjectReference Include="..\Wabbajack.Paths\Wabbajack.Paths.csproj" />
</ItemGroup> </ItemGroup>