Skip to content

Commit e069030

Browse files
committed
Draft of what a full sync could look like
1 parent 596ff08 commit e069030

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

app/lib/package/api_export/export_api_to_bucket.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import 'package:gcloud/storage.dart';
99
import 'package:logging/logging.dart';
1010
import 'package:meta/meta.dart';
1111
import 'package:pool/pool.dart';
12+
import 'package:pub_dev/service/security_advisories/backend.dart';
13+
import 'package:pub_dev/service/security_advisories/sync_security_advisories.dart';
14+
import 'package:pub_dev/shared/parallel_foreach.dart';
1215
import 'package:retry/retry.dart';
1316

1417
import '../../search/backend.dart';
@@ -78,6 +81,83 @@ class ApiExporter {
7881
.write(await searchBackend.getPackageNameCompletionData());
7982
}
8083

84+
Future<void> fullSync() async {
85+
final invisiblePackageNames = await dbService
86+
.query<ModeratedPackage>()
87+
.run()
88+
.map((mp) => mp.name!)
89+
.toSet();
90+
91+
final allPackageNames = <String>{};
92+
final packageQuery = dbService.query<Package>();
93+
await packageQuery.run().parallelForEach(_concurrency, (pkg) async {
94+
final name = pkg.name!;
95+
if (pkg.isNotVisible) {
96+
invisiblePackageNames.add(name);
97+
return;
98+
}
99+
allPackageNames.add(name);
100+
101+
// TODO: Consider retries around all this logic
102+
await syncPackage(name);
103+
});
104+
105+
final visibilityConflicts =
106+
allPackageNames.intersection(invisiblePackageNames);
107+
if (visibilityConflicts.isNotEmpty) {
108+
// TODO: Shout into logs
109+
}
110+
111+
await _api.garbageCollect(allPackageNames);
112+
}
113+
114+
/// Sync package and into [ExportedApi], this will GC, etc.
115+
///
116+
/// This is intended when:
117+
/// * Running a full background synchronization.
118+
/// * When a change in [Package.updated] is detected (maybe???)
119+
/// * A package is moderated, or other admin action is applied.
120+
Future<void> syncPackage(String package) async {
121+
final versionListing = await packageBackend.listVersions(package);
122+
// TODO: Consider skipping the cache when fetching security advisories
123+
final advisories = await securityAdvisoryBackend.listAdvisoriesResponse(
124+
package,
125+
);
126+
127+
await Future.wait(versionListing.versions.map((v) async {
128+
// TODO: Will v.version work here, is the canonicalized version number?
129+
final (bucket, prefix) =
130+
packageBackend.packageStorage.getBucketAndPrefix(package, v.version);
131+
132+
await _api.package(package).tarball(v.version).copyFrom(bucket, prefix);
133+
}));
134+
135+
await _api.package(package).advisories.write(advisories);
136+
await _api.package(package).versions.write(versionListing);
137+
138+
// TODO: Is this the canonoical version? (probably)
139+
final allVersions = versionListing.versions.map((v) => v.version).toSet();
140+
await _api.package(package).garbageCollect(allVersions);
141+
}
142+
143+
/// Upload a single version of a new package.
144+
///
145+
/// This is intended to be used when a new version of a package has been
146+
/// published.
147+
Future<void> uploadSingleVersion(
148+
String package,
149+
String version,
150+
) async {
151+
final versionListing = await packageBackend.listVersions(package);
152+
153+
// TODO: Will v.version work here, is the canonicalized version number?
154+
final (bucket, prefix) =
155+
packageBackend.packageStorage.getBucketAndPrefix(package, version);
156+
await _api.package(package).tarball(version).copyFrom(bucket, prefix);
157+
158+
await _api.package(package).versions.write(versionListing);
159+
}
160+
81161
/// Note: there is no global locking here, the full scan should be called
82162
/// only once every day, and it may be racing against the incremental
83163
/// updates.

app/lib/package/api_export/exported_api.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ final class ExportedPackage {
264264
/// Interace for writing `/api/archives/<package>-<version>.tar.gz`.
265265
ExportedBlob tarball(String version) => ExportedBlob._(
266266
_owner,
267-
'/api/archives/$_package-$version.tar.gz',
267+
'/api/archives/$_package-${Uri.encodeComponent(version)}.tar.gz',
268268
'$_package-$version.tar.gz',
269269
'application/octet',
270270
Duration(hours: 2),
@@ -288,7 +288,7 @@ final class ExportedPackage {
288288
return;
289289
}
290290
final version = item.name.without(prefix: pfx, suffix: '.tar.gz');
291-
if (allVersionNumbers.contains(version)) {
291+
if (allVersionNumbers.contains(Uri.decodeComponent(version))) {
292292
return;
293293
}
294294
if (await _owner._bucket.tryInfo(item.name) case final info?) {

app/lib/package/tarball_storage.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ class TarballStorage {
5959
return await _publicBucket.tryInfo(objectName);
6060
}
6161

62+
/// Return canonical bucket and location for the archive for [package] and
63+
/// [version].
64+
(Bucket, String) getBucketAndPrefix(String package, String version) {
65+
return (
66+
_canonicalBucket,
67+
tarballObjectName(package, version),
68+
);
69+
}
70+
6271
/// Returns the publicly available download URL from the storage bucket.
6372
Future<Uri> getPublicDownloadUrl(String package, String version) async {
6473
final object = tarballObjectName(package, Uri.encodeComponent(version));

0 commit comments

Comments
 (0)