diff --git a/Torch.API/ITorchConfig.cs b/Torch.API/ITorchConfig.cs index 9e348e1..8559fe0 100644 --- a/Torch.API/ITorchConfig.cs +++ b/Torch.API/ITorchConfig.cs @@ -32,7 +32,22 @@ namespace Torch UGCServiceType UgcServiceType { get; set; } bool EntityManagerEnabled { get; set; } string LoginToken { get; set; } + UpdateSource UpdateSource { get; set; } void Save(string path = null); } + + public class UpdateSource + { + public UpdateSourceType SourceType { get; set; } + public string Url { get; set; } + public string Repository { get; set; } + public string Branch { get; set; } + } + + public enum UpdateSourceType + { + Github, + Jenkins + } } \ No newline at end of file diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj index 81a68f2..c090b50 100644 --- a/Torch.API/Torch.API.csproj +++ b/Torch.API/Torch.API.csproj @@ -18,6 +18,7 @@ + diff --git a/Torch.API/WebAPI/GithubQuery.cs b/Torch.API/WebAPI/GithubQuery.cs new file mode 100644 index 0000000..1769bb1 --- /dev/null +++ b/Torch.API/WebAPI/GithubQuery.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; +using JorgeSerrano.Json; +using Version = SemanticVersioning.Version; + +namespace Torch.API.WebAPI; + +public class GithubQuery : IUpdateQuery +{ + private readonly HttpClient _client; + + public GithubQuery(string url) + { + if (url == null) throw new ArgumentNullException(nameof(url)); + + _client = new() + { + BaseAddress = new(url), + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher, + DefaultRequestHeaders = + { + {"User-Agent", "TorchAPI"} + } + }; + } + + public void Dispose() + { + _client?.Dispose(); + } + + public async Task GetLatestReleaseAsync(string repository, string branch = null) + { + var response = await _client.GetFromJsonAsync($"/repos/{repository}/releases/latest", new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy() + }); + + if (response is null) + throw new($"Unable to get latest release for {repository}"); + + return new(Version.Parse(response.TagName), response.Assets.First(b => b.Name == "torch-server.zip").BrowserDownloadUrl); + } + + private record Asset( + string Url, + int Id, + string NodeId, + string Name, + string Label, + Uploader Uploader, + string ContentType, + string State, + int Size, + int DownloadCount, + DateTime CreatedAt, + DateTime UpdatedAt, + string BrowserDownloadUrl + ); + + private record Author( + string Login, + int Id, + string NodeId, + string AvatarUrl, + string GravatarId, + string Url, + string HtmlUrl, + string FollowersUrl, + string FollowingUrl, + string GistsUrl, + string StarredUrl, + string SubscriptionsUrl, + string OrganizationsUrl, + string ReposUrl, + string EventsUrl, + string ReceivedEventsUrl, + string Type, + bool SiteAdmin + ); + + private record Release( + string Url, + string AssetsUrl, + string UploadUrl, + string HtmlUrl, + int Id, + Author Author, + string NodeId, + string TagName, + string TargetCommitish, + string Name, + bool Draft, + bool Prerelease, + DateTime CreatedAt, + DateTime PublishedAt, + IReadOnlyList Assets, + string TarballUrl, + string ZipballUrl, + string Body + ); + + private record Uploader( + string Login, + int Id, + string NodeId, + string AvatarUrl, + string GravatarId, + string Url, + string HtmlUrl, + string FollowersUrl, + string FollowingUrl, + string GistsUrl, + string StarredUrl, + string SubscriptionsUrl, + string OrganizationsUrl, + string ReposUrl, + string EventsUrl, + string ReceivedEventsUrl, + string Type, + bool SiteAdmin + ); +} diff --git a/Torch.API/WebAPI/IUpdateQuery.cs b/Torch.API/WebAPI/IUpdateQuery.cs new file mode 100644 index 0000000..60647f1 --- /dev/null +++ b/Torch.API/WebAPI/IUpdateQuery.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace Torch.API.WebAPI; + +public interface IUpdateQuery : IDisposable +{ + Task GetLatestReleaseAsync(string repository, string branch = null); +} \ No newline at end of file diff --git a/Torch.API/WebAPI/JenkinsQuery.cs b/Torch.API/WebAPI/JenkinsQuery.cs index d9d099b..62baaa5 100644 --- a/Torch.API/WebAPI/JenkinsQuery.cs +++ b/Torch.API/WebAPI/JenkinsQuery.cs @@ -1,82 +1,52 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Net; using System.Net.Http; using System.Net.Http.Json; -using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; -using NLog; using Torch.API.Utils; using Version = SemanticVersioning.Version; namespace Torch.API.WebAPI { - public class JenkinsQuery + public class JenkinsQuery : IUpdateQuery { - private const string BRANCH_QUERY = "http://136.243.80.164:2690/job/Torch/job/{0}/" + API_PATH; - private const string ARTIFACT_PATH = "artifact/bin/torch-server.zip"; - private const string API_PATH = "api/json"; - - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - - private static JenkinsQuery _instance; - public static JenkinsQuery Instance => _instance ??= new JenkinsQuery(); - private HttpClient _client; - - private JenkinsQuery() + private const string ApiPath = "api/json"; + private readonly HttpClient _client; + + public JenkinsQuery(string url) { - _client = new HttpClient(); - } - - public async Task GetLatestVersion(string branch) - { - var h = await _client.GetAsync(string.Format(BRANCH_QUERY, branch)); - if (!h.IsSuccessStatusCode) - { - Log.Error($"'{branch}' Branch query failed with code {h.StatusCode}"); - if(h.StatusCode == HttpStatusCode.NotFound) - Log.Error("This likely means you're trying to update a branch that is not public on Jenkins. Sorry :("); - return null; - } - - var branchResponse = await h.Content.ReadFromJsonAsync(); - - if (branchResponse is null) - { - Log.Error("Error reading branch response"); - return null; - } + if (url == null) throw new ArgumentNullException(nameof(url)); - h = await _client.GetAsync($"{branchResponse.LastStableBuild.Url}{API_PATH}"); - if (h.IsSuccessStatusCode) - return await h.Content.ReadFromJsonAsync(); - - Log.Error($"Job query failed with code {h.StatusCode}"); - return null; - - } - - public async Task DownloadRelease(Job job, string path) - { - var h = await _client.GetAsync(job.Url + ARTIFACT_PATH); - if (!h.IsSuccessStatusCode) + _client = new() { - Log.Error($"Job download failed with code {h.StatusCode}"); - return false; - } - var s = await h.Content.ReadAsStreamAsync(); -#if !NETFRAMEWORK - await using var fs = new FileStream(path, FileMode.Create); -#else - using var fs = new FileStream(path, FileMode.Create); -#endif - await s.CopyToAsync(fs); - return true; + BaseAddress = new(url) + }; } + public async Task GetLatestReleaseAsync(string repository, string branch = null) + { + branch ??= "master"; + + var response = await _client.GetFromJsonAsync($"/job/{repository}/job/{branch}/{ApiPath}"); + + if (response is null) + throw new($"Unable to get latest release for {repository}"); + + var job = await _client.GetFromJsonAsync( + $"/job/{repository}/job/{branch}/{response.LastBuild.Number}/{ApiPath}"); + + if (job is null) + throw new($"Unable to get latest release for job {repository}/{response.LastBuild.Number}"); + + return new(job.Version, job.Url + job.Artifacts.First(b => b.FileName == "torch-server.zip").RelativePath); + } + + public void Dispose() + { + _client?.Dispose(); + } } public record BranchResponse(string Name, string Url, Build LastBuild, Build LastStableBuild); @@ -84,5 +54,12 @@ namespace Torch.API.WebAPI public record Build(int Number, string Url); public record Job(int Number, bool Building, string Description, string Result, string Url, - [property: JsonConverter(typeof(SemanticVersionConverter))] Version Version); + [property: JsonConverter(typeof(SemanticVersionConverter))] Version Version, + IReadOnlyList Artifacts); + + public record Artifact( + string DisplayPath, + string FileName, + string RelativePath + ); } diff --git a/Torch.API/WebAPI/UpdateRelease.cs b/Torch.API/WebAPI/UpdateRelease.cs new file mode 100644 index 0000000..33a7db2 --- /dev/null +++ b/Torch.API/WebAPI/UpdateRelease.cs @@ -0,0 +1,5 @@ +using SemanticVersioning; + +namespace Torch.API.WebAPI; + +public record UpdateRelease(Version Version, string ArtifactUrl); \ No newline at end of file diff --git a/Torch.Server/TorchConfig.cs b/Torch.Server/TorchConfig.cs index b0bb9d6..e40dfea 100644 --- a/Torch.Server/TorchConfig.cs +++ b/Torch.Server/TorchConfig.cs @@ -111,7 +111,14 @@ public class TorchConfig : ViewModel, ITorchConfig [Display(Name = "Login Token", Description = "Steam GSLT (can be used if you have dynamic ip)", GroupName = "Server")] public string LoginToken { get; set; } - + + public UpdateSource UpdateSource { get; set; } = new() + { + Repository = "PveTeam/Torch", + Url = "https://api.github.com", + SourceType = UpdateSourceType.Github + }; + // for backward compatibility public void Save(string path = null) => Initializer.Instance?.ConfigPersistent?.Save(path); } \ No newline at end of file diff --git a/Torch/Managers/UpdateManager.cs b/Torch/Managers/UpdateManager.cs index 5d7461f..4793157 100644 --- a/Torch/Managers/UpdateManager.cs +++ b/Torch/Managers/UpdateManager.cs @@ -5,6 +5,7 @@ using System.IO.Compression; using System.IO.Packaging; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -21,8 +22,9 @@ namespace Torch.Managers public class UpdateManager : Manager { private readonly Timer _updatePollTimer; - private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName; - private Logger _log = LogManager.GetCurrentClassLogger(); + private readonly string _torchDir = ApplicationContext.Current.TorchDirectory.FullName; + private readonly Logger _log = LogManager.GetCurrentClassLogger(); + [Dependency] private FilesystemManager _fsManager = null!; @@ -50,26 +52,22 @@ namespace Torch.Managers try { - var job = await JenkinsQuery.Instance.GetLatestVersion(Torch.TorchVersion.Build); - if (job == null) - { - _log.Info("Failed to fetch latest version."); - return; - } + var updateSource = Torch.Config.UpdateSource; - if (job.Version > Torch.TorchVersion) + IUpdateQuery source = updateSource.SourceType switch { - _log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {job.Version}"); - var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip"); - //new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName); - if (!await JenkinsQuery.Instance.DownloadRelease(job, updateName)) - { - _log.Warn("Failed to download new release!"); - return; - } - UpdateFromZip(updateName, _torchDir); - File.Delete(updateName); - _log.Warn($"Torch version {job.Version} has been installed, please restart Torch to finish the process."); + UpdateSourceType.Github => new GithubQuery(updateSource.Url), + UpdateSourceType.Jenkins => new JenkinsQuery(updateSource.Url), + _ => throw new ArgumentOutOfRangeException() + }; + + var release = await source.GetLatestReleaseAsync(updateSource.Repository, updateSource.Branch); + + if (release.Version > Torch.TorchVersion) + { + _log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {release.Version}"); + await UpdateAsync(release, _torchDir); + _log.Warn($"Torch version {release.Version} has been installed, please restart Torch to finish the process."); } else { @@ -83,23 +81,29 @@ namespace Torch.Managers } } - private void UpdateFromZip(string zipFile, string extractPath) + private async Task UpdateAsync(UpdateRelease release, string extractPath) { - using (var zip = ZipFile.OpenRead(zipFile)) + using var client = new HttpClient(); + await using var stream = await client.GetStreamAsync(release.ArtifactUrl); + UpdateFromZip(stream, extractPath); + } + + private void UpdateFromZip(Stream zipStream, string extractPath) + { + using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read, true); + + foreach (var file in zip.Entries) { - foreach (var file in zip.Entries) - { - if(file.Name == "NLog-user.config" && File.Exists(Path.Combine(extractPath, file.FullName))) - continue; + if(file.Name == "NLog-user.config" && File.Exists(Path.Combine(extractPath, file.FullName))) + continue; - _log.Debug($"Unzipping {file.FullName}"); - var targetFile = Path.Combine(extractPath, file.FullName); - _fsManager.SoftDelete(extractPath, file.FullName); - file.ExtractToFile(targetFile, true); - } - - //zip.ExtractToDirectory(extractPath); //throws exceptions sometimes? + _log.Debug($"Unzipping {file.FullName}"); + var targetFile = Path.Combine(extractPath, file.FullName); + _fsManager.SoftDelete(extractPath, file.FullName); + file.ExtractToFile(targetFile, true); } + + //zip.ExtractToDirectory(extractPath); //throws exceptions sometimes? } /// diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs index 9753bab..b6f1661 100644 --- a/Torch/TorchBase.cs +++ b/Torch/TorchBase.cs @@ -104,14 +104,8 @@ namespace Torch #pragma warning restore CS0618 Config = config; - var versionString = GetType().Assembly - .GetCustomAttribute()! - .InformationalVersion; - - if (!SemanticVersioning.Version.TryParse(versionString, out var version)) - throw new TypeLoadException("Unable to parse the Torch version from the assembly."); - - TorchVersion = version; + var assemblyVersion = GetType().Assembly.GetName().Version ?? new Version(); + TorchVersion = new(assemblyVersion.Major, assemblyVersion.Minor, assemblyVersion.Build); RunArgs = Array.Empty(); @@ -129,7 +123,7 @@ namespace Torch Managers.AddManager(sessionManager); Managers.AddManager(new PatchManager(this)); Managers.AddManager(new FilesystemManager(this)); - // Managers.AddManager(new UpdateManager(this)); + Managers.AddManager(new UpdateManager(this)); Managers.AddManager(new EventManager(this)); #pragma warning disable CS0618 Managers.AddManager(Plugins);