Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/lib/admin/actions/package_reservation_create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:pub_dev/package/backend.dart';
import 'package:pub_dev/package/models.dart';
import 'package:pub_dev/shared/datastore.dart';
import 'package:pub_dev/shared/redis_cache.dart';

import 'actions.dart';

Expand Down Expand Up @@ -52,6 +53,8 @@ able to claim it.
return entry;
});

await cache.reservedPackagePrefixes().purge();

return {
'ReservedPackage': {
'name': entry.name,
Expand Down
2 changes: 2 additions & 0 deletions app/lib/admin/actions/package_reservation_delete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:pub_dev/package/backend.dart';
import 'package:pub_dev/shared/datastore.dart';
import 'package:pub_dev/shared/redis_cache.dart';

import 'actions.dart';

Expand All @@ -27,6 +28,7 @@ Deletes a ReservedPackage entity, allowing the package name use by any user.
}

await dbService.commit(deletes: [rp.key]);
await cache.reservedPackagePrefixes().purge();

return {
'ReservedPackage': {
Expand Down
37 changes: 36 additions & 1 deletion app/lib/package/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,30 @@ class PackageBackend {
return await db.lookupOrNull<ReservedPackage>(packageKey);
}

/// Lists the currently reserved package names. A reserved package prefix is a regular name
/// that ends with `_` (underscore).
///
/// List the prefixes in descending length for easier matching.
Future<List<String>> listReservedPackagePrefixes() async {
return await cache.reservedPackagePrefixes().get(() async {
final list = <String>[];
await for (final p in db.query<ReservedPackage>().run()) {
final name = p.name;
if (name != null && name.endsWith('_')) {
list.add(name);
}
}
list.sort((a, b) {
if (a.length != b.length) {
return -a.length.compareTo(b.length);
}
return a.compareTo(b);
});
return list;
})
as List<String>;
}

/// Looks up a package by name.
Future<List<Package>> lookupPackages(Iterable<String> packageNames) async {
return (await db.lookup(
Expand Down Expand Up @@ -1191,7 +1215,18 @@ class PackageBackend {
required String name,
required AuthenticatedAgent agent,
}) async {
final reservedPackage = await lookupReservedPackage(name);
// Apply either the exact-name reserved package lookup, or the closest prefix (ending with '_').
var reservedPackage = await lookupReservedPackage(name);
if (reservedPackage == null) {
// lookup prefixes
final prefixes = await listReservedPackagePrefixes();
for (final prefix in prefixes) {
if (name.startsWith(prefix)) {
reservedPackage = await lookupReservedPackage(prefix);
break;
}
}
}

bool isAllowedUser = false;
if (agent is AuthenticatedUser) {
Expand Down
11 changes: 11 additions & 0 deletions app/lib/shared/redis_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,17 @@ class CachePatterns {
PlaylistItemListResponse.fromJson(v as Map<String, dynamic>),
),
)[pageToken];

Entry<List<String>> reservedPackagePrefixes() => _cache
.withPrefix('reserved-package-prefixes/')
.withTTL(Duration(hours: 1))
.withCodec(utf8)
.withCodec(
wrapAsCodec(
encode: (List<String> v) => json.encode(v),
decode: (v) => (json.decode(v) as List).cast<String>(),
),
)[''];
}

/// The active cache.
Expand Down
18 changes: 18 additions & 0 deletions app/test/admin/package_reservation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,24 @@ void main() {
},
);

testWithProfile(
'prefix reserve',
fn: () async {
await _reserve('pkg_');

final pubspecContent = generatePubspecYaml('pkg_foo', '1.0.0');
final bytes = await packageArchiveBytes(pubspecContent: pubspecContent);
await expectApiException(
createPubApiClient(
authToken: userClientToken,
).uploadPackageBytes(bytes),
code: 'PackageRejected',
status: 400,
message: 'Package name pkg_foo is reserved.',
);
},
);

testWithProfile(
'list and delete',
fn: () async {
Expand Down