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 { /// /// Online NuGet package source for NuGet API v3. /// [Serializable] internal sealed class NugetPackageSourceV3 : INugetPackageSource, ISerializationCallbackReceiver { /// /// Default value for . /// public const int DefaultUpdateSearchBatchSize = 20; /// /// Default value for . /// public const bool DefaultSupportsPackageIdSearchFilter = true; [NotNull] private static readonly Dictionary ApiClientCache = new Dictionary(); [CanBeNull] [NonSerialized] private NugetApiClientV3 apiClient; /// /// Initializes a new instance of the class. /// /// The name of the package source. /// The path to the package source. /// The explicitly defined protocol version stored inside the 'NuGet.config'. 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(); } /// /// Gets password, with the values of environment variables expanded. /// [CanBeNull] public string ExpandedPassword { get { if (SavedPassword == null) { return null; } var expandedPassword = Environment.ExpandEnvironmentVariables(SavedPassword); return SavedPasswordIsEncrypted ? ConfigurationEncryptionHelper.DecryptString(expandedPassword) : expandedPassword; } } /// [field: SerializeField] public string Name { get; set; } /// [field: SerializeField] public string SavedPath { get; set; } /// [field: SerializeField] public string SavedProtocolVersion { get; set; } /// public bool SavedPasswordIsEncrypted { get; set; } /// [CanBeNull] public string PackageDownloadUrlTemplateOverwrite { get => ApiClient.PackageDownloadUrlTemplateOverwrite; set => ApiClient.PackageDownloadUrlTemplateOverwrite = value; } /// /// Gets or sets the size of each batch in witch available updates are fetched using the , /// to prevent the search query string to exceed the URI length limit we fetch the updates in groups. Defaults to 20. /// [field: SerializeField] public int UpdateSearchBatchSize { get; set; } = DefaultUpdateSearchBatchSize; /// /// Gets or sets a value indicating whether the package registry supports querying a package by its id /// using the syntax 'packageid:{packageId}'. 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. /// public bool SupportsPackageIdSearchFilter { get; set; } = DefaultSupportsPackageIdSearchFilter; /// [field: SerializeField] public string UserName { get; set; } /// [field: SerializeField] public string SavedPassword { get; set; } /// [field: SerializeField] public bool IsEnabled { get; set; } /// 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. } } } /// /// Gets the lazy initialized API-client. We need this because can't be called in /// . /// [NotNull] private NugetApiClientV3 ApiClient => apiClient ?? InitializeApiClient(); /// public List FindPackagesById(INugetPackageIdentifier package) { var fetchedPackage = Task.Run(() => ApiClient.GetPackageWithDetailsAsync(this, package, CancellationToken.None)).GetAwaiter().GetResult(); if (fetchedPackage is null) { return new List(); } if (!package.HasVersionRange && fetchedPackage.Equals(package)) { // exact match found return new List { fetchedPackage }; } var matchingVersion = fetchedPackage.Versions.FindLast(package.InRange); if (matchingVersion == null) { // no matching version found return new List(); } // overwrite the version so it is installed fetchedPackage.PackageVersion = matchingVersion; return new List { fetchedPackage }; } /// public INugetPackage GetSpecificPackage(INugetPackageIdentifier package) { return FindPackagesById(package).FirstOrDefault(); } /// public List GetUpdates( IEnumerable packages, bool includePrerelease = false, string targetFrameworks = "", string versionConstraints = "") { var packagesToFetch = packages as IList ?? packages.ToList(); var packagesFromServer = Task.Run( async () => { if (SupportsPackageIdSearchFilter) { var updates = new List(); 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}"); } updates.AddRange( await ApiClient.SearchPackageAsync( this, searchQueryBuilder.ToString(), 0, 0, includePrerelease, CancellationToken.None) .ConfigureAwait(false)); } 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, CancellationToken.None))) .ConfigureAwait(false); return fetchedPackages.ToList(); }) .GetAwaiter() .GetResult(); CopyIsManuallyInstalled(packagesFromServer, packagesToFetch); packagesFromServer.Sort(); return packagesFromServer; } /// public Task> SearchAsync( string searchTerm = "", bool includePrerelease = false, int numberToGet = 15, int numberToSkip = 0, CancellationToken cancellationToken = default) { return ApiClient.SearchPackageAsync(this, searchTerm, numberToSkip, numberToGet, includePrerelease, cancellationToken); } /// public void DownloadNupkgToFile(INugetPackageIdentifier package, string outputFilePath, string downloadUrlHint) { Task.Run(() => ApiClient.DownloadNupkgToFileAsync(this, package, outputFilePath, downloadUrlHint)).GetAwaiter().GetResult(); } /// public void Dispose() { apiClient?.Dispose(); ApiClientCache.Remove(SavedPath); } /// public void OnBeforeSerialize() { // do nothing } /// public void OnAfterDeserialize() { if (string.IsNullOrEmpty(SavedPassword)) { SavedPassword = null; } } /// [NotNull] [ItemNotNull] public Task> GetPackageDetailsAsync( [NotNull] INugetPackageIdentifier package, CancellationToken cancellationToken = default) { return ApiClient.GetPackageDetailsAsync(this, package, cancellationToken); } [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; } private void CopyIsManuallyInstalled(List newPackages, ICollection packagesToUpdate) { foreach (var newPackage in newPackages) { newPackage.IsManuallyInstalled = packagesToUpdate.FirstOrDefault(packageToUpdate => packageToUpdate.Id.Equals(newPackage.Id, StringComparison.OrdinalIgnoreCase)) ?.IsManuallyInstalled ?? false; } } } }