namespace Microsoft.PackageManagement.NuGetProvider { using System; using System.Net; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; using Resources; using System.Collections.Concurrent; using System.Globalization; using System.Security; using Microsoft.PackageManagement.Provider.Utility; /// /// Package repository for downloading data from remote galleries /// internal class HttpClientPackageRepository : IPackageRepository { private readonly string _nugetFindPackageIdQueryFormat; private readonly string _queryUri; /// /// Ctor's /// /// The nuget request object /// Packagesource location internal HttpClientPackageRepository(string queryUrl, NuGetRequest request) { // Validate the url Uri newUri; Uri validatedUri = null; if (Uri.TryCreate(queryUrl, UriKind.Absolute, out newUri)) { validatedUri = NuGetPathUtility.ValidateUri(newUri, request); } if (validatedUri == null) { throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Messages.InvalidQueryUrl, queryUrl)); } queryUrl = validatedUri.AbsoluteUri; // if a query is http://www.nuget.org/api/v2 then we add / to the end if (!queryUrl.EndsWith("/", StringComparison.OrdinalIgnoreCase)) { queryUrl = String.Concat(queryUrl, "/"); } _queryUri = queryUrl; //we are constructing the url query like http://www.nuget.org/api/v2/FindPackagesById()?id='JQuery' _nugetFindPackageIdQueryFormat = PathUtility.UriCombine(_queryUri, NuGetConstant.FindPackagesById); } /// /// Package source location /// public string Source { get { // Package source Uri return _queryUri; } } /// /// True if the packagesource is a file repository /// public bool IsFile { get { //false because this is not a local file repository return false; } } /// /// Find-Package /// /// package Id /// package version /// /// public IPackage FindPackage(string packageId, SemanticVersion version, NuGetRequest request) { if (string.IsNullOrWhiteSpace(packageId)) { return null; } request.Debug(Messages.DebugInfoCallMethod3, "HttpClientPackageRepository", "FindPackage", packageId); var query = packageId.MakeFindPackageByIdQuery(_nugetFindPackageIdQueryFormat); var packages = NuGetClient.FindPackage(query, request); //Usually versions has a limited number, ToArray should be ok. var versions = version.GetComparableVersionStrings().ToArray(); //Will only enumerate oackages once return packages.FirstOrDefault(package => packageId.Equals(package.Id, StringComparison.OrdinalIgnoreCase) && versions.Contains(package.Version,StringComparer.OrdinalIgnoreCase)); } /// /// Find-Package bases on the given package Id /// /// Package Id /// /// public IEnumerable FindPackagesById(string packageId, NuGetRequest request){ request.Debug(Messages.DebugInfoCallMethod3, "HttpClientPackageRepository", "FindPackagesById", packageId); var query = packageId.MakeFindPackageByIdQuery(_nugetFindPackageIdQueryFormat); //request.Verbose(query.ToString()); return NuGetClient.FindPackage(query, request); } /// /// Search the entire repository for the case when a user does not provider package name or uses wildcards in the name. /// /// /// /// public IEnumerable Search(string searchTerm, NuGetRequest nugetRequest) { if (nugetRequest == null) { yield break; } nugetRequest.Debug(Messages.DebugInfoCallMethod3, "HttpClientPackageRepository", "Search", searchTerm); var searchQuery = searchTerm.MakeSearchQuery(_queryUri, nugetRequest.AllowPrereleaseVersions.Value, nugetRequest.AllVersions.Value); foreach (var pkg in SendRequest(searchQuery, nugetRequest)) { yield return pkg; } } /// /// Send the request to the server with buffer size to account for the case where there are more data /// that we need to fetch /// /// /// /// public static IEnumerable SendRequest(string query, NuGetRequest request) { const int bufferSize = 40; // number of threads sending the requests const int numberOfSenders = 4; var startPoint = 0; var tasks = new List>(); bool stopSending = false; object stopLock = new Object(); // Send one request first // this initial query is of the form http://www.nuget.org/api/v2/FindPackagesById()?id='jquery'&$skip={0}&$top={1} UriBuilder initialQuery = new UriBuilder(query.InsertSkipAndTop()); PackageBase firstPackage = null; // Send out an initial request // we send out 1 initial request first to check for redirection and check whether repository supports odata using (Stream stream = NuGetClient.InitialDownloadDataToStream(initialQuery, startPoint, bufferSize, request)) { if (stream == null) { yield break; } XDocument document = XmlUtility.LoadSafe(stream, ignoreWhiteSpace: true); var entries = document.Root.ElementsNoNamespace("entry").ToList(); // If the initial request has different number of entries than the buffer size, return it because this means the server // does not understand odata request or there is no more data. in the former case, we have to stop to prevent infinite loop if (entries.Count != bufferSize) { request.Debug(Messages.PackagesReceived, entries.Count); stopSending = true; } foreach (XElement entry in entries) { var package = new PackageBase(); // set the first package of the request. this is used later to verify that the case when the number of packages in the repository // is the same as the buffer size and the repository does not support odata query. in that case, we want to check whether the first package // exists anywhere in the second call. if it is, then we cancel the request (this is to prevent infinite loop) if (firstPackage == null) { firstPackage = package; } PackageUtility.ReadEntryElement(ref package, entry); yield return package; } } if (stopSending || request.IsCanceled) { yield break; } // To avoid more redirection (for example, if the initial query is nuget.org, it will be changed to www.nuget.org query = initialQuery.Uri.ToString(); // Sending the initial requests for (var i = 0; i < numberOfSenders; i++) { // Update the start point to fetch the packages startPoint += bufferSize; // Get the query var newQuery = string.Format(query, startPoint, bufferSize); // Send it tasks.Add(Task.Factory.StartNew(() => { Stream items = NuGetClient.DownloadDataToStream(newQuery, request); return items; })); } //Wait for the responses, parse the data, and send to the user while (tasks.Count > 0) { //Cast because the compiler warning: Co-variant array conversion from Task[] to Task[] can cause run-time exception on write operation. var index = Task.WaitAny(tasks.Cast().ToArray()); using (Stream stream = tasks[index].Result) { if (stream == null) { yield break; } XDocument document = XmlUtility.LoadSafe(stream, ignoreWhiteSpace: true); var entries = document.Root.ElementsNoNamespace("entry").ToList(); if (entries.Count < bufferSize) { request.Debug(Messages.PackagesReceived, entries.Count); lock (stopLock) { stopSending = true; } } foreach (XElement entry in entries) { var package = new PackageBase(); PackageUtility.ReadEntryElement(ref package, entry); if (firstPackage != null) { // check whether first package in the first request exists anywhere in the second request if (string.Equals(firstPackage.GetFullName(), package.GetFullName(), StringComparison.OrdinalIgnoreCase) && string.Equals(firstPackage.Version, package.Version, StringComparison.OrdinalIgnoreCase)) { lock (stopLock) { stopSending = true; } break; } } yield return package; } // we only needs to check for the existence of the first package in the second request. don't need to do for subsequent request if (firstPackage != null) { firstPackage = null; } } // checks whether we should stop sending requests if (!stopSending && !request.IsCanceled) { // Make sure nobody else is updating the startPoint lock (stopLock) { // update the startPoint startPoint += bufferSize; } // Make a new request with the new startPoint var newQuery = string.Format(query, startPoint, bufferSize); //Keep sending a request tasks[index] = (Task.Factory.StartNew(searchQuery => { var items = NuGetClient.DownloadDataToStream(searchQuery.ToStringSafe(), request); return items; }, newQuery)); } else { if (request.IsCanceled) { request.Warning(Messages.RequestCanceled, "HttpClientPackageRepository", "SendRequest"); //stop sending request to the remote server stopSending = true; } tasks.RemoveAt(index); } } } #region SkipTokenCode // This code is commented out because powershellgallery has a bug with skip token. We can uncomment it once that is fixed. ///// ///// Create a new task that will automatically create a task and add it to taskCollection ///// if there are more links to be downloaded. Otherwise, it will signal to the taskCollection that ///// we are not expecting any more results. ///// The task will also add any packages that it produced to packageCollection ///// ///// ///// ///// ///// //private static Task CreateDownloadTask(string query, BlockingCollection> taskCollection, BlockingCollection packageCollection,Request request) //{ // Task taskStream = Task.Factory.StartNew(searchQuery => // { // var items = NuGetClient.DownloadDataToStream(searchQuery.ToStringSafe(), request); // return items; // }, query); // // After the task is done, we check whether we should create a new task // taskStream.ContinueWith(streamTask => // { // using (Stream stream = streamTask.Result) // { // XDocument document = XmlUtility.LoadSafe(stream, ignoreWhiteSpace: true); // // find the xelement of the form // XElement next = document.Root.Elements().FirstOrDefault(e => String.Equals(e.Name.LocalName, "link", StringComparison.OrdinalIgnoreCase) // && e.Attribute("rel") != null // && String.Equals(e.Attribute("rel").Value, "next", StringComparison.OrdinalIgnoreCase) // && e.Attribute("href") != null); // // If there is no next link or the request is cancelled, stop sending the request // if (next == null || request.IsCanceled) // { // // call completeadding to signal that there won't be anymore request // taskCollection.CompleteAdding(); // } // else // { // var newQuery = next.Attribute("href").Value; // taskCollection.Add(CreateDownloadTask(newQuery, taskCollection, packageCollection, request)); // } // foreach (XElement entry in document.Root.ElementsNoNamespace("entry")) // { // var package = new PackageBase(); // PackageUtility.ReadEntryElement(ref package, entry); // packageCollection.Add(package); // } // if (next == null || request.IsCanceled) // { // packageCollection.CompleteAdding(); // } // } // }); // return taskStream; //} ///// ///// Send the request to the server. We check whether the response has a next link ///// to account for the case where there are more data that we need to fetch ///// ///// ///// ///// //public static IEnumerable SendRequest(string query, Request request) //{ // var tasks = new List>(); // // A blocking collection of task stream. // BlockingCollection> taskCollection = new BlockingCollection>(); // // A blocking collection of packages. // BlockingCollection packageCollection = new BlockingCollection(); // // Populate the first task // Task firstTask = CreateDownloadTask(query, taskCollection, packageCollection, request); // while (!taskCollection.IsCompleted) // { // Task streamTask = null; // try // { // streamTask = taskCollection.Take(); // } // catch (InvalidOperationException) { } // // Try to yield package from packageCollection // while (!packageCollection.IsCompleted) // { // PackageBase package = null; // try // { // package = packageCollection.Take(); // } // catch (InvalidOperationException) { } // if (package != null) // { // yield return package; // } // } // } // // There may be packages in packageCollection // // Try to yield package from packageCollection // while (!packageCollection.IsCompleted) // { // PackageBase package = null; // try // { // package = packageCollection.Take(); // } // catch (InvalidOperationException) { } // if (package != null) // { // yield return package; // } // } //} #endregion } }