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
169 changes: 128 additions & 41 deletions app/lib/frontend/handlers/atom_feed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,38 @@ import 'package:clock/clock.dart';
import 'package:crypto/crypto.dart';
import 'package:shelf/shelf.dart' as shelf;

import '../../admin/actions/actions.dart';
import '../../package/backend.dart';
import '../../package/models.dart';
import '../../package/overrides.dart';
import '../../shared/configuration.dart';
import '../../shared/redis_cache.dart';
import '../../shared/urls.dart' as urls;
import '../../shared/utils.dart';
import '../dom/dom.dart' as d;

/// Handles requests for /feed.atom
Future<shelf.Response> atomFeedHandler(shelf.Request request) async {
/// Handles requests for `/feed.atom`
Future<shelf.Response> allPackagesAtomFeedhandler(shelf.Request request) async {
final feedContent =
await cache.atomFeedXml().get(buildAllPackagesAtomFeedContent);
await cache.allPackagesAtomFeedXml().get(buildAllPackagesAtomFeedContent);
return shelf.Response.ok(
feedContent,
headers: {
'content-type': 'application/atom+xml; charset="utf-8"',
'x-content-type-options': 'nosniff',
},
);
}

/// Handles requests for `/packages/<package>/feed.atom`
Future<shelf.Response> packageAtomFeedhandler(
shelf.Request request,
String package,
) async {
checkPackageVersionParams(package);
final feedContent = await cache
.packageAtomFeedXml(package)
.get(() => buildPackageAtomFeedContent(package));
return shelf.Response.ok(
feedContent,
headers: {
Expand All @@ -33,7 +53,26 @@ Future<shelf.Response> atomFeedHandler(shelf.Request request) async {
/// Builds the content of the /feed.atom endpoint.
Future<String> buildAllPackagesAtomFeedContent() async {
final versions = await packageBackend.latestPackageVersions(limit: 100);
final feed = _feedFromPackageVersions(versions);
versions.removeWhere((pv) => pv.isModerated || pv.isRetracted);
final feed = _allPackagesFeed(versions);
return feed.toXmlDocument();
}

/// Builds the content of the `/packages/<package>/feed.atom` endpoint.
Future<String> buildPackageAtomFeedContent(String package) async {
if (isSoftRemoved(package) ||
!await packageBackend.isPackageVisible(package)) {
throw NotFoundException.resource(package);
}
final versions = await packageBackend
.streamVersionsOfPackage(
package,
order: '-created',
limit: 10,
)
.toList();
versions.removeWhere((pv) => pv.isModerated || pv.isRetracted);
final feed = _packageFeed(package, versions);
return feed.toXmlDocument();
}

Expand All @@ -46,8 +85,15 @@ class FeedEntry {
final String alternateUrl;
final String? alternateTitle;

FeedEntry(this.id, this.title, this.updated, this.publisherId, this.content,
this.alternateUrl, this.alternateTitle);
FeedEntry({
required this.id,
required this.title,
required this.updated,
this.publisherId,
required this.content,
required this.alternateUrl,
required this.alternateTitle,
});

d.Node toNode() {
return d.element(
Expand Down Expand Up @@ -75,27 +121,33 @@ class FeedEntry {
class Feed {
final String id;
final String title;
final String subTitle;
final String? subTitle;
final DateTime updated;
final String author;
final String? author;
final String alternateUrl;
final String selfUrl;
final String generator;
final String generatorVersion;

final List<FeedEntry> entries;

Feed(
this.id,
this.title,
this.subTitle,
this.updated,
this.author,
this.alternateUrl,
this.selfUrl,
this.generator,
this.generatorVersion,
this.entries);
Feed({
required this.title,
this.subTitle,
this.author,
required this.alternateUrl,
required this.selfUrl,
this.generator = 'Pub Feed Generator',
this.generatorVersion = '0.1.0',
required this.entries,
}) : id = selfUrl,
// Set the updated timestamp to the latest version timestamp. This prevents
// unnecessary updates in the exported API bucket and makes tests consistent.
updated = entries.isNotEmpty
? entries
.map((v) => v.updated)
.reduce((a, b) => a.isAfter(b) ? a : b)
: clock.now().toUtc();

String toXmlDocument() {
final buffer = StringBuffer();
Expand All @@ -112,7 +164,8 @@ class Feed {
d.element('id', text: id),
d.element('title', text: title),
d.element('updated', text: updated.toIso8601String()),
d.element('author', child: d.element('name', text: author)),
if (author != null)
d.element('author', child: d.element('name', text: author)),
d.element(
'link',
attributes: {'href': alternateUrl, 'rel': 'alternate'},
Expand All @@ -123,14 +176,14 @@ class Feed {
attributes: {'version': generatorVersion},
text: generator,
),
d.element('subtitle', text: subTitle),
if (subTitle != null) d.element('subtitle', text: subTitle),
...entries.map((e) => e.toNode()),
],
);
}
}

Feed _feedFromPackageVersions(List<PackageVersion> versions) {
Feed _allPackagesFeed(List<PackageVersion> versions) {
final entries = <FeedEntry>[];
for (var i = 0; i < versions.length; i++) {
final version = versions[i];
Expand All @@ -145,25 +198,59 @@ Feed _feedFromPackageVersions(List<PackageVersion> versions) {
final id = createUuid(hash.bytes.sublist(0, 16));
final title = 'v${version.version} of ${version.package}';
final content = version.ellipsizedDescription ?? '[no description]';
entries.add(FeedEntry(id, title, version.created!, version.publisherId,
content, alternateUrl, alternateTitle));
entries.add(FeedEntry(
id: id,
title: title,
updated: version.created!,
publisherId: version.publisherId,
content: content,
alternateUrl: alternateUrl,
alternateTitle: alternateTitle,
));
}

final id =
activeConfiguration.primarySiteUri.resolve('/feed.atom').toString();
final selfUrl = id;

final title = 'Pub Packages for Dart';
final subTitle = 'Last Updated Packages';
final alternateUrl =
activeConfiguration.primarySiteUri.resolve('/').toString();
final author = 'Dart Team';
// Set the updated timestamp to the latest version timestamp. This prevents
// unnecessary updates in the exported API bucket and makes tests consistent.
final updated = versions.isNotEmpty
? versions.map((v) => v.created!).reduce((a, b) => a.isAfter(b) ? a : b)
: clock.now().toUtc();

return Feed(id, title, subTitle, updated, author, alternateUrl, selfUrl,
'Pub Feed Generator', '0.1.0', entries);
return Feed(
title: 'Pub Packages for Dart',
subTitle: 'Last Updated Packages',
author: 'Dart Team',
alternateUrl: activeConfiguration.primarySiteUri.resolve('/').toString(),
selfUrl:
activeConfiguration.primarySiteUri.resolve('/feed.atom').toString(),
entries: entries,
);
}

Feed _packageFeed(String package, List<PackageVersion> versions) {
return Feed(
title: 'Most recently published versions for package $package',
alternateUrl: activeConfiguration.primarySiteUri
.resolve(urls.pkgPageUrl(package))
.toString(),
subTitle: versions.firstOrNull?.ellipsizedDescription,
selfUrl: activeConfiguration.primarySiteUri
.resolve(urls.pkgFeedUrl(package))
.toString(),
author: versions.firstOrNull?.publisherId,
entries: versions.map((v) {
final hash =
sha512.convert(utf8.encode('package-feed/$package/${v.version}'));
final id = createUuid(hash.bytes.sublist(0, 16));
final alternateUrl = activeConfiguration.primarySiteUri
.replace(
path: urls.pkgPageUrl(
package,
version: v.version,
))
.toString();
return FeedEntry(
id: id,
title: 'v${v.version} of $package',
alternateUrl: alternateUrl,
alternateTitle: v.version,
content:
'${v.version} was published on ${shortDateFormat.format(v.created!)}.',
updated: v.created!,
);
}).toList(),
);
}
8 changes: 7 additions & 1 deletion app/lib/frontend/handlers/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ class PubSiteService {
Future<Response> packageVersions(Request request, String package) =>
packageVersionsListHandler(request, package);

/// Renders the Atom XML feed for the package.
@Route.get('/packages/<package>/feed.atom')
Future<Response> packageAtomFeed(Request request, String package) =>
packageAtomFeedhandler(request, package);

@Route.get('/packages/<package>')
Future<Response> package(Request request, String package) =>
packageVersionHandlerHtml(request, package);
Expand Down Expand Up @@ -261,7 +266,8 @@ class PubSiteService {

/// Renders the Atom XML feed
@Route.get('/feed.atom')
Future<Response> atomFeed(Request request) => atomFeedHandler(request);
Future<Response> allPackagesAtomFeed(Request request) =>
allPackagesAtomFeedhandler(request);

/// Renders the main help page
@Route.get('/help')
Expand Down
7 changes: 6 additions & 1 deletion app/lib/frontend/handlers/routes.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion app/lib/package/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,19 @@ class PackageBackend {
}

/// Looks up all versions of a package and return them as a [Stream].
Stream<PackageVersion> streamVersionsOfPackage(String packageName) {
Stream<PackageVersion> streamVersionsOfPackage(
String packageName, {
String? order,
int? limit,
}) {
final packageKey = db.emptyKey.append(Package, id: packageName);
final query = db.query<PackageVersion>(ancestorKey: packageKey);
if (order != null) {
query.order(order);
}
if (limit != null && limit > 0) {
query.limit(limit);
}
return query.run();
}

Expand Down Expand Up @@ -1727,13 +1737,15 @@ Future<void> purgePackageCache(String package) async {
cache.packageDataGz(package).purge(),
cache.packageLatestVersion(package).purge(),
cache.packageView(package).purge(),
cache.packageAtomFeedXml(package).purge(),
cache.uiPackagePage(package, null).purge(),
cache.uiPackageChangelog(package, null).purge(),
cache.uiPackageExample(package, null).purge(),
cache.uiPackageInstall(package, null).purge(),
cache.uiPackageScore(package, null).purge(),
cache.uiPackageVersions(package).purge(),
cache.uiIndexPage().purge(),
cache.allPackagesAtomFeedXml().purge(),
]);
}

Expand Down
7 changes: 6 additions & 1 deletion app/lib/shared/redis_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,16 @@ class CachePatterns {
decode: (d) => d as bool,
))[publisherId];

Entry<String> atomFeedXml() => _cache
Entry<String> allPackagesAtomFeedXml() => _cache
.withPrefix('atom-feed-xml/')
.withTTL(Duration(minutes: 3))
.withCodec(utf8)['/'];

Entry<String> packageAtomFeedXml(String package) => _cache
.withPrefix('package-atom-feed-xml/')
.withTTL(Duration(minutes: 10))
.withCodec(utf8)[package];

Entry<List<int>> topicNameCompletionDataJsonGz() => _cache
.withPrefix('topic-name-completion-data-json-gz/')
.withTTL(Duration(hours: 8))['-'];
Expand Down
4 changes: 4 additions & 0 deletions app/lib/shared/urls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ String pkgArchiveDownloadUrl(String package, String version, {Uri? baseUri}) {
}
}

String pkgFeedUrl(String package) {
return '/packages/$package/feed.atom';
}

String pkgDocUrl(
String package, {
String? version,
Expand Down
Loading