// 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;
}
}
}