-
Notifications
You must be signed in to change notification settings - Fork 58
Expand file tree
/
Copy pathNuGetProjectManager.cs
More file actions
425 lines (369 loc) · 19.2 KB
/
Copy pathNuGetProjectManager.cs
File metadata and controls
425 lines (369 loc) · 19.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// ReSharper disable FieldCanBeMadeReadOnly.Global
using Microsoft.CST.OpenSource.Extensions;
namespace Microsoft.CST.OpenSource.PackageManagers
{
using Contracts;
using Helpers;
using PackageUrl;
using Model;
using Model.Metadata;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Versioning;
using PackageActions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
public class NuGetProjectManager : BaseNuGetProjectManager
{
public override string ManagerType => Type;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")]
public string ENV_NUGET_ENDPOINT_API { get; set; } = "https://api.nuget.org";
// Unused currently.
public string ENV_NUGET_ENDPOINT { get; set; } = "https://www.nuget.org";
// These are named Default, do they need to be overridden as well?
public const string NUGET_DEFAULT_REGISTRATION_ENDPOINT = "https://api.nuget.org/v3/registration5-gz-semver2/";
public const string NUGET_DEFAULT_CONTENT_ENDPOINT = "https://api.nuget.org/v3-flatcontainer/";
public const string NUGET_DEFAULT_INDEX = "https://api.nuget.org/v3/index.json";
private string? RegistrationEndpoint { get; set; } = null;
public NuGetProjectManager(
string directory,
IManagerPackageActions<NuGetPackageVersionMetadata>? actions = null,
IHttpClientFactory? httpClientFactory = null,
TimeSpan? timeout = null)
: base(actions ?? NuGetPackageActions.CreateV3(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout)
{
GetRegistrationEndpointAsync().Wait();
}
/// <inheritdoc />
public override async IAsyncEnumerable<ArtifactUri<NuGetArtifactType>> GetArtifactDownloadUrisAsync(PackageURL purl, bool useCache = true)
{
Check.NotNull(nameof(purl.Version), purl.Version);
if (purl.TryGetRepositoryUrl(out string? repositoryUrlQualifier) && repositoryUrlQualifier != NUGET_DEFAULT_INDEX)
{
// Throw an exception until we implement proper support for service indices other than nuget.org
throw new NotImplementedException(
$"NuGet package URLs having a repository URL other than '{NUGET_DEFAULT_INDEX}' are not currently supported.");
}
yield return new ArtifactUri<NuGetArtifactType>(NuGetArtifactType.Nupkg, GetNupkgUrl(purl.Name, purl.Version));
yield return new ArtifactUri<NuGetArtifactType>(NuGetArtifactType.Nuspec, GetNuspecUrl(purl.Name, purl.Version));
}
/// <summary>
/// Dynamically identifies the registration endpoint.
/// </summary>
/// <returns>NuGet registration endpoint</returns>
private async Task<string> GetRegistrationEndpointAsync()
{
if (RegistrationEndpoint != null)
{
return RegistrationEndpoint;
}
try
{
HttpClient httpClient = CreateHttpClient();
JsonDocument doc = await GetJsonCache(httpClient, $"{ENV_NUGET_ENDPOINT_API}/v3/index.json");
JsonElement.ArrayEnumerator resources = doc.RootElement.GetProperty("resources").EnumerateArray();
foreach (JsonElement resource in resources)
{
try
{
string? _type = resource.GetProperty("@type").GetString();
if (_type != null && _type.Equals("RegistrationsBaseUrl/Versioned", StringComparison.InvariantCultureIgnoreCase))
{
string? _id = resource.GetProperty("@id").GetString();
if (!string.IsNullOrWhiteSpace(_id))
{
RegistrationEndpoint = _id;
return _id;
}
}
}
catch (Exception ex)
{
Logger.Debug(ex, "Error parsing NuGet API endpoint: {0}", ex.Message);
}
}
}
catch (Exception ex)
{
Logger.Debug(ex, "Error parsing NuGet API endpoint: {0}", ex.Message);
}
RegistrationEndpoint = NUGET_DEFAULT_REGISTRATION_ENDPOINT;
return RegistrationEndpoint;
}
/// <summary>
/// Gets the <see cref="DateTime"/> a package version was published at.
/// </summary>
/// <param name="purl">Package URL specifying the package. Version is mandatory.</param>
/// <param name="useCache">If the cache should be used when looking for the published time.</param>
/// <returns>The <see cref="DateTime"/> when this version was published, or null if not found.</returns>
public async Task<DateTime?> GetPublishedAtAsync(PackageURL purl, bool useCache = true)
{
Check.NotNull(nameof(purl.Version), purl.Version);
DateTime? uploadTime = (await this.GetPackageMetadataAsync(purl, useCache, includeRepositoryMetadata: false))?.UploadTime;
return uploadTime;
}
/// <inheritdoc />
public override async Task<string?> GetMetadataAsync(PackageURL purl, bool useCache = true)
{
try
{
string? packageName = purl.Name;
string? packageVersion = purl.Version;
if (packageName == null)
{
return null;
}
// If no package version provided, default to the latest version.
if (string.IsNullOrWhiteSpace(packageVersion))
{
string latestVersion = await Actions.GetLatestVersionAsync(purl) ??
throw new InvalidOperationException($"Can't find the latest version of {purl}");
packageVersion = latestVersion;
}
// Construct a new PackageURL that's guaranteed to have a version.
PackageURL purlWithVersion = new(purl.Type, purl.Namespace, packageName, packageVersion, purl.Qualifiers, purl.Subpath);
NuGetPackageVersionMetadata? packageVersionMetadata =
await Actions.GetMetadataAsync(purlWithVersion, useCache: useCache);
return JsonSerializer.Serialize(packageVersionMetadata);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
Logger.Debug(ex, $"Error fetching NuGet metadata: {ex.Message}");
return null;
}
catch (InvalidOperationException ex) when (purl.Version == null)
{
Logger.Debug(ex, $"Error fetching NuGet metadata: {ex.Message}");
return null;
}
}
public override Uri GetPackageAbsoluteUri(PackageURL purl)
{
return new Uri($"{ENV_NUGET_HOMEPAGE}/{purl?.Name}");
}
/// <inheritdoc />
public override async Task<PackageMetadata?> GetPackageMetadataAsync(PackageURL purl, bool includePrerelease = false, bool useCache = true, bool includeRepositoryMetadata = true)
{
string? latestVersion = await Actions.GetLatestVersionAsync(purl, includePrerelease: includePrerelease, useCache: useCache);
// Construct a new PackageURL that's guaranteed to have a version, the latest version is used if no version was provided.
PackageURL purlWithVersion = !string.IsNullOrWhiteSpace(purl.Version) ?
purl : new PackageURL(purl.Type, purl.Namespace, purl.Name, latestVersion, purl.Qualifiers, purl.Subpath);
NuGetPackageVersionMetadata? packageVersionMetadata =
await Actions.GetMetadataAsync(purlWithVersion, useCache: useCache);
if (packageVersionMetadata is null)
{
return null;
}
PackageMetadata metadata = new()
{
Name = packageVersionMetadata.Name,
Description = packageVersionMetadata.Description,
PackageManagerUri = ENV_NUGET_ENDPOINT_API,
Platform = "NUGET",
Language = "C#",
PackageUri = $"{ENV_NUGET_HOMEPAGE}/{packageVersionMetadata.Name.ToLowerInvariant()}",
ApiPackageUri = $"{RegistrationEndpoint}{packageVersionMetadata.Name.ToLowerInvariant()}/index.json",
PackageVersion = purlWithVersion.Version,
LatestPackageVersion = latestVersion
};
// Get the metadata for either the specified package version, or the latest package version
await UpdateVersionMetadata(metadata, packageVersionMetadata, includeRepositoryMetadata);
return metadata;
}
/// <inheritdoc/>
public override async Task<bool> PackageExistsAsync(PackageURL purl, bool useCache = true)
{
Logger.Trace("PackageExists {0}", purl?.ToString());
if (purl is null)
{
Logger.Trace("Provided PackageURL was null.");
throw new ArgumentNullException(nameof(purl), "Provided PackageURL was null.");
}
return await this.Actions.DoesPackageExistAsync(purl, useCache);
}
/// <inheritdoc />
public override async Task<bool> PackageVersionExistsAsync(PackageURL purl, bool useCache = true)
{
Logger.Trace("PackageVersionExists {0}", purl?.ToString());
if (string.IsNullOrEmpty(purl?.Name))
{
Logger.Trace("Provided PackageURL was null.");
return false;
}
return await this.Actions.DoesPackageExistAsync(purl, useCache);
}
/// <summary>
/// Updates the package version specific values in <see cref="PackageMetadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="PackageMetadata"/> object to update with the values for this version.</param>
/// <param name="packageVersionMetadata">The <see cref="NuGetPackageVersionMetadata"/> representing this version.</param>
private async Task UpdateVersionMetadata(PackageMetadata metadata, NuGetPackageVersionMetadata packageVersionMetadata, bool includeRepositoryMetadata)
{
if (metadata.PackageVersion is null)
{
return;
}
// Set the version specific URI values.
metadata.VersionUri = $"{metadata.PackageManagerUri}/packages/{packageVersionMetadata.Name}/{metadata.PackageVersion}";
metadata.ApiVersionUri = packageVersionMetadata.CatalogUri.ToString();
// Construct the artifact contents url.
metadata.VersionDownloadUri = GetNupkgUrl(packageVersionMetadata.Name, metadata.PackageVersion);
// TODO: size and hash
// Homepage url
metadata.Homepage = packageVersionMetadata.ProjectUrl?.ToString();
// Authors and Maintainers
UpdateMetadataAuthorsAndMaintainers(metadata, packageVersionMetadata);
// Repository
if (includeRepositoryMetadata)
{
await UpdateMetadataRepository(metadata);
}
// Dependencies
IList<PackageDependencyGroup> dependencyGroups = packageVersionMetadata.DependencySets?.ToList() ?? new List<PackageDependencyGroup>();
metadata.Dependencies ??= dependencyGroups.SelectMany(group => group.Packages, (dependencyGroup, package) => new { dependencyGroup, package })
.Select(dependencyGroupAndPackage => new Dependency() { Package = dependencyGroupAndPackage.package.ToString(), Framework = dependencyGroupAndPackage.dependencyGroup.TargetFramework?.ToString() })
.ToList();
// Keywords
metadata.Keywords = new List<string>((IEnumerable<string>?)packageVersionMetadata.Tags?.Split(", ") ?? new List<string>());
// Licenses
if (packageVersionMetadata.LicenseMetadata is not null)
{
metadata.Licenses ??= new List<License>();
metadata.Licenses.Add(new License()
{
Name = packageVersionMetadata.LicenseMetadata.License,
Url = packageVersionMetadata.LicenseMetadata.LicenseUrl.ToString()
});
}
// publishing info
metadata.UploadTime = packageVersionMetadata.Published?.DateTime;
}
/// <summary>
/// Updates the author(s) and maintainer(s) in <see cref="PackageMetadata"/> for this package version.
/// </summary>
/// <param name="metadata">The <see cref="PackageMetadata"/> object to set the author(s) and maintainer(s) for this version.</param>
/// <param name="packageVersionPackageVersionMetadata">The <see cref="NuGetPackageVersionMetadata"/> representing this version.</param>
private static void UpdateMetadataAuthorsAndMaintainers(PackageMetadata metadata, NuGetPackageVersionMetadata packageVersionPackageVersionMetadata)
{
// Author(s)
string? authors = packageVersionPackageVersionMetadata.Authors;
if (authors is not null)
{
metadata.Authors ??= new List<User>();
authors.Split(", ").ToList()
.ForEach(author => metadata.Authors.Add(new User() { Name = author }));
}
// TODO: Collect the data about a package's maintainers as well.
}
/// <summary>
/// Updates the <see cref="Repository"/> for this package version in the <see cref="PackageMetadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="PackageMetadata"/> object to update with the values for this version.</param>
private async Task UpdateMetadataRepository(PackageMetadata metadata)
{
NuspecReader? nuspecReader = GetNuspec(metadata.Name!, metadata.PackageVersion!);
RepositoryMetadata? repositoryMetadata = nuspecReader?.GetRepositoryMetadata();
if (repositoryMetadata != null && GitHubProjectManager.IsGitHubRepoUrl(repositoryMetadata.Url, out PackageURL? githubPurl))
{
Repository ghRepository = new()
{
Type = "github"
};
await ghRepository.ExtractRepositoryMetadata(githubPurl!);
metadata.Repository ??= new List<Repository>();
metadata.Repository.Add(ghRepository);
}
}
/// <summary>
/// Helper method to get the URL to download a NuGet package's .nupkg.
/// </summary>
/// <param name="id">The id/name of the package to get the .nupkg for.</param>
/// <param name="version">The version of the package to get the .nupkg for.</param>
/// <returns>The URL for the nupkg file.</returns>
private static string GetNupkgUrl(string id, string version)
{
string lowerId = id.ToLowerInvariant();
string lowerVersion = NuGetVersion.Parse(version).ToNormalizedString().ToLowerInvariant();
string url = $"{NUGET_DEFAULT_CONTENT_ENDPOINT.TrimEnd('/')}/{lowerId}/{lowerVersion}/{lowerId}.{lowerVersion}.nupkg";
return url;
}
/// <summary>
/// Helper method to get the URL to download a NuGet package's .nuspec.
/// </summary>
/// <param name="id">The id/name of the package to get the .nuspec for.</param>
/// <param name="version">The version of the package to get the .nuspec for.</param>
/// <returns>The URL for the nuspec file.</returns>
private static string GetNuspecUrl(string id, string version)
{
string lowerId = id.ToLowerInvariant();
string lowerVersion = NuGetVersion.Parse(version).ToNormalizedString().ToLowerInvariant();
string url = $"{NUGET_DEFAULT_CONTENT_ENDPOINT.TrimEnd('/')}/{lowerId}/{lowerVersion}/{lowerId}.nuspec";
return url;
}
/// <summary>
/// Searches the package manager metadata to figure out the source code repository.
/// </summary>
/// <param name="purl">The <see cref="PackageURL"/> that we need to find the source code repository.</param>
/// <param name="metadata">The json representation of this package's metadata.</param>
/// <remarks>If no version specified, defaults to latest version.</remarks>
/// <returns>
/// A dictionary, mapping each possible repo source entry to its probability/empty dictionary
/// </returns>
protected override async Task<Dictionary<PackageURL, double>> SearchRepoUrlsInPackageMetadata(PackageURL purl, string metadata)
{
Dictionary<PackageURL, double> mapping = new();
try
{
string? version = purl.Version;
if (string.IsNullOrEmpty(version))
{
version = (await EnumerateVersionsAsync(purl)).First();
}
NuspecReader? nuspecReader = GetNuspec(purl.Name, version);
RepositoryMetadata? repositoryMetadata = nuspecReader?.GetRepositoryMetadata();
if (repositoryMetadata != null && GitHubProjectManager.IsGitHubRepoUrl(repositoryMetadata.Url, out PackageURL? githubPurl))
{
if (githubPurl != null)
{
mapping.Add(githubPurl, 1.0F);
}
}
return mapping;
}
catch (Exception ex)
{
Logger.Debug(ex, $"Error fetching/parsing NuGet repository metadata: {ex.Message}");
}
// If nothing worked, return the default empty dictionary
return mapping;
}
private NuspecReader? GetNuspec(string id, string version)
{
string lowerId = id.ToLowerInvariant();
string lowerVersion = NuGetVersion.Parse(version).ToNormalizedString().ToLowerInvariant();
string uri = GetNuspecUrl(lowerId, lowerVersion);
try
{
HttpClient httpClient = this.CreateHttpClient();
HttpResponseMessage response = httpClient.GetAsync(uri).GetAwaiter().GetResult();
using (Stream stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
{
return new NuspecReader(stream);
}
}
catch
{
return null;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")]
public string ENV_NUGET_HOMEPAGE { get; set; } = "https://www.nuget.org/packages";
}
}