@@ -12,7 +12,9 @@ import 'package:clock/clock.dart';
1212import 'package:collection/collection.dart' ;
1313import 'package:convert/convert.dart' ;
1414import 'package:gcloud/service_scope.dart' as ss;
15+ import 'package:gcloud/storage.dart' ;
1516import 'package:logging/logging.dart' ;
17+ import 'package:meta/meta.dart' ;
1618import 'package:pool/pool.dart' ;
1719
1820import '../account/backend.dart' ;
@@ -22,14 +24,19 @@ import '../account/models.dart';
2224import '../admin/models.dart' ;
2325import '../audit/models.dart' ;
2426import '../package/backend.dart'
25- show checkPackageVersionParams, packageBackend, purgePackageCache;
27+ show
28+ checkPackageVersionParams,
29+ packageBackend,
30+ purgePackageCache,
31+ tarballObjectName;
2632import '../package/models.dart' ;
2733import '../publisher/models.dart' ;
2834import '../scorecard/backend.dart' ;
2935import '../shared/configuration.dart' ;
3036import '../shared/datastore.dart' ;
3137import '../shared/email.dart' ;
3238import '../shared/exceptions.dart' ;
39+ import '../shared/storage.dart' ;
3340import '../shared/versions.dart' ;
3441import '../task/backend.dart' ;
3542import 'actions/actions.dart' show AdminAction;
@@ -297,13 +304,27 @@ class AdminBackend {
297304 /// Creates a [ModeratedPackage] instance (if not already present) in
298305 /// Datastore representing the removed package. No new package with the same
299306 /// name can be published.
307+ ///
308+ /// Verifies the current authenticated user for admin permissions.
300309 Future <void > removePackage (String packageName) async {
301310 final caller =
302311 await requireAuthenticatedAdmin (AdminPermission .removePackage);
303-
304312 _logger.info ('${caller .displayId }) initiated the delete '
305313 'of package $packageName ' );
314+ await _removePackage (packageName);
315+ }
306316
317+ /// Removes the package from the Datastore and updates other related
318+ /// entities. It is safe to call [removePackage] on an already removed
319+ /// package, as the call is idempotent.
320+ ///
321+ /// Creates a [ModeratedPackage] instance (if not already present) in
322+ /// Datastore representing the removed package. No new package with the same
323+ /// name can be published.
324+ Future <void > _removePackage (
325+ String packageName, {
326+ DateTime ? moderated,
327+ }) async {
307328 final packageKey = _db.emptyKey.append (Package , id: packageName);
308329 final versions = (await _db
309330 .query <PackageVersion >(ancestorKey: packageKey)
@@ -312,6 +333,49 @@ class AdminBackend {
312333 .toList ())
313334 .toSet ();
314335
336+ final pool = Pool (10 );
337+ final futures = < Future > [];
338+ for (final v in versions) {
339+ // Deleting public and canonical archives, 404 errors are ignored.
340+ futures.add (pool.withResource (
341+ () => packageBackend.removePackageTarball (packageName, v)));
342+ }
343+ await Future .wait (futures);
344+ await pool.close ();
345+
346+ _logger.info ('Removing package from Package.replacedBy...' );
347+ final replacedByQuery = _db.query <Package >()
348+ ..filter ('replacedBy =' , packageName);
349+ await for (final pkg in replacedByQuery.run ()) {
350+ await withRetryTransaction (_db, (tx) async {
351+ final p = await tx.lookupOrNull <Package >(pkg.key);
352+ if (p == null ) {
353+ return ;
354+ }
355+ if (p.replacedBy == packageName) {
356+ p.replacedBy = null ;
357+ tx.insert (p);
358+ }
359+ });
360+ }
361+
362+ _logger.info ('Removing package from PackageVersionInfo ...' );
363+ await _db.deleteWithQuery (
364+ _db.query <PackageVersionInfo >()..filter ('package =' , packageName));
365+
366+ _logger.info ('Removing package from PackageVersionAsset ...' );
367+ await _db.deleteWithQuery (
368+ _db.query <PackageVersionAsset >()..filter ('package =' , packageName));
369+
370+ _logger.info ('Removing package from Like ...' );
371+ await _db.deleteWithQuery (
372+ _db.query <Like >()..filter ('packageName =' , packageName));
373+
374+ _logger.info ('Removing package from AuditLogRecord...' );
375+ await _db.deleteWithQuery (
376+ _db.query <AuditLogRecord >()..filter ('packages =' , packageName));
377+
378+ _logger.info ('Removing Package from Datastore...' );
315379 await withRetryTransaction (_db, (tx) async {
316380 final package = await tx.lookupOrNull <Package >(packageKey);
317381 if (package == null ) {
@@ -335,11 +399,13 @@ class AdminBackend {
335399 .map ((pv) => pv.version! )
336400 .toList ());
337401
402+ versions.addAll (package.deletedVersions ?? const < String > []);
403+
338404 tx.insert (ModeratedPackage ()
339405 ..parentKey = _db.emptyKey
340406 ..id = packageName
341407 ..name = packageName
342- ..moderated = clock.now ().toUtc ()
408+ ..moderated = moderated ?? clock.now ().toUtc ()
343409 ..versions = versions.toList ()
344410 ..publisherId = package.publisherId
345411 ..uploaders = package.uploaders);
@@ -348,31 +414,10 @@ class AdminBackend {
348414 }
349415 });
350416
351- final pool = Pool (10 );
352- final futures = < Future > [];
353- versions.forEach ((final v) {
354- futures.add (pool.withResource (
355- () => packageBackend.removePackageTarball (packageName, v)));
356- });
357- await Future .wait (futures);
358- await pool.close ();
359-
360417 _logger.info ('Removing package from PackageVersion ...' );
361418 await _db
362419 .deleteWithQuery (_db.query <PackageVersion >(ancestorKey: packageKey));
363420
364- _logger.info ('Removing package from PackageVersionInfo ...' );
365- await _db.deleteWithQuery (
366- _db.query <PackageVersionInfo >()..filter ('package =' , packageName));
367-
368- _logger.info ('Removing package from PackageVersionAsset ...' );
369- await _db.deleteWithQuery (
370- _db.query <PackageVersionAsset >()..filter ('package =' , packageName));
371-
372- _logger.info ('Removing package from Like ...' );
373- await _db.deleteWithQuery (
374- _db.query <Like >()..filter ('packageName =' , packageName));
375-
376421 _logger.info ('Package "$packageName " got successfully removed.' );
377422 _logger.info (
378423 'NOTICE: Redis caches referencing the package will expire given time.' );
@@ -740,4 +785,96 @@ class AdminBackend {
740785 }
741786 return refCase;
742787 }
788+
789+ /// Scans datastore and deletes moderated subjects where the last action
790+ /// was more than 3 years ago.
791+ Future <void > deleteModeratedSubjects ({
792+ @visibleForTesting DateTime ? before,
793+ }) async {
794+ before ?? = clock.ago (days: 3 * 366 ).toUtc (); // extra buffer days
795+ final canonicalBucket =
796+ storageService.bucket (activeConfiguration.canonicalPackagesBucketName! );
797+
798+ // delete packages
799+ final pQuery = _db.query <Package >()
800+ ..filter ('moderatedAt <' , before)
801+ ..order ('moderatedAt' );
802+ await for (final package in pQuery.run ()) {
803+ // sanity check
804+ if (! package.isModerated) {
805+ continue ;
806+ }
807+
808+ _logger.info ('Deleting moderated package: ${package .name }' );
809+ await _removePackage (
810+ package.name! ,
811+ moderated: package.moderatedAt,
812+ );
813+ _logger.info ('Deleted moderated package: ${package .name }' );
814+ }
815+
816+ // delete package versions
817+ final pvQuery = _db.query <PackageVersion >()
818+ ..filter ('moderatedAt <' , before)
819+ ..order ('moderatedAt' );
820+ await for (final version in pvQuery.run ()) {
821+ // sanity check
822+ if (! version.isModerated) {
823+ continue ;
824+ }
825+
826+ _logger.info (
827+ 'Deleting moderated package version: ${version .qualifiedVersionKey }' );
828+
829+ // deleting from canonical bucket
830+ final objectName = tarballObjectName (version.package, version.version! );
831+ final info = await canonicalBucket.tryInfo (objectName);
832+ if (info != null ) {
833+ await canonicalBucket.delete (objectName);
834+ }
835+
836+ // deleting from datastore
837+ await withRetryTransaction (_db, (tx) async {
838+ final pv = await tx.lookupOrNull <PackageVersion >(version.key);
839+ if (pv == null ) {
840+ return null ;
841+ }
842+ final p = await tx.lookupOrNull <Package >(version.packageKey! );
843+ if (p == null ) {
844+ return ;
845+ }
846+ final pvi = await tx.lookupOrNull <PackageVersionInfo >(_db.emptyKey
847+ .append (PackageVersionInfo ,
848+ id: version.qualifiedVersionKey.qualifiedVersion));
849+
850+ p.deletedVersions ?? = [];
851+ p.deletedVersions! .add (version.version! );
852+ p.deletedVersions! .sort ();
853+ p.updated = clock.now ().toUtc ();
854+ tx.insert (p);
855+
856+ // delete version + info + assets
857+ tx.delete (pv.key);
858+ if (pvi != null ) {
859+ tx.delete (pvi.key);
860+
861+ for (final assetKind in pvi.assets) {
862+ tx.delete (
863+ _db.emptyKey.append (PackageVersionAsset ,
864+ id: Uri (pathSegments: [
865+ version.package,
866+ version.version! ,
867+ assetKind
868+ ]).path),
869+ );
870+ }
871+ }
872+ });
873+ _logger.info (
874+ 'Deleted moderated package version: ${version .qualifiedVersionKey }' );
875+ }
876+
877+ // TODO: delete publisher instances
878+ // TODO: mark user instances deleted
879+ }
743880}
0 commit comments