From b862725bca9a53beac4979eefc99e8b6eb0c4812 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 27 Jan 2025 11:28:16 +0700 Subject: [PATCH] change FwLitePlatform to edition (#1402) * rename FwLitePlatform to edition to better reflect that it does not just reflect a single OS * start work on generating an AppInstaller for fw lite, blocked by github not using the correct content type for msixbundles --- .../LexBoxApi/Config/FwLiteReleaseConfig.cs | 4 +- .../Controllers/FwLiteReleaseController.cs | 32 +++++---- .../FwLiteReleases/FwLiteReleaseService.cs | 67 +++++++++++++++---- backend/LexBoxApi/appsettings.json | 2 +- backend/LexCore/Entities/FwLiteRelease.cs | 6 +- .../Services/FwLiteReleaseServiceTests.cs | 18 ++--- 6 files changed, 91 insertions(+), 38 deletions(-) diff --git a/backend/LexBoxApi/Config/FwLiteReleaseConfig.cs b/backend/LexBoxApi/Config/FwLiteReleaseConfig.cs index 096342c4a..eef7db11a 100644 --- a/backend/LexBoxApi/Config/FwLiteReleaseConfig.cs +++ b/backend/LexBoxApi/Config/FwLiteReleaseConfig.cs @@ -9,10 +9,10 @@ namespace LexBoxApi.Config; public class FwLiteReleaseConfig { - public Dictionary Platforms { get; set; } = new(); + public Dictionary Editions { get; set; } = new(); } -public class FwLitePlatformConfig +public class FwLiteEditionConfig { [Required] public required string FileNameRegex { get; init; } diff --git a/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs b/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs index f5a6eb015..c0a960cde 100644 --- a/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs +++ b/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using LexBoxApi.Config; +using System.Text; using LexBoxApi.Otel; using LexBoxApi.Services.FwLiteReleases; using LexCore.Entities; @@ -13,15 +13,23 @@ namespace LexBoxApi.Controllers; [ApiExplorerSettings(GroupName = LexBoxKernel.OpenApiPublicDocumentName)] public class FwLiteReleaseController(FwLiteReleaseService releaseService) : ControllerBase { - [HttpGet("download-latest")] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DownloadLatest([FromQuery] FwLitePlatform platform = FwLitePlatform.Windows) + public async Task DownloadLatest([FromQuery] FwLiteEdition edition = FwLiteEdition.Windows) { using var activity = LexBoxActivitySource.Get().StartActivity(); - activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString()); - var latestRelease = await releaseService.GetLatestRelease(platform); + activity?.AddTag(FwLiteReleaseService.FwLiteEditionTag, edition.ToString()); + if (edition == FwLiteEdition.WindowsAppInstaller) + { + //note this doesn't really work because the github server doesn't return the correct content-type of application/msixbundle + //in order for this to work we would need to proxy the request to github + //but then we would need to support range requests https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests + //which is too complicated for now + var appInstallerContent = await releaseService.GenerateAppInstaller(); + return File(Encoding.UTF8.GetBytes(appInstallerContent), "application/appinstaller", "FieldWorksLite.appinstaller"); + } + var latestRelease = await releaseService.GetLatestRelease(edition); if (latestRelease is null) { activity?.SetStatus(ActivityStatusCode.Error, "Latest release not found"); @@ -36,13 +44,13 @@ public async Task DownloadLatest([FromQuery] FwLitePlatform platfo [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async ValueTask> LatestRelease([FromQuery] FwLitePlatform platform = - FwLitePlatform.Windows, string? appVersion = null) + public async ValueTask> LatestRelease([FromQuery] FwLiteEdition edition = + FwLiteEdition.Windows, string? appVersion = null) { using var activity = LexBoxActivitySource.Get().StartActivity(); activity?.AddTag(FwLiteReleaseService.FwLiteClientVersionTag, appVersion ?? "unknown"); - activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString()); - var latestRelease = await releaseService.GetLatestRelease(platform); + activity?.AddTag(FwLiteReleaseService.FwLiteEditionTag, edition.ToString()); + var latestRelease = await releaseService.GetLatestRelease(edition); activity?.AddTag(FwLiteReleaseService.FwLiteReleaseVersionTag, latestRelease?.Version); if (latestRelease is null) return NotFound(); return latestRelease; @@ -50,12 +58,12 @@ public async ValueTask> LatestRelease([FromQuery] Fw [HttpGet("should-update")] [AllowAnonymous] - public async Task> ShouldUpdate([FromQuery] string appVersion, [FromQuery] FwLitePlatform platform = FwLitePlatform.Windows) + public async Task> ShouldUpdate([FromQuery] string appVersion, [FromQuery] FwLiteEdition edition = FwLiteEdition.Windows) { using var activity = LexBoxActivitySource.Get().StartActivity(); activity?.AddTag(FwLiteReleaseService.FwLiteClientVersionTag, appVersion); - activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString()); - var response = await releaseService.ShouldUpdate(platform, appVersion); + activity?.AddTag(FwLiteReleaseService.FwLiteEditionTag, edition.ToString()); + var response = await releaseService.ShouldUpdate(edition, appVersion); activity?.AddTag(FwLiteReleaseService.FwLiteReleaseVersionTag, response.Release?.Version); return response; } diff --git a/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs b/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs index 24afcac35..60215175a 100644 --- a/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs +++ b/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs @@ -11,21 +11,25 @@ public class FwLiteReleaseService(IHttpClientFactory factory, HybridCache cache, { private const string GithubLatestRelease = "GithubLatestRelease"; public const string FwLiteClientVersionTag = "app.fw-lite.client.version"; - public const string FwLitePlatformTag = "app.fw-lite.platform"; + public const string FwLiteEditionTag = "app.fw-lite.edition"; public const string FwLiteReleaseVersionTag = "app.fw-lite.release.version"; - public async ValueTask GetLatestRelease(FwLitePlatform platform, CancellationToken token = default) + public async ValueTask GetLatestRelease(FwLiteEdition edition, CancellationToken token = default) { - return await cache.GetOrCreateAsync($"{GithubLatestRelease}|{platform}", - platform, + if (edition == FwLiteEdition.WindowsAppInstaller) + { + throw new ArgumentException("WindowsAppInstaller edition is not supported"); + } + return await cache.GetOrCreateAsync($"{GithubLatestRelease}|{edition}", + edition, FetchLatestReleaseFromGithub, new HybridCacheEntryOptions() { Expiration = TimeSpan.FromDays(1) }, cancellationToken: token, tags: [GithubLatestRelease]); } - public async ValueTask ShouldUpdate(FwLitePlatform platform, string appVersion) + public async ValueTask ShouldUpdate(FwLiteEdition edition, string appVersion) { - var latestRelease = await GetLatestRelease(platform); + var latestRelease = await GetLatestRelease(edition); if (latestRelease is null) return new ShouldUpdateResponse(null); var shouldUpdateToRelease = ShouldUpdateToRelease(appVersion, latestRelease.Version); @@ -42,15 +46,15 @@ public async ValueTask InvalidateReleaseCache() await cache.RemoveByTagAsync(GithubLatestRelease); } - private async ValueTask FetchLatestReleaseFromGithub(FwLitePlatform platform, CancellationToken token) + private async ValueTask FetchLatestReleaseFromGithub(FwLiteEdition edition, CancellationToken token) { - var platformConfig = config.Value.Platforms.GetValueOrDefault(platform); - if (platformConfig is null) + var editionConfig = config.Value.Editions.GetValueOrDefault(edition); + if (editionConfig is null) { - throw new ArgumentException($"No config for platform {platform}"); + throw new ArgumentException($"No config for edition {edition}"); } using var activity = LexBoxActivitySource.Get().StartActivity(); - activity?.AddTag(FwLitePlatformTag, platform.ToString()); + activity?.AddTag(FwLiteEditionTag, edition.ToString()); var response = await factory.CreateClient("Github") .SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/sillsdev/languageforge-lexbox/releases") @@ -83,7 +87,7 @@ public async ValueTask InvalidateReleaseCache() continue; } - var releaseAsset = release.Assets.FirstOrDefault(a => platformConfig.FileName.IsMatch(a.Name)); + var releaseAsset = release.Assets.FirstOrDefault(a => editionConfig.FileName.IsMatch(a.Name)); if (releaseAsset is not null) { activity?.AddTag(FwLiteReleaseVersionTag, release.TagName); @@ -96,4 +100,43 @@ public async ValueTask InvalidateReleaseCache() activity?.AddTag(FwLiteReleaseVersionTag, null); return null; } + + public async ValueTask GenerateAppInstaller(CancellationToken token = default) + { + var windowsRelease = await GetLatestRelease(FwLiteEdition.Windows, token); + if (windowsRelease is null) throw new InvalidOperationException("Windows release not found"); + var version = ConvertVersionToAppInstallerVersion(windowsRelease.Version); + //lang=xml + return $""" + + + + + + false + + + +"""; + } + + private static string ConvertVersionToAppInstallerVersion(string version) + { + //version is something like v2025-01-17-a62c709c which should be converted to 2025.1.17.1 always adding .1 on the end and trimming zeros + return version.Split('-') switch + { + [var year, var month, var day, ..] => $"{year.TrimStart('v')}.{month.TrimStart('0')}.{day.TrimStart('0')}.1", + _ => throw new ArgumentException($"Invalid version {version}") + }; + } } diff --git a/backend/LexBoxApi/appsettings.json b/backend/LexBoxApi/appsettings.json index 7a7dc9a08..1f224fced 100644 --- a/backend/LexBoxApi/appsettings.json +++ b/backend/LexBoxApi/appsettings.json @@ -77,7 +77,7 @@ } }, "FwLiteRelease": { - "Platforms": { + "Editions": { "Windows": { // ends with .msixbundle regex "FileNameRegex": "(?i)\\.msixbundle$" diff --git a/backend/LexCore/Entities/FwLiteRelease.cs b/backend/LexCore/Entities/FwLiteRelease.cs index 68f8910bb..7f5a6382e 100644 --- a/backend/LexCore/Entities/FwLiteRelease.cs +++ b/backend/LexCore/Entities/FwLiteRelease.cs @@ -6,14 +6,16 @@ namespace LexCore.Entities; public record FwLiteRelease(string Version, string Url); [JsonConverter(typeof(JsonStringEnumConverter))] -public enum FwLitePlatform +public enum FwLiteEdition { Windows, Linux, Android, // ReSharper disable once InconsistentNaming iOS, - Mac + Mac, + //not supported for now, see note in FwLiteReleaseController.DownloadLatest + WindowsAppInstaller } public record ShouldUpdateResponse(FwLiteRelease? Release) diff --git a/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs b/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs index 91125fe9e..5264e7542 100644 --- a/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs +++ b/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs @@ -21,8 +21,8 @@ public FwLiteReleaseServiceTests() .AddHttpClient() .AddOptions().Configure(config => { - config.Platforms.Add(FwLitePlatform.Windows, new FwLitePlatformConfig() { FileNameRegex = "(?i)\\.msixbundle$" }); - config.Platforms.Add(FwLitePlatform.Linux, new FwLitePlatformConfig() { FileNameRegex = "(?i)linux\\.zip$" }); + config.Editions.Add(FwLiteEdition.Windows, new FwLiteEditionConfig() { FileNameRegex = "(?i)\\.msixbundle$" }); + config.Editions.Add(FwLiteEdition.Linux, new FwLiteEditionConfig() { FileNameRegex = "(?i)linux\\.zip$" }); }) .Services .AddHybridCache() @@ -32,11 +32,11 @@ public FwLiteReleaseServiceTests() } [Theory] - [InlineData(FwLitePlatform.Windows)] - [InlineData(FwLitePlatform.Linux)] - public async Task CanGetLatestRelease(FwLitePlatform platform) + [InlineData(FwLiteEdition.Windows)] + [InlineData(FwLiteEdition.Linux)] + public async Task CanGetLatestRelease(FwLiteEdition edition) { - var latestRelease = await _fwLiteReleaseService.GetLatestRelease(platform); + var latestRelease = await _fwLiteReleaseService.GetLatestRelease(edition); latestRelease.Should().NotBeNull(); latestRelease.Version.Should().NotBeNullOrEmpty(); latestRelease.Url.Should().NotBeNullOrEmpty(); @@ -46,7 +46,7 @@ public async Task CanGetLatestRelease(FwLitePlatform platform) [InlineData("v2024-11-20-d04e9b96")] public async Task IsConsideredAnOldVersion(string appVersion) { - var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLitePlatform.Windows, appVersion); + var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLiteEdition.Windows, appVersion); shouldUpdate.Should().NotBeNull(); shouldUpdate.Release.Should().NotBeNull(); shouldUpdate.Update.Should().BeTrue(); @@ -55,9 +55,9 @@ public async Task IsConsideredAnOldVersion(string appVersion) [Fact] public async Task ShouldUpdateWithLatestVersionShouldReturnFalse() { - var latestRelease = await _fwLiteReleaseService.GetLatestRelease(FwLitePlatform.Windows); + var latestRelease = await _fwLiteReleaseService.GetLatestRelease(FwLiteEdition.Windows); latestRelease.Should().NotBeNull(); - var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLitePlatform.Windows, latestRelease.Version); + var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLiteEdition.Windows, latestRelease.Version); shouldUpdate.Should().NotBeNull(); shouldUpdate.Release.Should().BeNull(); shouldUpdate.Update.Should().BeFalse();