diff --git a/app/lib/frontend/handlers/pubapi.dart b/app/lib/frontend/handlers/pubapi.dart index d35ebd3f3b..d218501a98 100644 --- a/app/lib/frontend/handlers/pubapi.dart +++ b/app/lib/frontend/handlers/pubapi.dart @@ -154,14 +154,9 @@ class PubApi { @EndPoint.get('/api/packages/versions/newUploadFinish/') Future finishPackageUpload( Request request, String uploadId) async { - final pv = await packageBackend.publishUploadedBlob(uploadId); + final messages = await packageBackend.publishUploadedBlob(uploadId); return SuccessMessage( - success: Message( - message: 'Successfully uploaded ' - '${urls.pkgPageUrl(pv.package, includeHost: true)} ' - 'version ${pv.version}, ' - 'it may take up-to 10 minutes before the new version is available.', - ), + success: Message(message: messages.join('\n')), ); } diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index 92fc67a18f..50f99b8343 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -882,8 +882,9 @@ class PackageBackend { ); } - /// Finishes the upload of a package. - Future publishUploadedBlob(String guid) async { + /// Finishes the upload of a package and returns the list of messages + /// related to the publishing. + Future> publishUploadedBlob(String guid) async { final restriction = await getUploadRestrictionStatus(); if (restriction == UploadRestrictionStatus.noUploads) { throw PackageRejectedException.uploadRestricted(); @@ -984,7 +985,7 @@ class PackageBackend { sw.reset(); final entities = await _createUploadEntities(db, agent, archive, sha256Hash: sha256Hash); - final version = await _performTarballUpload( + final (version, uploadMessages) = await _performTarballUpload( entities: entities, agent: agent, archive: archive, @@ -998,7 +999,13 @@ class PackageBackend { sw.reset(); await _incomingBucket.deleteWithRetry(tmpObjectName(guid)); _logger.info('Temporary object removed in ${sw.elapsed}.'); - return version; + return [ + 'Successfully uploaded ' + '${urls.pkgPageUrl(version.package, includeHost: true)} ' + 'version ${version.version}, ' + 'it may take up-to 10 minutes before the new version is available.', + ...uploadMessages, + ]; }); } @@ -1057,7 +1064,7 @@ class PackageBackend { } } - Future _performTarballUpload({ + Future<(PackageVersion, List)> _performTarballUpload({ required _UploadEntities entities, required AuthenticatedAgent agent, required PackageSummary archive, @@ -1065,6 +1072,7 @@ class PackageBackend { required bool hasCanonicalArchiveObject, }) async { final sw = Stopwatch()..start(); + final uploadMessages = []; final newVersion = entities.packageVersion; final [currentDartSdk, currentFlutterSdk] = await Future.wait([ getCachedDartSdkVersion(lastKnownStable: toolStableDartSdkVersion), @@ -1100,14 +1108,6 @@ class PackageBackend { package: newVersion.package, isNew: isNew, ); - final email = createPackageUploadedEmail( - packageName: newVersion.package, - packageVersion: newVersion.version!, - displayId: agent.displayId, - authorizedUploaders: - uploaderEmails.map((email) => EmailAddress(email)).toList(), - ); - final outgoingEmail = emailBackend.prepareEntity(email); Package? package; final existingVersions = await db .query(ancestorKey: newVersion.packageKey!) @@ -1116,7 +1116,7 @@ class PackageBackend { // Add the new package to the repository by storing the tarball and // inserting metadata to datastore (which happens atomically). - final pv = await withRetryTransaction(db, (tx) async { + final (pv, outgoingEmail) = await withRetryTransaction(db, (tx) async { _logger.info('Starting datastore transaction.'); final tuple = (await tx.lookup([ @@ -1156,10 +1156,21 @@ class PackageBackend { final maxVersionCount = maxVersionsPerPackageOverrides[package!.name] ?? maxVersionsPerPackage; - if (package!.versionCount >= maxVersionCount) { + final remainingVersionCount = maxVersionCount - package!.versionCount; + if (remainingVersionCount <= 0) { throw PackageRejectedException.maxVersionCountReached( newVersion.package, maxVersionCount); } + if (remainingVersionCount <= 100) { + // We need to decrease the remaining version count as the newly uploaded + // version is not yet in it. + final limitAfterUpload = remainingVersionCount - 1; + final s = limitAfterUpload == 1 ? '' : 's'; + uploadMessages.add( + 'The package "${package!.name!}" has $limitAfterUpload version$s left ' + 'before reaching the limit of $maxVersionCount. ' + 'Please contact support@pub.dev'); + } if (package!.deletedVersions != null && package!.deletedVersions!.contains(newVersion.version!)) { @@ -1196,6 +1207,16 @@ class PackageBackend { await tarballStorage.copyArchiveFromCanonicalToPublicBucket( newVersion.package, newVersion.version!); + final email = createPackageUploadedEmail( + packageName: newVersion.package, + packageVersion: newVersion.version!, + displayId: agent.displayId, + authorizedUploaders: + uploaderEmails.map((email) => EmailAddress(email)).toList(), + uploadMessages: uploadMessages, + ); + final outgoingEmail = emailBackend.prepareEntity(email); + final inserts = [ package!, newVersion, @@ -1221,7 +1242,7 @@ class PackageBackend { _logger.info('Trying to commit datastore changes.'); tx.queueMutations(inserts: inserts); - return newVersion; + return (newVersion, outgoingEmail); }); _logger.info('Upload successful. [package-uploaded]'); _logger.info('Upload transaction completed in ${sw.elapsed}.'); @@ -1237,7 +1258,7 @@ class PackageBackend { .addAsyncFn(() => _postUploadTasks(package, newVersion, outgoingEmail)); _logger.info('Post-upload tasks completed in ${sw.elapsed}.'); - return pv; + return (pv, uploadMessages); } /// The post-upload tasks are not critical and could fail without any impact on diff --git a/app/lib/service/email/email_templates.dart b/app/lib/service/email/email_templates.dart index 81e0b900dc..7037964ac9 100644 --- a/app/lib/service/email/email_templates.dart +++ b/app/lib/service/email/email_templates.dart @@ -204,21 +204,21 @@ EmailMessage createPackageUploadedEmail({ required String packageVersion, required String displayId, required List authorizedUploaders, + required List uploadMessages, }) { final url = pkgPageUrl(packageName, version: packageVersion, includeHost: true); final subject = 'Package uploaded: $packageName $packageVersion'; - final bodyText = '''Dear package maintainer, - -$displayId has published a new version ($packageVersion) of the $packageName package to the Dart package site ($primaryHost). - -For details, go to $url - -${_footer('package')} -'''; - - return EmailMessage( - _notificationsFrom, authorizedUploaders, subject, bodyText); + final paragraphs = [ + 'Dear package maintainer,', + '$displayId has published a new version ($packageVersion) of the $packageName package to the Dart package site ($primaryHost).', + 'For details, go to $url', + ...uploadMessages, + _footer('package'), + ]; + + return EmailMessage(_notificationsFrom, authorizedUploaders, subject, + paragraphs.join('\n\n')); } /// Creates the [EmailMessage] that will be sent to users about new invitations diff --git a/app/test/package/upload_test.dart b/app/test/package/upload_test.dart index 608114a243..a75c74ba47 100644 --- a/app/test/package/upload_test.dart +++ b/app/test/package/upload_test.dart @@ -1238,7 +1238,30 @@ void main() { ], ), fn: () async { - packageBackend.maxVersionsPerPackage = 100; + packageBackend.maxVersionsPerPackage = 102; + + final tarball101 = await packageArchiveBytes( + pubspecContent: generatePubspecYaml('busy_pkg', '1.0.101')); + final rs101 = await createPubApiClient(authToken: adminClientToken) + .uploadPackageBytes(tarball101); + expect( + rs101.success.message, + contains( + 'The package "busy_pkg" has 1 version left before reaching the limit of 102. ' + 'Please contact support@pub.dev')); + + final tarball102 = await packageArchiveBytes( + pubspecContent: generatePubspecYaml('busy_pkg', '1.0.102')); + final rs102 = await createPubApiClient(authToken: adminClientToken) + .uploadPackageBytes(tarball102); + expect( + rs102.success.message, + contains( + 'The package "busy_pkg" has 0 versions left before reaching the limit of 102. ' + 'Please contact support@pub.dev')); + expect(fakeEmailSender.sentMessages.last.bodyText, + contains('has 0 versions left before reaching the limit')); + final tarball = await packageArchiveBytes( pubspecContent: generatePubspecYaml('busy_pkg', '2.0.0')); final rs = createPubApiClient(authToken: adminClientToken) diff --git a/app/test/service/email/email_templates_test.dart b/app/test/service/email/email_templates_test.dart index adcd973cd3..1b482fd4ad 100644 --- a/app/test/service/email/email_templates_test.dart +++ b/app/test/service/email/email_templates_test.dart @@ -169,6 +169,7 @@ void main() { EmailAddress(name: 'Joe', 'joe@example.com'), EmailAddress('uploader@example.com') ], + uploadMessages: [], ); expect(message.from.toString(), contains('@pub.dev')); expect(message.recipients.map((e) => e.toString()).toList(), [