Skip to content

Commit 2aadb0a

Browse files
authored
Updated admin removal for packages: using the flag on the action (#8808)
1 parent 79b090a commit 2aadb0a

File tree

5 files changed

+220
-114
lines changed

5 files changed

+220
-114
lines changed

app/lib/admin/actions/moderate_package.dart

Lines changed: 78 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -33,80 +33,101 @@ Note: the action may take a longer time to complete as the public archive bucket
3333
final caseId = options['case'];
3434

3535
final package = options['package'];
36-
InvalidInputException.check(
37-
package != null && package.isNotEmpty,
38-
'package must be given',
39-
);
40-
4136
final state = options['state'];
42-
bool? valueToSet;
43-
switch (state) {
44-
case 'true':
45-
valueToSet = true;
46-
break;
47-
case 'false':
48-
valueToSet = false;
49-
break;
50-
}
51-
5237
final note = options['note'];
5338

5439
final refCase =
5540
await adminBackend.loadAndVerifyModerationCaseForAdminAction(caseId);
5641

57-
final p = await packageBackend.lookupPackage(package!);
58-
if (p == null) {
59-
throw NotFoundException.resource(package);
60-
}
61-
62-
Package? p2;
63-
if (valueToSet != null) {
64-
p2 = await withRetryTransaction(dbService, (tx) async {
65-
final pkg = await tx.lookupValue<Package>(p.key);
66-
pkg.updateIsModerated(isModerated: valueToSet!);
67-
tx.insert(pkg);
42+
return await adminMarkPackageVisibility(
43+
package,
44+
state: state,
45+
whenUpdating: (tx, p, valueToSet) async {
46+
p.updateIsModerated(isModerated: valueToSet);
6847

6948
if (refCase != null) {
7049
final mc = await tx.lookupValue<ModerationCase>(refCase.key);
7150
mc.addActionLogEntry(
72-
ModerationSubject.package(package).fqn,
51+
ModerationSubject.package(package!).fqn,
7352
valueToSet ? ModerationAction.apply : ModerationAction.revert,
7453
note,
7554
);
7655
tx.insert(mc);
7756
}
78-
79-
return pkg;
80-
});
81-
82-
// make sure visibility cache is updated immediately
83-
await purgePackageCache(package);
84-
85-
// sync exported API(s)
86-
await apiExporter.synchronizePackage(package, forceDelete: true);
87-
88-
// retract or re-populate public archive files
89-
await packageBackend.tarballStorage.updatePublicArchiveBucket(
90-
package: package,
91-
ageCheckThreshold: Duration.zero,
92-
deleteIfOlder: Duration.zero,
93-
);
94-
95-
await taskBackend.trackPackage(package);
96-
await purgePackageCache(package);
97-
}
98-
99-
return {
100-
'package': p.name,
101-
'before': {
57+
},
58+
valueFn: (p) => {
10259
'isModerated': p.isModerated,
10360
'moderatedAt': p.moderatedAt?.toIso8601String(),
10461
},
105-
if (p2 != null)
106-
'after': {
107-
'isModerated': p2.isModerated,
108-
'moderatedAt': p2.moderatedAt?.toIso8601String(),
109-
},
110-
};
62+
);
11163
},
11264
);
65+
66+
/// Changes the moderated or the admin-deleted flag and timestamp on a [package].
67+
Future<Map<String, dynamic>> adminMarkPackageVisibility(
68+
String? package, {
69+
/// `true`, `false` or `null`
70+
required String? state,
71+
72+
/// The updates to apply during the transaction.
73+
required Future<void> Function(
74+
TransactionWrapper tx,
75+
Package v,
76+
bool valueToSet,
77+
) whenUpdating,
78+
79+
/// The debug information to return.
80+
required Map Function(Package v) valueFn,
81+
}) async {
82+
InvalidInputException.check(
83+
package != null && package.isNotEmpty,
84+
'package must be given',
85+
);
86+
87+
bool? valueToSet;
88+
switch (state) {
89+
case 'true':
90+
valueToSet = true;
91+
break;
92+
case 'false':
93+
valueToSet = false;
94+
break;
95+
}
96+
97+
final p = await packageBackend.lookupPackage(package!);
98+
if (p == null) {
99+
throw NotFoundException.resource(package);
100+
}
101+
102+
Package? p2;
103+
if (valueToSet != null) {
104+
p2 = await withRetryTransaction(dbService, (tx) async {
105+
final pkg = await tx.lookupValue<Package>(p.key);
106+
await whenUpdating(tx, pkg, valueToSet!);
107+
tx.insert(pkg);
108+
return pkg;
109+
});
110+
111+
// make sure visibility cache is updated immediately
112+
await purgePackageCache(package);
113+
114+
// sync exported API(s)
115+
await apiExporter.synchronizePackage(package, forceDelete: true);
116+
117+
// retract or re-populate public archive files
118+
await packageBackend.tarballStorage.updatePublicArchiveBucket(
119+
package: package,
120+
ageCheckThreshold: Duration.zero,
121+
deleteIfOlder: Duration.zero,
122+
);
123+
124+
await taskBackend.trackPackage(package);
125+
await purgePackageCache(package);
126+
}
127+
128+
return {
129+
'package': p.name,
130+
'before': valueFn(p),
131+
if (p2 != null) 'after': valueFn(p2),
132+
};
133+
}

app/lib/admin/actions/package_delete.dart

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,40 @@
44

55
import '../../account/backend.dart';
66
import '../../shared/configuration.dart';
7-
import '../backend.dart';
87
import 'actions.dart';
8+
import 'moderate_package.dart';
99

1010
final packageDelete = AdminAction(
11-
name: 'package-delete',
12-
options: {
13-
'package': 'name of package to delete',
14-
},
15-
summary: 'Deletes package <package>.',
16-
description: '''
17-
Deletes package <package>.
11+
name: 'package-delete',
12+
summary: 'Set the admin-deleted flag on a package (making it not visible).',
13+
description: '''
14+
Set the admin-deleted flag on a package (updating the flag and the timestamp).
1815
19-
Deletes all associated resources:
16+
A package in this state will appear deleted from the public. But its archive file will still exist in the canonical bucket, and the metadata will still be present.
2017
21-
* PackageVersions
22-
* Likes
23-
* AuditLogRecords
24-
* PackageVersionAsset
25-
* replacedBy references
26-
* archives (might be retrievable from backup)
18+
After 2 months it will be fully purged.
2719
28-
The package will be "tombstoned" and no package with the same name can be
29-
published later.
20+
To undo a deletion run the same command with `state: false`.
3021
''',
31-
invoke: (args) async {
32-
final packageName = args['package'];
33-
if (packageName == null) {
34-
throw InvalidInputException('Missing `package` argument');
35-
}
36-
37-
await requireAuthenticatedAdmin(AdminPermission.removePackage);
38-
final result = await adminBackend.removePackage(packageName);
39-
40-
return {
41-
'message': '''
42-
Package and all associated resources deleted.
43-
44-
A tombstone has been created
45-
46-
'NOTICE: Redis caches referencing the package will expire given time.'
47-
''',
48-
'package': packageName,
49-
'deletedPackages': result.deletedPackages,
50-
'deletedPackageVersions': result.deletedPackageVersions,
51-
'deletedPackageVersionInfos': result.deletedPackageVersionInfos,
52-
'deletedPackageVersionAssets': result.deletedPackageVersionAssets,
53-
'deletedLikes': result.deletedLikes,
54-
'deletedAuditLogs': result.deletedAuditLogs,
55-
'replacedByFixes': result.replacedByFixes,
56-
};
57-
});
22+
options: {
23+
'package': 'The package name to be deleted',
24+
'state':
25+
'Set admin-deleted state true / false. Returns current state if omitted.',
26+
},
27+
invoke: (args) async {
28+
await requireAuthenticatedAdmin(AdminPermission.removePackage);
29+
final package = args['package'];
30+
final state = args['state'];
31+
return await adminMarkPackageVisibility(
32+
package,
33+
state: state,
34+
whenUpdating: (tx, p, valueToSet) async {
35+
p.updateIsAdminDeleted(isAdminDeleted: valueToSet);
36+
},
37+
valueFn: (p) => {
38+
'isAdminDeleted': p.isAdminDeleted,
39+
'adminDeletedAt': p.adminDeletedAt?.toIso8601String(),
40+
},
41+
);
42+
},
43+
);

app/lib/admin/actions/package_version_delete.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ final packageVersionDelete = AdminAction(
1212
summary:
1313
'Set the admin-deleted flag on a package version (making it not visible).',
1414
description: '''
15-
Set the admin-deleted flag on a package version (updating the flag and the timestamp). After 2 months it will be fully deleted.
15+
Set the admin-deleted flag on a package version (updating the flag and the timestamp).
16+
17+
A package version in this state will appear deleted from the public. But its archive file will still exist in the canonical bucket, and the metadata will still be present.
18+
19+
After 2 months it will be fully purged.
20+
21+
To undo a deletion run the same command with `state: false`.
1622
''',
1723
options: {
1824
'package': 'The package name to be deleted',

app/lib/admin/backend.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,21 @@ class AdminBackend {
881881
}) async {
882882
before ??= clock.ago(days: 62).toUtc(); // extra buffer days
883883

884+
// delete packages
885+
final pQuery = _db.query<Package>()
886+
..filter('adminDeletedAt <', before)
887+
..order('adminDeletedAt');
888+
await for (final package in pQuery.run()) {
889+
// sanity check
890+
if (!(package.isAdminDeleted ?? false)) {
891+
continue;
892+
}
893+
894+
_logger.info('Deleting admin-deleted package: ${package.name}');
895+
await removePackage(package.name!);
896+
_logger.info('Deleted moderated package: ${package.name}');
897+
}
898+
884899
// delete package versions
885900
final pvQuery = _db.query<PackageVersion>()
886901
..filter('adminDeletedAt <', before)

0 commit comments

Comments
 (0)