Skip to content

Commit 72aff87

Browse files
authored
Sync exported APIs on moderation + delete a moderated version's exported files. (#8282)
1 parent 93d748f commit 72aff87

File tree

6 files changed

+90
-16
lines changed

6 files changed

+90
-16
lines changed

app/lib/admin/actions/moderate_package.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import '../../admin/backend.dart';
66
import '../../admin/models.dart';
7+
import '../../package/api_export/api_exporter.dart';
78
import '../../package/backend.dart';
89
import '../../package/models.dart';
910
import '../../shared/datastore.dart';
@@ -81,6 +82,9 @@ Note: the action may take a longer time to complete as the public archive bucket
8182
return pkg;
8283
});
8384

85+
// sync exported API(s)
86+
await apiExporter?.synchronizePackage(package, forceDelete: true);
87+
8488
// retract or re-populate public archive files
8589
await packageBackend.tarballStorage.updatePublicArchiveBucket(
8690
package: package,

app/lib/admin/actions/moderate_package_versions.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:_pub_shared/utils/sdk_version_cache.dart';
66
import 'package:clock/clock.dart';
77

8+
import '../../package/api_export/api_exporter.dart';
89
import '../../package/backend.dart';
910
import '../../package/models.dart';
1011
import '../../scorecard/backend.dart';
@@ -114,6 +115,9 @@ Set the moderated flag on a package version (updating the flag and the timestamp
114115
return v;
115116
});
116117

118+
// sync exported API(s)
119+
await apiExporter?.synchronizePackage(package, forceDelete: true);
120+
117121
// retract or re-populate public archive files
118122
await packageBackend.tarballStorage.updatePublicArchiveBucket(
119123
package: package,

app/lib/package/api_export/api_exporter.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,16 @@ final class ApiExporter {
189189
/// * Running a full background synchronization.
190190
/// * When a change in [Package.updated] is detected.
191191
/// * A package is moderated, or other admin action is applied.
192+
///
193+
/// When [forceDelete] is set, the age threshold limit for stray files is
194+
/// ignored, they will be deleted even if they were updated recently.
195+
///
196+
/// When [forceDelete] is set, the age threshold limit for stray files is
197+
/// ignored, they will be deleted even if they were updated recently.
192198
Future<void> synchronizePackage(
193199
String package, {
194200
bool forceWrite = false,
201+
bool forceDelete = false,
195202
}) async {
196203
_log.info('synchronizePackage("$package")');
197204

@@ -225,6 +232,7 @@ final class ApiExporter {
225232
await _api.package(package).synchronizeTarballs(
226233
versions,
227234
forceWrite: forceWrite,
235+
forceDelete: forceDelete,
228236
);
229237
await _api.package(package).advisories.write(
230238
advisories,

app/lib/package/api_export/exported_api.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ final class ExportedPackage {
293293
Future<void> synchronizeTarballs(
294294
Map<String, SourceObjectInfo> versions, {
295295
bool forceWrite = false,
296+
bool forceDelete = false,
296297
}) async {
297298
await Future.wait([
298299
..._owner._prefixes.map((prefix) async {
@@ -331,7 +332,8 @@ final class ExportedPackage {
331332
}
332333

333334
// Delete the item, if it's old enough.
334-
if (item.updated.isBefore(clock.agoBy(_minGarbageAge))) {
335+
if (forceDelete ||
336+
item.updated.isBefore(clock.agoBy(_minGarbageAge))) {
335337
// Only delete if the item if it's older than _minGarbageAge
336338
// This avoids any races where we delete files we've just created
337339
await _owner._bucket.tryDelete(item.name);

app/test/admin/moderate_package_test.dart

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'package:pub_dev/scorecard/backend.dart';
2020
import 'package:pub_dev/search/backend.dart';
2121
import 'package:pub_dev/shared/configuration.dart';
2222
import 'package:pub_dev/shared/datastore.dart';
23+
import 'package:pub_dev/shared/versions.dart';
2324
import 'package:test/test.dart';
2425

2526
import '../admin/models_test.dart';
@@ -294,15 +295,27 @@ void main() {
294295
expect(docs3!.where((d) => d.package == 'oxygen'), isNotEmpty);
295296
});
296297

297-
testWithProfile('archives are removed from public bucket', fn: () async {
298-
final publicUri = Uri.parse('${activeConfiguration.storageBaseUrl}'
299-
'/${activeConfiguration.publicPackagesBucketName}'
300-
'/packages/oxygen-1.0.0.tar.gz');
298+
testWithProfile('archives are removed from public buckets', fn: () async {
299+
final publicUrls = [
300+
'${activeConfiguration.storageBaseUrl}'
301+
'/${activeConfiguration.publicPackagesBucketName}'
302+
'/packages/oxygen-1.0.0.tar.gz',
303+
'${activeConfiguration.storageBaseUrl}'
304+
'/${activeConfiguration.exportedApiBucketName}'
305+
'/latest/api/archives/oxygen-1.0.0.tar.gz',
306+
'${activeConfiguration.storageBaseUrl}'
307+
'/${activeConfiguration.exportedApiBucketName}'
308+
'/$runtimeVersion/api/archives/oxygen-1.0.0.tar.gz',
309+
];
301310

302311
Future<Uint8List?> expectStatusCode(int statusCode) async {
303-
final rs1 = await http.get(publicUri);
304-
expect(rs1.statusCode, statusCode);
305-
return rs1.bodyBytes;
312+
final rs = await Future.wait(
313+
publicUrls.map((url) => http.get(Uri.parse(url))));
314+
for (final r in rs) {
315+
expect(r.statusCode, statusCode);
316+
expect(r.bodyBytes, rs.first.bodyBytes);
317+
}
318+
return rs.first.bodyBytes;
306319
}
307320

308321
final bytes = await expectStatusCode(200);

app/test/admin/moderate_package_version_test.dart

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:convert';
6+
import 'dart:io';
57
import 'dart:typed_data';
68

79
import 'package:_pub_shared/data/account_api.dart';
@@ -19,6 +21,7 @@ import 'package:pub_dev/search/backend.dart';
1921
import 'package:pub_dev/shared/configuration.dart';
2022
import 'package:pub_dev/shared/datastore.dart';
2123
import 'package:pub_dev/shared/exceptions.dart';
24+
import 'package:pub_dev/shared/versions.dart';
2225
import 'package:pub_dev/task/backend.dart';
2326
import 'package:test/test.dart';
2427

@@ -141,7 +144,7 @@ void main() {
141144
expect(optionsUpdates.isRetracted, true);
142145
});
143146

144-
testWithProfile('cannot moderated last visible version', fn: () async {
147+
testWithProfile('cannot moderate last visible version', fn: () async {
145148
await _moderate('oxygen', '1.2.0', state: true);
146149
final p1 = await packageBackend.lookupPackage('oxygen');
147150
expect(p1!.latestVersion, '1.0.0');
@@ -186,15 +189,29 @@ void main() {
186189
);
187190
});
188191

189-
testWithProfile('archive file is removed from public bucket', fn: () async {
192+
testWithProfile('archive file is removed from public buckets',
193+
fn: () async {
190194
Future<Uint8List?> expectStatusCode(int statusCode,
191195
{String version = '1.0.0'}) async {
192-
final publicUri = Uri.parse('${activeConfiguration.storageBaseUrl}'
193-
'/${activeConfiguration.publicPackagesBucketName}'
194-
'/packages/oxygen-$version.tar.gz');
195-
final rs1 = await http.get(publicUri);
196-
expect(rs1.statusCode, statusCode);
197-
return rs1.bodyBytes;
196+
final publicUrls = [
197+
'${activeConfiguration.storageBaseUrl}'
198+
'/${activeConfiguration.publicPackagesBucketName}'
199+
'/packages/oxygen-$version.tar.gz',
200+
'${activeConfiguration.storageBaseUrl}'
201+
'/${activeConfiguration.exportedApiBucketName}'
202+
'/latest/api/archives/oxygen-$version.tar.gz',
203+
'${activeConfiguration.storageBaseUrl}'
204+
'/${activeConfiguration.exportedApiBucketName}'
205+
'/$runtimeVersion/api/archives/oxygen-$version.tar.gz',
206+
];
207+
208+
final rs = await Future.wait(
209+
publicUrls.map((url) => http.get(Uri.parse(url))));
210+
for (final r in rs) {
211+
expect(r.statusCode, statusCode);
212+
expect(r.bodyBytes, rs.first.bodyBytes);
213+
}
214+
return rs.first.bodyBytes;
198215
}
199216

200217
final bytes = await expectStatusCode(200);
@@ -215,6 +232,32 @@ void main() {
215232
expect(restoredBytes, bytes);
216233
});
217234

235+
testWithProfile('versions file is updated in exported bucket',
236+
fn: () async {
237+
Future<void> expectIncluded(String version, bool isIncluded) async {
238+
final prefixes = ['latest', runtimeVersion];
239+
for (final prefix in prefixes) {
240+
final url = '${activeConfiguration.storageBaseUrl}'
241+
'/${activeConfiguration.exportedApiBucketName}'
242+
'/$prefix/api/packages/oxygen';
243+
final rs = await http.get(Uri.parse(url));
244+
expect(rs.statusCode, 200);
245+
final data = json.decode(utf8.decode(gzip.decode(rs.bodyBytes)))
246+
as Map<String, dynamic>;
247+
final versions = (data['versions'] as List)
248+
.map((i) => (i as Map)['version'])
249+
.toSet();
250+
expect(versions.contains(version), isIncluded);
251+
}
252+
}
253+
254+
await expectIncluded('1.0.0', true);
255+
await _moderate('oxygen', '1.0.0', state: true);
256+
await expectIncluded('1.0.0', false);
257+
await _moderate('oxygen', '1.0.0', state: false);
258+
await expectIncluded('1.0.0', true);
259+
});
260+
218261
testWithProfile('search is updated with new version', fn: () async {
219262
await searchBackend.doCreateAndUpdateSnapshot(
220263
FakeGlobalLockClaim(clock.now().add(Duration(seconds: 3))),

0 commit comments

Comments
 (0)