Skip to content

Commit

Permalink
[NuGet Symbol Server] Add download symbols support to Package details…
Browse files Browse the repository at this point in the history
… page (#6320)
  • Loading branch information
shishirx34 authored Aug 16, 2018
1 parent baa5bc0 commit bc0954b
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 71 deletions.
6 changes: 6 additions & 0 deletions src/NuGetGallery/App_Start/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,12 @@ public static void RegisterApiV2Routes(RouteCollection routes)
defaults: new { controller = "Api", action = "GetPackageApi", version = UrlParameter.Optional },
constraints: new { httpMethod = new HttpMethodConstraint("GET") });

routes.MapRoute(
"v2" + RouteName.DownloadSymbolsPackage,
"api/v2/symbolpackage/{id}/{version}",
defaults: new { controller = "Api", action = "GetSymbolPackageApi", version = UrlParameter.Optional },
constraints: new { httpMethod = new HttpMethodConstraint("GET") });

routes.MapRoute(
"v2" + RouteName.PushPackageApi,
"api/v2/package",
Expand Down
120 changes: 81 additions & 39 deletions src/NuGetGallery/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public partial class ApiController
public IReservedNamespaceService ReservedNamespaceService { get; set; }
public IPackageUploadService PackageUploadService { get; set; }
public IPackageDeleteService PackageDeleteService { get; set; }
public ISymbolPackageFileService SymbolPackageFileService { get; set; }
public ISymbolPackageService SymbolPackageService { get; set; }
public ISymbolPackageUploadService SymbolPackageUploadService { get; set; }
public IContentObjectService ContentObjectService { get; set; }
Expand Down Expand Up @@ -85,6 +86,7 @@ public ApiController(
IReservedNamespaceService reservedNamespaceService,
IPackageUploadService packageUploadService,
IPackageDeleteService packageDeleteService,
ISymbolPackageFileService symbolPackageFileService,
ISymbolPackageService symbolPackageService,
ISymbolPackageUploadService symbolPackageUploadService,
IContentObjectService contentObjectService)
Expand All @@ -109,6 +111,7 @@ public ApiController(
ReservedNamespaceService = reservedNamespaceService;
PackageUploadService = packageUploadService;
StatisticsService = null;
SymbolPackageFileService = symbolPackageFileService;
SymbolPackageService = symbolPackageService;
SymbolPackageUploadService = symbolPackageUploadService;
ContentObjectService = contentObjectService;
Expand Down Expand Up @@ -136,21 +139,35 @@ public ApiController(
IReservedNamespaceService reservedNamespaceService,
IPackageUploadService packageUploadService,
IPackageDeleteService packageDeleteService,
ISymbolPackageFileService symbolPackageFileService,
ISymbolPackageService symbolPackageService,
ISymbolPackageUploadService symbolPackageUploadServivce,
IContentObjectService contentObjectService)
: this(apiScopeEvaluator, entitiesContext, packageService, packageFileService, userService, contentService,
indexingService, searchService, autoCuratePackage, statusService, messageService, auditingService,
configurationService, telemetryService, authenticationService, credentialBuilder, securityPolicies,
reservedNamespaceService, packageUploadService, packageDeleteService, symbolPackageService, symbolPackageUploadServivce,
contentObjectService)
reservedNamespaceService, packageUploadService, packageDeleteService, symbolPackageFileService,
symbolPackageService, symbolPackageUploadServivce, contentObjectService)
{
StatisticsService = statisticsService;
}


[HttpGet]
[ActionName("GetSymbolPackageApi")]
public virtual async Task<ActionResult> GetSymbolPackage(string id, string version)
{
return await GetPackageInternal(id, version, isSymbolPackage: true);
}

[HttpGet]
[ActionName("GetPackageApi")]
public virtual async Task<ActionResult> GetPackage(string id, string version)
{
return await GetPackageInternal(id, version, isSymbolPackage: false);
}

protected internal async Task<ActionResult> GetPackageInternal(string id, string version, bool isSymbolPackage = false)
{
// some security paranoia about URL hacking somehow creating e.g. open redirects
// validate user input: explicit calls to the same validators used during Package Registrations
Expand All @@ -160,61 +177,86 @@ public virtual async Task<ActionResult> GetPackage(string id, string version)
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The format of the package id is invalid");
}

// if version is non-null, check if it's semantically correct and normalize it.
if (!String.IsNullOrEmpty(version))
Package package = null;
try
{
NuGetVersion dummy;
if (!NuGetVersion.TryParse(version, out dummy))
if (!string.IsNullOrEmpty(version))
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The package version is not a valid semantic version");
}
// if version is non-null, check if it's semantically correct and normalize it.
NuGetVersion dummy;
if (!NuGetVersion.TryParse(version, out dummy))
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The package version is not a valid semantic version");
}

// Normalize the version
version = NuGetVersionFormatter.Normalize(version);
}
else
{
// If version is null, get the latest version from the database.
// This ensures that on package restore scenario where version will be non null, we don't hit the database.
try
// Normalize the version
version = NuGetVersionFormatter.Normalize(version);

if (isSymbolPackage)
{
package = PackageService.FindPackageByIdAndVersionStrict(id, version);
}
}
else
{
var package = PackageService.FindPackageByIdAndVersion(
// If version is null, get the latest version from the database.
// This ensures that on package restore scenario where version will be non null, we don't hit the database.
package = PackageService.FindPackageByIdAndVersion(
id,
version,
SemVerLevelKey.SemVer2,
allowPrerelease: false);

if (package == null)
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
}
version = package.NormalizedVersion;

version = package.NormalizedVersion;
}
catch (SqlException e)
{
QuietLog.LogHandledException(e);
}
catch (SqlException e)
{
QuietLog.LogHandledException(e);

// Database was unavailable and we don't have a version, return a 503
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
}
catch (DataException e)
{
QuietLog.LogHandledException(e);
// Database was unavailable and we don't have a version, return a 503
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
}
catch (DataException e)
{
QuietLog.LogHandledException(e);

// Database was unavailable and we don't have a version, return a 503
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
}
// Database was unavailable and we don't have a version, return a 503
return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion);
}

if (ConfigurationService.Features.TrackPackageDownloadCountInLocalDatabase)
if (isSymbolPackage)
{
await PackageService.IncrementDownloadCountAsync(id, version);
var latestSymbolPackage = package?
.SymbolPackages
.OrderByDescending(sp => sp.Created)
.FirstOrDefault();

if (latestSymbolPackage == null || latestSymbolPackage.StatusKey != PackageStatus.Available)
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.SymbolsPackage_PackageNotAvailable, id, version));
}

return await SymbolPackageFileService.CreateDownloadSymbolPackageActionResultAsync(
HttpContext.Request.Url,
id, version);
}
else
{
if (ConfigurationService.Features.TrackPackageDownloadCountInLocalDatabase)
{
await PackageService.IncrementDownloadCountAsync(id, version);
}

return await PackageFileService.CreateDownloadPackageActionResultAsync(
HttpContext.Request.Url,
id, version);
return await PackageFileService.CreateDownloadPackageActionResultAsync(
HttpContext.Request.Url,
id, version);
}
}

[HttpGet]
Expand Down Expand Up @@ -303,7 +345,7 @@ private async Task<HttpStatusCodeWithBodyResult> VerifyPackageKeyInternalAsync(U
if (package == null)
{
return new HttpStatusCodeWithBodyResult(
HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
HttpStatusCode.NotFound, string.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version));
}

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

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

// Check if the current user's scopes allow listing/unlisting the current package ID
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/RouteName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public static class RouteName
public const string Profile = "Profile";
public const string DisplayPackage = "package-route";
public const string DownloadPackage = "DownloadPackage";
public const string DownloadSymbolsPackage = "DownloadSymbolsPackage";
public const string DownloadNuGetExe = "DownloadNuGetExe";
public const string Home = "Home";
public const string Stats = "Stats";
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/Services/FileSystemFileStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ private static string GetContentType(string folderName)
switch (folderName)
{
case CoreConstants.PackagesFolderName:
case CoreConstants.SymbolPackagesFolderName:
return CoreConstants.PackageContentType;

case CoreConstants.DownloadsFolderName:
Expand Down
9 changes: 9 additions & 0 deletions src/NuGetGallery/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/NuGetGallery/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -968,4 +968,7 @@ Policy violations: {0}</value>
<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>
<comment>{0} is the previous package's normalized version.</comment>
</data>
<data name="SymbolsPackage_PackageNotAvailable" xml:space="preserve">
<value>No available symbols package found for ID {0} and version {1}.</value>
</data>
</root>
20 changes: 20 additions & 0 deletions src/NuGetGallery/UrlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ public static string PackageDownload(
return version == null ? EnsureTrailingSlash(result) : result;
}

public static string SymbolPackageDownload(
this UrlHelper url,
int feedVersion,
string id,
string version,
bool relativeUrl = true)
{
string result = GetRouteLink(
url,
routeName: $"v{feedVersion}{RouteName.DownloadSymbolsPackage}",
relativeUrl: false,
routeValues: new RouteValueDictionary
{
{ "Id", id },
{ "Version", version }
});

// Ensure trailing slashes for versionless package URLs, as a fix for package filenames that look like known file extensions
return version == null ? EnsureTrailingSlash(result) : result;
}
public static string ExplorerDeepLink(
this UrlHelper url,
int feedVersion,
Expand Down
6 changes: 6 additions & 0 deletions src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public DisplayPackageViewModel(Package package, User currentUser, IOrderedEnumer
PushedBy = GetPushedBy(package, currentUser);
PackageFileSize = package.PackageFileSize;

LatestSymbolPackage = package
.SymbolPackages
.OrderByDescending(sp => sp.Created)
.FirstOrDefault();

if (packageHistory.Any())
{
// calculate the number of days since the package registration was created
Expand Down Expand Up @@ -62,6 +67,7 @@ public DisplayPackageViewModel(Package package, User currentUser, string pushedB
public int DownloadsPerDay { get; private set; }
public int TotalDaysSinceCreated { get; private set; }
public long PackageFileSize { get; private set; }
public SymbolPackage LatestSymbolPackage { get; private set; }

public bool HasSemVer2Version { get; }
public bool HasSemVer2Dependency { get; }
Expand Down
12 changes: 11 additions & 1 deletion src/NuGetGallery/Views/Packages/DisplayPackage.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

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

var hasSymbolPackageAvailable = Model.LatestSymbolPackage != null && Model.LatestSymbolPackage.StatusKey == PackageStatus.Available;

PackageManagerViewModel[] packageManagers;

if (Model.IsDotnetToolPackageType)
Expand Down Expand Up @@ -627,9 +629,17 @@
{
<li>
<i class="ms-Icon ms-Icon--CloudDownload" aria-hidden="true"></i>
<a href="@Url.PackageDownload(2, Model.Id, Model.Version)" data-track="outbound-manual-download" title="Download the raw nupkg file." rel="nofollow">Download</a>
<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>
&nbsp;(@Model.PackageFileSize.ToUserFriendlyBytesLabel())
</li>
if (hasSymbolPackageAvailable)
{
<li>
<i class="ms-Icon ms-Icon--CloudDownload" aria-hidden="true"></i>
<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>
&nbsp;(@Model.LatestSymbolPackage.FileSize.ToUserFriendlyBytesLabel())
</li>
}
<li class="no-clickonce">
<i class="ms-Icon ms-Icon--OpenInNewWindow" aria-hidden="true"></i>
<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>
Expand Down
Loading

0 comments on commit bc0954b

Please sign in to comment.