Files
Hasebe/Library/PackageCache/com.github-glitchenzo.nugetforunity@5e9f9e02b402/Editor/PackageSource/NugetPackageSourceV3.cs
T
2026-09-21 12:15:57 +03:00

365 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NugetForUnity.Configuration;
using NugetForUnity.Models;
using UnityEngine;
namespace NugetForUnity.PackageSource
{
/// <summary>
/// Online NuGet package source for NuGet API v3.
/// </summary>
[Serializable]
internal sealed class NugetPackageSourceV3 : INugetPackageSource, ISerializationCallbackReceiver
{
/// <summary>
/// Default value for <see cref="UpdateSearchBatchSize" />.
/// </summary>
public const int DefaultUpdateSearchBatchSize = 20;
/// <summary>
/// Default value for <see cref="SupportsPackageIdSearchFilter" />.
/// </summary>
public const bool DefaultSupportsPackageIdSearchFilter = true;
[NotNull]
private static readonly Dictionary<string, NugetApiClientV3> ApiClientCache = new Dictionary<string, NugetApiClientV3>();
[CanBeNull]
[NonSerialized]
private NugetApiClientV3 apiClient;
/// <summary>
/// Initializes a new instance of the <see cref="NugetPackageSourceV3" /> class.
/// </summary>
/// <param name="name">The name of the package source.</param>
/// <param name="url">The path to the package source.</param>
/// <param name="savedProtocolVersion">The explicitly defined protocol version stored inside the 'NuGet.config'.</param>
public NugetPackageSourceV3([NotNull] string name, [NotNull] string url, string savedProtocolVersion)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name));
}
if (string.IsNullOrEmpty(url))
{
throw new ArgumentException($"'{nameof(url)}' cannot be null or empty.", nameof(url));
}
Name = name;
SavedPath = url;
SavedProtocolVersion = savedProtocolVersion;
IsEnabled = true;
InitializeApiClient();
}
/// <summary>
/// Gets password, with the values of environment variables expanded.
/// </summary>
[CanBeNull]
public string ExpandedPassword
{
get
{
if (SavedPassword == null)
{
return null;
}
var expandedPassword = Environment.ExpandEnvironmentVariables(SavedPassword);
return SavedPasswordIsEncrypted ? ConfigurationEncryptionHelper.DecryptString(expandedPassword) : expandedPassword;
}
}
/// <summary>
/// Gets user-name, with the values of environment variables expanded.
/// </summary>
[CanBeNull]
public string ExpandedUserName
{
get
{
if (SavedUserName == null)
{
return null;
}
return Environment.ExpandEnvironmentVariables(SavedUserName);
}
}
/// <inheritdoc />
[field: SerializeField]
public string Name { get; set; }
/// <inheritdoc />
[field: SerializeField]
public string SavedPath { get; set; }
/// <inheritdoc />
[field: SerializeField]
public string SavedProtocolVersion { get; set; }
/// <inheritdoc />
public bool SavedPasswordIsEncrypted { get; set; }
/// <inheritdoc cref="NugetApiClientV3.PackageDownloadUrlTemplateOverwrite" />
[CanBeNull]
public string PackageDownloadUrlTemplateOverwrite
{
get => ApiClient.PackageDownloadUrlTemplateOverwrite;
set => ApiClient.PackageDownloadUrlTemplateOverwrite = value;
}
/// <summary>
/// Gets or sets the size of each batch in which available updates are fetched using the <see cref="SearchAsync" />,
/// to prevent the search query string to exceed the URI length limit we fetch the updates in groups. Defaults to <c>20</c>.
/// </summary>
[field: SerializeField]
public int UpdateSearchBatchSize { get; set; } = DefaultUpdateSearchBatchSize;
/// <summary>
/// Gets or sets a value indicating whether the package registry supports querying a package by its id
/// using the syntax <c>'packageid:{packageId}'</c>. This syntax is used to easily search for package updates.
/// If it is disabled package updates are searched by loading the information of all packages one by one.
/// </summary>
public bool SupportsPackageIdSearchFilter { get; set; } = DefaultSupportsPackageIdSearchFilter;
/// <inheritdoc />
[field: SerializeField]
public string SavedUserName { get; set; }
/// <inheritdoc />
[field: SerializeField]
public string SavedPassword { get; set; }
/// <inheritdoc />
[field: SerializeField]
public bool CredentialsStoredInExternalFile { get; set; }
/// <inheritdoc />
[field: SerializeField]
public bool EnableCredentialProvider { get; set; } = true;
/// <inheritdoc />
[field: SerializeField]
public bool IsEnabled { get; set; }
/// <inheritdoc />
public bool HasPassword
{
get => SavedPassword != null;
set
{
if (value)
{
if (SavedPassword == null)
{
SavedPassword = string.Empty; // Initialize newly-enabled password to empty string.
}
}
else
{
SavedPassword = null; // Clear password to null when disabled.
}
}
}
/// <summary>
/// Gets the lazy initialized API-client. We need this because <see cref="InitializeApiClient" /> can't be called in
/// <see cref="OnAfterDeserialize" />.
/// </summary>
[NotNull]
private NugetApiClientV3 ApiClient => apiClient ?? InitializeApiClient();
/// <inheritdoc />
public List<INugetPackage> FindPackagesById(INugetPackageIdentifier package)
{
var fetchedPackage = Task.Run(() => ApiClient.GetPackageWithDetailsAsync(this, package, CancellationToken.None)).GetAwaiter().GetResult();
if (fetchedPackage is null)
{
return new List<INugetPackage>();
}
if (!package.HasVersionRange && fetchedPackage.Equals(package))
{
// exact match found
return new List<INugetPackage> { fetchedPackage };
}
var matchingVersion = fetchedPackage.Versions.FindLast(package.InRange);
if (matchingVersion == null)
{
// no matching version found
return new List<INugetPackage>();
}
// overwrite the version so it is installed
fetchedPackage.PackageVersion = matchingVersion;
return new List<INugetPackage> { fetchedPackage };
}
/// <inheritdoc />
public INugetPackage GetSpecificPackage(INugetPackageIdentifier package)
{
return FindPackagesById(package).FirstOrDefault();
}
/// <inheritdoc />
public List<INugetPackage> GetUpdates(
IEnumerable<INugetPackage> packages,
bool includePrerelease = false,
string targetFrameworks = "",
string versionConstraints = "",
CancellationToken token = default)
{
var packagesToFetch = packages as IList<INugetPackage> ?? packages.ToList();
var packagesFromServer = Task.Run(
async () =>
{
try
{
if (SupportsPackageIdSearchFilter)
{
var fetchTasks = new List<Task<List<INugetPackage>>>();
for (var i = 0; i < packagesToFetch.Count;)
{
var searchQueryBuilder = new StringBuilder();
for (var inner = 0; inner < UpdateSearchBatchSize && i < packagesToFetch.Count; inner++, i++)
{
if (i > 0)
{
searchQueryBuilder.Append(' ');
}
searchQueryBuilder.Append($"packageid:{packagesToFetch[i].Id}");
}
fetchTasks.Add(ApiClient.SearchPackageAsync(this, searchQueryBuilder.ToString(), 0, 0, includePrerelease, token));
}
var nestedUpdates = await Task.WhenAll(fetchTasks).ConfigureAwait(false);
var updates = nestedUpdates.SelectMany(nestedPackages => nestedPackages).ToList();
if (updates.Count > packagesToFetch.Count)
{
Debug.LogWarningFormat(
"Fetching updates using a filter with the format 'packageid:{{packageId}}' resulted in more packages as requested. This probably means that the syntax is not supported by the package source {0}. So consider switching to a different 'update search mechanism' by disabling the '{1}' setting for this package source.",
SavedPath,
nameof(SupportsPackageIdSearchFilter));
}
return updates;
}
var fetchedPackages = await Task.WhenAll(
packagesToFetch.Select(package =>
ApiClient.GetPackageWithAllVersionsAsync(this, package, includePrerelease, CancellationToken.None)))
.ConfigureAwait(false);
return fetchedPackages.Where(fetchedPackage => !(fetchedPackage is null)).ToList<INugetPackage>();
}
catch (OperationCanceledException)
{
return new List<INugetPackage>();
}
catch (Exception e)
{
Debug.LogException(e);
return new List<INugetPackage>();
}
},
token)
.GetAwaiter()
.GetResult();
CopyIsManuallyInstalled(packagesFromServer, packagesToFetch);
packagesFromServer.Sort();
return packagesFromServer;
}
/// <inheritdoc />
public Task<List<INugetPackage>> SearchAsync(
string searchTerm = "",
bool includePrerelease = false,
int numberToGet = 15,
int numberToSkip = 0,
CancellationToken cancellationToken = default)
{
return ApiClient.SearchPackageAsync(this, searchTerm, numberToSkip, numberToGet, includePrerelease, cancellationToken);
}
/// <inheritdoc />
public void DownloadNupkgToFile(INugetPackageIdentifier package, string outputFilePath, string downloadUrlHint)
{
Task.Run(() => ApiClient.DownloadNupkgToFileAsync(this, package, outputFilePath, downloadUrlHint)).GetAwaiter().GetResult();
}
/// <inheritdoc />
public void Dispose()
{
apiClient?.Dispose();
ApiClientCache.Remove(SavedPath);
}
/// <inheritdoc />
public void OnBeforeSerialize()
{
// do nothing
}
/// <inheritdoc />
public void OnAfterDeserialize()
{
if (string.IsNullOrEmpty(SavedPassword))
{
SavedPassword = null;
}
}
/// <inheritdoc cref="NugetApiClientV3.GetPackageDetailsAsync" />
[NotNull]
[ItemNotNull]
public Task<List<NugetFrameworkGroup>> GetPackageDetailsAsync(
[NotNull] INugetPackageIdentifier package,
CancellationToken cancellationToken = default)
{
return ApiClient.GetPackageDetailsAsync(this, package, cancellationToken);
}
private static void CopyIsManuallyInstalled(List<INugetPackage> newPackages, ICollection<INugetPackage> packagesToUpdate)
{
foreach (var newPackage in newPackages)
{
newPackage.IsManuallyInstalled =
packagesToUpdate.FirstOrDefault(packageToUpdate => packageToUpdate.Id.Equals(newPackage.Id, StringComparison.OrdinalIgnoreCase))
?.IsManuallyInstalled ??
false;
}
}
[NotNull]
private NugetApiClientV3 InitializeApiClient()
{
if (ApiClientCache.TryGetValue(SavedPath, out apiClient))
{
Debug.Assert(apiClient != null, nameof(apiClient) + " != null");
return apiClient;
}
apiClient = new NugetApiClientV3(SavedPath);
ApiClientCache.Add(SavedPath, apiClient);
return apiClient;
}
}
}