Skip to content

Commit bc0954b

Browse files
authored
[NuGet Symbol Server] Add download symbols support to Package details page (#6320)
1 parent baa5bc0 commit bc0954b

12 files changed

Lines changed: 311 additions & 71 deletions

File tree

src/NuGetGallery/App_Start/Routes.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,12 @@ public static void RegisterApiV2Routes(RouteCollection routes)
656656
defaults: new { controller = "Api", action = "GetPackageApi", version = UrlParameter.Optional },
657657
constraints: new { httpMethod = new HttpMethodConstraint("GET") });
658658

659+
routes.MapRoute(
660+
"v2" + RouteName.DownloadSymbolsPackage,
661+
"api/v2/symbolpackage/{id}/{version}",
662+
defaults: new { controller = "Api", action = "GetSymbolPackageApi", version = UrlParameter.Optional },
663+
constraints: new { httpMethod = new HttpMethodConstraint("GET") });
664+
659665
routes.MapRoute(
660666
"v2" + RouteName.PushPackageApi,
661667
"api/v2/package",

src/NuGetGallery/Controllers/ApiController.cs

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public partial class ApiController
5555
public IReservedNamespaceService ReservedNamespaceService { get; set; }
5656
public IPackageUploadService PackageUploadService { get; set; }
5757
public IPackageDeleteService PackageDeleteService { get; set; }
58+
public ISymbolPackageFileService SymbolPackageFileService { get; set; }
5859
public ISymbolPackageService SymbolPackageService { get; set; }
5960
public ISymbolPackageUploadService SymbolPackageUploadService { get; set; }
6061
public IContentObjectService ContentObjectService { get; set; }
@@ -85,6 +86,7 @@ public ApiController(
8586
IReservedNamespaceService reservedNamespaceService,
8687
IPackageUploadService packageUploadService,
8788
IPackageDeleteService packageDeleteService,
89+
ISymbolPackageFileService symbolPackageFileService,
8890
ISymbolPackageService symbolPackageService,
8991
ISymbolPackageUploadService symbolPackageUploadService,
9092
IContentObjectService contentObjectService)
@@ -109,6 +111,7 @@ public ApiController(
109111
ReservedNamespaceService = reservedNamespaceService;
110112
PackageUploadService = packageUploadService;
111113
StatisticsService = null;
114+
SymbolPackageFileService = symbolPackageFileService;
112115
SymbolPackageService = symbolPackageService;
113116
SymbolPackageUploadService = symbolPackageUploadService;
114117
ContentObjectService = contentObjectService;
@@ -136,21 +139,35 @@ public ApiController(
136139
IReservedNamespaceService reservedNamespaceService,
137140
IPackageUploadService packageUploadService,
138141
IPackageDeleteService packageDeleteService,
142+
ISymbolPackageFileService symbolPackageFileService,
139143
ISymbolPackageService symbolPackageService,
140144
ISymbolPackageUploadService symbolPackageUploadServivce,
141145
IContentObjectService contentObjectService)
142146
: this(apiScopeEvaluator, entitiesContext, packageService, packageFileService, userService, contentService,
143147
indexingService, searchService, autoCuratePackage, statusService, messageService, auditingService,
144148
configurationService, telemetryService, authenticationService, credentialBuilder, securityPolicies,
145-
reservedNamespaceService, packageUploadService, packageDeleteService, symbolPackageService, symbolPackageUploadServivce,
146-
contentObjectService)
149+
reservedNamespaceService, packageUploadService, packageDeleteService, symbolPackageFileService,
150+
symbolPackageService, symbolPackageUploadServivce, contentObjectService)
147151
{
148152
StatisticsService = statisticsService;
149153
}
150154

155+
156+
[HttpGet]
157+
[ActionName("GetSymbolPackageApi")]
158+
public virtual async Task<ActionResult> GetSymbolPackage(string id, string version)
159+
{
160+
return await GetPackageInternal(id, version, isSymbolPackage: true);
161+
}
162+
151163
[HttpGet]
152164
[ActionName("GetPackageApi")]
153165
public virtual async Task<ActionResult> GetPackage(string id, string version)
166+
{
167+
return await GetPackageInternal(id, version, isSymbolPackage: false);
168+
}
169+
170+
protected internal async Task<ActionResult> GetPackageInternal(string id, string version, bool isSymbolPackage = false)
154171
{
155172
// some security paranoia about URL hacking somehow creating e.g. open redirects
156173
// validate user input: explicit calls to the same validators used during Package Registrations
@@ -160,61 +177,86 @@ public virtual async Task<ActionResult> GetPackage(string id, string version)
160177
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The format of the package id is invalid");
161178
}
162179

163-
// if version is non-null, check if it's semantically correct and normalize it.
164-
if (!String.IsNullOrEmpty(version))
180+
Package package = null;
181+
try
165182
{
166-
NuGetVersion dummy;
167-
if (!NuGetVersion.TryParse(version, out dummy))
183+
if (!string.IsNullOrEmpty(version))
168184
{
169-
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The package version is not a valid semantic version");
170-
}
185+
// if version is non-null, check if it's semantically correct and normalize it.
186+
NuGetVersion dummy;
187+
if (!NuGetVersion.TryParse(version, out dummy))
188+
{
189+
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The package version is not a valid semantic version");
190+
}
171191

172-
// Normalize the version
173-
version = NuGetVersionFormatter.Normalize(version);
174-
}
175-
else
176-
{
177-
// If version is null, get the latest version from the database.
178-
// This ensures that on package restore scenario where version will be non null, we don't hit the database.
179-
try
192+
// Normalize the version
193+
version = NuGetVersionFormatter.Normalize(version);
194+
195+
if (isSymbolPackage)
196+
{
197+
package = PackageService.FindPackageByIdAndVersionStrict(id, version);
198+
}
199+
}
200+
else
180201
{
181-
var package = PackageService.FindPackageByIdAndVersion(
202+
// If version is null, get the latest version from the database.
203+
// This ensures that on package restore scenario where version will be non null, we don't hit the database.
204+
package = PackageService.FindPackageByIdAndVersion(
182205
id,
183206
version,
184207
SemVerLevelKey.SemVer2,
185208
allowPrerelease: false);
186209

187210
if (package == null)
188211
{
189-
return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
212+
return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
190213
}
191-
version = package.NormalizedVersion;
192214

215+
version = package.NormalizedVersion;
193216
}
194-
catch (SqlException e)
195-
{
196-
QuietLog.LogHandledException(e);
217+
}
218+
catch (SqlException e)
219+
{
220+
QuietLog.LogHandledException(e);
197221

198-
// Database was unavailable and we don't have a version, return a 503
199-
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
200-
}
201-
catch (DataException e)
202-
{
203-
QuietLog.LogHandledException(e);
222+
// Database was unavailable and we don't have a version, return a 503
223+
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
224+
}
225+
catch (DataException e)
226+
{
227+
QuietLog.LogHandledException(e);
204228

205-
// Database was unavailable and we don't have a version, return a 503
206-
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
207-
}
229+
// Database was unavailable and we don't have a version, return a 503
230+
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
208231
}
209232

210-
if (ConfigurationService.Features.TrackPackageDownloadCountInLocalDatabase)
233+
if (isSymbolPackage)
211234
{
212-
await PackageService.IncrementDownloadCountAsync(id, version);
235+
var latestSymbolPackage = package?
236+
.SymbolPackages
237+
.OrderByDescending(sp => sp.Created)
238+
.FirstOrDefault();
239+
240+
if (latestSymbolPackage == null || latestSymbolPackage.StatusKey != PackageStatus.Available)
241+
{
242+
return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.SymbolsPackage_PackageNotAvailable, id, version));
243+
}
244+
245+
return await SymbolPackageFileService.CreateDownloadSymbolPackageActionResultAsync(
246+
HttpContext.Request.Url,
247+
id, version);
213248
}
249+
else
250+
{
251+
if (ConfigurationService.Features.TrackPackageDownloadCountInLocalDatabase)
252+
{
253+
await PackageService.IncrementDownloadCountAsync(id, version);
254+
}
214255

215-
return await PackageFileService.CreateDownloadPackageActionResultAsync(
216-
HttpContext.Request.Url,
217-
id, version);
256+
return await PackageFileService.CreateDownloadPackageActionResultAsync(
257+
HttpContext.Request.Url,
258+
id, version);
259+
}
218260
}
219261

220262
[HttpGet]
@@ -303,7 +345,7 @@ private async Task<HttpStatusCodeWithBodyResult> VerifyPackageKeyInternalAsync(U
303345
if (package == null)
304346
{
305347
return new HttpStatusCodeWithBodyResult(
306-
HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
348+
HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
307349
}
308350

309351
// Write an audit record
@@ -806,7 +848,7 @@ public virtual async Task<ActionResult> DeletePackage(string id, string version)
806848
if (package == null)
807849
{
808850
return new HttpStatusCodeWithBodyResult(
809-
HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
851+
HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
810852
}
811853

812854
// Check if the current user's scopes allow listing/unlisting the current package ID
@@ -838,7 +880,7 @@ public virtual async Task<ActionResult> PublishPackage(string id, string version
838880
if (package == null)
839881
{
840882
return new HttpStatusCodeWithBodyResult(
841-
HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
883+
HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
842884
}
843885

844886
// Check if the current user's scopes allow listing/unlisting the current package ID

src/NuGetGallery/RouteName.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public static class RouteName
2727
public const string Profile = "Profile";
2828
public const string DisplayPackage = "package-route";
2929
public const string DownloadPackage = "DownloadPackage";
30+
public const string DownloadSymbolsPackage = "DownloadSymbolsPackage";
3031
public const string DownloadNuGetExe = "DownloadNuGetExe";
3132
public const string Home = "Home";
3233
public const string Stats = "Stats";

src/NuGetGallery/Services/FileSystemFileStorageService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ private static string GetContentType(string folderName)
278278
switch (folderName)
279279
{
280280
case CoreConstants.PackagesFolderName:
281+
case CoreConstants.SymbolPackagesFolderName:
281282
return CoreConstants.PackageContentType;
282283

283284
case CoreConstants.DownloadsFolderName:

src/NuGetGallery/Strings.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/NuGetGallery/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,4 +968,7 @@ Policy violations: {0}</value>
968968
<value>The previous package version '{0}' is author signed but the uploaded package is unsigned. To avoid this warning, sign the package before uploading.</value>
969969
<comment>{0} is the previous package's normalized version.</comment>
970970
</data>
971+
<data name="SymbolsPackage_PackageNotAvailable" xml:space="preserve">
972+
<value>No available symbols package found for ID {0} and version {1}.</value>
973+
</data>
971974
</root>

src/NuGetGallery/UrlExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,26 @@ public static string PackageDownload(
307307
return version == null ? EnsureTrailingSlash(result) : result;
308308
}
309309

310+
public static string SymbolPackageDownload(
311+
this UrlHelper url,
312+
int feedVersion,
313+
string id,
314+
string version,
315+
bool relativeUrl = true)
316+
{
317+
string result = GetRouteLink(
318+
url,
319+
routeName: $"v{feedVersion}{RouteName.DownloadSymbolsPackage}",
320+
relativeUrl: false,
321+
routeValues: new RouteValueDictionary
322+
{
323+
{ "Id", id },
324+
{ "Version", version }
325+
});
326+
327+
// Ensure trailing slashes for versionless package URLs, as a fix for package filenames that look like known file extensions
328+
return version == null ? EnsureTrailingSlash(result) : result;
329+
}
310330
public static string ExplorerDeepLink(
311331
this UrlHelper url,
312332
int feedVersion,

src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public DisplayPackageViewModel(Package package, User currentUser, IOrderedEnumer
2626
PushedBy = GetPushedBy(package, currentUser);
2727
PackageFileSize = package.PackageFileSize;
2828

29+
LatestSymbolPackage = package
30+
.SymbolPackages
31+
.OrderByDescending(sp => sp.Created)
32+
.FirstOrDefault();
33+
2934
if (packageHistory.Any())
3035
{
3136
// calculate the number of days since the package registration was created
@@ -62,6 +67,7 @@ public DisplayPackageViewModel(Package package, User currentUser, string pushedB
6267
public int DownloadsPerDay { get; private set; }
6368
public int TotalDaysSinceCreated { get; private set; }
6469
public long PackageFileSize { get; private set; }
70+
public SymbolPackage LatestSymbolPackage { get; private set; }
6571

6672
public bool HasSemVer2Version { get; }
6773
public bool HasSemVer2Dependency { get; }

src/NuGetGallery/Views/Packages/DisplayPackage.cshtml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
var absolutePackageUrl = Url.Absolute(Url.Package(Model.Id));
1111

12+
var hasSymbolPackageAvailable = Model.LatestSymbolPackage != null && Model.LatestSymbolPackage.StatusKey == PackageStatus.Available;
13+
1214
PackageManagerViewModel[] packageManagers;
1315

1416
if (Model.IsDotnetToolPackageType)
@@ -627,9 +629,17 @@
627629
{
628630
<li>
629631
<i class="ms-Icon ms-Icon--CloudDownload" aria-hidden="true"></i>
630-
<a href="@Url.PackageDownload(2, Model.Id, Model.Version)" data-track="outbound-manual-download" title="Download the raw nupkg file." rel="nofollow">Download</a>
632+
<a href="@Url.PackageDownload(2, Model.Id, Model.Version)" data-track="outbound-manual-download" title="Download the raw nupkg file." rel="nofollow">Download Package</a>
631633
&nbsp;(@Model.PackageFileSize.ToUserFriendlyBytesLabel())
632634
</li>
635+
if (hasSymbolPackageAvailable)
636+
{
637+
<li>
638+
<i class="ms-Icon ms-Icon--CloudDownload" aria-hidden="true"></i>
639+
<a href="@Url.SymbolPackageDownload(2, Model.Id, Model.Version)" data-track="outbound-manual-download" title="Download the raw snupkg file." rel="nofollow">Download Symbols</a>
640+
&nbsp;(@Model.LatestSymbolPackage.FileSize.ToUserFriendlyBytesLabel())
641+
</li>
642+
}
633643
<li class="no-clickonce">
634644
<i class="ms-Icon ms-Icon--OpenInNewWindow" aria-hidden="true"></i>
635645
<a href="@Url.ExplorerDeepLink(2, Model.Id, Model.Version)" title="Explore the nupkg with the NuGet Package Explorer (IE only)" rel="nofollow">Open in Package Explorer</a>

0 commit comments

Comments
 (0)