Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement NuGet's legacy V2 APIs #694

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/BaGet.Core/Extensions/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ private static void AddBaGetServices(this IServiceCollection services)
services.TryAddSingleton<IPackageDownloadsSource, PackageDownloadsJsonSource>();

services.TryAddSingleton<ISearchResponseBuilder, SearchResponseBuilder>();
services.TryAddSingleton<IV2Builder, V2Builder>();
services.TryAddSingleton<NuGetClient>();
services.TryAddSingleton<NullSearchIndexer>();
services.TryAddSingleton<NullSearchService>();
Expand Down
18 changes: 18 additions & 0 deletions src/BaGet.Core/IUrlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public interface IUrlGenerator
/// </summary>
string GetServiceIndexUrl();

/// <summary>
/// Get the URL for the package source that implements the legacy NuGet V2 API.
/// </summary>
string GetServiceIndexV2Url();

/// <summary>
/// Get the URL for the root of the package content resource.
/// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
Expand Down Expand Up @@ -80,6 +85,13 @@ public interface IUrlGenerator
/// <param name="id">The package's ID</param>
string GetPackageVersionsUrl(string id);

/// <summary>
/// Get the URL to download a package (.nupkg).
/// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
/// </summary>
/// <param name="package">The package to download</param>
string GetPackageDownloadUrl(Package package);

/// <summary>
/// Get the URL to download a package (.nupkg).
/// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
Expand All @@ -101,5 +113,11 @@ public interface IUrlGenerator
/// <param name="id">The package's ID</param>
/// <param name="version">The package's version</param>
string GetPackageIconDownloadUrl(string id, NuGetVersion version);

/// <summary>
/// Get the URL for the metadata of a single package version.
/// </summary>
/// <param name="package">The package to lookup</param>
string GetPackageMetadataV2Url(Package package);
}
}
6 changes: 3 additions & 3 deletions src/BaGet.Core/Metadata/RegistrationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public virtual RegistrationLeafResponse BuildLeaf(Package package)
Listed = package.Listed,
Published = package.Published,
RegistrationLeafUrl = _url.GetRegistrationLeafUrl(id, version),
PackageContentUrl = _url.GetPackageDownloadUrl(id, version),
PackageContentUrl = _url.GetPackageDownloadUrl(package),
RegistrationIndexUrl = _url.GetRegistrationIndexUrl(id)
};
}
Expand All @@ -61,7 +61,7 @@ private BaGetRegistrationIndexPageItem ToRegistrationIndexPageItem(Package packa
new BaGetRegistrationIndexPageItem
{
RegistrationLeafUrl = _url.GetRegistrationLeafUrl(package.Id, package.Version),
PackageContentUrl = _url.GetPackageDownloadUrl(package.Id, package.Version),
PackageContentUrl = _url.GetPackageDownloadUrl(package),
PackageMetadata = new BaGetPackageMetadata
{
PackageId = package.Id,
Expand All @@ -78,7 +78,7 @@ private BaGetRegistrationIndexPageItem ToRegistrationIndexPageItem(Package packa
Listed = package.Listed,
MinClientVersion = package.MinClientVersion,
ReleaseNotes = package.ReleaseNotes,
PackageContentUrl = _url.GetPackageDownloadUrl(package.Id, package.Version),
PackageContentUrl = _url.GetPackageDownloadUrl(package),
PackageTypes = package.PackageTypes.Select(t => t.Name).ToList(),
ProjectUrl = package.ProjectUrlString,
RepositoryUrl = package.RepositoryUrlString,
Expand Down
28 changes: 12 additions & 16 deletions src/BaGet.Core/Upstream/Clients/V2UpstreamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,26 @@ namespace BaGet.Core
/// </summary>
public class V2UpstreamClient : IUpstreamClient, IDisposable
{
private readonly SourceCacheContext _cache;
private readonly SourceRepository _repository;
private readonly INuGetLogger _ngLogger;
private readonly ILogger _logger;

private readonly SourceCacheContext _cache = new SourceCacheContext();
private readonly INuGetLogger _ngLogger = NullLogger.Instance;

public V2UpstreamClient(
IOptionsSnapshot<MirrorOptions> options,
ILogger logger)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}

if (options.Value?.PackageSource?.AbsolutePath == null)
{
throw new ArgumentException("No mirror package source has been set.");
}
var source = new PackageSource(options.Value.PackageSource.AbsoluteUri);

_repository = Repository.Factory.GetCoreV2(source);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

_ngLogger = NullLogger.Instance;
_cache = new SourceCacheContext();
_repository = Repository.Factory.GetCoreV2(new PackageSource(options.Value.PackageSource.AbsoluteUri));
public V2UpstreamClient(SourceRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<IReadOnlyList<NuGetVersion>> ListPackageVersionsAsync(string id, CancellationToken cancellationToken)
Expand Down Expand Up @@ -138,9 +134,9 @@ private Package ToPackage(IPackageSearchMetadata package)
Description = package.Description,
Downloads = 0,
HasReadme = false,
Language = null,
Language = string.Empty,
Listed = package.IsListed,
MinClientVersion = null,
MinClientVersion = string.Empty,
Published = package.Published?.UtcDateTime ?? DateTime.MinValue,
RequireLicenseAcceptance = package.RequireLicenseAcceptance,
Summary = package.Summary,
Expand Down
13 changes: 13 additions & 0 deletions src/BaGet.Core/V2/IV2Builder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Xml.Linq;

namespace BaGet.Core
{
// TODO: comments
public interface IV2Builder
{
XElement BuildIndex();
XElement BuildPackages(IReadOnlyList<Package> packages);
XElement BuildPackage(Package package);
}
}
231 changes: 231 additions & 0 deletions src/BaGet.Core/V2/V2Builder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace BaGet.Core
{
public class V2Builder : IV2Builder
{
private readonly IUrlGenerator _url;

public V2Builder(IUrlGenerator url)
{
_url = url;
}

public XElement BuildIndex()
{
var serviceIndex = _url.GetServiceIndexV2Url();

return XElement.Parse($@"
<service xmlns=""http://www.w3.org/2007/app"" xmlns:atom=""http://www.w3.org/2005/Atom"" xml:base=""{serviceIndex}"">
<workspace>
<atom:title type=""text"">Default</atom:title>
<collection href=""Packages"">
<atom:title type=""text"">Packages</atom:title>
</collection>
</workspace>
</service>");
}

public XElement BuildPackages(IReadOnlyList<Package> packages)
{
// See: https://joelverhagen.github.io/NuGetUndocs/#endpoint-find-packages-by-id
var serviceIndex = _url.GetServiceIndexV2Url();

// TODO: Add <?xml version="1.0" encoding="utf-8"?> to top
// TODO: nuget.org adds `m:null="true"` attribute to elements with no value. Is that necessary?
return new XElement(
N.feed,
new XAttribute(N.baze, XNamespace.Get(serviceIndex)),
new XAttribute(N.m, NS.m),
new XAttribute(N.d, NS.d),
new XAttribute(N.georss, NS.georss),
new XAttribute(N.gml, NS.gml),
new XElement(N.m_count, packages.Count),

packages.Select(package =>
{
var packageV2Url = _url.GetPackageMetadataV2Url(package);
var downloadUrl = _url.GetPackageDownloadUrl(package);

return new XElement(
N.entry,
new XElement(N.id, packageV2Url),
new XElement(N.title, package.Id),
new XElement(
N.content,
new XAttribute("type", "application/zip"),
new XAttribute("src", downloadUrl)
),

BuildAuthor(package),
BuildProperties(package)
);
})
);
}

public XElement BuildPackage(Package package)
{
// See: https://joelverhagen.github.io/NuGetUndocs/#endpoint-get-a-single-package
var serviceIndex = _url.GetServiceIndexV2Url();
var packageV2Url = _url.GetPackageMetadataV2Url(package);
var downloadUrl = _url.GetPackageDownloadUrl(package);

return new XElement(
N.entry,
new XAttribute(N.baze, XNamespace.Get(serviceIndex)),
new XAttribute(N.m, NS.m),
new XAttribute(N.d, NS.d),
new XAttribute(N.georss, NS.georss),
new XAttribute(N.gml, NS.gml),

new XElement(N.id, packageV2Url),
new XElement(
N.content,
new XAttribute("type", "application/zip"),
new XAttribute("src", downloadUrl)
),
new XElement(N.summary, package.Summary),
new XElement(N.title, package.Title),

BuildAuthor(package),
BuildProperties(package)
);
}

private XElement BuildProperties(Package package)
{
// See: https://joelverhagen.github.io/NuGetUndocs/#package-entity
return new XElement(
N.m_properties,
new XElement(N.d_Authors, string.Join(", ", package.Authors)),
new XElement(N.d_Copyright, ""), // TODO
new XElement(N.d_Description, package.Description),
new XElement(
N.d_DownloadCount,
new XAttribute(N.m_type, "Edm.Int32"),
package.Downloads),
new XElement(N.d_IconUrl, package.IconUrl), // TODO, URL logic
new XElement(N.d_Id, package.Id),
new XElement(N.d_IsPrerelease, package.Version.IsPrerelease),
new XElement(N.d_Language, package.Language),
new XElement(N.d_LastEdited, package.Published),
new XElement(N.d_LicenseUrl, package.LicenseUrl), // TODO
new XElement(N.d_MinClientVersion, package.MinClientVersion),
new XElement(N.d_NormalizedVersion, package.NormalizedVersionString),
new XElement(N.d_PackageHash, ""),
new XElement(N.d_PackageHashAlgorithm, ""),
new XElement(N.d_PackageSize, 0),
new XElement(N.d_ProjectUrl, package.ProjectUrl),
new XElement(N.d_Published, package.Published),
new XElement(N.d_ReleaseNotes, package.ReleaseNotes),
new XElement(N.d_RequireLicenseAcceptance, package.RequireLicenseAcceptance),
new XElement(N.d_Summary, package.Summary),
new XElement(N.d_Tags, string.Join(" ", package.Tags)),
new XElement(N.d_Title, package.Title),
new XElement(N.d_Version, package.OriginalVersionString),

BuildDependencies(package)
);
}

private XElement BuildAuthor(Package package)
{
return new XElement(
N.author,
new XElement(N.name, string.Join(", ", package.Authors))
);
}

private XElement BuildDependencies(Package package)
{
var flattenedDependencies = new List<string>();

flattenedDependencies.AddRange(
package
.Dependencies
.Where(IsFrameworkDependency)
.Select(dependency => dependency.TargetFramework)
.Distinct()
.Select(targetFramework => $"::{targetFramework}"));

flattenedDependencies.AddRange(
package
.Dependencies
.Where(dependency => !IsFrameworkDependency(dependency))
.Select(dependency => $"{dependency.Id}:{dependency.VersionRange}:{dependency.TargetFramework}"));

var result = string.Join("|", flattenedDependencies);

return new XElement(N.d_Dependencies, result);
}

private bool IsFrameworkDependency(PackageDependency dependency)
{
return dependency.Id == null && dependency.VersionRange == null;
}

private static class NS
{
public static readonly XNamespace xmlns = "http://www.w3.org/2005/Atom";
// TODO: Remove?
//public static readonly XNamespace baze = "https://www.nuget.org/api/v2/curated-feeds/microsoftdotnet";
public static readonly XNamespace m = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";
public static readonly XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices";
public static readonly XNamespace georss = "http://www.georss.org/georss";
public static readonly XNamespace gml = "http://www.opengis.net/gml";
}

private static class N
{
public static readonly XName feed = NS.xmlns + "feed";
public static readonly XName entry = NS.xmlns + "entry";
public static readonly XName title = NS.xmlns + "title";
public static readonly XName author = NS.xmlns + "author";
public static readonly XName name = NS.xmlns + "name";
public static readonly XName link = NS.xmlns + "link";
public static readonly XName id = NS.xmlns + "id";
public static readonly XName content = NS.xmlns + "content";
public static readonly XName summary = NS.xmlns + "summary";

public static readonly XName m_count = NS.m + "count";
public static readonly XName m_properties = NS.m + "properties";
public static readonly XName m_type = NS.m + "type";

public static readonly XName d_Authors = NS.d + "Authors";
public static readonly XName d_Copyright = NS.d + "Copyright";
public static readonly XName d_Created = NS.d + "Created";
public static readonly XName d_Dependencies = NS.d + "Dependencies";
public static readonly XName d_Description = NS.d + "Description";
public static readonly XName d_DownloadCount = NS.d + "DownloadCount";
public static readonly XName d_IconUrl = NS.d + "IconUrl";
public static readonly XName d_Id = NS.d + "Id";
public static readonly XName d_IsPrerelease = NS.d + "IsPrerelease";
public static readonly XName d_Language = NS.d + "Language";
public static readonly XName d_LastEdited = NS.d + "LastEdited";
public static readonly XName d_LicenseUrl = NS.d + "LicenseUrl";
public static readonly XName d_MinClientVersion = NS.d + "MinClientVersion";
public static readonly XName d_NormalizedVersion = NS.d + "NormalizedVersion";
public static readonly XName d_PackageHash = NS.d + "PackageHash";
public static readonly XName d_PackageHashAlgorithm = NS.d + "PackageHashAlgorithm";
public static readonly XName d_PackageSize = NS.d + "PackageSize";
public static readonly XName d_ProjectUrl = NS.d + "ProjectUrl";
public static readonly XName d_Published = NS.d + "Published";
public static readonly XName d_ReleaseNotes = NS.d + "ReleaseNotes";
public static readonly XName d_ReportAbuseUrl = NS.d + "ReportAbuseUrl";
public static readonly XName d_RequireLicenseAcceptance = NS.d + "RequireLicenseAcceptance";
public static readonly XName d_Summary = NS.d + "Summary";
public static readonly XName d_Tags = NS.d + "Tags";
public static readonly XName d_Title = NS.d + "Title";
public static readonly XName d_Version = NS.d + "Version";

public static readonly XName baze = XNamespace.Xmlns + "base";
public static readonly XName m = XNamespace.Xmlns + "m";
public static readonly XName d = XNamespace.Xmlns + "d";
public static readonly XName georss = XNamespace.Xmlns + "georss";
public static readonly XName gml = XNamespace.Xmlns + "gml";
}
}
}
Loading