Skip to content

Commit 8500015

Browse files
authored
License storage related methods extracted into their own class (#6674)
* Extracted license-related storage code into its own class with its own interface, update the DI configuration. * One test fixed * Updated storage tests * Moved the RevalidationStateService into StorageDependent list, added its own connection string to the configuration. Need to test that it still works. * Renamed the revalidation connection string key. * Updated the StorageDependentsHaveExpectedTypes test. * Moved license file extraction code to CoreLicenseFileService. Moved some test utilities to Core test project to enable testing moved code. * Test fix, organized usings. * Tests for the ExtractAndSaveLicenseFileAsync method. * Extracted content-related file settings into their own interface/implementation and added it to DI. * Added flatcontainer to the list of known folders. * Allowing license files. * Put back warnings about using the license URL and about not specifying any license at all. * Enabled previously disabled tests and did some fixes to accomodate for wording and logic changes.
1 parent 12b0850 commit 8500015

37 files changed

Lines changed: 981 additions & 689 deletions

src/NuGetGallery.Core/CoreConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static class Folders
4040
public const string StatusFolderName = "status";
4141
public const string SymbolPackagesFolderName = "symbol-packages";
4242
public const string SymbolPackageBackupsFolderName = "symbol-package-backups";
43+
public const string FlatContainerFolderName = "v3-flatcontainer";
4344
}
4445

4546
public const string NuGetSymbolPackageFileExtension = ".snupkg";

src/NuGetGallery.Core/NuGetGallery.Core.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,14 @@
166166
<Compile Include="Services\CloudBlobCoreFileStorageService.cs" />
167167
<Compile Include="Services\CloudBlobWrapper.cs" />
168168
<Compile Include="Services\CloudFileReference.cs" />
169+
<Compile Include="Services\CoreLicenseFileService.cs" />
169170
<Compile Include="Services\CorePackageFileService.cs" />
170171
<Compile Include="Services\CoreSymbolPackageService.cs" />
171172
<Compile Include="Services\CorePackageService.cs" />
172173
<Compile Include="Services\CryptographyService.cs" />
174+
<Compile Include="Services\FileNameHelper.cs" />
175+
<Compile Include="Services\IContentFileMetadataService.cs" />
176+
<Compile Include="Services\ICoreLicenseFileService.cs" />
173177
<Compile Include="Services\PackageAlreadyExistsException.cs" />
174178
<Compile Include="Services\FileAlreadyExistsException.cs" />
175179
<Compile Include="Services\FileUriPermissions.cs" />

src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public class CloudBlobCoreFileStorageService : ICoreFileStorageService
3232
CoreConstants.Folders.PackageBackupsFolderName,
3333
CoreConstants.Folders.DownloadsFolderName,
3434
CoreConstants.Folders.SymbolPackagesFolderName,
35-
CoreConstants.Folders.SymbolPackageBackupsFolderName
35+
CoreConstants.Folders.SymbolPackageBackupsFolderName,
36+
CoreConstants.Folders.FlatContainerFolderName,
3637
};
3738

3839
private static readonly HashSet<string> KnownPrivateFolders = new HashSet<string> {
@@ -620,6 +621,7 @@ private static string GetContentType(string folderName)
620621
case CoreConstants.Folders.ValidationFolderName:
621622
case CoreConstants.Folders.SymbolPackagesFolderName:
622623
case CoreConstants.Folders.SymbolPackageBackupsFolderName:
624+
case CoreConstants.Folders.FlatContainerFolderName:
623625
return CoreConstants.PackageContentType;
624626

625627
case CoreConstants.Folders.DownloadsFolderName:
@@ -664,6 +666,7 @@ private static string GetCacheControl(string folderName)
664666
case CoreConstants.Folders.StatusFolderName:
665667
case CoreConstants.Folders.UserCertificatesFolderName:
666668
case CoreConstants.Folders.PackagesContentFolderName:
669+
case CoreConstants.Folders.FlatContainerFolderName:
667670
return null;
668671

669672
default:
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
using System;
5+
using System.IO;
6+
using System.Threading.Tasks;
7+
using NuGet.Packaging;
8+
using NuGet.Services.Entities;
9+
using NuGetGallery.Packaging;
10+
11+
namespace NuGetGallery
12+
{
13+
public class CoreLicenseFileService : ICoreLicenseFileService
14+
{
15+
private const string LicenseFileName = "license";
16+
17+
private readonly ICoreFileStorageService _fileStorageService;
18+
private readonly IContentFileMetadataService _metadata;
19+
20+
public CoreLicenseFileService(ICoreFileStorageService fileStorageService, IContentFileMetadataService metadata)
21+
{
22+
_fileStorageService = fileStorageService ?? throw new ArgumentNullException(nameof(fileStorageService));
23+
_metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
24+
}
25+
26+
public Task SaveLicenseFileAsync(Package package, Stream licenseFile)
27+
{
28+
if (package == null)
29+
{
30+
throw new ArgumentNullException(nameof(package));
31+
}
32+
33+
if (licenseFile == null)
34+
{
35+
throw new ArgumentNullException(nameof(licenseFile));
36+
}
37+
38+
if (package.EmbeddedLicenseType == EmbeddedLicenseFileType.Absent)
39+
{
40+
throw new ArgumentException("Package must have an embedded license", nameof(package));
41+
}
42+
43+
var fileName = BuildLicenseFileName(package);
44+
45+
// Gallery will generally ignore the content type on license files and will use the value from the DB,
46+
// but we'll be nice and try to specify correct content type for them.
47+
var contentType = package.EmbeddedLicenseType == EmbeddedLicenseFileType.Markdown
48+
? CoreConstants.MarkdownContentType
49+
: CoreConstants.TextContentType;
50+
51+
return _fileStorageService.SaveFileAsync(_metadata.PackageContentFolderName, fileName, contentType, licenseFile, overwrite: true);
52+
}
53+
54+
public async Task ExtractAndSaveLicenseFileAsync(Package package, Stream packageStream)
55+
{
56+
if (package == null)
57+
{
58+
throw new ArgumentNullException(nameof(package));
59+
}
60+
61+
if (packageStream == null)
62+
{
63+
throw new ArgumentNullException(nameof(packageStream));
64+
}
65+
66+
packageStream.Seek(0, SeekOrigin.Begin);
67+
using (var packageArchiveReader = new PackageArchiveReader(packageStream, leaveStreamOpen: true))
68+
{
69+
var packageMetadata = PackageMetadata.FromNuspecReader(packageArchiveReader.GetNuspecReader(), strict: true);
70+
if (packageMetadata.LicenseMetadata == null || packageMetadata.LicenseMetadata.Type != LicenseType.File || string.IsNullOrWhiteSpace(packageMetadata.LicenseMetadata.License))
71+
{
72+
throw new InvalidOperationException("No license file specified in the nuspec");
73+
}
74+
75+
var filename = packageMetadata.LicenseMetadata.License;
76+
var licenseFileEntry = packageArchiveReader.GetEntry(filename); // throws on non-existent file
77+
using (var licenseFileStream = licenseFileEntry.Open())
78+
{
79+
await SaveLicenseFileAsync(package, licenseFileStream);
80+
}
81+
}
82+
}
83+
84+
public Task<Stream> DownloadLicenseFileAsync(Package package)
85+
{
86+
var fileName = BuildLicenseFileName(package);
87+
return _fileStorageService.GetFileAsync(_metadata.PackageContentFolderName, fileName);
88+
}
89+
90+
public Task DeleteLicenseFileAsync(string id, string version)
91+
{
92+
if (id == null)
93+
{
94+
throw new ArgumentNullException(nameof(id));
95+
}
96+
97+
if (string.IsNullOrWhiteSpace(id))
98+
{
99+
throw new ArgumentException($"{nameof(id)} cannot be empty", nameof(id));
100+
}
101+
102+
if (version == null)
103+
{
104+
throw new ArgumentNullException(nameof(version));
105+
}
106+
107+
if (string.IsNullOrWhiteSpace(version))
108+
{
109+
throw new ArgumentException($"{nameof(version)} cannot be empty", nameof(version));
110+
}
111+
112+
var normalizedVersion = NuGetVersionFormatter.Normalize(version);
113+
var fileName = BuildLicenseFileName(id, normalizedVersion);
114+
115+
return _fileStorageService.DeleteFileAsync(_metadata.PackageContentFolderName, fileName);
116+
}
117+
118+
private string LicensePathTemplate => $"{_metadata.PackageContentPathTemplate}/{LicenseFileName}";
119+
120+
private string BuildLicenseFileName(Package package)
121+
=> FileNameHelper.BuildFileName(package, LicensePathTemplate, string.Empty);
122+
123+
private string BuildLicenseFileName(string id, string version)
124+
=> FileNameHelper.BuildFileName(id, version, LicensePathTemplate, string.Empty);
125+
126+
}
127+
}

src/NuGetGallery.Core/Services/CorePackageFileService.cs

Lines changed: 10 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,25 @@ public Task SavePackageFileAsync(Package package, Stream packageFile, bool overw
3434
throw new ArgumentNullException(nameof(packageFile));
3535
}
3636

37-
var fileName = BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
37+
var fileName = FileNameHelper.BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
3838
return _fileStorageService.SaveFileAsync(_metadata.FileFolderName, fileName, packageFile, overwrite);
3939
}
4040

4141
public Task<Stream> DownloadPackageFileAsync(Package package)
4242
{
43-
var fileName = BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
43+
var fileName = FileNameHelper.BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
4444
return _fileStorageService.GetFileAsync(_metadata.FileFolderName, fileName);
4545
}
4646

4747
public Task<Uri> GetPackageReadUriAsync(Package package)
4848
{
49-
var fileName = BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
49+
var fileName = FileNameHelper.BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
5050
return _fileStorageService.GetFileReadUriAsync(_metadata.FileFolderName, fileName, endOfAccess: null);
5151
}
5252

5353
public Task<bool> DoesPackageFileExistAsync(Package package)
5454
{
55-
var fileName = BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
55+
var fileName = FileNameHelper.BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
5656
return _fileStorageService.FileExistsAsync(_metadata.FileFolderName, fileName);
5757
}
5858

@@ -63,7 +63,7 @@ public Task SaveValidationPackageFileAsync(Package package, Stream packageFile)
6363
throw new ArgumentNullException(nameof(packageFile));
6464
}
6565

66-
var fileName = BuildFileName(
66+
var fileName = FileNameHelper.BuildFileName(
6767
package,
6868
_metadata.FileSavePathTemplate,
6969
_metadata.FileExtension);
@@ -77,7 +77,7 @@ public Task SaveValidationPackageFileAsync(Package package, Stream packageFile)
7777

7878
public Task<Stream> DownloadValidationPackageFileAsync(Package package)
7979
{
80-
var fileName = BuildFileName(
80+
var fileName = FileNameHelper.BuildFileName(
8181
package,
8282
_metadata.FileSavePathTemplate,
8383
_metadata.FileExtension);
@@ -93,7 +93,7 @@ public Task DeleteValidationPackageFileAsync(string id, string version)
9393
}
9494

9595
var normalizedVersion = NuGetVersionFormatter.Normalize(version);
96-
var fileName = BuildFileName(
96+
var fileName = FileNameHelper.BuildFileName(
9797
id,
9898
normalizedVersion,
9999
_metadata.FileSavePathTemplate,
@@ -116,15 +116,15 @@ public Task DeletePackageFileAsync(string id, string version)
116116

117117
var normalizedVersion = NuGetVersionFormatter.Normalize(version);
118118

119-
var fileName = BuildFileName(id, normalizedVersion, _metadata.FileSavePathTemplate, _metadata.FileExtension);
119+
var fileName = FileNameHelper.BuildFileName(id, normalizedVersion, _metadata.FileSavePathTemplate, _metadata.FileExtension);
120120
return _fileStorageService.DeleteFileAsync(_metadata.FileFolderName, fileName);
121121
}
122122

123123
public Task<Uri> GetValidationPackageReadUriAsync(Package package, DateTimeOffset endOfAccess)
124124
{
125125
package = package ?? throw new ArgumentNullException(nameof(package));
126126

127-
var fileName = BuildFileName(
127+
var fileName = FileNameHelper.BuildFileName(
128128
package,
129129
_metadata.FileSavePathTemplate,
130130
_metadata.FileExtension);
@@ -134,7 +134,7 @@ public Task<Uri> GetValidationPackageReadUriAsync(Package package, DateTimeOffse
134134

135135
public Task<bool> DoesValidationPackageFileExistAsync(Package package)
136136
{
137-
var fileName = BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
137+
var fileName = FileNameHelper.BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension);
138138
return _fileStorageService.FileExistsAsync(_metadata.ValidationFolderName, fileName);
139139
}
140140

@@ -199,76 +199,6 @@ public async Task StorePackageFileInBackupLocationAsync(Package package, Stream
199199
}
200200
}
201201

202-
public Task SaveLicenseFileAsync(Package package, Stream licenseFile)
203-
{
204-
if (package == null)
205-
{
206-
throw new ArgumentNullException(nameof(package));
207-
}
208-
209-
if (licenseFile == null)
210-
{
211-
throw new ArgumentNullException(nameof(licenseFile));
212-
}
213-
214-
if (package.EmbeddedLicenseType == EmbeddedLicenseFileType.Absent)
215-
{
216-
throw new ArgumentException("Package must have an embedded license", nameof(package));
217-
}
218-
219-
var fileName = BuildLicenseFileName(package);
220-
221-
// Gallery will generally ignore the content type on license files and will use the value from the DB,
222-
// but we'll be nice and try to specify correct content type for them.
223-
var contentType = package.EmbeddedLicenseType == EmbeddedLicenseFileType.Markdown
224-
? CoreConstants.MarkdownContentType
225-
: CoreConstants.TextContentType;
226-
227-
return _fileStorageService.SaveFileAsync(_metadata.PackageContentFolderName, fileName, contentType, licenseFile, overwrite: true);
228-
}
229-
230-
public Task<Stream> DownloadLicenseFileAsync(Package package)
231-
{
232-
var fileName = BuildLicenseFileName(package);
233-
return _fileStorageService.GetFileAsync(_metadata.PackageContentFolderName, fileName);
234-
}
235-
236-
public Task DeleteLicenseFileAsync(string id, string version)
237-
{
238-
if (id == null)
239-
{
240-
throw new ArgumentNullException(nameof(id));
241-
}
242-
243-
if (string.IsNullOrWhiteSpace(id))
244-
{
245-
throw new ArgumentException($"{nameof(id)} cannot be empty", nameof(id));
246-
}
247-
248-
if (version == null)
249-
{
250-
throw new ArgumentNullException(nameof(version));
251-
}
252-
253-
if (string.IsNullOrWhiteSpace(version))
254-
{
255-
throw new ArgumentException($"{nameof(version)} cannot be empty", nameof(version));
256-
}
257-
258-
var normalizedVersion = NuGetVersionFormatter.Normalize(version);
259-
var fileName = BuildLicenseFileName(id, normalizedVersion);
260-
261-
return _fileStorageService.DeleteFileAsync(_metadata.PackageContentFolderName, fileName);
262-
}
263-
264-
private string LicensePathTemplate => $"{_metadata.PackageContentPathTemplate}/{CoreConstants.LicenseFileName}";
265-
266-
private string BuildLicenseFileName(Package package)
267-
=> BuildFileName(package, LicensePathTemplate, string.Empty);
268-
269-
private string BuildLicenseFileName(string id, string version)
270-
=> BuildFileName(id, version, LicensePathTemplate, string.Empty);
271-
272202
private static string BuildBackupFileName(string id, string version, string hash, string extension, string fileBackupSavePathTemplate)
273203
{
274204
if (id == null)
@@ -296,51 +226,5 @@ private static string BuildBackupFileName(string id, string version, string hash
296226
HttpServerUtility.UrlTokenEncode(hashBytes),
297227
extension);
298228
}
299-
300-
protected static string BuildFileName(Package package, string format, string extension)
301-
{
302-
if (package == null)
303-
{
304-
throw new ArgumentNullException(nameof(package));
305-
}
306-
307-
if (package.PackageRegistration == null ||
308-
String.IsNullOrWhiteSpace(package.PackageRegistration.Id) ||
309-
(String.IsNullOrWhiteSpace(package.NormalizedVersion) && String.IsNullOrWhiteSpace(package.Version)))
310-
{
311-
throw new ArgumentException(CoreStrings.PackageIsMissingRequiredData, nameof(package));
312-
}
313-
314-
return BuildFileName(
315-
package.PackageRegistration.Id,
316-
string.IsNullOrEmpty(package.NormalizedVersion) ?
317-
NuGetVersionFormatter.Normalize(package.Version) :
318-
package.NormalizedVersion, format, extension);
319-
}
320-
321-
protected static string BuildFileName(string id, string version, string pathTemplate, string extension)
322-
{
323-
if (id == null)
324-
{
325-
throw new ArgumentNullException(nameof(id));
326-
}
327-
328-
if (version == null)
329-
{
330-
throw new ArgumentNullException(nameof(version));
331-
}
332-
333-
// Note: packages should be saved and retrieved in blob storage using the lower case version of their filename because
334-
// a) package IDs can and did change case over time
335-
// b) blob storage is case sensitive
336-
// c) we don't want to hit the database just to look up the right case
337-
// and remember - version can contain letters too.
338-
return String.Format(
339-
CultureInfo.InvariantCulture,
340-
pathTemplate,
341-
id.ToLowerInvariant(),
342-
version.ToLowerInvariant(),
343-
extension);
344-
}
345229
}
346230
}

0 commit comments

Comments
 (0)