diff --git a/Shoko.Server/API/WebUI/WebUIHelper.cs b/Shoko.Server/API/WebUI/WebUIHelper.cs index b999a94cc..2fdf1ce19 100644 --- a/Shoko.Server/API/WebUI/WebUIHelper.cs +++ b/Shoko.Server/API/WebUI/WebUIHelper.cs @@ -236,7 +236,7 @@ private static void AddReleaseDate(DateTime releaseDate) /// Repository name. /// /// An error occurred while downloading the resource. - internal static dynamic? DownloadApiResponse(string endpoint, string? repoName = null) + internal static dynamic DownloadApiResponse(string endpoint, string? repoName = null) { repoName ??= ClientRepoName; using var client = new HttpClient(); @@ -244,6 +244,6 @@ private static void AddReleaseDate(DateTime releaseDate) client.DefaultRequestHeaders.Add("User-Agent", $"ShokoServer/{Utils.GetApplicationVersion()}"); var response = client.GetStringAsync(new Uri($"https://api.github.com/repos/{repoName}/{endpoint}")) .ConfigureAwait(false).GetAwaiter().GetResult(); - return JsonConvert.DeserializeObject(response); + return JsonConvert.DeserializeObject(response)!; } } diff --git a/Shoko.Server/API/v3/Controllers/WebUIController.cs b/Shoko.Server/API/v3/Controllers/WebUIController.cs index fadd1639b..fe1c2dfeb 100644 --- a/Shoko.Server/API/v3/Controllers/WebUIController.cs +++ b/Shoko.Server/API/v3/Controllers/WebUIController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Net; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -31,6 +32,7 @@ using Input = Shoko.Server.API.v3.Models.Shoko.WebUI.Input; #pragma warning disable CA1822 +#nullable enable namespace Shoko.Server.API.v3.Controllers; /// @@ -41,7 +43,7 @@ namespace Shoko.Server.API.v3.Controllers; [ApiController] [Route("/api/v{version:apiVersion}/[controller]")] [ApiV3] -public class WebUIController : BaseController +public partial class WebUIController : BaseController { private static readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions() { @@ -334,8 +336,8 @@ public ActionResult GetSeries([FromRoute, Range(1, int.MaxValu [HttpGet("Series/{seriesID}/FileSummary")] public ActionResult GetSeriesFileSummary( [FromRoute, Range(1, int.MaxValue)] int seriesID, - [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet type = null, - [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet groupBy = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet? type = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet? groupBy = null, [FromQuery] bool includeEpisodeDetails = false, [FromQuery] bool includeMissingUnknownEpisodes = false, [FromQuery] bool includeMissingFutureEpisodes = false) @@ -381,7 +383,10 @@ public ActionResult InstallWebUI([FromQuery] ReleaseChannel channel = ReleaseCha var result = LatestWebUIVersion(channel); if (result.Value is null) - return result.Result; + return result.Result!; + + if (result.Value.Tag is null) + return BadRequest("Unable to install web UI because a GitHub release was not found."); try { @@ -423,7 +428,10 @@ public ActionResult UpdateWebUI([FromQuery] ReleaseChannel channel = ReleaseChan channel = GetCurrentWebUIReleaseChannel(); var result = LatestWebUIVersion(channel); if (result.Value is null) - return result.Result; + return result.Result!; + + if (result.Value.Tag is null) + return BadRequest("Unable to update web UI because a GitHub release was not found."); try { @@ -450,6 +458,9 @@ public ActionResult UpdateWebUI([FromQuery] ReleaseChannel channel = ReleaseChan public ActionResult UpdateWebUIOld([FromQuery] ReleaseChannel channel = ReleaseChannel.Auto) => UpdateWebUI(channel); + [GeneratedRegex(@"^[Vv]?(?(?\d+)\.(?\d+)\.(?\d+))(?:-dev.(?\d+))?$", RegexOptions.Compiled, "en-US")] + private static partial Regex ReleaseVersionRegex(); + /// /// Check for latest version for the selected and /// return a containing the version @@ -469,17 +480,25 @@ public ActionResult LatestWebUIVersion([FromQuery] ReleaseChan channel = GetCurrentWebUIReleaseChannel(); var key = $"webui:{channel}"; if (!force && _cache.TryGetValue(key, out var componentVersion)) - return componentVersion; + return componentVersion!; switch (channel) { // Check for dev channel updates. case ReleaseChannel.Dev: { + var regex = ReleaseVersionRegex(); var releases = WebUIHelper.DownloadApiResponse("releases?per_page=10&page=1"); foreach (var release in releases) { string tagName = release.tag_name; - var version = tagName[0] == 'v' ? tagName[1..] : tagName; + if (regex.Match(tagName) is not { Success: true } regexResult) + continue; + + var version = regexResult.Groups["version"].Value; + if (regexResult.Groups["buildNumber"].Success) + version += "." + regexResult.Groups["buildNumber"].Value; + else + version += ".0"; foreach (var asset in release.assets) { // We don't care what the zip is named, only that it is attached. @@ -519,7 +538,7 @@ public ActionResult LatestWebUIVersion([FromQuery] ReleaseChan DateTime releaseDate = latestRelease.published_at; releaseDate = releaseDate.ToUniversalTime(); string description = latestRelease.body; - return _cache.Set(key, new ComponentVersion + return _cache.Set(key, new ComponentVersion { Version = version, Commit = commit[0..7], @@ -558,18 +577,39 @@ public ActionResult LatestServerWebUIVersion([FromQuery] Relea channel = GetCurrentServerReleaseChannel(); var key = $"server:{channel}"; if (!force && _cache.TryGetValue(key, out var componentVersion)) - return componentVersion; + return componentVersion!; switch (channel) { // Check for dev channel updates. case ReleaseChannel.Dev: { - var latestRelease = WebUIHelper.DownloadApiResponse("releases/latest", WebUIHelper.ServerRepoName); - var masterBranch = WebUIHelper.DownloadApiResponse("git/ref/heads/master", WebUIHelper.ServerRepoName); - string commitSha = masterBranch["object"].sha; + var latestTags = WebUIHelper.DownloadApiResponse($"tags?per_page=100&page=1", WebUIHelper.ServerRepoName); + var version = string.Empty; + var tagName = string.Empty; + var commitSha = string.Empty; + var regex = ReleaseVersionRegex(); + foreach (var tagInfo in latestTags) + { + string localTagName = tagInfo.name; + if (regex.Match(localTagName) is { Success: true } regexResult) + { + tagName = localTagName; + commitSha = tagInfo.commit.sha; + version = regexResult.Groups["version"].Value; + if (regexResult.Groups["buildNumber"].Success) + version += "." + regexResult.Groups["buildNumber"].Value; + else + version += ".0"; + break; + } + } + + if (string.IsNullOrEmpty(commitSha)) + { + return BadRequest("Unable to locate the latest release to use."); + } + var latestCommit = WebUIHelper.DownloadApiResponse($"commits/{commitSha}", WebUIHelper.ServerRepoName); - string tagName = latestRelease.tag_name; - var version = tagName[1..] + ".0"; DateTime releaseDate = latestCommit.commit.author.date; releaseDate = releaseDate.ToUniversalTime(); string description; @@ -597,6 +637,7 @@ public ActionResult LatestServerWebUIVersion([FromQuery] Relea Commit = commitSha, ReleaseChannel = ReleaseChannel.Dev, ReleaseDate = releaseDate, + Tag = tagName, Description = description, }, _cacheTTL); } @@ -640,7 +681,7 @@ public ActionResult LatestServerWebUIVersion([FromQuery] Relea ReleaseChannel = ReleaseChannel.Stable, ReleaseDate = releaseDate, Tag = tagName, - Description = description.Trim() + Description = description.Trim(), }, _cacheTTL); } }