Skip to content

Commit 95cbe09

Browse files
authored
Embedded license file ingestion for Gallery (#6580)
Enabling accepting license metadata. License files are not yet allowed.
1 parent 663cd34 commit 95cbe09

38 files changed

Lines changed: 2440 additions & 76 deletions

src/NuGetGallery.Core/CoreConstants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public static class CoreConstants
99

1010
public const string PackageFileSavePathTemplate = "{0}.{1}{2}";
1111
public const string PackageFileBackupSavePathTemplate = "{0}/{1}/{2}.{3}";
12+
public const string PackageContentFileSavePathTemplate = "{0}/{1}";
1213

1314
public const string NuGetPackageFileExtension = ".nupkg";
1415
public const string CertificateFileExtension = ".cer";
@@ -18,6 +19,7 @@ public static class CoreConstants
1819
public const string PackageContentType = "binary/octet-stream";
1920
public const string OctetStreamContentType = "application/octet-stream";
2021
public const string TextContentType = "text/plain";
22+
public const string MarkdownContentType = "text/markdown"; // rfc7763
2123
public const string CertificateContentType = "application/pkix-cert";
2224
public const string JsonContentType = "application/json";
2325

@@ -29,6 +31,7 @@ public static class CoreConstants
2931
public const string PackageBackupsFolderName = "package-backups";
3032
public const string PackageReadMesFolderName = "readmes";
3133
public const string PackagesFolderName = "packages";
34+
public const string PackagesContentFolderName = "packages-content";
3235
public const string UploadsFolderName = "uploads";
3336
public const string ValidationFolderName = "validation";
3437
public const string RevalidationFolderName = "revalidation";
@@ -39,5 +42,7 @@ public static class CoreConstants
3942
public const string SymbolPackageBackupsFolderName = "symbol-package-backups";
4043

4144
public const string UploadTracingKeyHeaderName = "upload-id";
45+
46+
public const string LicenseFileName = "license";
4247
}
4348
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace NuGetGallery
5+
{
6+
/// <summary>
7+
/// Specifies the type of the license file used in the package
8+
/// </summary>
9+
public enum EmbeddedLicenseFileType
10+
{
11+
/// <summary>
12+
/// Indicates that package has no license file embedded.
13+
/// </summary>
14+
Absent = 0,
15+
16+
/// <summary>
17+
/// Indicates that embedded license file is plain text.
18+
/// </summary>
19+
PlainText = 1,
20+
21+
/// <summary>
22+
/// Indicates that embedded license file is markdown.
23+
/// </summary>
24+
Markdown = 2,
25+
}
26+
}

src/NuGetGallery.Core/NuGetGallery.Core.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@
203203
</ItemGroup>
204204
<ItemGroup>
205205
<PackageReference Include="NuGet.Services.Entities">
206-
<Version>2.31.0</Version>
206+
<Version>2.32.0-agr-license-2170862</Version>
207207
</PackageReference>
208208
<PackageReference Include="NuGet.Services.Messaging">
209209
<Version>2.31.0</Version>
@@ -239,7 +239,7 @@
239239
<PrivateAssets>all</PrivateAssets>
240240
</PackageReference>
241241
<PackageReference Include="NuGet.Packaging">
242-
<Version>4.8.0</Version>
242+
<Version>5.0.0-preview1.5665</Version>
243243
</PackageReference>
244244
<PackageReference Include="NuGet.Services.Validation">
245245
<Version>2.31.0</Version>

src/NuGetGallery.Core/Packaging/PackageMetadata.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ public PackageMetadata(
5151
IEnumerable<FrameworkSpecificGroup> frameworkGroups,
5252
IEnumerable<NuGet.Packaging.Core.PackageType> packageTypes,
5353
NuGetVersion minClientVersion,
54-
RepositoryMetadata repositoryMetadata)
54+
RepositoryMetadata repositoryMetadata,
55+
LicenseMetadata licenseMetadata = null)
5556
{
5657
_metadata = new Dictionary<string, string>(metadata, StringComparer.OrdinalIgnoreCase);
5758
_dependencyGroups = dependencyGroups.ToList().AsReadOnly();
@@ -67,6 +68,8 @@ public PackageMetadata(
6768
RepositoryUrl = repoUrl;
6869
RepositoryType = repositoryMetadata.Type;
6970
}
71+
72+
LicenseMetadata = licenseMetadata;
7073
}
7174

7275
private void SetPropertiesFromMetadata()
@@ -123,6 +126,12 @@ private void SetPropertiesFromMetadata()
123126
public string Language { get; private set; }
124127
public NuGetVersion MinClientVersion { get; set; }
125128

129+
/// <summary>
130+
/// Contains license metadata taken from the 'license' node of the nuspec file.
131+
/// Null if no 'license' node present.
132+
/// </summary>
133+
public LicenseMetadata LicenseMetadata { get; }
134+
126135
public string GetValueFromMetadata(string key)
127136
{
128137
return GetValue(key, (string)null);
@@ -244,7 +253,8 @@ public static PackageMetadata FromNuspecReader(NuspecReader nuspecReader, bool s
244253
nuspecReader.GetFrameworkReferenceGroups(),
245254
nuspecReader.GetPackageTypes(),
246255
nuspecReader.GetMinClientVersion(),
247-
nuspecReader.GetRepositoryMetadata());
256+
nuspecReader.GetRepositoryMetadata(),
257+
nuspecReader.GetLicenseMetadata());
248258
}
249259

250260
private class StrictNuspecReader : NuspecReader

src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,19 @@ private static string Log(AccessCondition accessCondition)
293293
return "(none)";
294294
}
295295

296-
public async Task SaveFileAsync(string folderName, string fileName, Stream file, bool overwrite = true)
296+
public Task SaveFileAsync(string folderName, string fileName, Stream file, bool overwrite = true)
297297
{
298+
var contentType = GetContentType(folderName);
299+
return SaveFileAsync(folderName, fileName, contentType, file, overwrite);
300+
}
301+
302+
public async Task SaveFileAsync(string folderName, string fileName, string contentType, Stream file, bool overwrite = true)
303+
{
304+
if (contentType == null)
305+
{
306+
throw new ArgumentNullException(nameof(contentType));
307+
}
308+
298309
ICloudBlobContainer container = await GetContainerAsync(folderName);
299310
var blob = container.GetBlobReference(fileName);
300311

@@ -313,7 +324,7 @@ public async Task SaveFileAsync(string folderName, string fileName, Stream file,
313324
ex);
314325
}
315326

316-
blob.Properties.ContentType = GetContentType(folderName);
327+
blob.Properties.ContentType = contentType;
317328
blob.Properties.CacheControl = GetCacheControl(folderName);
318329
await blob.SetPropertiesAsync();
319330
}
@@ -575,6 +586,9 @@ private static string GetContentType(string folderName)
575586
case CoreConstants.UserCertificatesFolderName:
576587
return CoreConstants.CertificateContentType;
577588

589+
case CoreConstants.PackagesContentFolderName:
590+
return CoreConstants.OctetStreamContentType;
591+
578592
default:
579593
throw new InvalidOperationException(
580594
String.Format(CultureInfo.CurrentCulture, "The folder name {0} is not supported.", folderName));
@@ -587,6 +601,7 @@ private static string GetCacheControl(string folderName)
587601
{
588602
case CoreConstants.PackagesFolderName:
589603
case CoreConstants.SymbolPackagesFolderName:
604+
case CoreConstants.PackagesContentFolderName:
590605
return CoreConstants.DefaultCacheControl;
591606

592607
case CoreConstants.PackageBackupsFolderName:

src/NuGetGallery.Core/Services/CorePackageFileService.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ namespace NuGetGallery
1313
{
1414
public class CorePackageFileService : ICorePackageFileService
1515
{
16+
private const string LicenseFileName = "license";
17+
1618
private readonly ICoreFileStorageService _fileStorageService;
1719
private readonly IFileMetadataService _metadata;
1820

@@ -199,6 +201,76 @@ public async Task StorePackageFileInBackupLocationAsync(Package package, Stream
199201
}
200202
}
201203

204+
public Task SaveLicenseFileAsync(Package package, Stream licenseFile)
205+
{
206+
if (package == null)
207+
{
208+
throw new ArgumentNullException(nameof(package));
209+
}
210+
211+
if (licenseFile == null)
212+
{
213+
throw new ArgumentNullException(nameof(licenseFile));
214+
}
215+
216+
if (package.EmbeddedLicenseType == EmbeddedLicenseFileType.Absent)
217+
{
218+
throw new ArgumentException("Package must have an embedded license", nameof(package));
219+
}
220+
221+
var fileName = BuildLicenseFileName(package);
222+
223+
// Gallery will generally ignore the content type on license files and will use the value from the DB,
224+
// but we'll be nice and try to specify correct content type for them.
225+
var contentType = package.EmbeddedLicenseType == EmbeddedLicenseFileType.Markdown
226+
? CoreConstants.MarkdownContentType
227+
: CoreConstants.TextContentType;
228+
229+
return _fileStorageService.SaveFileAsync(_metadata.PackageContentFolderName, fileName, contentType, licenseFile, overwrite: true);
230+
}
231+
232+
public Task<Stream> DownloadLicenseFileAsync(Package package)
233+
{
234+
var fileName = BuildLicenseFileName(package);
235+
return _fileStorageService.GetFileAsync(_metadata.PackageContentFolderName, fileName);
236+
}
237+
238+
public Task DeleteLicenseFileAsync(string id, string version)
239+
{
240+
if (id == null)
241+
{
242+
throw new ArgumentNullException(nameof(id));
243+
}
244+
245+
if (string.IsNullOrWhiteSpace(id))
246+
{
247+
throw new ArgumentException($"{nameof(id)} cannot be empty", nameof(id));
248+
}
249+
250+
if (version == null)
251+
{
252+
throw new ArgumentNullException(nameof(version));
253+
}
254+
255+
if (string.IsNullOrWhiteSpace(version))
256+
{
257+
throw new ArgumentException($"{nameof(version)} cannot be empty", nameof(version));
258+
}
259+
260+
var normalizedVersion = NuGetVersionFormatter.Normalize(version);
261+
var fileName = BuildLicenseFileName(id, normalizedVersion);
262+
263+
return _fileStorageService.DeleteFileAsync(_metadata.PackageContentFolderName, fileName);
264+
}
265+
266+
private string LicensePathTemplate => $"{_metadata.PackageContentPathTemplate}/{LicenseFileName}";
267+
268+
private string BuildLicenseFileName(Package package)
269+
=> BuildFileName(package, LicensePathTemplate, string.Empty);
270+
271+
private string BuildLicenseFileName(string id, string version)
272+
=> BuildFileName(id, version, LicensePathTemplate, string.Empty);
273+
202274
private static string BuildBackupFileName(string id, string version, string hash, string extension, string fileBackupSavePathTemplate)
203275
{
204276
if (id == null)

src/NuGetGallery.Core/Services/ICoreFileStorageService.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ Task<Uri> GetPriviledgedFileUriAsync(
5353

5454
Task SaveFileAsync(string folderName, string fileName, Stream file, bool overwrite = true);
5555

56+
/// <summary>
57+
/// Saves the file. If storage supports setting the content type for the file,
58+
/// it will be set to the specified value
59+
/// </summary>
60+
/// <param name="folderName">The folder that will contain the file.</param>
61+
/// <param name="fileName">The name of the file.</param>
62+
/// <param name="contentType">The content type to set for the saved file if storage supports it.</param>
63+
/// <param name="file">The content that should be saved.</param>
64+
/// <param name="overwrite">Indicates whether file should be overwritten if exists.</param>
65+
/// <exception cref="FileAlreadyExistsException">
66+
/// Thrown when <paramref name="overwrite"/> is false and file already exists
67+
/// in destination.
68+
/// </exception>
69+
Task SaveFileAsync(string folderName, string fileName, string contentType, Stream file, bool overwrite = true);
70+
5671
/// <summary>
5772
/// Saves the file. An exception should be thrown if the access condition is not met.
5873
/// </summary>

src/NuGetGallery.Core/Services/ICorePackageFileService.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,21 @@ public interface ICorePackageFileService
2020
/// </summary>
2121
Task SavePackageFileAsync(Package package, Stream packageFile, bool overwrite);
2222

23+
/// <summary>
24+
/// Saves the license file to the public container for package content.
25+
/// </summary>
26+
Task SaveLicenseFileAsync(Package package, Stream licenseFile);
27+
2328
/// <summary>
2429
/// Downloads the package from the file storage and reads it into a stream.
2530
/// </summary>
2631
Task<Stream> DownloadPackageFileAsync(Package package);
2732

33+
/// <summary>
34+
/// Downloads previously saved license file for a specified package.
35+
/// </summary>
36+
Task<Stream> DownloadLicenseFileAsync(Package package);
37+
2838
/// <summary>
2939
/// Generates the URL for the specified package in the public container for available packages.
3040
/// </summary>
@@ -88,6 +98,14 @@ public interface ICorePackageFileService
8898
/// <param name="version">The package version. This value is case-insensitive and need not be normalized.</param>
8999
Task DeletePackageFileAsync(string id, string version);
90100

101+
/// <summary>
102+
/// Deletes the license file for the package from the publicly available storage for the package content.
103+
/// </summary>
104+
/// <param name="id">The package ID. This value is case-insensitive.</param>
105+
/// <param name="version">The package version. This value is case-insensitive and need not be normalized.</param>
106+
/// <returns></returns>
107+
Task DeleteLicenseFileAsync(string id, string version);
108+
91109
/// <summary>
92110
/// Copies the contents of the package represented by the stream into the file storage backup location.
93111
/// </summary>

src/NuGetGallery.Core/Services/IFileMetadataService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ public interface IFileMetadataService
1010
/// </summary>
1111
string FileFolderName { get; }
1212

13+
/// <summary>
14+
/// The name of the public folder where bits of package content can be extracted to.
15+
/// </summary>
16+
string PackageContentFolderName { get; }
17+
18+
/// <summary>
19+
/// The template for the path to save user content. File name will be appended to it.
20+
/// </summary>
21+
string PackageContentPathTemplate { get; }
22+
1323
/// <summary>
1424
/// The save file path template. For example <see cref="CoreConstants.PackageFileSavePathTemplate"/>
1525
/// </summary>

src/NuGetGallery.Core/Services/PackageFileServiceMetadata.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ public class PackageFileMetadataService : IFileMetadataService
77
{
88
public string FileFolderName => CoreConstants.PackagesFolderName;
99

10+
public string PackageContentFolderName => CoreConstants.PackagesContentFolderName;
11+
12+
public string PackageContentPathTemplate => CoreConstants.PackageContentFileSavePathTemplate;
13+
1014
public string FileSavePathTemplate => CoreConstants.PackageFileSavePathTemplate;
1115

1216
public string FileExtension => CoreConstants.NuGetPackageFileExtension;

0 commit comments

Comments
 (0)