// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using OpenTap.Authentication; namespace OpenTap.Package { /// /// A client interface for a package repository. Implementations include and . /// public interface IPackageRepository { /// /// The url of the repository. /// string Url { get; } /// /// Downloads a package from this repository to a file. /// /// The package to download. /// The destination path where the package should be stored. /// A cancellation token that can be used to cancel the download. void DownloadPackage(IPackageIdentifier package, string destination, CancellationToken cancellationToken); /// /// Get all names of packages. /// /// A cancellation token that can be used to cancel the download. /// Any packages that the package to download must be compatible with. /// An array of package names. string[] GetPackageNames(CancellationToken cancellationToken, params IPackageIdentifier[] compatibleWith); /// /// Returns all package version information about a package. /// /// The name package to retrieve version info about. /// A cancellation token that can be used to cancel the download. /// Any packages that the package to download must be compatible with. /// An array of package versions. PackageVersion[] GetPackageVersions(string packageName, CancellationToken cancellationToken, params IPackageIdentifier[] compatibleWith); /// /// This returns the latest version of a package that matches a number of specified parameters. /// If multiple packages have that same version number they all will be returned. /// /// A package identifier. If not specified, packages with any name will be returned. /// A cancellation token that can be used to cancel the download. /// Any packages that the package to download must be compatible with. /// An array of package definitions . PackageDef[] GetPackages(PackageSpecifier package, CancellationToken cancellationToken, params IPackageIdentifier[] compatibleWith); /// /// Returns a list of all packages that have an updated version. /// /// /// A cancellation token that can be used to cancel the download. /// An array of package definitions . PackageDef[] CheckForUpdates(IPackageIdentifier[] packages, CancellationToken cancellationToken); } /// /// Represents a version of a package. Objects of this type is returned by. /// [DebuggerDisplay("{Name} : {Version.ToString()}")] public class PackageVersion : PackageIdentifier, IEquatable { internal bool IsUnlisted { get; private set; } internal static PackageVersion FromDictionary(Dictionary dict) { var name = dict["Name"] as string; Enum.TryParse(dict["Architecture"] as string, out CpuArchitecture arch); var version = SemanticVersion.Parse(dict["Version"] as string); var os = dict["OS"] as string; var date = (DateTime)dict["Date"]; var licenses = new List(); if (dict.TryGetValue("LicenseRequired", out var licenseRequired)) { if (licenseRequired is string license) licenses.Add(license); else if (licenseRequired is string[] licenseArray) licenses.AddRange(licenseArray); } return new PackageVersion(name, version, os, arch, date, licenses) { IsUnlisted = dict.TryGetValue("IsUnlisted", out var unlisted) && unlisted is bool b && b }; } /// /// Initializes a new instance of a PackageVersion. /// public PackageVersion(string name, SemanticVersion version, string os, CpuArchitecture architechture, DateTime date, List licenses) : base(name, version, architechture, os) { this.Date = date; this.Licenses = licenses; } internal PackageVersion() { } /// /// The date that the package was build. /// public DateTime Date { get; set; } /// /// License(s) required to use this package. /// public List Licenses { get; set; } /// /// Compares this PackageVersion with another. /// public bool Equals(PackageVersion other) { return (this as PackageIdentifier).Equals(other); } } internal static class PackageRepositoryExtension { public static void DownloadPackage(this IPackageRepository repository, IPackageIdentifier package, string destination) { repository.DownloadPackage(package, destination, TapThread.Current.AbortToken); } public static string[] GetPackageNames(this IPackageRepository repository, params IPackageIdentifier[] compatibleWith) { return repository.GetPackageNames(TapThread.Current.AbortToken, compatibleWith); } public static PackageVersion[] GetPackageVersions(this IPackageRepository repository, string packageName, params IPackageIdentifier[] compatibleWith) { return repository.GetPackageVersions(packageName, TapThread.Current.AbortToken, compatibleWith); } public static PackageDef[] GetPackages(this IPackageRepository repository, PackageSpecifier package, params IPackageIdentifier[] compatibleWith) { return repository.GetPackages(package, TapThread.Current.AbortToken, compatibleWith); } public static PackageDef[] CheckForUpdates(this IPackageRepository repository, IPackageIdentifier[] packages) { return repository.CheckForUpdates(packages, TapThread.Current.AbortToken); } } internal class PackageRepositoryHelpers { private static TraceSource log = Log.CreateSource("PackageRepository"); static void ParallelTryForEach(IEnumerable source, Action body) { try { Parallel.ForEach(source, body); } catch (AggregateException ex) { foreach (var inner in ex.InnerExceptions) { log.Info(inner.Message); log.Debug(inner); } } } internal static List GetPackageNameAndVersionFromAllRepos(List repositories, PackageSpecifier id, params IPackageIdentifier[] compatibleWith) { var list = new List(); ParallelTryForEach(repositories, repo => { if (repo is HttpPackageRepository httprepo) { var parameters = HttpPackageRepository.GetQueryParameters(version: id.Version, os: id.OS, architecture: id.Architecture, distinctName: true); var repoClient = HttpPackageRepository.GetAuthenticatedClient(new Uri(httprepo.Url, UriKind.Absolute)); var result = repoClient.Query(parameters, CancellationToken.None, "name", "version"); var packages = result.Select(p => new PackageDef() { Name = p["name"] as string, Version = SemanticVersion.Parse(p["version"] as string), PackageSource = new HttpRepositoryPackageDefSource() { RepositoryUrl = httprepo.Url } }); lock (list) { list.AddRange(packages); } } else { var packages = repo.GetPackages(id, compatibleWith); lock (list) { list.AddRange(packages); } } }); return list; } internal static List GetPackagesFromAllRepos(List repositories, PackageSpecifier id, params IPackageIdentifier[] compatibleWith) { var list = new List(); ParallelTryForEach(repositories, repo => { var packages = repo.GetPackages(id, compatibleWith); lock (list) { list.AddRange(packages); } }); return list; } internal static List GetAllVersionsFromAllRepos(List repositories, string packageName, params IPackageIdentifier[] compatibleWith) { var list = new List(); ParallelTryForEach(repositories, repo => { var packages = repo.GetPackageVersions(packageName, compatibleWith); lock (list) { list.AddRange(packages); } }); return list; } /// /// Returns FilePackageRepository if either of the following is true: /// - Url is explicitly defined with file:/// /// - The url is relative and directory exists /// - Starts with classic windows absolute path like 'C:/' /// - Starts with '\\' /// Otherwise returns HttpPackageRepository /// /// url to be determined to be file path or http path /// Determined repository type internal static IPackageRepository DetermineRepositoryType(string url) { if (registeredRepositories.TryGetValue(url, out var repo)) return repo; if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri uri)) { if(uri.IsAbsoluteUri) { switch(uri.Scheme) { case "http": case "https": return new HttpPackageRepository(url); case "file": return new FilePackageRepository(url); default: throw new NotSupportedException($"Scheme {uri.Scheme} is not supported as a package repository ({url})."); } } if (!Directory.Exists(url)) { try { var repo2 = new HttpPackageRepository(url); if (repo2.Version != null) return repo2; } catch { // probably not an http repo } } // If the url starts with a slash, and we are not on windows, it could be an absolute path if (OperatingSystem.Current != OperatingSystem.Windows && url.StartsWith("/") && Directory.Exists(url)) { return new FilePackageRepository(url); } if (!string.IsNullOrWhiteSpace(AuthenticationSettings.Current.BaseAddress)) return DetermineRepositoryType(new Uri(new Uri(AuthenticationSettings.Current.BaseAddress), url).AbsoluteUri); // This is a relative URI, and it's scheme cannot be determined. The best we can do is guess. // If the path contains any invalid path chars, it cannot be a file repository // Trailing slashes don't matter. Simplify the logic by stripping them url = url.TrimEnd('/'); var canBeFile = Path.GetInvalidPathChars().Any(url.Contains) == false; if (canBeFile) { try { // GetFullPath detects issues not detected by GetInvalidPathChars _ = Path.GetFullPath(url); } catch { canBeFile = false; } } var canBeUrl = Uri.IsWellFormedUriString("https://" + url, UriKind.Absolute); if (canBeFile && canBeUrl) { // The URI is ambiguous. Assume it is a http repository if it ends with something that looks like a top-level domain var segments = url.Split('.'); var topLevel = segments.LastOrDefault(); if (segments.Count() > 1 && !string.IsNullOrWhiteSpace(topLevel)) { canBeFile = false; } } if (canBeFile) return new FilePackageRepository(Path.GetFullPath(url)); if (canBeUrl) return new HttpPackageRepository("http://" + url); } throw new NotSupportedException($"Unable to determine repository type of '{url}'. Try specifying a scheme using 'http://' or 'file:///'."); } static Dictionary registeredRepositories = new Dictionary(); internal static void RegisterRepository(IPackageRepository repo) { registeredRepositories[repo.Url] = repo; } } }