File: LocalRepositories\LocalPackageSearchResource.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Protocol\NuGet.Protocol.csproj (NuGet.Protocol)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;

namespace NuGet.Protocol
{
    public class LocalPackageSearchResource : PackageSearchResource
    {
        private readonly FindLocalPackagesResource _localResource;

        public LocalPackageSearchResource(FindLocalPackagesResource localResource)
        {
            if (localResource == null)
            {
                throw new ArgumentNullException(nameof(localResource));
            }

            _localResource = localResource;
        }

        public async override Task<IEnumerable<IPackageSearchMetadata>> SearchAsync(
            string searchTerm,
            SearchFilter filters,
            int skip,
            int take,
            ILogger log,
            CancellationToken token)
        {
            return await Task.Factory.StartNew(() =>
            {
                // Check if source is available.
                if (!IsLocalOrUNC(_localResource.Root))
                {
                    throw new InvalidOperationException(string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.Protocol_Search_LocalSourceNotFound,
                        _localResource.Root));
                }

                var query = _localResource.GetPackages(log, token);

                // Filter on prerelease
                query = query.Where(package => filters.IncludePrerelease || !package.Identity.Version.IsPrerelease);

                // Filter on search terms
                if (!string.IsNullOrEmpty(searchTerm))
                {
                    var terms = searchTerm.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                    query = query.Where(package => ContainsAnyTerm(terms, package));
                }

                // Collapse to the highest version per id, if necessary
                var collapsedQuery = filters?.Filter == SearchFilterType.IsLatestVersion ||
                                     filters?.Filter == SearchFilterType.IsAbsoluteLatestVersion
                                     ? CollapseToHighestVersion(query) : query;

                // execute the query
                var packages = collapsedQuery
                    .Skip(skip)
                    .Take(take)
                    .ToArray();

                // Create final results and retrieve all versions for each package.
                return packages
                    .Select(package => CreatePackageSearchResult(package, filters, log, token))
                    .ToArray();
            }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
        }

        /// <summary>
        /// Search Id, Tags, and Description to match the legacy local search behavior.
        /// </summary>
        private static bool ContainsAnyTerm(string[] terms, LocalPackageInfo package)
        {
            var id = package.Identity.Id;
            var tags = package.Nuspec.GetTags();
            var description = package.Nuspec.GetDescription();

            foreach (var term in terms)
            {
                if (ContainsTerm(term, id)
                    || ContainsTerm(term, tags)
                    || ContainsTerm(term, description))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool ContainsTerm(string search, string property)
        {
            int? pos = property?.IndexOf(search, StringComparison.OrdinalIgnoreCase);

            return (pos.HasValue && pos.Value > -1);
        }

        private IPackageSearchMetadata CreatePackageSearchResult(
            LocalPackageInfo package,
            SearchFilter filter,
            ILogger log,
            CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var metadata = new LocalPackageSearchMetadata(package);

            return metadata
                .WithVersions(() => GetVersions(_localResource, package, filter, log, CancellationToken.None));
        }

        private static List<VersionInfo> GetVersions(
            FindLocalPackagesResource localResource,
            LocalPackageInfo package,
            SearchFilter filter,
            ILogger log,
            CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            // apply the filters to the version list returned
            var versions = localResource.FindPackagesById(package.Identity.Id, log, token)
                .Where(v => filter.IncludePrerelease || !v.Identity.Version.IsPrerelease)
                .Select(p => new VersionInfo(p.Identity.Version, downloadCount: 0))
                .OrderByDescending(v => v.Version, VersionComparer.Default)
                .ToList();

            // Add in the current package if it does not already exist
            if (!versions.Any(v => v.Version == package.Identity.Version))
            {
                var packageVersionInfo = new VersionInfo(package.Identity.Version, downloadCount: 0)
                {
                    PackageSearchMetadata = new LocalPackageSearchMetadata(package)
                };

                versions.Add(packageVersionInfo);
            }

            return versions;
        }

        private static bool IsLocalOrUNC(string currentSource)
        {
            Uri currentURI = UriUtility.TryCreateSourceUri(currentSource, UriKind.Absolute);
            if (currentURI != null)
            {
                if (currentURI.IsFile || currentURI.IsUnc)
                {
                    if (Directory.Exists(UriUtility.GetLocalPath(currentSource)))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Returns a distinct set of elements using the comparer specified. This implementation will pick the last occurrence
        /// of each element instead of picking the first. This method assumes that similar items occur in order.
        /// </summary>        
        private static IEnumerable<LocalPackageInfo> CollapseToHighestVersion(IEnumerable<LocalPackageInfo> source)
        {
            bool first = true;
            bool maxElementHasValue = false;
            LocalPackageInfo previousElement = null;
            LocalPackageInfo maxElement = null;

            foreach (LocalPackageInfo element in source)
            {
                // If we're starting a new group then return the max element from the last group
                if (!first && !StringComparer.OrdinalIgnoreCase.Equals(element.Identity.Id, previousElement.Identity.Id))
                {
                    yield return maxElement;

                    // Reset the max element
                    maxElementHasValue = false;
                }

                // If the current max element has a value and is bigger or doesn't have a value then update the max
                if (!maxElementHasValue
                    || (maxElementHasValue
                        && VersionComparer.VersionRelease.Compare(maxElement.Identity.Version, element.Identity.Version) < 0))
                {
                    maxElement = element;
                    maxElementHasValue = true;
                }

                previousElement = element;
                first = false;
            }

            if (!first)
            {
                yield return maxElement;
            }

            yield break;
        }
    }
}