mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
0.7.9
更新功能优化、插件市场优化,反正就是优化了很多东西
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.Services.PluginMarket;
|
||||
|
||||
internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly bool _ownsHttpClient;
|
||||
private readonly ConcurrentDictionary<string, string> _defaultBranchCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public AirAppMarketMetadataResolverService(HttpClient? httpClient = null)
|
||||
{
|
||||
if (httpClient is null)
|
||||
{
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(20)
|
||||
};
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LanMountainDesktop-PluginMarketplace/1.0");
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
|
||||
_ownsHttpClient = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_ownsHttpClient = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AirAppMarketIndexDocument> EnrichAsync(
|
||||
AirAppMarketIndexDocument document,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
if (document.Plugins.Count == 0)
|
||||
{
|
||||
return document;
|
||||
}
|
||||
|
||||
var enrichedPlugins = new List<AirAppMarketPluginEntry>(document.Plugins.Count);
|
||||
foreach (var plugin in document.Plugins)
|
||||
{
|
||||
enrichedPlugins.Add(await EnrichPluginAsync(plugin, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return new AirAppMarketIndexDocument
|
||||
{
|
||||
SchemaVersion = document.SchemaVersion,
|
||||
SourceId = document.SourceId,
|
||||
SourceName = document.SourceName,
|
||||
GeneratedAt = document.GeneratedAt,
|
||||
Contracts = document.Contracts,
|
||||
Plugins = enrichedPlugins
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_ownsHttpClient)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AirAppMarketPluginEntry> EnrichPluginAsync(
|
||||
AirAppMarketPluginEntry entry,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!AirAppMarketDefaults.TryParseGitHubRepositoryUrl(entry.RepositoryUrl, out var owner, out var repositoryName) &&
|
||||
!AirAppMarketDefaults.TryParseGitHubRepositoryUrl(entry.ProjectUrl, out owner, out repositoryName))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
var branchCandidates = await GetBranchCandidatesAsync(owner, repositoryName, cancellationToken).ConfigureAwait(false);
|
||||
PluginManifest? manifest = null;
|
||||
AirAppMarketRepositoryTemplate? template = null;
|
||||
|
||||
foreach (var branch in branchCandidates)
|
||||
{
|
||||
manifest ??= await TryLoadPluginManifestAsync(owner, repositoryName, branch, cancellationToken).ConfigureAwait(false);
|
||||
template ??= await TryLoadTemplateAsync(owner, repositoryName, branch, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (manifest is not null && template is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var repository = entry.Repository ?? new AirAppMarketPluginRepositoryEntry();
|
||||
var resolvedManifest = manifest;
|
||||
var resolvedPackageSources = entry.PackageSources.Count > 0
|
||||
? entry.PackageSources
|
||||
: entry.Publication?.PackageSources ?? [];
|
||||
var firstPackageSourceUrl = resolvedPackageSources.FirstOrDefault()?.Url ?? entry.DownloadUrl;
|
||||
|
||||
return new AirAppMarketPluginEntry
|
||||
{
|
||||
PluginId = AirAppMarketIndexDocument.NormalizeValue(entry.PluginId) ?? entry.PluginId,
|
||||
Manifest = resolvedManifest is null
|
||||
? entry.Manifest
|
||||
: new AirAppMarketPluginManifestEntry
|
||||
{
|
||||
Id = resolvedManifest.Id,
|
||||
Name = resolvedManifest.Name,
|
||||
Description = resolvedManifest.Description ?? string.Empty,
|
||||
Author = resolvedManifest.Author ?? string.Empty,
|
||||
Version = resolvedManifest.Version ?? string.Empty,
|
||||
ApiVersion = resolvedManifest.ApiVersion ?? string.Empty,
|
||||
EntranceAssembly = resolvedManifest.EntranceAssembly,
|
||||
SharedContracts = resolvedManifest.SharedContracts?
|
||||
.Select(contract => new AirAppMarketPluginDependencyEntry
|
||||
{
|
||||
Id = contract.Id,
|
||||
Version = contract.Version,
|
||||
AssemblyName = contract.AssemblyName
|
||||
})
|
||||
.ToList()
|
||||
?? []
|
||||
},
|
||||
Compatibility = entry.Compatibility is not null || template is not null || !string.IsNullOrWhiteSpace(entry.MinHostVersion) || !string.IsNullOrWhiteSpace(entry.ApiVersion)
|
||||
? new AirAppMarketPluginCompatibilityEntry
|
||||
{
|
||||
MinHostVersion = FirstNonEmpty(
|
||||
template?.MinHostVersion,
|
||||
entry.MinHostVersion),
|
||||
PluginApiVersion = FirstNonEmpty(
|
||||
resolvedManifest?.ApiVersion,
|
||||
entry.ApiVersion)
|
||||
?? string.Empty
|
||||
}
|
||||
: null,
|
||||
Repository = new AirAppMarketPluginRepositoryEntry
|
||||
{
|
||||
IconUrl = FirstNonEmpty(template?.IconUrl, repository.IconUrl, entry.IconUrl) ?? string.Empty,
|
||||
ProjectUrl = FirstNonEmpty(template?.ProjectUrl, repository.ProjectUrl, entry.ProjectUrl) ?? string.Empty,
|
||||
ReadmeUrl = FirstNonEmpty(template?.ReadmeUrl, repository.ReadmeUrl, entry.ReadmeUrl) ?? string.Empty,
|
||||
HomepageUrl = FirstNonEmpty(template?.HomepageUrl, repository.HomepageUrl, entry.HomepageUrl) ?? string.Empty,
|
||||
RepositoryUrl = FirstNonEmpty(template?.RepositoryUrl, repository.RepositoryUrl, entry.RepositoryUrl, entry.ProjectUrl)
|
||||
?? string.Empty,
|
||||
Tags = FirstNonEmptyList(template?.Tags, repository.Tags, entry.Tags),
|
||||
ReleaseNotes = FirstNonEmpty(template?.ReleaseNotes, repository.ReleaseNotes, entry.ReleaseNotes) ?? string.Empty
|
||||
},
|
||||
Publication = entry.Publication,
|
||||
Capabilities = entry.Capabilities,
|
||||
Id = FirstNonEmpty(resolvedManifest?.Id, entry.Id, entry.PluginId) ?? entry.PluginId,
|
||||
Name = FirstNonEmpty(resolvedManifest?.Name, entry.Name) ?? string.Empty,
|
||||
Description = FirstNonEmpty(resolvedManifest?.Description, entry.Description) ?? string.Empty,
|
||||
Author = FirstNonEmpty(resolvedManifest?.Author, entry.Author) ?? string.Empty,
|
||||
Version = FirstNonEmpty(resolvedManifest?.Version, entry.Version) ?? string.Empty,
|
||||
ApiVersion = FirstNonEmpty(resolvedManifest?.ApiVersion, entry.ApiVersion) ?? string.Empty,
|
||||
MinHostVersion = FirstNonEmpty(template?.MinHostVersion, entry.MinHostVersion) ?? string.Empty,
|
||||
DownloadUrl = FirstNonEmpty(firstPackageSourceUrl, entry.DownloadUrl) ?? string.Empty,
|
||||
Sha256 = entry.Sha256,
|
||||
PackageSizeBytes = entry.PackageSizeBytes,
|
||||
IconUrl = FirstNonEmpty(template?.IconUrl, repository.IconUrl, entry.IconUrl) ?? string.Empty,
|
||||
ReleaseTag = entry.ReleaseTag,
|
||||
ReleaseAssetName = entry.ReleaseAssetName,
|
||||
ProjectUrl = FirstNonEmpty(template?.ProjectUrl, repository.ProjectUrl, entry.ProjectUrl) ?? string.Empty,
|
||||
ReadmeUrl = FirstNonEmpty(template?.ReadmeUrl, repository.ReadmeUrl, entry.ReadmeUrl) ?? string.Empty,
|
||||
HomepageUrl = FirstNonEmpty(template?.HomepageUrl, repository.HomepageUrl, entry.HomepageUrl) ?? string.Empty,
|
||||
RepositoryUrl = FirstNonEmpty(template?.RepositoryUrl, repository.RepositoryUrl, entry.RepositoryUrl, entry.ProjectUrl)
|
||||
?? string.Empty,
|
||||
Tags = FirstNonEmptyList(template?.Tags, repository.Tags, entry.Tags),
|
||||
SharedContracts = resolvedManifest?.SharedContracts
|
||||
?.Select(contract => new AirAppMarketPluginDependencyEntry
|
||||
{
|
||||
Id = contract.Id,
|
||||
Version = contract.Version,
|
||||
AssemblyName = contract.AssemblyName
|
||||
})
|
||||
.ToList()
|
||||
?? entry.SharedContracts,
|
||||
PackageSources = resolvedPackageSources,
|
||||
Md5 = entry.Md5,
|
||||
PublishedAt = entry.PublishedAt,
|
||||
UpdatedAt = entry.UpdatedAt,
|
||||
ReleaseNotes = FirstNonEmpty(template?.ReleaseNotes, repository.ReleaseNotes, entry.ReleaseNotes) ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<PluginManifest?> TryLoadPluginManifestAsync(
|
||||
string owner,
|
||||
string repositoryName,
|
||||
string branch,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var candidateUrl = AirAppMarketDefaults.BuildGitHubRawUrl(owner, repositoryName, branch, "plugin.json");
|
||||
var text = await TryReadTextAsync(candidateUrl, cancellationToken).ConfigureAwait(false);
|
||||
if (text is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(text));
|
||||
return PluginManifest.Load(stream, candidateUrl);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AirAppMarketRepositoryTemplate?> TryLoadTemplateAsync(
|
||||
string owner,
|
||||
string repositoryName,
|
||||
string branch,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var candidateUrl = AirAppMarketDefaults.BuildGitHubRawUrl(owner, repositoryName, branch, "airappmarket-entry.template.json");
|
||||
var text = await TryReadTextAsync(candidateUrl, cancellationToken).ConfigureAwait(false);
|
||||
if (text is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<AirAppMarketRepositoryTemplate>(text, JsonOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> GetBranchCandidatesAsync(
|
||||
string owner,
|
||||
string repositoryName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var candidates = new List<string>(4);
|
||||
|
||||
if (_defaultBranchCache.TryGetValue(FormatRepositoryKey(owner, repositoryName), out var cachedBranch) &&
|
||||
!string.IsNullOrWhiteSpace(cachedBranch))
|
||||
{
|
||||
candidates.Add(cachedBranch);
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultBranch = await TryGetDefaultBranchAsync(owner, repositoryName, cancellationToken).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(defaultBranch))
|
||||
{
|
||||
_defaultBranchCache[FormatRepositoryKey(owner, repositoryName)] = defaultBranch;
|
||||
candidates.Add(defaultBranch);
|
||||
}
|
||||
}
|
||||
|
||||
candidates.Add("main");
|
||||
candidates.Add("master");
|
||||
|
||||
return candidates
|
||||
.Where(branch => !string.IsNullOrWhiteSpace(branch))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private async Task<string?> TryGetDefaultBranchAsync(
|
||||
string owner,
|
||||
string repositoryName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var url = $"https://api.github.com/repos/{owner}/{repositoryName}";
|
||||
try
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
var responseText = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(responseText);
|
||||
if (document.RootElement.TryGetProperty("default_branch", out var branchNode))
|
||||
{
|
||||
return AirAppMarketIndexDocument.NormalizeValue(branchNode.GetString());
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to conventional branches.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string?> TryReadTextAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
if (AirAppMarketDefaults.TryResolveWorkspaceFile(url, out var localPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await File.ReadAllTextAsync(localPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatRepositoryKey(string owner, string repositoryName)
|
||||
{
|
||||
return $"{owner.Trim()}/{repositoryName.Trim()}";
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(params string?[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var normalized = AirAppMarketIndexDocument.NormalizeValue(value);
|
||||
if (!string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<string> FirstNonEmptyList(params IReadOnlyList<string>?[] lists)
|
||||
{
|
||||
foreach (var list in lists)
|
||||
{
|
||||
if (list is null || list.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalized = list
|
||||
.Select(AirAppMarketIndexDocument.NormalizeValue)
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => value!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (normalized.Count > 0)
|
||||
{
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private sealed record AirAppMarketRepositoryTemplate(
|
||||
string? MinHostVersion,
|
||||
string? IconUrl,
|
||||
string? ProjectUrl,
|
||||
string? ReadmeUrl,
|
||||
string? HomepageUrl,
|
||||
string? RepositoryUrl,
|
||||
List<string>? Tags,
|
||||
string? ReleaseNotes);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public sealed class AirAppMarketIconService : IDisposable
|
||||
}
|
||||
|
||||
public async Task<Bitmap> LoadAsync(
|
||||
LanMountainDesktop.Services.Settings.PluginMarketPluginInfo plugin,
|
||||
LanMountainDesktop.Services.Settings.PluginCatalogItemInfo plugin,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plugin);
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace LanMountainDesktop.Services.PluginMarket;
|
||||
internal sealed class AirAppMarketIndexService : IDisposable
|
||||
{
|
||||
private readonly AirAppMarketCacheService _cacheService;
|
||||
private readonly AirAppMarketMetadataResolverService _metadataResolver;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public AirAppMarketIndexService(AirAppMarketCacheService cacheService)
|
||||
@@ -22,6 +23,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LanMountainDesktop-PluginMarketplace/1.0");
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
_metadataResolver = new AirAppMarketMetadataResolverService(_httpClient);
|
||||
}
|
||||
|
||||
public async Task<AirAppMarketLoadResult> LoadAsync(CancellationToken cancellationToken = default)
|
||||
@@ -34,6 +36,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(localIndexPath, cancellationToken).ConfigureAwait(false);
|
||||
var document = AirAppMarketIndexDocument.Load(json, localIndexPath);
|
||||
document = await _metadataResolver.EnrichAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
_cacheService.SaveIndexJson(json);
|
||||
return new AirAppMarketLoadResult(
|
||||
true,
|
||||
@@ -66,6 +69,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var document = AirAppMarketIndexDocument.Load(json, AirAppMarketDefaults.DefaultIndexUrl);
|
||||
document = await _metadataResolver.EnrichAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
_cacheService.SaveIndexJson(json);
|
||||
return new AirAppMarketLoadResult(
|
||||
true,
|
||||
@@ -93,6 +97,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
|
||||
try
|
||||
{
|
||||
var cachedDocument = AirAppMarketIndexDocument.Load(cachedJson, _cacheService.CacheFilePath);
|
||||
cachedDocument = await _metadataResolver.EnrichAsync(cachedDocument, cancellationToken).ConfigureAwait(false);
|
||||
return new AirAppMarketLoadResult(
|
||||
true,
|
||||
cachedDocument,
|
||||
@@ -124,6 +129,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_metadataResolver.Dispose();
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
var localCopyResult = await _downloadService.DownloadAsync(
|
||||
localPackagePath,
|
||||
attemptPath,
|
||||
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes),
|
||||
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes > 0 ? plugin.PackageSizeBytes : null),
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (!localCopyResult.Success)
|
||||
{
|
||||
@@ -208,7 +208,7 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
var downloadResult = await _downloadService.DownloadAsync(
|
||||
resolvedDownloadUrl,
|
||||
attemptPath,
|
||||
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes),
|
||||
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes > 0 ? plugin.PackageSizeBytes : null),
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (!downloadResult.Success)
|
||||
{
|
||||
@@ -231,14 +231,25 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
if (actualSize != plugin.PackageSizeBytes || !string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
if (plugin.PackageSizeBytes > 0 && actualSize != plugin.PackageSizeBytes)
|
||||
{
|
||||
AppLogger.Error(
|
||||
"PluginMarket",
|
||||
$"Package verification failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{attemptPath}'; ExpectedHash='{plugin.Sha256}'; ActualHash='{actualHash}'; ExpectedSize='{plugin.PackageSizeBytes}'; ActualSize='{actualSize}'.");
|
||||
$"Package verification failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{attemptPath}'; ExpectedSize='{plugin.PackageSizeBytes}'; ActualSize='{actualSize}'.");
|
||||
return new AirAppMarketVerificationResult(
|
||||
false,
|
||||
$"Package verification failed. Expected SHA-256 {plugin.Sha256}, actual {actualHash}. Expected size {plugin.PackageSizeBytes}, actual size {actualSize}.");
|
||||
$"Package verification failed. Expected size {plugin.PackageSizeBytes}, actual size {actualSize}.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(plugin.Sha256) &&
|
||||
!string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AppLogger.Error(
|
||||
"PluginMarket",
|
||||
$"Package hash verification failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{attemptPath}'; ExpectedHash='{plugin.Sha256}'; ActualHash='{actualHash}'.");
|
||||
return new AirAppMarketVerificationResult(
|
||||
false,
|
||||
$"Package verification failed. Expected SHA-256 {plugin.Sha256}, actual {actualHash}.");
|
||||
}
|
||||
|
||||
return new AirAppMarketVerificationResult(true, null);
|
||||
|
||||
@@ -678,30 +678,39 @@ internal sealed class AirAppMarketPluginRepositoryEntry
|
||||
|
||||
public AirAppMarketPluginRepositoryEntry ValidateAndNormalize(string sourceName)
|
||||
{
|
||||
var normalizedIconUrl = AirAppMarketIndexDocument.NormalizeValue(IconUrl)
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing repository.iconUrl.");
|
||||
AirAppMarketIndexDocument.EnsureUrl(normalizedIconUrl, nameof(IconUrl), sourceName);
|
||||
|
||||
var normalizedProjectUrl = AirAppMarketIndexDocument.NormalizeGitHubRepositoryUrl(
|
||||
AirAppMarketIndexDocument.NormalizeValue(ProjectUrl)
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing repository.projectUrl."),
|
||||
nameof(ProjectUrl),
|
||||
sourceName);
|
||||
|
||||
var normalizedReadmeUrl = AirAppMarketIndexDocument.NormalizeValue(ReadmeUrl)
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing repository.readmeUrl.");
|
||||
AirAppMarketIndexDocument.EnsureUrl(normalizedReadmeUrl, nameof(ReadmeUrl), sourceName);
|
||||
|
||||
var normalizedHomepageUrl = AirAppMarketIndexDocument.NormalizeValue(HomepageUrl)
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing repository.homepageUrl.");
|
||||
AirAppMarketIndexDocument.EnsureUrl(normalizedHomepageUrl, nameof(HomepageUrl), sourceName);
|
||||
|
||||
var normalizedRepositoryUrl = AirAppMarketIndexDocument.NormalizeGitHubRepositoryUrl(
|
||||
AirAppMarketIndexDocument.NormalizeValue(RepositoryUrl)
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing repository.repositoryUrl."),
|
||||
nameof(RepositoryUrl),
|
||||
sourceName);
|
||||
|
||||
var normalizedIconUrl = AirAppMarketIndexDocument.NormalizeValue(IconUrl) ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(normalizedIconUrl))
|
||||
{
|
||||
AirAppMarketIndexDocument.EnsureUrl(normalizedIconUrl, nameof(IconUrl), sourceName);
|
||||
}
|
||||
|
||||
var normalizedProjectUrl = AirAppMarketIndexDocument.NormalizeValue(ProjectUrl) ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(normalizedProjectUrl))
|
||||
{
|
||||
AirAppMarketIndexDocument.NormalizeGitHubRepositoryUrl(
|
||||
normalizedProjectUrl,
|
||||
nameof(ProjectUrl),
|
||||
sourceName);
|
||||
}
|
||||
|
||||
var normalizedReadmeUrl = AirAppMarketIndexDocument.NormalizeValue(ReadmeUrl) ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(normalizedReadmeUrl))
|
||||
{
|
||||
AirAppMarketIndexDocument.EnsureUrl(normalizedReadmeUrl, nameof(ReadmeUrl), sourceName);
|
||||
}
|
||||
|
||||
var normalizedHomepageUrl = AirAppMarketIndexDocument.NormalizeValue(HomepageUrl) ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(normalizedHomepageUrl))
|
||||
{
|
||||
AirAppMarketIndexDocument.EnsureUrl(normalizedHomepageUrl, nameof(HomepageUrl), sourceName);
|
||||
}
|
||||
|
||||
var normalizedTags = (Tags ?? [])
|
||||
.Select(AirAppMarketIndexDocument.NormalizeValue)
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
@@ -718,8 +727,7 @@ internal sealed class AirAppMarketPluginRepositoryEntry
|
||||
HomepageUrl = normalizedHomepageUrl,
|
||||
RepositoryUrl = normalizedRepositoryUrl,
|
||||
Tags = normalizedTags,
|
||||
ReleaseNotes = AirAppMarketIndexDocument.NormalizeValue(ReleaseNotes)
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing repository.releaseNotes.")
|
||||
ReleaseNotes = AirAppMarketIndexDocument.NormalizeValue(ReleaseNotes) ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -808,30 +816,20 @@ internal sealed class AirAppMarketPluginPublicationEntry
|
||||
|
||||
public AirAppMarketPluginPublicationEntry ValidateAndNormalize(string sourceName, string pluginId)
|
||||
{
|
||||
var normalizedReleaseTag = AirAppMarketIndexDocument.NormalizeReleaseTag(
|
||||
ReleaseTag,
|
||||
nameof(ReleaseTag),
|
||||
sourceName);
|
||||
var normalizedReleaseAssetName = AirAppMarketIndexDocument.NormalizeValue(ReleaseAssetName)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing publication.releaseAssetName for plugin '{pluginId}'.");
|
||||
|
||||
if (PublishedAt == default || UpdatedAt == default)
|
||||
var normalizedPackageSources = NormalizePackageSources(PackageSources, sourceName, pluginId);
|
||||
var normalizedReleaseTag = AirAppMarketIndexDocument.NormalizeValue(ReleaseTag) ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(normalizedReleaseTag))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing valid publication timestamps for plugin '{pluginId}'.");
|
||||
normalizedReleaseTag = AirAppMarketIndexDocument.NormalizeReleaseTag(
|
||||
normalizedReleaseTag,
|
||||
nameof(ReleaseTag),
|
||||
sourceName);
|
||||
}
|
||||
|
||||
if (PackageSizeBytes <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid packageSizeBytes '{PackageSizeBytes}' for plugin '{pluginId}'.");
|
||||
}
|
||||
|
||||
var normalizedSha256 = AirAppMarketIndexDocument.NormalizeValue(Sha256)?.ToLowerInvariant()
|
||||
?? throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing publication.sha256 for plugin '{pluginId}'.");
|
||||
if (normalizedSha256.Length != 64 || normalizedSha256.Any(ch => !Uri.IsHexDigit(ch)))
|
||||
var normalizedReleaseAssetName = AirAppMarketIndexDocument.NormalizeValue(ReleaseAssetName) ?? string.Empty;
|
||||
var normalizedSha256 = AirAppMarketIndexDocument.NormalizeValue(Sha256)?.ToLowerInvariant() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(normalizedSha256) &&
|
||||
(normalizedSha256.Length != 64 || normalizedSha256.Any(ch => !Uri.IsHexDigit(ch))))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid SHA-256 '{normalizedSha256}' for plugin '{pluginId}'.");
|
||||
@@ -845,8 +843,6 @@ internal sealed class AirAppMarketPluginPublicationEntry
|
||||
$"Market index '{sourceName}' declares invalid MD5 '{normalizedMd5}' for plugin '{pluginId}'.");
|
||||
}
|
||||
|
||||
var normalizedPackageSources = NormalizePackageSources(PackageSources, sourceName, pluginId);
|
||||
|
||||
return new AirAppMarketPluginPublicationEntry
|
||||
{
|
||||
ReleaseTag = normalizedReleaseTag,
|
||||
@@ -1039,120 +1035,10 @@ internal sealed class AirAppMarketPluginEntry
|
||||
? Publication!.ValidateAndNormalize(sourceName, resolvedPluginId)
|
||||
: null;
|
||||
|
||||
var resolvedName = FirstNonEmpty(
|
||||
normalizedManifest?.Name,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Name))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin name.");
|
||||
var resolvedDescription = FirstNonEmpty(
|
||||
normalizedManifest?.Description,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Description))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin description.");
|
||||
var resolvedAuthor = FirstNonEmpty(
|
||||
normalizedManifest?.Author,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Author))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin author.");
|
||||
var resolvedVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
||||
FirstNonEmpty(normalizedManifest?.Version, Version),
|
||||
nameof(Version),
|
||||
sourceName);
|
||||
var resolvedApiVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
||||
FirstNonEmpty(
|
||||
normalizedCompatibility?.PluginApiVersion,
|
||||
normalizedManifest?.ApiVersion,
|
||||
ApiVersion),
|
||||
nameof(ApiVersion),
|
||||
sourceName);
|
||||
var resolvedMinHostVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
||||
FirstNonEmpty(normalizedCompatibility?.MinHostVersion, MinHostVersion),
|
||||
nameof(MinHostVersion),
|
||||
sourceName);
|
||||
|
||||
var resolvedIconUrl = FirstNonEmpty(
|
||||
normalizedRepository?.IconUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(IconUrl))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin iconUrl.");
|
||||
AirAppMarketIndexDocument.EnsureUrl(resolvedIconUrl, nameof(IconUrl), sourceName);
|
||||
var resolvedProjectUrl = AirAppMarketIndexDocument.NormalizeGitHubRepositoryUrl(
|
||||
FirstNonEmpty(
|
||||
normalizedRepository?.ProjectUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ProjectUrl))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin projectUrl."),
|
||||
nameof(ProjectUrl),
|
||||
sourceName);
|
||||
var resolvedReadmeUrl = FirstNonEmpty(
|
||||
normalizedRepository?.ReadmeUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ReadmeUrl))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin readmeUrl.");
|
||||
AirAppMarketIndexDocument.EnsureUrl(resolvedReadmeUrl, nameof(ReadmeUrl), sourceName);
|
||||
var resolvedHomepageUrl = FirstNonEmpty(
|
||||
normalizedRepository?.HomepageUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(HomepageUrl))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin homepageUrl.");
|
||||
AirAppMarketIndexDocument.EnsureUrl(resolvedHomepageUrl, nameof(HomepageUrl), sourceName);
|
||||
var resolvedRepositoryUrl = AirAppMarketIndexDocument.NormalizeGitHubRepositoryUrl(
|
||||
FirstNonEmpty(
|
||||
normalizedRepository?.RepositoryUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(RepositoryUrl))
|
||||
?? throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin repositoryUrl."),
|
||||
nameof(RepositoryUrl),
|
||||
sourceName);
|
||||
|
||||
var resolvedReleaseTag = FirstNonEmpty(
|
||||
normalizedPublication?.ReleaseTag,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ReleaseTag));
|
||||
var resolvedReleaseAssetName = FirstNonEmpty(
|
||||
normalizedPublication?.ReleaseAssetName,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ReleaseAssetName));
|
||||
if (string.IsNullOrWhiteSpace(resolvedReleaseTag) != string.IsNullOrWhiteSpace(resolvedReleaseAssetName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' must declare both '{nameof(ReleaseTag)}' and '{nameof(ReleaseAssetName)}' together for plugin '{resolvedPluginId}'.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resolvedReleaseTag))
|
||||
{
|
||||
resolvedReleaseTag = AirAppMarketIndexDocument.NormalizeReleaseTag(
|
||||
resolvedReleaseTag,
|
||||
nameof(ReleaseTag),
|
||||
sourceName);
|
||||
}
|
||||
|
||||
var resolvedPackageSize = normalizedPublication?.PackageSizeBytes ?? PackageSizeBytes;
|
||||
if (resolvedPackageSize <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid packageSizeBytes '{resolvedPackageSize}' for plugin '{resolvedPluginId}'.");
|
||||
}
|
||||
|
||||
var resolvedSha256 = FirstNonEmpty(
|
||||
normalizedPublication?.Sha256,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Sha256)?.ToLowerInvariant())
|
||||
?? throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing SHA-256 for plugin '{resolvedPluginId}'.");
|
||||
if (resolvedSha256.Length != 64 || resolvedSha256.Any(ch => !Uri.IsHexDigit(ch)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid SHA-256 '{resolvedSha256}' for plugin '{resolvedPluginId}'.");
|
||||
}
|
||||
|
||||
var resolvedMd5 = FirstNonEmpty(
|
||||
normalizedPublication?.Md5,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Md5)?.ToLowerInvariant())
|
||||
?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(resolvedMd5) &&
|
||||
(resolvedMd5.Length != 32 || resolvedMd5.Any(ch => !Uri.IsHexDigit(ch))))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid MD5 '{resolvedMd5}' for plugin '{resolvedPluginId}'.");
|
||||
}
|
||||
|
||||
var resolvedPackageSources = NormalizePackageSources(
|
||||
normalizedPublication?.PackageSources,
|
||||
normalizedPublication?.PackageSources ?? PackageSources,
|
||||
sourceName,
|
||||
resolvedPluginId,
|
||||
resolvedReleaseTag,
|
||||
resolvedReleaseAssetName,
|
||||
resolvedRepositoryUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(DownloadUrl));
|
||||
if (resolvedPackageSources.Count == 0)
|
||||
{
|
||||
@@ -1160,19 +1046,84 @@ internal sealed class AirAppMarketPluginEntry
|
||||
$"Market index '{sourceName}' is missing package sources for plugin '{resolvedPluginId}'.");
|
||||
}
|
||||
|
||||
var resolvedDownloadUrl = resolvedPackageSources[0].Url;
|
||||
var resolvedPublishedAt = normalizedPublication?.PublishedAt ?? PublishedAt;
|
||||
var resolvedUpdatedAt = normalizedPublication?.UpdatedAt ?? UpdatedAt;
|
||||
if (resolvedPublishedAt == default || resolvedUpdatedAt == default)
|
||||
var resolvedRepositoryUrl = FirstNonEmpty(
|
||||
normalizedRepository?.RepositoryUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(RepositoryUrl));
|
||||
if (string.IsNullOrWhiteSpace(resolvedRepositoryUrl))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing valid publish timestamps for plugin '{resolvedPluginId}'.");
|
||||
throw new InvalidOperationException($"Market index '{sourceName}' is missing plugin repositoryUrl.");
|
||||
}
|
||||
|
||||
var resolvedDownloadUrl = FirstNonEmpty(
|
||||
resolvedPackageSources.FirstOrDefault()?.Url,
|
||||
AirAppMarketIndexDocument.NormalizeValue(DownloadUrl))
|
||||
?? string.Empty;
|
||||
|
||||
var resolvedName = FirstNonEmpty(
|
||||
normalizedManifest?.Name,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Name))
|
||||
?? string.Empty;
|
||||
var resolvedDescription = FirstNonEmpty(
|
||||
normalizedManifest?.Description,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Description))
|
||||
?? string.Empty;
|
||||
var resolvedAuthor = FirstNonEmpty(
|
||||
normalizedManifest?.Author,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Author))
|
||||
?? string.Empty;
|
||||
var resolvedVersion = FirstNonEmpty(
|
||||
normalizedManifest?.Version,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Version))
|
||||
?? string.Empty;
|
||||
var resolvedApiVersion = FirstNonEmpty(
|
||||
normalizedCompatibility?.PluginApiVersion,
|
||||
normalizedManifest?.ApiVersion,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ApiVersion))
|
||||
?? string.Empty;
|
||||
var resolvedMinHostVersion = FirstNonEmpty(
|
||||
normalizedCompatibility?.MinHostVersion,
|
||||
AirAppMarketIndexDocument.NormalizeValue(MinHostVersion))
|
||||
?? string.Empty;
|
||||
|
||||
var resolvedIconUrl = FirstNonEmpty(
|
||||
normalizedRepository?.IconUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(IconUrl))
|
||||
?? string.Empty;
|
||||
var resolvedProjectUrl = FirstNonEmpty(
|
||||
normalizedRepository?.ProjectUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ProjectUrl))
|
||||
?? string.Empty;
|
||||
var resolvedReadmeUrl = FirstNonEmpty(
|
||||
normalizedRepository?.ReadmeUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ReadmeUrl))
|
||||
?? string.Empty;
|
||||
var resolvedHomepageUrl = FirstNonEmpty(
|
||||
normalizedRepository?.HomepageUrl,
|
||||
AirAppMarketIndexDocument.NormalizeValue(HomepageUrl))
|
||||
?? string.Empty;
|
||||
|
||||
var resolvedReleaseTag = FirstNonEmpty(
|
||||
normalizedPublication?.ReleaseTag,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ReleaseTag))
|
||||
?? string.Empty;
|
||||
var resolvedReleaseAssetName = FirstNonEmpty(
|
||||
normalizedPublication?.ReleaseAssetName,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ReleaseAssetName))
|
||||
?? string.Empty;
|
||||
var resolvedPackageSize = normalizedPublication?.PackageSizeBytes ?? PackageSizeBytes;
|
||||
var resolvedSha256 = FirstNonEmpty(
|
||||
normalizedPublication?.Sha256,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Sha256)?.ToLowerInvariant())
|
||||
?? string.Empty;
|
||||
var resolvedMd5 = FirstNonEmpty(
|
||||
normalizedPublication?.Md5,
|
||||
AirAppMarketIndexDocument.NormalizeValue(Md5)?.ToLowerInvariant())
|
||||
?? string.Empty;
|
||||
var resolvedPublishedAt = normalizedPublication?.PublishedAt ?? PublishedAt;
|
||||
var resolvedUpdatedAt = normalizedPublication?.UpdatedAt ?? UpdatedAt;
|
||||
|
||||
var resolvedDependencies = NormalizeDependencies(
|
||||
normalizedManifest?.SharedContracts,
|
||||
normalizedCapabilities?.SharedContracts,
|
||||
SharedContracts,
|
||||
normalizedManifest?.SharedContracts ?? SharedContracts,
|
||||
sourceName,
|
||||
resolvedPluginId);
|
||||
var resolvedTags = (normalizedRepository?.Tags ?? Tags ?? [])
|
||||
@@ -1185,8 +1136,7 @@ internal sealed class AirAppMarketPluginEntry
|
||||
var resolvedReleaseNotes = FirstNonEmpty(
|
||||
normalizedRepository?.ReleaseNotes,
|
||||
AirAppMarketIndexDocument.NormalizeValue(ReleaseNotes))
|
||||
?? throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing release notes for plugin '{resolvedPluginId}'.");
|
||||
?? string.Empty;
|
||||
|
||||
return new AirAppMarketPluginEntry
|
||||
{
|
||||
@@ -1225,6 +1175,13 @@ internal sealed class AirAppMarketPluginEntry
|
||||
|
||||
public string GetVersionSummary()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Version) &&
|
||||
string.IsNullOrWhiteSpace(ApiVersion) &&
|
||||
string.IsNullOrWhiteSpace(MinHostVersion))
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"v{0} | API {1} | Host >= {2}",
|
||||
@@ -1309,21 +1266,13 @@ internal sealed class AirAppMarketPluginEntry
|
||||
}
|
||||
|
||||
private static List<AirAppMarketPluginDependencyEntry> NormalizeDependencies(
|
||||
IReadOnlyList<AirAppMarketPluginDependencyEntry>? manifestDependencies,
|
||||
IReadOnlyList<AirAppMarketPluginDependencyEntry>? capabilityDependencies,
|
||||
IReadOnlyList<AirAppMarketPluginDependencyEntry>? legacyDependencies,
|
||||
IReadOnlyList<AirAppMarketPluginDependencyEntry>? dependencies,
|
||||
string sourceName,
|
||||
string pluginId)
|
||||
{
|
||||
IReadOnlyList<AirAppMarketPluginDependencyEntry> dependencies = manifestDependencies is { Count: > 0 }
|
||||
? manifestDependencies
|
||||
: capabilityDependencies is { Count: > 0 }
|
||||
? capabilityDependencies
|
||||
: legacyDependencies ?? [];
|
||||
|
||||
var normalizedDependencies = new List<AirAppMarketPluginDependencyEntry>(dependencies.Count);
|
||||
var normalizedDependencies = new List<AirAppMarketPluginDependencyEntry>((dependencies ?? []).Count);
|
||||
var seenDependencies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var dependency in dependencies)
|
||||
foreach (var dependency in dependencies ?? [])
|
||||
{
|
||||
var normalizedDependency = dependency.ValidateAndNormalize(sourceName);
|
||||
var dependencyKey = $"{normalizedDependency.Id}@{normalizedDependency.Version}";
|
||||
@@ -1343,9 +1292,6 @@ internal sealed class AirAppMarketPluginEntry
|
||||
IReadOnlyList<AirAppMarketPluginPackageSourceEntry>? packageSources,
|
||||
string sourceName,
|
||||
string pluginId,
|
||||
string? releaseTag,
|
||||
string? releaseAssetName,
|
||||
string repositoryUrl,
|
||||
string? legacyDownloadUrl)
|
||||
{
|
||||
var normalizedSources = new List<AirAppMarketPluginPackageSourceEntry>((packageSources ?? []).Count + 1);
|
||||
@@ -1364,42 +1310,16 @@ internal sealed class AirAppMarketPluginEntry
|
||||
var normalizedLegacyDownloadUrl = AirAppMarketIndexDocument.NormalizeValue(legacyDownloadUrl);
|
||||
if (!string.IsNullOrWhiteSpace(normalizedLegacyDownloadUrl))
|
||||
{
|
||||
var legacyKind = !string.IsNullOrWhiteSpace(releaseTag) && !string.IsNullOrWhiteSpace(releaseAssetName)
|
||||
? PluginPackageSourceKind.ReleaseAsset
|
||||
: PluginPackageSourceKind.RawFallback;
|
||||
var legacySource = new AirAppMarketPluginPackageSourceEntry
|
||||
{
|
||||
Kind = legacyKind switch
|
||||
{
|
||||
PluginPackageSourceKind.ReleaseAsset => "releaseAsset",
|
||||
PluginPackageSourceKind.RawFallback => "rawFallback",
|
||||
PluginPackageSourceKind.WorkspaceLocal => "workspaceLocal",
|
||||
_ => "rawFallback"
|
||||
},
|
||||
Kind = "rawFallback",
|
||||
Url = normalizedLegacyDownloadUrl,
|
||||
SourceKind = legacyKind
|
||||
SourceKind = PluginPackageSourceKind.RawFallback
|
||||
};
|
||||
normalizedSources.Add(legacySource.ValidateAndNormalize(sourceName, pluginId));
|
||||
return normalizedSources;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(releaseTag) &&
|
||||
!string.IsNullOrWhiteSpace(releaseAssetName) &&
|
||||
AirAppMarketDefaults.TryParseGitHubRepositoryUrl(repositoryUrl, out var owner, out var repositoryName))
|
||||
{
|
||||
var releaseUrl = AirAppMarketDefaults.BuildGitHubReleaseDownloadUrl(
|
||||
owner,
|
||||
repositoryName,
|
||||
releaseTag,
|
||||
releaseAssetName);
|
||||
normalizedSources.Add(new AirAppMarketPluginPackageSourceEntry
|
||||
{
|
||||
Kind = "releaseAsset",
|
||||
Url = releaseUrl,
|
||||
SourceKind = PluginPackageSourceKind.ReleaseAsset
|
||||
}.ValidateAndNormalize(sourceName, pluginId));
|
||||
}
|
||||
|
||||
return normalizedSources;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class AirAppMarketReadmeService : IDisposable
|
||||
}
|
||||
|
||||
public async Task<string> LoadAsync(
|
||||
LanMountainDesktop.Services.Settings.PluginMarketPluginInfo plugin,
|
||||
LanMountainDesktop.Services.Settings.PluginCatalogItemInfo plugin,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plugin);
|
||||
|
||||
Reference in New Issue
Block a user