Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 20 additions & 22 deletions src/TableCloth.App/Components/Implementations/AppUpdateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,7 @@ public async Task DownloadAndApplyUpdatesAsync(IProgress<int>? progress = null,
// 현재 아키텍처 확인
var arch = GetCurrentArchitecture();

// 파일 이름 규칙(이슈 #296): Retail = TableCloth_{ver}_Release_{arch}.exe,
// Preview = TableCloth-Preview_{ver}_Release_{arch}.exe. 채널에 맞는 자산만 고른다
// ("-Preview" 포함 여부로 링 구분 — Retail 이 프리뷰 자산을 잡지 않도록).
var wantPreview = _channel == ReleaseChannel.Preview;
var matchingAsset = releaseInfo.Assets.FirstOrDefault(a =>
a.Name != null &&
a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) &&
a.Name.Contains($"_Release_{arch}", StringComparison.OrdinalIgnoreCase) &&
a.Name.Contains("-Preview", StringComparison.OrdinalIgnoreCase) == wantPreview);
var matchingAsset = FindInstallerAsset(releaseInfo.Assets, arch, _channel);

if (matchingAsset?.BrowserDownloadUrl != null &&
Uri.TryCreate(matchingAsset.BrowserDownloadUrl, UriKind.Absolute, out var downloadUri))
Expand All @@ -213,18 +205,8 @@ public async Task DownloadAndApplyUpdatesAsync(IProgress<int>? progress = null,
return downloadUri;
}

// 아키텍처별 파일을 찾지 못하면 첫 번째 exe 파일 사용
var fallbackAsset = releaseInfo.Assets.FirstOrDefault(a =>
a.Name != null &&
a.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));

if (fallbackAsset?.BrowserDownloadUrl != null &&
Uri.TryCreate(fallbackAsset.BrowserDownloadUrl, UriKind.Absolute, out var fallbackUri))
{
_logger.LogInformation("Using fallback release asset: {AssetName}", fallbackAsset.Name);
return fallbackUri;
}

// 일치하는 설치 파일이 없으면 호출자가 릴리스 페이지로 안내한다.
// Spork/SporkBootstrap 등 다른 프로그램을 업데이트로 선택하지 않는다(#283).
return null;
}
catch (Exception ex)
Expand All @@ -234,6 +216,22 @@ public async Task DownloadAndApplyUpdatesAsync(IProgress<int>? progress = null,
}
}

internal static GitHubAssetInfo? FindInstallerAsset(
GitHubAssetInfo[] assets,
string arch,
ReleaseChannel channel)
{
// Retail = TableCloth_{ver}_Release_{arch}.exe,
// Preview = TableCloth-Preview_{ver}_Release_{arch}.exe.
var prefix = channel == ReleaseChannel.Preview ? "TableCloth-Preview_" : "TableCloth_";
var suffix = $"_Release_{arch}.exe";

return assets.FirstOrDefault(a =>
a.Name != null &&
a.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
a.Name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase));
}

// 이슈 #296: 채널에 따라 GitHub 릴리스 폴백 소스를 분기. Retail 은 /releases/latest(프리릴리스 제외),
// Preview 는 /releases 목록에서 최신 프리릴리스를 고른다.
private async Task<GitHubReleaseInfo?> GetLatestReleaseInfoAsync(CancellationToken cancellationToken)
Expand Down Expand Up @@ -336,7 +334,7 @@ private sealed class GitHubReleaseInfo
public GitHubAssetInfo[]? Assets { get; set; }
}

private sealed class GitHubAssetInfo
internal sealed class GitHubAssetInfo
{
public string? Name { get; set; }
public string? BrowserDownloadUrl { get; set; }
Expand Down
95 changes: 95 additions & 0 deletions src/TableCloth.Test/AppUpdateManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using TableCloth.Components.Implementations;
using TableCloth.Models.Configuration;

namespace TableCloth.Test;

[TestClass]
public sealed class AppUpdateManagerTests
{
[TestMethod]
[DataRow("x64", ReleaseChannel.Retail, "TableCloth_1.21.1.0_Release_x64.exe")]
[DataRow("arm64", ReleaseChannel.Retail, "TableCloth_1.21.1.0_Release_arm64.exe")]
[DataRow("x64", ReleaseChannel.Preview, "TableCloth-Preview_1.22.0.0_Release_x64.exe")]
[DataRow("arm64", ReleaseChannel.Preview, "TableCloth-Preview_1.22.0.0_Release_arm64.exe")]
public void FindInstallerAsset_MixedRelease_SelectsRequestedProductArchitectureAndChannel(
string arch,
ReleaseChannel channel,
string expectedName)
{
var assets = CreateAssets(
"SporkBootstrap_1.21.1.0_Release_x64.exe",
"Spork_1.21.1.0_Release_x64.exe",
"SporkBootstrap_1.21.1.0_Release_arm64.exe",
"Spork_1.21.1.0_Release_arm64.exe",
"TableCloth-Preview_1.22.0.0_Release_arm64.exe",
"TableCloth-Preview_1.22.0.0_Release_x64.exe",
"TableCloth_1.21.1.0_Release_arm64.exe",
"TableCloth_1.21.1.0_Release_x64.exe");

var asset = AppUpdateManager.FindInstallerAsset(assets, arch, channel);

Assert.IsNotNull(asset);
Assert.AreEqual(expectedName, asset.Name);
}

[TestMethod]
[DataRow("x64", ReleaseChannel.Retail)]
[DataRow("arm64", ReleaseChannel.Retail)]
[DataRow("x64", ReleaseChannel.Preview)]
[DataRow("arm64", ReleaseChannel.Preview)]
public void FindInstallerAsset_NoMatchingInstaller_DoesNotFallBackToAnotherExecutable(
string arch,
ReleaseChannel channel)
{
var otherArch = arch == "x64" ? "arm64" : "x64";
var otherProduct = channel == ReleaseChannel.Retail ? "TableCloth-Preview" : "TableCloth";
var assets = CreateAssets(
$"SporkBootstrap_1.21.1.0_Release_{arch}.exe",
$"Spork_1.21.1.0_Release_{arch}.exe",
$"TableCloth_1.21.1.0_Release_{otherArch}.exe",
$"TableCloth-Preview_1.22.0.0_Release_{otherArch}.exe",
$"{otherProduct}_1.22.0.0_Release_{arch}.exe");

var asset = AppUpdateManager.FindInstallerAsset(assets, arch, channel);

Assert.IsNull(asset);
}

[TestMethod]
public void FindInstallerAsset_RejectsUnrelatedNamesAndNonInstallerAssets()
{
var assets = CreateAssets(
null,
"setup.exe",
"OtherTableCloth_1.21.1.0_Release_x64.exe",
"TableClothBootstrap_1.21.1.0_Release_x64.exe",
"TableCloth_1.21.1.0_Release_x64_Portable.zip",
"TableCloth_1.21.1.0_Release_x64_symbols.exe",
"TableCloth_1.21.1.0_Release_x64.exe.bak");

var asset = AppUpdateManager.FindInstallerAsset(assets, "x64", ReleaseChannel.Retail);

Assert.IsNull(asset);
}

[TestMethod]
public void FindInstallerAsset_EmptyRelease_ReturnsNull()
{
var asset = AppUpdateManager.FindInstallerAsset([], "x64", ReleaseChannel.Retail);

Assert.IsNull(asset);
}

[TestMethod]
public void FindInstallerAsset_NameComparison_IsCaseInsensitive()
{
var assets = CreateAssets("tablecloth_1.21.1.0_release_X64.EXE");

var asset = AppUpdateManager.FindInstallerAsset(assets, "x64", ReleaseChannel.Retail);

Assert.AreSame(assets[0], asset);
}

private static AppUpdateManager.GitHubAssetInfo[] CreateAssets(params string?[] names)
=> names.Select(name => new AppUpdateManager.GitHubAssetInfo { Name = name }).ToArray();
}
Loading