From ffd0c3c5b67446e71cd7480a4ca98f1f0767e935 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 13 Jan 2025 14:58:27 +0700 Subject: [PATCH 1/2] introduce configuration to define fw lite platform releases and how release assets are matched per platform, add a platform as a parameter to the http api --- .../LexBoxApi/Config/FwLiteReleaseConfig.cs | 22 ++++++++++++ .../Controllers/FwLiteReleaseController.cs | 20 ++++++----- backend/LexBoxApi/LexBoxApi.csproj | 1 + backend/LexBoxApi/LexBoxKernel.cs | 4 +++ .../FwLiteReleases/FwLiteReleaseService.cs | 34 ++++++++++++------- backend/LexBoxApi/appsettings.json | 12 +++++++ backend/LexCore/Entities/FwLiteRelease.cs | 8 +++++ .../Services/FwLiteReleaseServiceTests.cs | 30 +++++++++++----- 8 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 backend/LexBoxApi/Config/FwLiteReleaseConfig.cs diff --git a/backend/LexBoxApi/Config/FwLiteReleaseConfig.cs b/backend/LexBoxApi/Config/FwLiteReleaseConfig.cs new file mode 100644 index 000000000..096342c4a --- /dev/null +++ b/backend/LexBoxApi/Config/FwLiteReleaseConfig.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using LexBoxApi.Controllers; +using LexCore.Entities; + +namespace LexBoxApi.Config; + +public class FwLiteReleaseConfig +{ + public Dictionary Platforms { get; set; } = new(); +} + +public class FwLitePlatformConfig +{ + [Required] + public required string FileNameRegex { get; init; } + + [field: AllowNull] + public Regex FileName => field ??= new Regex(FileNameRegex); +} diff --git a/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs b/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs index 24338c781..f5a6eb015 100644 --- a/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs +++ b/backend/LexBoxApi/Controllers/FwLiteReleaseController.cs @@ -1,12 +1,10 @@ using System.Diagnostics; -using System.Text.Json; -using System.Text.Json.Serialization; +using LexBoxApi.Config; using LexBoxApi.Otel; using LexBoxApi.Services.FwLiteReleases; using LexCore.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Hybrid; namespace LexBoxApi.Controllers; @@ -19,10 +17,11 @@ public class FwLiteReleaseController(FwLiteReleaseService releaseService) : Cont [HttpGet("download-latest")] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DownloadLatest() + public async Task DownloadLatest([FromQuery] FwLitePlatform platform = FwLitePlatform.Windows) { using var activity = LexBoxActivitySource.Get().StartActivity(); - var latestRelease = await releaseService.GetLatestRelease(); + activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString()); + var latestRelease = await releaseService.GetLatestRelease(platform); if (latestRelease is null) { activity?.SetStatus(ActivityStatusCode.Error, "Latest release not found"); @@ -37,11 +36,13 @@ public async Task DownloadLatest() [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async ValueTask> LatestRelease(string? appVersion = null) + public async ValueTask> LatestRelease([FromQuery] FwLitePlatform platform = + FwLitePlatform.Windows, string? appVersion = null) { using var activity = LexBoxActivitySource.Get().StartActivity(); activity?.AddTag(FwLiteReleaseService.FwLiteClientVersionTag, appVersion ?? "unknown"); - var latestRelease = await releaseService.GetLatestRelease(); + activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString()); + var latestRelease = await releaseService.GetLatestRelease(platform); activity?.AddTag(FwLiteReleaseService.FwLiteReleaseVersionTag, latestRelease?.Version); if (latestRelease is null) return NotFound(); return latestRelease; @@ -49,11 +50,12 @@ public async ValueTask> LatestRelease(string? appVer [HttpGet("should-update")] [AllowAnonymous] - public async Task> ShouldUpdate([FromQuery] string appVersion) + public async Task> ShouldUpdate([FromQuery] string appVersion, [FromQuery] FwLitePlatform platform = FwLitePlatform.Windows) { using var activity = LexBoxActivitySource.Get().StartActivity(); activity?.AddTag(FwLiteReleaseService.FwLiteClientVersionTag, appVersion); - var response = await releaseService.ShouldUpdate(appVersion); + activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString()); + var response = await releaseService.ShouldUpdate(platform, appVersion); activity?.AddTag(FwLiteReleaseService.FwLiteReleaseVersionTag, response.Release?.Version); return response; } diff --git a/backend/LexBoxApi/LexBoxApi.csproj b/backend/LexBoxApi/LexBoxApi.csproj index d8960afa8..5aaa28276 100644 --- a/backend/LexBoxApi/LexBoxApi.csproj +++ b/backend/LexBoxApi/LexBoxApi.csproj @@ -5,6 +5,7 @@ true 7392cddf-9b3b-441c-9316-203bb5c4a6bc 1 + preview diff --git a/backend/LexBoxApi/LexBoxKernel.cs b/backend/LexBoxApi/LexBoxKernel.cs index ae464fc3f..ebfedd429 100644 --- a/backend/LexBoxApi/LexBoxKernel.cs +++ b/backend/LexBoxApi/LexBoxKernel.cs @@ -49,6 +49,10 @@ public static void AddLexBoxApi(this IServiceCollection services, .BindConfiguration("HealthChecks") .ValidateDataAnnotations() .ValidateOnStart(); + services.AddOptions() + .BindConfiguration("FwLiteRelease") + .ValidateDataAnnotations() + .ValidateOnStart(); services.AddHttpClient(); services.AddServiceDiscovery(); services.AddHttpClient(client => client.BaseAddress = new ("http://fwHeadless")) diff --git a/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs b/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs index 294a2618f..24afcac35 100644 --- a/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs +++ b/backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs @@ -1,28 +1,31 @@ using System.Diagnostics; -using LexBoxApi.Controllers; +using LexBoxApi.Config; using LexBoxApi.Otel; using LexCore.Entities; using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Options; namespace LexBoxApi.Services.FwLiteReleases; -public class FwLiteReleaseService(IHttpClientFactory factory, HybridCache cache) +public class FwLiteReleaseService(IHttpClientFactory factory, HybridCache cache, IOptions config) { 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 FwLiteReleaseVersionTag = "app.fw-lite.release.version"; - public async ValueTask GetLatestRelease(CancellationToken token = default) + public async ValueTask GetLatestRelease(FwLitePlatform platform, CancellationToken token = default) { - return await cache.GetOrCreateAsync(GithubLatestRelease, + return await cache.GetOrCreateAsync($"{GithubLatestRelease}|{platform}", + platform, FetchLatestReleaseFromGithub, new HybridCacheEntryOptions() { Expiration = TimeSpan.FromDays(1) }, - cancellationToken: token); + cancellationToken: token, tags: [GithubLatestRelease]); } - public async ValueTask ShouldUpdate(string appVersion) + public async ValueTask ShouldUpdate(FwLitePlatform platform, string appVersion) { - var latestRelease = await GetLatestRelease(); + var latestRelease = await GetLatestRelease(platform); if (latestRelease is null) return new ShouldUpdateResponse(null); var shouldUpdateToRelease = ShouldUpdateToRelease(appVersion, latestRelease.Version); @@ -36,12 +39,18 @@ public static bool ShouldUpdateToRelease(string appVersion, string latestVersion public async ValueTask InvalidateReleaseCache() { - await cache.RemoveAsync(GithubLatestRelease); + await cache.RemoveByTagAsync(GithubLatestRelease); } - private async ValueTask FetchLatestReleaseFromGithub(CancellationToken token) + private async ValueTask FetchLatestReleaseFromGithub(FwLitePlatform platform, CancellationToken token) { + var platformConfig = config.Value.Platforms.GetValueOrDefault(platform); + if (platformConfig is null) + { + throw new ArgumentException($"No config for platform {platform}"); + } using var activity = LexBoxActivitySource.Get().StartActivity(); + activity?.AddTag(FwLitePlatformTag, platform.ToString()); var response = await factory.CreateClient("Github") .SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/sillsdev/languageforge-lexbox/releases") @@ -74,12 +83,11 @@ public async ValueTask InvalidateReleaseCache() continue; } - var msixBundle = release.Assets.FirstOrDefault(a => - a.Name.EndsWith(".msixbundle", StringComparison.InvariantCultureIgnoreCase)); - if (msixBundle is not null) + var releaseAsset = release.Assets.FirstOrDefault(a => platformConfig.FileName.IsMatch(a.Name)); + if (releaseAsset is not null) { activity?.AddTag(FwLiteReleaseVersionTag, release.TagName); - return new FwLiteRelease(release.TagName, msixBundle.BrowserDownloadUrl); + return new FwLiteRelease(release.TagName, releaseAsset.BrowserDownloadUrl); } } } diff --git a/backend/LexBoxApi/appsettings.json b/backend/LexBoxApi/appsettings.json index 3ae6b1f17..7a7dc9a08 100644 --- a/backend/LexBoxApi/appsettings.json +++ b/backend/LexBoxApi/appsettings.json @@ -75,5 +75,17 @@ "fwHeadless": { "http": ["fw-headless"] } + }, + "FwLiteRelease": { + "Platforms": { + "Windows": { + // ends with .msixbundle regex + "FileNameRegex": "(?i)\\.msixbundle$" + }, + "Linux": { + // ends with linux.zip regex + "FileNameRegex": "(?i)linux\\.zip$" + } + } } } diff --git a/backend/LexCore/Entities/FwLiteRelease.cs b/backend/LexCore/Entities/FwLiteRelease.cs index 895deb5e5..a9d881e38 100644 --- a/backend/LexCore/Entities/FwLiteRelease.cs +++ b/backend/LexCore/Entities/FwLiteRelease.cs @@ -1,9 +1,17 @@ using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace LexCore.Entities; public record FwLiteRelease(string Version, string Url); +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum FwLitePlatform +{ + Windows, + Linux, +} + public record ShouldUpdateResponse(FwLiteRelease? Release) { [MemberNotNullWhen(true, nameof(Release))] diff --git a/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs b/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs index d3ea7e2fc..91125fe9e 100644 --- a/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs +++ b/backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; using LexBoxApi; +using LexBoxApi.Config; using LexBoxApi.Services.FwLiteReleases; +using LexCore.Entities; using Microsoft.Extensions.DependencyInjection; using Testing.Fixtures; @@ -14,17 +16,27 @@ public FwLiteReleaseServiceTests() { //disable warning about hybrid cache being experimental #pragma warning disable EXTEXP0018 - var services = new ServiceCollection().AddSingleton().AddHttpClient() - .AddHybridCache() - .Services.BuildServiceProvider(); + var services = new ServiceCollection() + .AddSingleton() + .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$" }); + }) + .Services + .AddHybridCache() + .Services.BuildServiceProvider(); #pragma warning restore EXTEXP0018 _fwLiteReleaseService = services.GetRequiredService(); } - [Fact] - public async Task CanGetLatestRelease() + [Theory] + [InlineData(FwLitePlatform.Windows)] + [InlineData(FwLitePlatform.Linux)] + public async Task CanGetLatestRelease(FwLitePlatform platform) { - var latestRelease = await _fwLiteReleaseService.GetLatestRelease(default); + var latestRelease = await _fwLiteReleaseService.GetLatestRelease(platform); latestRelease.Should().NotBeNull(); latestRelease.Version.Should().NotBeNullOrEmpty(); latestRelease.Url.Should().NotBeNullOrEmpty(); @@ -34,7 +46,7 @@ public async Task CanGetLatestRelease() [InlineData("v2024-11-20-d04e9b96")] public async Task IsConsideredAnOldVersion(string appVersion) { - var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(appVersion); + var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLitePlatform.Windows, appVersion); shouldUpdate.Should().NotBeNull(); shouldUpdate.Release.Should().NotBeNull(); shouldUpdate.Update.Should().BeTrue(); @@ -43,9 +55,9 @@ public async Task IsConsideredAnOldVersion(string appVersion) [Fact] public async Task ShouldUpdateWithLatestVersionShouldReturnFalse() { - var latestRelease = await _fwLiteReleaseService.GetLatestRelease(default); + var latestRelease = await _fwLiteReleaseService.GetLatestRelease(FwLitePlatform.Windows); latestRelease.Should().NotBeNull(); - var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(latestRelease.Version); + var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLitePlatform.Windows, latestRelease.Version); shouldUpdate.Should().NotBeNull(); shouldUpdate.Release.Should().BeNull(); shouldUpdate.Update.Should().BeFalse(); From 56ce255ba1bb96116940cb0b11ec87e68f67c40a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 13 Jan 2025 17:02:32 +0700 Subject: [PATCH 2/2] add other platforms --- backend/LexCore/Entities/FwLiteRelease.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/LexCore/Entities/FwLiteRelease.cs b/backend/LexCore/Entities/FwLiteRelease.cs index a9d881e38..68f8910bb 100644 --- a/backend/LexCore/Entities/FwLiteRelease.cs +++ b/backend/LexCore/Entities/FwLiteRelease.cs @@ -10,6 +10,10 @@ public enum FwLitePlatform { Windows, Linux, + Android, + // ReSharper disable once InconsistentNaming + iOS, + Mac } public record ShouldUpdateResponse(FwLiteRelease? Release)