@@ -20,6 +20,13 @@ import '../../shared/versions.dart'
2020
2121final _log = Logger ('api_export:exported_bucket' );
2222
23+ /// Minimum age before an item can be consider garbage.
24+ ///
25+ /// This ensures that we don't delete files we've just created.
26+ /// It's entirely possible that one process is writing files, while another
27+ /// process is running garbage collection.
28+ const _minGarbageAge = Duration (days: 1 );
29+
2330/// Interface for [Bucket] containing exported API that is served directly from
2431/// Google Cloud Storage.
2532///
@@ -94,8 +101,8 @@ final class ExportedApi {
94101 }
95102 if (! allPackageNames.contains (packageName)) {
96103 final info = await _bucket.info (item.name);
97- if (info.updated.isBefore (clock.ago (days : 1 ))) {
98- // Only delete if the item is more than one day old
104+ if (info.updated.isBefore (clock.agoBy (_minGarbageAge ))) {
105+ // Only delete the item if it's older than _minGarbageAge
99106 // This avoids any races where we delete files we've just created
100107 await package (packageName).delete ();
101108 }
@@ -111,8 +118,8 @@ final class ExportedApi {
111118 final packageName = item.name.without (suffix: '-' ).split ('/' ).last;
112119 if (! allPackageNames.contains (packageName)) {
113120 final info = await _bucket.info (item.name);
114- if (info.updated.isBefore (clock.ago (days : 1 ))) {
115- // Only delete if the item is more than one day old
121+ if (info.updated.isBefore (clock.agoBy (_minGarbageAge ))) {
122+ // Only delete the item if it's older than _minGarbageAge
116123 // This avoids any races where we delete files we've just created
117124 await package (packageName).delete ();
118125 }
@@ -153,7 +160,7 @@ final class ExportedApi {
153160 await _listBucket (
154161 prefix: entry.name,
155162 delimiter: '' ,
156- (entry) async => await _bucket.delete (entry.name),
163+ (entry) async => await _bucket.tryDelete (entry.name),
157164 );
158165 }
159166 }));
@@ -211,6 +218,33 @@ final class ExportedPackage {
211218 Duration (hours: 2 ),
212219 );
213220
221+ /// Garbage collect versions from this package not in [allVersionNumbers] .
222+ ///
223+ /// [allVersionNumbers] must be encoded as canonical versions.
224+ Future <void > garbageCollect (Set <String > allVersionNumbers) async {
225+ await Future .wait ([
226+ ..._owner._prefixes.map ((prefix) async {
227+ final pfx = '/api/archives/$_package -' ;
228+ await _owner._listBucket (prefix: pfx, delimiter: '' , (item) async {
229+ assert (item.isObject);
230+ final version = item.name.without (prefix: pfx, suffix: '.tar.gz' );
231+ if (allVersionNumbers.contains (version)) {
232+ return ;
233+ }
234+ if (await _owner._bucket.tryInfo (item.name) case final info? ) {
235+ if (info.updated.isBefore (clock.agoBy (_minGarbageAge))) {
236+ // Only delete if the item if it's older than _minGarbageAge
237+ // This avoids any races where we delete files we've just created
238+ await _owner._bucket.tryDelete (item.name);
239+ }
240+ }
241+ // Ignore cases where tryInfo fails, assuming the object has been
242+ // deleted by another process.
243+ });
244+ }),
245+ ]);
246+ }
247+
214248 /// Delete all files related to this package.
215249 Future <void > delete () async {
216250 await Future .wait ([
@@ -220,7 +254,7 @@ final class ExportedPackage {
220254 await _owner._listBucket (
221255 prefix: prefix + '/api/archives/$_package -' ,
222256 delimiter: '' ,
223- (item) async => await _owner._bucket.delete (item.name),
257+ (item) async => await _owner._bucket.tryDelete (item.name),
224258 );
225259 }),
226260 ]);
@@ -237,7 +271,7 @@ sealed class ExportedObject {
237271 Future <void > delete () async {
238272 await Future .wait (_owner._prefixes.map ((prefix) async {
239273 await _owner._pool.withResource (() async {
240- await _owner._bucket.delete (prefix + _objectName);
274+ await _owner._bucket.tryDelete (prefix + _objectName);
241275 });
242276 }));
243277 }
0 commit comments