Skip to content

Commit

Permalink
change FwLitePlatform to edition (#1402)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hahn-kev authored Jan 27, 2025
1 parent 3bb27e7 commit b862725
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 38 deletions.
4 changes: 2 additions & 2 deletions backend/LexBoxApi/Config/FwLiteReleaseConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ namespace LexBoxApi.Config;

public class FwLiteReleaseConfig
{
public Dictionary<FwLitePlatform, FwLitePlatformConfig> Platforms { get; set; } = new();
public Dictionary<FwLiteEdition, FwLiteEditionConfig> Editions { get; set; } = new();
}

public class FwLitePlatformConfig
public class FwLiteEditionConfig
{
[Required]
public required string FileNameRegex { get; init; }
Expand Down
32 changes: 20 additions & 12 deletions backend/LexBoxApi/Controllers/FwLiteReleaseController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Diagnostics;
using LexBoxApi.Config;
using System.Text;
using LexBoxApi.Otel;
using LexBoxApi.Services.FwLiteReleases;
using LexCore.Entities;
Expand All @@ -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<ActionResult> DownloadLatest([FromQuery] FwLitePlatform platform = FwLitePlatform.Windows)
public async Task<ActionResult> 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");
Expand All @@ -36,26 +44,26 @@ public async Task<ActionResult> DownloadLatest([FromQuery] FwLitePlatform platfo
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async ValueTask<ActionResult<FwLiteRelease>> LatestRelease([FromQuery] FwLitePlatform platform =
FwLitePlatform.Windows, string? appVersion = null)
public async ValueTask<ActionResult<FwLiteRelease>> 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;
}

[HttpGet("should-update")]
[AllowAnonymous]
public async Task<ActionResult<ShouldUpdateResponse>> ShouldUpdate([FromQuery] string appVersion, [FromQuery] FwLitePlatform platform = FwLitePlatform.Windows)
public async Task<ActionResult<ShouldUpdateResponse>> 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;
}
Expand Down
67 changes: 55 additions & 12 deletions backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FwLiteRelease?> GetLatestRelease(FwLitePlatform platform, CancellationToken token = default)
public async ValueTask<FwLiteRelease?> 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<ShouldUpdateResponse> ShouldUpdate(FwLitePlatform platform, string appVersion)
public async ValueTask<ShouldUpdateResponse> 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);
Expand All @@ -42,15 +46,15 @@ public async ValueTask InvalidateReleaseCache()
await cache.RemoveByTagAsync(GithubLatestRelease);
}

private async ValueTask<FwLiteRelease?> FetchLatestReleaseFromGithub(FwLitePlatform platform, CancellationToken token)
private async ValueTask<FwLiteRelease?> 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")
Expand Down Expand Up @@ -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);
Expand All @@ -96,4 +100,43 @@ public async ValueTask InvalidateReleaseCache()
activity?.AddTag(FwLiteReleaseVersionTag, null);
return null;
}

public async ValueTask<string> 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 $"""
<?xml version="1.0" encoding="utf-8"?>
<AppInstaller
Uri="https://lexbox.org/api/fwlite-release/download-latest?edition=windowsAppInstaller"
Version="{version}"
xmlns="http://schemas.microsoft.com/appx/appinstaller/2021">
<MainBundle
Name="FwLiteDesktop"
Publisher="CN=&quot;Summer Institute of Linguistics, Inc.&quot;, O=&quot;Summer Institute of Linguistics, Inc.&quot;, L=Dallas, S=Texas, C=US"
Version="{version}"
Uri="{windowsRelease.Url}" />
<UpdateSettings>
<OnLaunch
HoursBetweenUpdateChecks="8"
ShowPrompt="true"
UpdateBlocksActivation="false" />
<ForceUpdateFromAnyVersion>false</ForceUpdateFromAnyVersion>
<AutomaticBackgroundTask />
</UpdateSettings>
</AppInstaller>
""";
}

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}")
};
}
}
2 changes: 1 addition & 1 deletion backend/LexBoxApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
}
},
"FwLiteRelease": {
"Platforms": {
"Editions": {
"Windows": {
// ends with .msixbundle regex
"FileNameRegex": "(?i)\\.msixbundle$"
Expand Down
6 changes: 4 additions & 2 deletions backend/LexCore/Entities/FwLiteRelease.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 9 additions & 9 deletions backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public FwLiteReleaseServiceTests()
.AddHttpClient()
.AddOptions<FwLiteReleaseConfig>().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()
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down

0 comments on commit b862725

Please sign in to comment.