Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add admin panel for bulk unlisted or relisting packages #8691

Merged
merged 16 commits into from
Jul 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ public void TrackPackageRevalidate(Package package)
throw new NotImplementedException();
}

public void TrackPackagesUpdateListed(IReadOnlyList<Package> packages, bool listed)
{
throw new NotImplementedException();
}

public void TrackPackageUnlisted(Package package)
{
throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ public interface IPackageUpdateService
/// <param name="updateIndex">If true, <see cref="IIndexingService.UpdatePackage(Package)"/> will be called.</param>
Task MarkPackageUnlistedAsync(Package package, bool commitChanges = true, bool updateIndex = true);

/// <summary>
/// Updates the listed status on a batch of packages. All of the packages must be related to the same package registration.
/// Packages that are deleted or have failed validation are not allowed. Packages that already have a matching listed state
/// will not be skipped, to enable reflow of listed status.
/// </summary>
/// <param name="packages">The packages to update.</param>
/// <param name="listed">True to make the packages listed, false to make the packages unlisted.</param>
Task UpdateListedInBulkAsync(IReadOnlyList<Package> packages, bool listed);

/// <summary>
/// Marks <paramref name="package"/> as listed.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using NuGet.Services.Entities;
using NuGetGallery.Auditing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NuGet.Services.Entities;
using NuGetGallery.Auditing;

namespace NuGetGallery
{
Expand All @@ -32,6 +32,59 @@ public PackageUpdateService(
_indexingService = indexingService ?? throw new ArgumentNullException(nameof(indexingService));
}

public async Task UpdateListedInBulkAsync(IReadOnlyList<Package> packages, bool listed)
joelverhagen marked this conversation as resolved.
Show resolved Hide resolved
{
if (packages == null || !packages.Any())
{
throw new ArgumentException("At least one package must be provided.");
}

foreach (var package in packages)
{
if (package.PackageStatusKey == PackageStatus.Deleted)
{
throw new ArgumentException("A deleted package cannot have its listed status changed.");
}

if (package.PackageStatusKey == PackageStatus.FailedValidation)
{
throw new ArgumentException("A package that failed validation cannot have its listed status changed.");
}
}

var registration = packages.First().PackageRegistration;
if (packages.Select(p => p.PackageRegistrationKey).Distinct().Count() > 1)
{
throw new ArgumentException("All packages to change the listing status of must have the same ID.", nameof(packages));
}

using (var strategy = new SuspendDbExecutionStrategy())
using (var transaction = _entitiesContext.GetDatabase().BeginTransaction())
{
foreach (var package in packages)
{
package.Listed = listed;
}

await _packageService.UpdateIsLatestAsync(registration, commitChanges: false);

await _entitiesContext.SaveChangesAsync();

await UpdatePackagesAsync(packages, updateIndex: true);

transaction.Commit();

_telemetryService.TrackPackagesUpdateListed(packages, listed);

foreach (var package in packages)
{
await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(
package,
listed ? AuditedPackageAction.List : AuditedPackageAction.Unlist));
}
}
}

public async Task MarkPackageListedAsync(Package package, bool commitChanges = true, bool updateIndex = true)
{
if (package == null)
Expand All @@ -46,12 +99,12 @@ public async Task MarkPackageListedAsync(Package package, bool commitChanges = t

if (package.PackageStatusKey == PackageStatus.Deleted)
{
throw new InvalidOperationException("A deleted package should never be listed!");
throw new InvalidOperationException("A deleted package should never be listed.");
}

if (package.PackageStatusKey == PackageStatus.FailedValidation)
{
throw new InvalidOperationException("A package that failed validation should never be listed!");
throw new InvalidOperationException("A package that failed validation should never be listed.");
}

package.Listed = true;
Expand Down
2 changes: 2 additions & 0 deletions src/NuGetGallery.Services/Telemetry/ITelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public interface ITelemetryService

void TrackPackageListed(Package package);

void TrackPackagesUpdateListed(IReadOnlyList<Package> packages, bool listed);

void TrackPackageDelete(Package package, bool isHardDelete);

void TrackPackageReupload(Package package);
Expand Down
15 changes: 15 additions & 0 deletions src/NuGetGallery.Services/Telemetry/TelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class Events
public const string PackageReflow = "PackageReflow";
public const string PackageUnlisted = "PackageUnlisted";
public const string PackageListed = "PackageListed";
public const string PackagesUpdateListed = "PackagesUpdateListed";
public const string PackageDelete = "PackageDelete";
public const string PackageDeprecate = "PackageDeprecate";
public const string PackageReupload = "PackageReupload";
Expand Down Expand Up @@ -131,6 +132,9 @@ public class Events
public const string PackageVersion = "PackageVersion";
public const string PackageVersions = "PackageVersions";

// Package listed properties
public const string Listed = "Listed";

// Package deprecate properties
public const string DeprecationReason = "PackageDeprecationReason";
public const string DeprecationAlternatePackageId = "PackageDeprecationAlternatePackageId";
Expand Down Expand Up @@ -460,6 +464,17 @@ public void TrackPackageListed(Package package)
TrackMetricForPackage(Events.PackageListed, package);
}

public void TrackPackagesUpdateListed(IReadOnlyList<Package> packages, bool listed)
{
TrackMetricForPackageVersions(
Events.PackagesUpdateListed,
packages,
properties =>
{
properties.Add(Listed, listed.ToString());
});
}

public void TrackPackageDelete(Package package, bool isHardDelete)
{
TrackMetricForPackage(Events.PackageDelete, package, properties =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,109 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using NuGet.Services.Entities;
using NuGet.Versioning;
using NuGetGallery.Areas.Admin.ViewModels;
using NuGetGallery.Filters;

namespace NuGetGallery.Areas.Admin.Controllers
{
[UIAuthorize(Roles="Admins")]
public class AdminControllerBase : AppController
{
internal List<Package> SearchForPackages(IPackageService packageService, string query)
{
// Search supports several options:
// 1) Full package id (e.g. jQuery)
// 2) Full package id + version (e.g. jQuery 1.9.0, jQuery/1.9.0)
// 3) Any of the above separated by comma
// We are not using Lucene index here as we want to have the database values.

var queryParts = query.Split(new[] { ',', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

var packages = new List<Package>();
var completedQueries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var queryPart in queryParts)
{
var splitQuery = queryPart.Split(new[] { ' ', '/' }, StringSplitOptions.RemoveEmptyEntries);
if (splitQuery.Length == 1)
{
// Don't make the same query twice.
var id = splitQuery[0].Trim();
if (!completedQueries.Add(id))
{
continue;
}

var resultingRegistration = packageService.FindPackageRegistrationById(id);
if (resultingRegistration != null)
{
packages.AddRange(resultingRegistration
.Packages
.OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
}
}
else if (splitQuery.Length == 2)
{
// Don't make the same query twice.
var id = splitQuery[0].Trim();
var version = splitQuery[1].Trim();
if (!completedQueries.Add(id + "/" + version))
{
continue;
}

var resultingPackage = packageService.FindPackageByIdAndVersionStrict(id, version);
if (resultingPackage != null)
{
packages.Add(resultingPackage);
}
}
}

// Ensure only unique package instances are returned.
var uniquePackagesKeys = new HashSet<int>();
var uniquePackages = new List<Package>();
foreach (var package in packages)
{
if (!uniquePackagesKeys.Add(package.Key))
{
continue;
}

uniquePackages.Add(package);
}

return uniquePackages;
}

internal PackageSearchResult CreatePackageSearchResult(Package package)
{
return new PackageSearchResult
{
PackageId = package.Id,
PackageVersionNormalized = !string.IsNullOrEmpty(package.NormalizedVersion)
? package.NormalizedVersion
: NuGetVersion.Parse(package.Version).ToNormalizedString(),
DownloadCount = package.DownloadCount,
Created = package.Created.ToNuGetShortDateString(),
Listed = package.Listed,
PackageStatus = package.PackageStatusKey.ToString(),
Owners = package
.PackageRegistration
.Owners
.Select(u => u.Username)
.OrderBy(u => u, StringComparer.OrdinalIgnoreCase)
.Select(username => new UserViewModel
{
Username = username,
ProfileUrl = Url.User(username),
})
.ToList()
};
}
}
}
}
81 changes: 4 additions & 77 deletions src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@

namespace NuGetGallery.Areas.Admin.Controllers
{
public partial class DeleteController : AdminControllerBase
public class DeleteController : AdminControllerBase
{
private readonly IPackageService _packageService;
private readonly IPackageDeleteService _packageDeleteService;
private readonly ITelemetryService _telemetryService;

protected DeleteController() { }

public DeleteController(
IPackageService packageService,
IPackageDeleteService packageDeleteService,
Expand Down Expand Up @@ -50,87 +48,16 @@ public virtual ActionResult Index()
[HttpGet]
public virtual ActionResult Search(string query)
{
// Search suports several options:
// 1) Full package id (e.g. jQuery)
// 2) Full package id + version (e.g. jQuery 1.9.0)
// 3) Any of the above separated by comma
// We are not using Lucene index here as we want to have the database values.

var queryParts = query.Split(new[] { ',', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

var packages = new List<Package>();
var completedQueryParts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var queryPart in queryParts)
{
// Don't make the same query twice.
if (!completedQueryParts.Add(queryPart.Trim()))
{
continue;
}

var splitQueryPart = queryPart.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);
if (splitQueryPart.Length == 1)
{
var resultingRegistration = _packageService.FindPackageRegistrationById(splitQueryPart[0].Trim());
if (resultingRegistration != null)
{
packages.AddRange(resultingRegistration
.Packages
.OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
}
}
else if (splitQueryPart.Length == 2)
{
var resultingPackage = _packageService.FindPackageByIdAndVersionStrict(splitQueryPart[0].Trim(), splitQueryPart[1].Trim());
if (resultingPackage != null)
{
packages.Add(resultingPackage);
}
}
}

// Filter out duplicate packages and create the view model.
var uniquePackagesKeys = new HashSet<int>();
var results = new List<DeleteSearchResult>();
var packages = SearchForPackages(_packageService, query);
var results = new List<PackageSearchResult>();
foreach (var package in packages)
{
if (!uniquePackagesKeys.Add(package.Key))
{
continue;
}

results.Add(CreateDeleteSearchResult(package));
results.Add(CreatePackageSearchResult(package));
}

return Json(results, JsonRequestBehavior.AllowGet);
}

private DeleteSearchResult CreateDeleteSearchResult(Package package)
{
return new DeleteSearchResult
{
PackageId = package.Id,
PackageVersionNormalized = !string.IsNullOrEmpty(package.NormalizedVersion)
? package.NormalizedVersion
: NuGetVersion.Parse(package.Version).ToNormalizedString(),
DownloadCount = package.DownloadCount,
Created = package.Created.ToNuGetShortDateString(),
Listed = package.Listed,
PackageStatus = package.PackageStatusKey.ToString(),
Owners = package
.PackageRegistration
.Owners
.Select(u => u.Username)
.OrderBy(u => u)
.Select(username => new UserViewModel
{
Username = username,
ProfileUrl = Url.User(username),
})
.ToList()
};
}

[HttpGet]
public virtual ActionResult Reflow()
{
Expand Down
Loading