mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
feat.Penguin Logistics Online Network Distribution System
This commit is contained in:
@@ -35,9 +35,9 @@ public sealed record UpdateCheckResult(
|
||||
GitHubReleaseAsset? PreferredAsset,
|
||||
string? ErrorMessage,
|
||||
bool ForceMode = false,
|
||||
PdcUpdatePayload? PdcPayload = null);
|
||||
PlondsUpdatePayload? PlondsPayload = null);
|
||||
|
||||
public sealed record PdcUpdatePayload(
|
||||
public sealed record PlondsUpdatePayload(
|
||||
string DistributionId,
|
||||
string ChannelId,
|
||||
string SubChannel,
|
||||
|
||||
@@ -11,15 +11,17 @@ using System.Threading.Tasks;
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort PDC client that maps PDC responses to the existing update result model.
|
||||
/// This keeps launcher update contracts stable while allowing a gradual migration.
|
||||
/// Thin PLONDS client used by the host app.
|
||||
/// The host keeps responsibility for checking and downloading updates; Launcher only applies staged payloads.
|
||||
/// </summary>
|
||||
public sealed class PdcReleaseUpdateService : IDisposable
|
||||
public sealed class PlondsReleaseUpdateService : IDisposable
|
||||
{
|
||||
private const string DefaultApiBasePath = "/api/plonds/v1";
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly bool _ownsHttpClient;
|
||||
|
||||
public PdcReleaseUpdateService(HttpClient? httpClient = null)
|
||||
public PlondsReleaseUpdateService(HttpClient? httpClient = null)
|
||||
{
|
||||
if (httpClient is null)
|
||||
{
|
||||
@@ -79,25 +81,40 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
LatestVersionText: "-",
|
||||
Release: null,
|
||||
PreferredAsset: null,
|
||||
ErrorMessage: "PDC endpoint is not configured.",
|
||||
ErrorMessage: "PLONDS endpoint is not configured.",
|
||||
ForceMode: isForce);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var metadataUrl = BuildUri(endpoint, "api/v1/public/distributions/metadata");
|
||||
var metadata = await GetContentNodeAsync(metadataUrl, cancellationToken).ConfigureAwait(false);
|
||||
var apiBasePath = ResolveApiBasePath();
|
||||
var metadataUrl = BuildApiUrl(endpoint, apiBasePath, "metadata");
|
||||
var metadata = await GetJsonNodeAsync(metadataUrl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var channelId = ResolveChannelId(metadata, includePrerelease);
|
||||
if (string.IsNullOrWhiteSpace(channelId))
|
||||
{
|
||||
channelId = includePrerelease ? "preview" : "stable";
|
||||
}
|
||||
|
||||
var latestUrl = BuildUri(
|
||||
var channelId = ResolveChannelId(includePrerelease);
|
||||
var platform = ResolvePlatform();
|
||||
var latestUrl = BuildApiUrl(
|
||||
endpoint,
|
||||
$"api/v1/public/distributions/latest/{Uri.EscapeDataString(channelId)}?appVersion={Uri.EscapeDataString(normalizedCurrentVersionText)}");
|
||||
var latestNode = await GetContentNodeAsync(latestUrl, cancellationToken).ConfigureAwait(false);
|
||||
apiBasePath,
|
||||
$"channels/{Uri.EscapeDataString(channelId)}/{Uri.EscapeDataString(platform)}/latest?currentVersion={Uri.EscapeDataString(normalizedCurrentVersionText)}");
|
||||
|
||||
JsonElement latestNode;
|
||||
try
|
||||
{
|
||||
latestNode = await GetJsonNodeAsync(latestUrl, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.StartsWith("HTTP 204", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new UpdateCheckResult(
|
||||
Success: true,
|
||||
IsUpdateAvailable: false,
|
||||
CurrentVersionText: normalizedCurrentVersionText,
|
||||
LatestVersionText: normalizedCurrentVersionText,
|
||||
Release: null,
|
||||
PreferredAsset: null,
|
||||
ErrorMessage: null,
|
||||
ForceMode: isForce);
|
||||
}
|
||||
|
||||
var latestVersionText = ReadString(latestNode, "version") ?? "-";
|
||||
if (!TryParseVersion(latestVersionText, out var latestVersion) || latestVersion is null)
|
||||
@@ -109,7 +126,7 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
LatestVersionText: latestVersionText,
|
||||
Release: null,
|
||||
PreferredAsset: null,
|
||||
ErrorMessage: "PDC latest distribution version is invalid.",
|
||||
ErrorMessage: "PLONDS latest distribution version is invalid.",
|
||||
ForceMode: isForce);
|
||||
}
|
||||
|
||||
@@ -123,7 +140,7 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
LatestVersionText: latestVersionText,
|
||||
Release: null,
|
||||
PreferredAsset: null,
|
||||
ErrorMessage: "PDC latest distribution id is missing.",
|
||||
ErrorMessage: "PLONDS latest distribution id is missing.",
|
||||
ForceMode: isForce);
|
||||
}
|
||||
|
||||
@@ -141,15 +158,15 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
ForceMode: false);
|
||||
}
|
||||
|
||||
var subChannel = ResolveSubChannel();
|
||||
var distributionUrl = BuildUri(
|
||||
var distributionUrl = BuildApiUrl(
|
||||
endpoint,
|
||||
$"api/v1/public/distributions/{Uri.EscapeDataString(distributionId)}/{Uri.EscapeDataString(subChannel)}");
|
||||
var distributionNode = await GetContentNodeAsync(distributionUrl, cancellationToken).ConfigureAwait(false);
|
||||
apiBasePath,
|
||||
$"distributions/{Uri.EscapeDataString(distributionId)}");
|
||||
var distributionNode = await GetJsonNodeAsync(distributionUrl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var assets = ResolveAssets(distributionNode);
|
||||
var pdcPayload = ResolvePdcPayload(distributionNode, distributionId, channelId, subChannel);
|
||||
if (assets.Count == 0 && !HasPdcPayload(pdcPayload))
|
||||
var assets = ResolveInstallerAssets(distributionNode);
|
||||
var payload = ResolvePlondsPayload(distributionNode, distributionId, channelId, platform);
|
||||
if (assets.Count == 0 && !HasPlondsPayload(payload))
|
||||
{
|
||||
return new UpdateCheckResult(
|
||||
Success: false,
|
||||
@@ -158,18 +175,18 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
LatestVersionText: latestVersionText,
|
||||
Release: null,
|
||||
PreferredAsset: null,
|
||||
ErrorMessage: "PDC distribution response does not expose downloadable update assets.",
|
||||
ErrorMessage: "PLONDS distribution response does not expose downloadable update assets.",
|
||||
ForceMode: isForce);
|
||||
}
|
||||
|
||||
var publishedAt = ParsePublishedAt(distributionNode) ?? DateTimeOffset.UtcNow;
|
||||
var release = new GitHubReleaseInfo(
|
||||
TagName: $"v{latestVersionText}",
|
||||
Name: $"PDC Distribution {latestVersionText}",
|
||||
Name: $"PLONDS Distribution {latestVersionText}",
|
||||
IsPrerelease: includePrerelease,
|
||||
IsDraft: false,
|
||||
PublishedAt: DateTimeOffset.UtcNow,
|
||||
PublishedAt: publishedAt,
|
||||
Assets: assets);
|
||||
var preferredAsset = SelectPreferredInstallerAsset(assets);
|
||||
|
||||
return new UpdateCheckResult(
|
||||
Success: true,
|
||||
@@ -177,10 +194,10 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
CurrentVersionText: normalizedCurrentVersionText,
|
||||
LatestVersionText: latestVersionText,
|
||||
Release: release,
|
||||
PreferredAsset: preferredAsset,
|
||||
PreferredAsset: SelectPreferredInstallerAsset(assets),
|
||||
ErrorMessage: null,
|
||||
ForceMode: isForce,
|
||||
PdcPayload: pdcPayload);
|
||||
PlondsPayload: payload);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -195,12 +212,12 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
LatestVersionText: "-",
|
||||
Release: null,
|
||||
PreferredAsset: null,
|
||||
ErrorMessage: $"PDC request failed: {ex.Message}",
|
||||
ErrorMessage: $"PLONDS request failed: {ex.Message}",
|
||||
ForceMode: isForce);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JsonElement> GetContentNodeAsync(string url, CancellationToken cancellationToken)
|
||||
private async Task<JsonElement> GetJsonNodeAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
var token = ResolveToken();
|
||||
@@ -227,15 +244,39 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
return root.Clone();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<GitHubReleaseAsset> ResolveAssets(JsonElement distributionNode)
|
||||
private static IReadOnlyList<GitHubReleaseAsset> ResolveInstallerAssets(JsonElement distributionNode)
|
||||
{
|
||||
var assets = new List<GitHubReleaseAsset>();
|
||||
if (distributionNode.ValueKind != JsonValueKind.Object)
|
||||
|
||||
if (TryGetPropertyIgnoreCase(distributionNode, "installerMirrors", out var installersNode) &&
|
||||
installersNode.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var installerNode in installersNode.EnumerateArray())
|
||||
{
|
||||
if (installerNode.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = ReadString(installerNode, "name");
|
||||
var url = ReadString(installerNode, "url") ?? ReadString(installerNode, "downloadUrl");
|
||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var size = ReadInt64(installerNode, "size") ?? 0L;
|
||||
var sha256 = ReadString(installerNode, "sha256");
|
||||
assets.Add(new GitHubReleaseAsset(name, url, size, sha256));
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.Count > 0)
|
||||
{
|
||||
return assets;
|
||||
}
|
||||
|
||||
if (distributionNode.TryGetProperty("assets", out var assetsNode) &&
|
||||
if (TryGetPropertyIgnoreCase(distributionNode, "assets", out var assetsNode) &&
|
||||
assetsNode.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var assetNode in assetsNode.EnumerateArray())
|
||||
@@ -246,9 +287,9 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
}
|
||||
|
||||
var name = ReadString(assetNode, "name");
|
||||
var url = ReadString(assetNode, "url") ??
|
||||
ReadString(assetNode, "downloadUrl") ??
|
||||
ReadString(assetNode, "browserDownloadUrl");
|
||||
var url = ReadString(assetNode, "url")
|
||||
?? ReadString(assetNode, "downloadUrl")
|
||||
?? ReadString(assetNode, "browserDownloadUrl");
|
||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
continue;
|
||||
@@ -260,43 +301,14 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.Count > 0)
|
||||
{
|
||||
return assets;
|
||||
}
|
||||
|
||||
// Field-level fallback for service-side URL projection.
|
||||
var manifestUrl = ReadString(distributionNode, "manifestUrl")
|
||||
?? ReadString(distributionNode, "fileMapUrl");
|
||||
var signatureUrl = ReadString(distributionNode, "signatureUrl")
|
||||
?? ReadString(distributionNode, "fileMapSignatureUrl");
|
||||
var archiveUrl = ReadString(distributionNode, "archiveUrl")
|
||||
?? ReadString(distributionNode, "updateArchiveUrl")
|
||||
?? ReadString(distributionNode, "payloadUrl");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(manifestUrl))
|
||||
{
|
||||
assets.Add(new GitHubReleaseAsset("files.json", manifestUrl, 0, null));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(signatureUrl))
|
||||
{
|
||||
assets.Add(new GitHubReleaseAsset("files.json.sig", signatureUrl, 0, null));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(archiveUrl))
|
||||
{
|
||||
assets.Add(new GitHubReleaseAsset("update.zip", archiveUrl, 0, null));
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
private static PdcUpdatePayload ResolvePdcPayload(
|
||||
private static PlondsUpdatePayload ResolvePlondsPayload(
|
||||
JsonElement distributionNode,
|
||||
string distributionId,
|
||||
string channelId,
|
||||
string subChannel)
|
||||
string platform)
|
||||
{
|
||||
var fileMapJson = ReadString(distributionNode, "fileMapJson");
|
||||
var fileMapSignature = ReadString(distributionNode, "fileMapSignature");
|
||||
@@ -305,17 +317,18 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
?? ReadString(distributionNode, "manifestUrl");
|
||||
var fileMapSignatureUrl = ReadString(distributionNode, "fileMapSignatureUrl")
|
||||
?? ReadString(distributionNode, "signatureUrl");
|
||||
return new PdcUpdatePayload(
|
||||
|
||||
return new PlondsUpdatePayload(
|
||||
DistributionId: distributionId,
|
||||
ChannelId: channelId,
|
||||
SubChannel: subChannel,
|
||||
SubChannel: platform,
|
||||
FileMapJson: fileMapJson,
|
||||
FileMapSignature: fileMapSignature,
|
||||
FileMapJsonUrl: fileMapJsonUrl,
|
||||
FileMapSignatureUrl: fileMapSignatureUrl);
|
||||
}
|
||||
|
||||
private static bool HasPdcPayload(PdcUpdatePayload payload)
|
||||
private static bool HasPlondsPayload(PlondsUpdatePayload payload)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(payload.FileMapJson)
|
||||
|| !string.IsNullOrWhiteSpace(payload.FileMapJsonUrl);
|
||||
@@ -336,6 +349,7 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
Architecture.X86 => "x86",
|
||||
_ => "x64"
|
||||
};
|
||||
|
||||
return assets
|
||||
.Select(asset => (Asset: asset, Score: ScoreInstallerAsset(asset.Name, ".exe", ".msi", archToken)))
|
||||
.OrderByDescending(x => x.Score)
|
||||
@@ -405,59 +419,15 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
return score;
|
||||
}
|
||||
|
||||
private static string ResolveChannelId(JsonElement metadataNode, bool includePrerelease)
|
||||
private static string ResolveChannelId(bool includePrerelease)
|
||||
{
|
||||
if (metadataNode.ValueKind != JsonValueKind.Object ||
|
||||
!metadataNode.TryGetProperty("channels", out var channelsNode))
|
||||
{
|
||||
return includePrerelease ? "preview" : "stable";
|
||||
}
|
||||
|
||||
var defaultChannelId = ReadString(metadataNode, "defaultChannelId") ?? string.Empty;
|
||||
if (channelsNode.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return defaultChannelId;
|
||||
}
|
||||
|
||||
string? matchedPreview = null;
|
||||
string? matchedStable = null;
|
||||
|
||||
foreach (var channel in channelsNode.EnumerateObject())
|
||||
{
|
||||
var name = ReadString(channel.Value, "name") ?? channel.Name;
|
||||
if (string.IsNullOrWhiteSpace(matchedPreview) &&
|
||||
(name.Contains("preview", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("beta", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("dev", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
matchedPreview = channel.Name;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(matchedStable) &&
|
||||
(name.Contains("stable", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("release", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
matchedStable = channel.Name;
|
||||
}
|
||||
}
|
||||
|
||||
if (includePrerelease)
|
||||
{
|
||||
return matchedPreview ?? defaultChannelId ?? "preview";
|
||||
}
|
||||
|
||||
return matchedStable ?? defaultChannelId ?? "stable";
|
||||
return includePrerelease
|
||||
? UpdateSettingsValues.ChannelPreview
|
||||
: UpdateSettingsValues.ChannelStable;
|
||||
}
|
||||
|
||||
private static string ResolveSubChannel()
|
||||
private static string ResolvePlatform()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("LANMOUNTAIN_PDC_SUBCHANNEL")
|
||||
?? Environment.GetEnvironmentVariable("PDC_SUBCHANNEL");
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
return configured.Trim();
|
||||
}
|
||||
|
||||
var os = OperatingSystem.IsWindows()
|
||||
? "windows"
|
||||
: OperatingSystem.IsLinux()
|
||||
@@ -474,43 +444,58 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
_ => "x64"
|
||||
};
|
||||
|
||||
return $"{os}_{arch}_release_folderClassic";
|
||||
return $"{os}-{arch}";
|
||||
}
|
||||
|
||||
private static string? ResolveEndpoint()
|
||||
{
|
||||
var endpoint = Environment.GetEnvironmentVariable("LANMOUNTAIN_PDC_ENDPOINT")
|
||||
?? Environment.GetEnvironmentVariable("PDC_ENDPOINT");
|
||||
var endpoint = Environment.GetEnvironmentVariable("LANMOUNTAIN_PLONDS_ENDPOINT")
|
||||
?? Environment.GetEnvironmentVariable("PLONDS_ENDPOINT");
|
||||
return string.IsNullOrWhiteSpace(endpoint) ? null : endpoint.Trim().TrimEnd('/');
|
||||
}
|
||||
|
||||
private static string? ResolveToken()
|
||||
{
|
||||
var token = Environment.GetEnvironmentVariable("LANMOUNTAIN_PDC_TOKEN")
|
||||
?? Environment.GetEnvironmentVariable("PDC_TOKEN");
|
||||
var token = Environment.GetEnvironmentVariable("LANMOUNTAIN_PLONDS_TOKEN")
|
||||
?? Environment.GetEnvironmentVariable("PLONDS_TOKEN");
|
||||
return string.IsNullOrWhiteSpace(token) ? null : token.Trim();
|
||||
}
|
||||
|
||||
private static string BuildUri(string endpoint, string relativePath)
|
||||
private static string ResolveApiBasePath()
|
||||
{
|
||||
return $"{endpoint.TrimEnd('/')}/{relativePath.TrimStart('/')}";
|
||||
var configured = Environment.GetEnvironmentVariable("LANMOUNTAIN_PLONDS_API_BASE_PATH")
|
||||
?? Environment.GetEnvironmentVariable("PLONDS_API_BASE_PATH");
|
||||
if (string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
return DefaultApiBasePath;
|
||||
}
|
||||
|
||||
var normalized = configured.Trim();
|
||||
return normalized.StartsWith("/", StringComparison.Ordinal) ? normalized : "/" + normalized;
|
||||
}
|
||||
|
||||
private static string BuildApiUrl(string endpoint, string apiBasePath, string relativePath)
|
||||
{
|
||||
return $"{endpoint.TrimEnd('/')}/{apiBasePath.Trim('/').TrimEnd('/')}/{relativePath.TrimStart('/')}";
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement node, string propertyName)
|
||||
{
|
||||
if (node.ValueKind != JsonValueKind.Object || !node.TryGetProperty(propertyName, out var value))
|
||||
if (!TryGetPropertyIgnoreCase(node, propertyName, out var value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: value.ToString();
|
||||
: value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined
|
||||
? null
|
||||
: value.ToString();
|
||||
}
|
||||
|
||||
private static long? ReadInt64(JsonElement node, string propertyName)
|
||||
{
|
||||
if (node.ValueKind != JsonValueKind.Object || !node.TryGetProperty(propertyName, out var value))
|
||||
if (!TryGetPropertyIgnoreCase(node, propertyName, out var value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -526,6 +511,37 @@ public sealed class PdcReleaseUpdateService : IDisposable
|
||||
: null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParsePublishedAt(JsonElement node)
|
||||
{
|
||||
var text = ReadString(node, "publishedAt");
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTimeOffset.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool TryGetPropertyIgnoreCase(JsonElement node, string propertyName, out JsonElement value)
|
||||
{
|
||||
if (node.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var property in node.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = property.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseVersion(string? value, out Version? version)
|
||||
{
|
||||
version = null;
|
||||
@@ -356,7 +356,7 @@ public interface IUpdateSettingsService
|
||||
void Save(UpdateSettingsState state);
|
||||
Task<UpdateCheckResult> CheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default);
|
||||
Task<UpdateCheckResult> ForceCheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default);
|
||||
Task<PdcUpdatePayload?> GetPdcUpdatePayloadAsync(Version currentVersion, bool includePrerelease, bool isForce = false, CancellationToken cancellationToken = default);
|
||||
Task<PlondsUpdatePayload?> GetPlondsUpdatePayloadAsync(Version currentVersion, bool includePrerelease, bool isForce = false, CancellationToken cancellationToken = default);
|
||||
Task<UpdateDownloadResult> DownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
|
||||
@@ -752,7 +752,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly GitHubReleaseUpdateService _githubReleaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
|
||||
private readonly PdcReleaseUpdateService _pdcReleaseUpdateService = new();
|
||||
private readonly PlondsReleaseUpdateService _plondsReleaseUpdateService = new();
|
||||
|
||||
public UpdateSettingsService(ISettingsService settingsService)
|
||||
{
|
||||
@@ -842,16 +842,16 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
return CheckForUpdatesCoreAsync(currentVersion, includePrerelease, isForce: true, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PdcUpdatePayload?> GetPdcUpdatePayloadAsync(
|
||||
public async Task<PlondsUpdatePayload?> GetPlondsUpdatePayloadAsync(
|
||||
Version currentVersion,
|
||||
bool includePrerelease,
|
||||
bool isForce = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = isForce
|
||||
? await _pdcReleaseUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken)
|
||||
: await _pdcReleaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
|
||||
return result.Success ? result.PdcPayload : null;
|
||||
? await _plondsReleaseUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken)
|
||||
: await _plondsReleaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
|
||||
return result.Success ? result.PlondsPayload : null;
|
||||
}
|
||||
|
||||
public Task<UpdateDownloadResult> DownloadAssetAsync(
|
||||
@@ -891,7 +891,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
public void Dispose()
|
||||
{
|
||||
_githubReleaseUpdateService.Dispose();
|
||||
_pdcReleaseUpdateService.Dispose();
|
||||
_plondsReleaseUpdateService.Dispose();
|
||||
}
|
||||
|
||||
private async Task<UpdateCheckResult> CheckForUpdatesCoreAsync(
|
||||
@@ -901,20 +901,20 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var source = UpdateSettingsValues.NormalizeDownloadSource(_settingsService.Load().UpdateDownloadSource);
|
||||
if (string.Equals(source, UpdateSettingsValues.DownloadSourcePdc, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(source, UpdateSettingsValues.DownloadSourcePlonds, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var pdcResult = isForce
|
||||
? await _pdcReleaseUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken)
|
||||
: await _pdcReleaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
|
||||
var plondsResult = isForce
|
||||
? await _plondsReleaseUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken)
|
||||
: await _plondsReleaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
|
||||
|
||||
if (pdcResult.Success)
|
||||
if (plondsResult.Success)
|
||||
{
|
||||
return pdcResult;
|
||||
return plondsResult;
|
||||
}
|
||||
|
||||
AppLogger.Warn(
|
||||
"UpdateSettings",
|
||||
$"PDC update check failed and will fallback to GitHub. Error: {pdcResult.ErrorMessage}");
|
||||
$"PLONDS update check failed and will fallback to GitHub. Error: {plondsResult.ErrorMessage}");
|
||||
}
|
||||
|
||||
return isForce
|
||||
@@ -1271,14 +1271,14 @@ internal sealed class ApplicationInfoService : IApplicationInfoService
|
||||
|
||||
public string GetAppVersionText()
|
||||
{
|
||||
// 优先从环境变量读取(Launcher 传递)
|
||||
// 浼樺厛浠庣幆澧冨彉閲忚鍙栵紙Launcher 浼犻€掞級
|
||||
var envVersion = Environment.GetEnvironmentVariable(LanMountainDesktop.Shared.Contracts.Launcher.LauncherIpcConstants.VersionEnvVar);
|
||||
if (!string.IsNullOrWhiteSpace(envVersion))
|
||||
{
|
||||
return envVersion;
|
||||
}
|
||||
|
||||
// 回退:从程序集读取
|
||||
// Fallback: read from application assembly.
|
||||
var assembly = typeof(App).Assembly;
|
||||
var informationalVersion = assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
|
||||
@@ -1318,14 +1318,14 @@ internal sealed class ApplicationInfoService : IApplicationInfoService
|
||||
|
||||
public string GetAppCodenameText()
|
||||
{
|
||||
// 优先从环境变量读取(Launcher 传递)
|
||||
// 浼樺厛浠庣幆澧冨彉閲忚鍙栵紙Launcher 浼犻€掞級
|
||||
var envCodename = Environment.GetEnvironmentVariable(LanMountainDesktop.Shared.Contracts.Launcher.LauncherIpcConstants.CodenameEnvVar);
|
||||
if (!string.IsNullOrWhiteSpace(envCodename))
|
||||
{
|
||||
return envCodename;
|
||||
}
|
||||
|
||||
// 回退:使用默认开发代号
|
||||
// Fallback: use default codename.
|
||||
return DefaultCodename;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ public static class UpdateSettingsValues
|
||||
public const string ModeSilentOnExit = "silent_on_exit";
|
||||
|
||||
// NOTE: keep constant name for compatibility with existing call sites.
|
||||
public const string DownloadSourcePdc = "stcn";
|
||||
public const string DownloadSourceStcn = DownloadSourcePdc;
|
||||
public const string LegacyDownloadSourcePdc = "pdc";
|
||||
public const string DownloadSourcePlonds = "stcn";
|
||||
public const string DownloadSourcePdc = DownloadSourcePlonds;
|
||||
public const string DownloadSourceStcn = DownloadSourcePlonds;
|
||||
public const string LegacyDownloadSourcePlonds = "pdc";
|
||||
public const string LegacyDownloadSourcePdc = LegacyDownloadSourcePlonds;
|
||||
public const string DownloadSourceGitHub = "github";
|
||||
public const string DownloadSourceGhProxy = "gh-proxy";
|
||||
|
||||
@@ -55,14 +57,14 @@ public static class UpdateSettingsValues
|
||||
|
||||
public static string NormalizeDownloadSource(string? value)
|
||||
{
|
||||
if (string.Equals(value, LegacyDownloadSourcePdc, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(value, LegacyDownloadSourcePlonds, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return DownloadSourceStcn;
|
||||
}
|
||||
|
||||
if (string.Equals(value, DownloadSourcePdc, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(value, DownloadSourcePlonds, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return DownloadSourcePdc;
|
||||
return DownloadSourcePlonds;
|
||||
}
|
||||
|
||||
if (string.Equals(value, DownloadSourceGhProxy, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -75,7 +77,7 @@ public static class UpdateSettingsValues
|
||||
return DownloadSourceGitHub;
|
||||
}
|
||||
|
||||
// Default to STCN(PDC/S3). Runtime will fallback to GitHub if STCN is unavailable.
|
||||
// Default to STCN(PLONDS/S3). Runtime will fallback to GitHub if STCN is unavailable.
|
||||
return DownloadSourceStcn;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,16 +61,16 @@ public sealed class UpdateWorkflowService
|
||||
private const string SignedFileMapName = "files.json";
|
||||
private const string SignedFileMapSignatureName = "files.json.sig";
|
||||
private const string UpdateArchiveName = "update.zip";
|
||||
private const string PdcFileMapName = "pdc-filemap.json";
|
||||
private const string PdcFileMapSignatureName = "pdc-filemap.sig";
|
||||
private const string PdcUpdateStateName = "pdc-update.json";
|
||||
private const string PlondsFileMapName = "plonds-filemap.json";
|
||||
private const string PlondsFileMapSignatureName = "plonds-filemap.sig";
|
||||
private const string PlondsUpdateStateName = "plonds-update.json";
|
||||
|
||||
private static readonly HttpClient PdcHttpClient = new()
|
||||
private static readonly HttpClient PlondsHttpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(5)
|
||||
};
|
||||
|
||||
private static readonly ResumableDownloadService PdcDownloadService = new(PdcHttpClient);
|
||||
private static readonly ResumableDownloadService PlondsDownloadService = new(PlondsHttpClient);
|
||||
|
||||
public UpdateWorkflowService(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
@@ -116,7 +116,7 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
public static bool IsDeltaUpdateAvailable(UpdateCheckResult checkResult)
|
||||
{
|
||||
if (checkResult.PdcPayload is not null)
|
||||
if (checkResult.PlondsPayload is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -139,14 +139,14 @@ public sealed class UpdateWorkflowService
|
||||
return new UpdateDownloadResult(false, null, "No update available for delta download.");
|
||||
}
|
||||
|
||||
if (checkResult.PdcPayload is null && checkResult.Release is null)
|
||||
if (checkResult.PlondsPayload is null && checkResult.Release is null)
|
||||
{
|
||||
return new UpdateDownloadResult(false, null, "No update payload is available for delta download.");
|
||||
}
|
||||
|
||||
if (checkResult.PdcPayload is not null)
|
||||
if (checkResult.PlondsPayload is not null)
|
||||
{
|
||||
return await DownloadPdcDeltaUpdateAsync(checkResult, progress, cancellationToken);
|
||||
return await DownloadPlondsDeltaUpdateAsync(checkResult, progress, cancellationToken);
|
||||
}
|
||||
|
||||
var release = checkResult.Release;
|
||||
@@ -243,15 +243,15 @@ public sealed class UpdateWorkflowService
|
||||
return new UpdateDownloadResult(true, Path.Combine(incomingDir, SignedFileMapName), null);
|
||||
}
|
||||
|
||||
private async Task<UpdateDownloadResult> DownloadPdcDeltaUpdateAsync(
|
||||
private async Task<UpdateDownloadResult> DownloadPlondsDeltaUpdateAsync(
|
||||
UpdateCheckResult checkResult,
|
||||
IProgress<double>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload = checkResult.PdcPayload;
|
||||
var payload = checkResult.PlondsPayload;
|
||||
if (payload is null)
|
||||
{
|
||||
return new UpdateDownloadResult(false, null, "PDC payload is missing.");
|
||||
return new UpdateDownloadResult(false, null, "PLONDS payload is missing.");
|
||||
}
|
||||
|
||||
var incomingDir = GetLauncherIncomingDirectory();
|
||||
@@ -271,33 +271,33 @@ public sealed class UpdateWorkflowService
|
||||
{
|
||||
var state = _settingsFacade.Update.Get();
|
||||
var downloadThreads = Math.Max(1, state.UpdateDownloadThreads);
|
||||
var fileMapPath = Path.Combine(incomingDir, PdcFileMapName);
|
||||
var signaturePath = Path.Combine(incomingDir, PdcFileMapSignatureName);
|
||||
var updateStatePath = Path.Combine(incomingDir, PdcUpdateStateName);
|
||||
var fileMapPath = Path.Combine(incomingDir, PlondsFileMapName);
|
||||
var signaturePath = Path.Combine(incomingDir, PlondsFileMapSignatureName);
|
||||
var updateStatePath = Path.Combine(incomingDir, PlondsUpdateStateName);
|
||||
|
||||
var fileMapJson = await EnsurePdcTextResourceAsync(
|
||||
var fileMapJson = await EnsurePlondsTextResourceAsync(
|
||||
payload.FileMapJson,
|
||||
payload.FileMapJsonUrl,
|
||||
fileMapPath,
|
||||
cancellationToken);
|
||||
|
||||
var fileMapSignature = await EnsurePdcTextResourceAsync(
|
||||
var fileMapSignature = await EnsurePlondsTextResourceAsync(
|
||||
payload.FileMapSignature,
|
||||
payload.FileMapSignatureUrl,
|
||||
signaturePath,
|
||||
cancellationToken);
|
||||
|
||||
var downloadEntries = ParsePdcDownloadEntries(fileMapJson);
|
||||
var downloadEntries = ParsePlondsDownloadEntries(fileMapJson);
|
||||
if (downloadEntries.Count == 0)
|
||||
{
|
||||
return new UpdateDownloadResult(false, null, "PDC file map does not contain downloadable objects.");
|
||||
return new UpdateDownloadResult(false, null, "PLONDS file map does not contain downloadable objects.");
|
||||
}
|
||||
|
||||
var expectedObjectCount = downloadEntries.Count;
|
||||
var completedItems = 2;
|
||||
progress?.Report(expectedObjectCount == 0 ? 1d : (double)completedItems / (expectedObjectCount + 2));
|
||||
|
||||
var objectResults = new List<PdcDownloadedObjectInfo>(expectedObjectCount);
|
||||
var objectResults = new List<PlondsDownloadedObjectInfo>(expectedObjectCount);
|
||||
var objectTargets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var totalSteps = expectedObjectCount + 2;
|
||||
|
||||
@@ -310,7 +310,7 @@ public sealed class UpdateWorkflowService
|
||||
continue;
|
||||
}
|
||||
|
||||
var destinationPath = GetPdcObjectDestinationPath(objectsDir, entry.ObjectHashHex);
|
||||
var destinationPath = GetPlondsObjectDestinationPath(objectsDir, entry.ObjectHashHex);
|
||||
var destinationDirectory = Path.GetDirectoryName(destinationPath);
|
||||
if (!string.IsNullOrWhiteSpace(destinationDirectory))
|
||||
{
|
||||
@@ -319,10 +319,10 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
if (File.Exists(destinationPath))
|
||||
{
|
||||
var existingHash = await ComputeFileSha512HexAsync(destinationPath, cancellationToken);
|
||||
var existingHash = await ComputeFileSha256HexAsync(destinationPath, cancellationToken);
|
||||
if (string.Equals(existingHash, entry.ObjectHashHex, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
objectResults.Add(new PdcDownloadedObjectInfo(entry.ComponentId, entry.RelativePath, entry.DownloadUrl, entry.ObjectHashHex, destinationPath));
|
||||
objectResults.Add(new PlondsDownloadedObjectInfo(entry.ComponentId, entry.RelativePath, entry.DownloadUrl, entry.ObjectHashHex, destinationPath));
|
||||
completedItems++;
|
||||
progress?.Report((double)completedItems / totalSteps);
|
||||
continue;
|
||||
@@ -330,7 +330,7 @@ public sealed class UpdateWorkflowService
|
||||
}
|
||||
|
||||
var downloadOptions = new DownloadOptions(MaxParallelSegments: downloadThreads);
|
||||
var downloadResult = await PdcDownloadService.DownloadAsync(
|
||||
var downloadResult = await PlondsDownloadService.DownloadAsync(
|
||||
entry.DownloadUrl,
|
||||
destinationPath,
|
||||
downloadOptions,
|
||||
@@ -339,22 +339,22 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
if (!downloadResult.Success)
|
||||
{
|
||||
return new UpdateDownloadResult(false, null, $"Failed to download PDC object {entry.RelativePath}: {downloadResult.ErrorMessage}");
|
||||
return new UpdateDownloadResult(false, null, $"Failed to download PLONDS object {entry.RelativePath}: {downloadResult.ErrorMessage}");
|
||||
}
|
||||
|
||||
var actualHash = await ComputeFileSha512HexAsync(destinationPath, cancellationToken);
|
||||
var actualHash = await ComputeFileSha256HexAsync(destinationPath, cancellationToken);
|
||||
if (!string.IsNullOrWhiteSpace(actualHash) &&
|
||||
!string.Equals(actualHash, entry.ObjectHashHex, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new UpdateDownloadResult(false, null, $"PDC object hash mismatch for {entry.RelativePath}. Expected: {entry.ObjectHashHex}, Actual: {actualHash}");
|
||||
return new UpdateDownloadResult(false, null, $"PLONDS object hash mismatch for {entry.RelativePath}. Expected: {entry.ObjectHashHex}, Actual: {actualHash}");
|
||||
}
|
||||
|
||||
objectResults.Add(new PdcDownloadedObjectInfo(entry.ComponentId, entry.RelativePath, entry.DownloadUrl, entry.ObjectHashHex, destinationPath));
|
||||
objectResults.Add(new PlondsDownloadedObjectInfo(entry.ComponentId, entry.RelativePath, entry.DownloadUrl, entry.ObjectHashHex, destinationPath));
|
||||
completedItems++;
|
||||
progress?.Report((double)completedItems / totalSteps);
|
||||
}
|
||||
|
||||
var updateState = new PdcUpdateState(
|
||||
var updateState = new PlondsUpdateState(
|
||||
checkResult.LatestVersionText,
|
||||
payload.DistributionId,
|
||||
payload.ChannelId,
|
||||
@@ -381,7 +381,7 @@ public sealed class UpdateWorkflowService
|
||||
});
|
||||
|
||||
progress?.Report(1d);
|
||||
AppLogger.Info("UpdateWorkflow", $"PDC update payload downloaded to {incomingDir}. Will be applied by Launcher on next startup.");
|
||||
AppLogger.Info("UpdateWorkflow", $"PLONDS update payload downloaded to {incomingDir}. Will be applied by Launcher on next startup.");
|
||||
return new UpdateDownloadResult(true, updateStatePath, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -390,7 +390,7 @@ public sealed class UpdateWorkflowService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UpdateWorkflow", "Failed to download PDC incremental payload.", ex);
|
||||
AppLogger.Warn("UpdateWorkflow", "Failed to download PLONDS incremental payload.", ex);
|
||||
return new UpdateDownloadResult(false, null, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -414,20 +414,20 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
// Incoming payload updates are identified by the local manifest or incoming directory path.
|
||||
return pendingPath.EndsWith(SignedFileMapName, StringComparison.OrdinalIgnoreCase)
|
||||
|| pendingPath.EndsWith(PdcUpdateStateName, StringComparison.OrdinalIgnoreCase)
|
||||
|| pendingPath.EndsWith(PdcFileMapName, StringComparison.OrdinalIgnoreCase)
|
||||
|| pendingPath.EndsWith(PdcFileMapSignatureName, StringComparison.OrdinalIgnoreCase)
|
||||
|| pendingPath.EndsWith(PlondsUpdateStateName, StringComparison.OrdinalIgnoreCase)
|
||||
|| pendingPath.EndsWith(PlondsFileMapName, StringComparison.OrdinalIgnoreCase)
|
||||
|| pendingPath.EndsWith(PlondsFileMapSignatureName, StringComparison.OrdinalIgnoreCase)
|
||||
|| pendingPath.Contains(IncomingDirectoryName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetPdcObjectDestinationPath(string objectsDirectory, string objectHashHex)
|
||||
private static string GetPlondsObjectDestinationPath(string objectsDirectory, string objectHashHex)
|
||||
{
|
||||
var normalizedHash = objectHashHex.Trim().ToLowerInvariant();
|
||||
var shard = normalizedHash.Length >= 2 ? normalizedHash[..2] : normalizedHash;
|
||||
return Path.Combine(objectsDirectory, shard, normalizedHash);
|
||||
}
|
||||
|
||||
private static async Task<string> EnsurePdcTextResourceAsync(
|
||||
private static async Task<string> EnsurePlondsTextResourceAsync(
|
||||
string? inlineContent,
|
||||
string? sourceUrl,
|
||||
string destinationPath,
|
||||
@@ -441,25 +441,25 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sourceUrl))
|
||||
{
|
||||
throw new InvalidOperationException("PDC payload does not contain a file map source.");
|
||||
throw new InvalidOperationException("PLONDS payload does not contain a file map source.");
|
||||
}
|
||||
|
||||
var downloadResult = await PdcDownloadService.DownloadAsync(
|
||||
var downloadResult = await PlondsDownloadService.DownloadAsync(
|
||||
sourceUrl,
|
||||
destinationPath,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (!downloadResult.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to download PDC file map resource: {downloadResult.ErrorMessage}");
|
||||
throw new InvalidOperationException($"Failed to download PLONDS file map resource: {downloadResult.ErrorMessage}");
|
||||
}
|
||||
|
||||
return await File.ReadAllTextAsync(destinationPath, cancellationToken);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PdcDownloadEntry> ParsePdcDownloadEntries(string fileMapJson)
|
||||
private static IReadOnlyList<PlondsDownloadEntry> ParsePlondsDownloadEntries(string fileMapJson)
|
||||
{
|
||||
var entries = new List<PdcDownloadEntry>();
|
||||
var entries = new List<PlondsDownloadEntry>();
|
||||
if (string.IsNullOrWhiteSpace(fileMapJson))
|
||||
{
|
||||
return entries;
|
||||
@@ -472,25 +472,56 @@ public sealed class UpdateWorkflowService
|
||||
return entries;
|
||||
}
|
||||
|
||||
if (!TryGetPropertyIgnoreCase(root, "components", out var componentsNode) ||
|
||||
componentsNode.ValueKind != JsonValueKind.Object)
|
||||
if (!TryGetPropertyIgnoreCase(root, "components", out var componentsNode))
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
foreach (var component in componentsNode.EnumerateObject())
|
||||
if (componentsNode.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (component.Value.ValueKind != JsonValueKind.Object)
|
||||
foreach (var component in componentsNode.EnumerateObject())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (component.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetPropertyIgnoreCase(component.Value, "files", out var filesNode) ||
|
||||
filesNode.ValueKind != JsonValueKind.Object)
|
||||
if (!TryGetPropertyIgnoreCase(component.Value, "files", out var filesNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendDownloadEntries(entries, component.Name, filesNode);
|
||||
}
|
||||
}
|
||||
else if (componentsNode.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var component in componentsNode.EnumerateArray())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (component.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var componentId = ReadStringIgnoreCase(component, "id")
|
||||
?? ReadStringIgnoreCase(component, "name")
|
||||
?? "app";
|
||||
if (!TryGetPropertyIgnoreCase(component, "files", out var filesNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendDownloadEntries(entries, componentId, filesNode);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static void AppendDownloadEntries(ICollection<PlondsDownloadEntry> entries, string componentId, JsonElement filesNode)
|
||||
{
|
||||
if (filesNode.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var fileEntry in filesNode.EnumerateObject())
|
||||
{
|
||||
if (fileEntry.Value.ValueKind != JsonValueKind.Object)
|
||||
@@ -498,30 +529,82 @@ public sealed class UpdateWorkflowService
|
||||
continue;
|
||||
}
|
||||
|
||||
var downloadUrl = ReadStringIgnoreCase(fileEntry.Value, "archivedownloadurl")
|
||||
?? ReadStringIgnoreCase(fileEntry.Value, "downloadurl")
|
||||
?? ReadStringIgnoreCase(fileEntry.Value, "url");
|
||||
var hashBytes = ReadByteArrayIgnoreCase(fileEntry.Value, "archivesha512")
|
||||
?? ReadByteArrayIgnoreCase(fileEntry.Value, "filesha512");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(downloadUrl) || hashBytes is null || hashBytes.Length == 0)
|
||||
if (TryCreateDownloadEntry(componentId, fileEntry.Name, fileEntry.Value, out var entry))
|
||||
{
|
||||
continue;
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
var hashHex = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
entries.Add(new PdcDownloadEntry(
|
||||
component.Name,
|
||||
fileEntry.Name,
|
||||
downloadUrl,
|
||||
hashHex));
|
||||
return;
|
||||
}
|
||||
|
||||
if (filesNode.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var fileEntry in filesNode.EnumerateArray())
|
||||
{
|
||||
if (fileEntry.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relativePath = ReadStringIgnoreCase(fileEntry, "path");
|
||||
if (TryCreateDownloadEntry(componentId, relativePath, fileEntry, out var entry))
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryCreateDownloadEntry(
|
||||
string componentId,
|
||||
string? relativePath,
|
||||
JsonElement fileNode,
|
||||
out PlondsDownloadEntry entry)
|
||||
{
|
||||
entry = default!;
|
||||
|
||||
var normalizedPath = string.IsNullOrWhiteSpace(relativePath)
|
||||
? null
|
||||
: relativePath.Trim();
|
||||
var downloadUrl = ReadStringIgnoreCase(fileNode, "objecturl")
|
||||
?? ReadStringIgnoreCase(fileNode, "downloadurl")
|
||||
?? ReadStringIgnoreCase(fileNode, "archivedownloadurl")
|
||||
?? ReadStringIgnoreCase(fileNode, "url");
|
||||
var hashHex = ReadStringIgnoreCase(fileNode, "sha256")
|
||||
?? ReadStringIgnoreCase(fileNode, "filesha256")
|
||||
?? ReadStringIgnoreCase(fileNode, "contenthash");
|
||||
|
||||
if ((string.IsNullOrWhiteSpace(hashHex) || string.IsNullOrWhiteSpace(downloadUrl)) &&
|
||||
TryGetPropertyIgnoreCase(fileNode, "hash", out var hashNode) &&
|
||||
hashNode.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var algorithm = ReadStringIgnoreCase(hashNode, "algorithm");
|
||||
if (string.IsNullOrWhiteSpace(algorithm) ||
|
||||
algorithm.Contains("sha256", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hashHex ??= ReadStringIgnoreCase(hashNode, "value");
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
if (string.IsNullOrWhiteSpace(normalizedPath) ||
|
||||
string.IsNullOrWhiteSpace(downloadUrl) ||
|
||||
string.IsNullOrWhiteSpace(hashHex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = new PlondsDownloadEntry(
|
||||
componentId,
|
||||
normalizedPath,
|
||||
downloadUrl,
|
||||
NormalizeHashText(hashHex));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<string?> ComputeFileSha512HexAsync(string filePath, CancellationToken cancellationToken)
|
||||
private static async Task<string?> ComputeFileSha256HexAsync(string filePath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
@@ -529,10 +612,22 @@ public sealed class UpdateWorkflowService
|
||||
}
|
||||
|
||||
await using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var hashBytes = await SHA512.HashDataAsync(stream, cancellationToken);
|
||||
var hashBytes = await SHA256.HashDataAsync(stream, cancellationToken);
|
||||
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string NormalizeHashText(string hash)
|
||||
{
|
||||
var normalized = hash.Trim();
|
||||
var separator = normalized.IndexOf(':');
|
||||
if (separator >= 0 && separator < normalized.Length - 1)
|
||||
{
|
||||
normalized = normalized[(separator + 1)..];
|
||||
}
|
||||
|
||||
return normalized.Replace("-", string.Empty).Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool TryGetPropertyIgnoreCase(JsonElement node, string propertyName, out JsonElement value)
|
||||
{
|
||||
if (node.ValueKind == JsonValueKind.Object)
|
||||
@@ -641,20 +736,20 @@ public sealed class UpdateWorkflowService
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed record PdcDownloadEntry(
|
||||
private sealed record PlondsDownloadEntry(
|
||||
string ComponentId,
|
||||
string RelativePath,
|
||||
string DownloadUrl,
|
||||
string ObjectHashHex);
|
||||
|
||||
private sealed record PdcDownloadedObjectInfo(
|
||||
private sealed record PlondsDownloadedObjectInfo(
|
||||
string ComponentId,
|
||||
string RelativePath,
|
||||
string SourceUrl,
|
||||
string ObjectHashHex,
|
||||
string LocalPath);
|
||||
|
||||
private sealed record PdcUpdateState(
|
||||
private sealed record PlondsUpdateState(
|
||||
string VersionText,
|
||||
string DistributionId,
|
||||
string ChannelId,
|
||||
@@ -665,7 +760,7 @@ public sealed class UpdateWorkflowService
|
||||
DateTimeOffset DownloadedAtUtc,
|
||||
string FileMapJson,
|
||||
string FileMapSignature,
|
||||
IReadOnlyList<PdcDownloadedObjectInfo> Objects);
|
||||
IReadOnlyList<PlondsDownloadedObjectInfo> Objects);
|
||||
|
||||
private static bool TryResolveDeltaAssets(
|
||||
IReadOnlyList<GitHubReleaseAsset> assets,
|
||||
@@ -776,7 +871,7 @@ public sealed class UpdateWorkflowService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(checkResult);
|
||||
|
||||
if (checkResult.PdcPayload is not null)
|
||||
if (checkResult.PlondsPayload is not null)
|
||||
{
|
||||
return await DownloadDeltaUpdateAsync(checkResult, progress, cancellationToken);
|
||||
}
|
||||
@@ -837,7 +932,7 @@ public sealed class UpdateWorkflowService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(checkResult);
|
||||
|
||||
if (checkResult.PdcPayload is not null)
|
||||
if (checkResult.PlondsPayload is not null)
|
||||
{
|
||||
ClearPendingUpdate();
|
||||
return await DownloadDeltaUpdateAsync(checkResult, progress, cancellationToken);
|
||||
@@ -912,14 +1007,14 @@ public sealed class UpdateWorkflowService
|
||||
if (IsPendingDeltaUpdate())
|
||||
{
|
||||
var pdcUpdatePath = pending.InstallerPath;
|
||||
var pdcFileMapPath = Path.Combine(Path.GetDirectoryName(pdcUpdatePath) ?? string.Empty, PdcFileMapName);
|
||||
var pdcSignaturePath = Path.Combine(Path.GetDirectoryName(pdcUpdatePath) ?? string.Empty, PdcFileMapSignatureName);
|
||||
var pdcFileMapPath = Path.Combine(Path.GetDirectoryName(pdcUpdatePath) ?? string.Empty, PlondsFileMapName);
|
||||
var pdcSignaturePath = Path.Combine(Path.GetDirectoryName(pdcUpdatePath) ?? string.Empty, PlondsFileMapSignatureName);
|
||||
if (File.Exists(pdcUpdatePath) && File.Exists(pdcFileMapPath) && File.Exists(pdcSignaturePath))
|
||||
{
|
||||
return new UpdateVerifyResult(true, true, null, null, null);
|
||||
}
|
||||
|
||||
return new UpdateVerifyResult(false, false, null, null, "PDC update payload is incomplete.");
|
||||
return new UpdateVerifyResult(false, false, null, null, "PLONDS update payload is incomplete.");
|
||||
}
|
||||
|
||||
return new UpdateVerifyResult(false, false, null, null, "Installer file does not exist.");
|
||||
@@ -961,7 +1056,7 @@ public sealed class UpdateWorkflowService
|
||||
{
|
||||
// Always check for updates on startup (removed AutoCheckUpdates check)
|
||||
var result = await CheckForUpdatesAsync(currentVersion, isForce: false, cancellationToken);
|
||||
if (!result.Success || !result.IsUpdateAvailable || (result.Release is null && result.PdcPayload is null))
|
||||
if (!result.Success || !result.IsUpdateAvailable || (result.Release is null && result.PlondsPayload is null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user