@@ -9,18 +9,38 @@ import 'package:clock/clock.dart';
99import 'package:crypto/crypto.dart' ;
1010import 'package:shelf/shelf.dart' as shelf;
1111
12+ import '../../admin/actions/actions.dart' ;
1213import '../../package/backend.dart' ;
1314import '../../package/models.dart' ;
15+ import '../../package/overrides.dart' ;
1416import '../../shared/configuration.dart' ;
1517import '../../shared/redis_cache.dart' ;
1618import '../../shared/urls.dart' as urls;
1719import '../../shared/utils.dart' ;
1820import '../dom/dom.dart' as d;
1921
20- /// Handles requests for /feed.atom
21- Future <shelf.Response > atomFeedHandler (shelf.Request request) async {
22+ /// Handles requests for ` /feed.atom`
23+ Future <shelf.Response > allPackagesAtomFeedhandler (shelf.Request request) async {
2224 final feedContent =
23- await cache.atomFeedXml ().get (buildAllPackagesAtomFeedContent);
25+ await cache.allPackagesAtomFeedXml ().get (buildAllPackagesAtomFeedContent);
26+ return shelf.Response .ok (
27+ feedContent,
28+ headers: {
29+ 'content-type' : 'application/atom+xml; charset="utf-8"' ,
30+ 'x-content-type-options' : 'nosniff' ,
31+ },
32+ );
33+ }
34+
35+ /// Handles requests for `/packages/<package>/feed.atom`
36+ Future <shelf.Response > packageAtomFeedhandler (
37+ shelf.Request request,
38+ String package,
39+ ) async {
40+ checkPackageVersionParams (package);
41+ final feedContent = await cache
42+ .packageAtomFeedXml (package)
43+ .get (() => buildPackageAtomFeedContent (package));
2444 return shelf.Response .ok (
2545 feedContent,
2646 headers: {
@@ -33,7 +53,26 @@ Future<shelf.Response> atomFeedHandler(shelf.Request request) async {
3353/// Builds the content of the /feed.atom endpoint.
3454Future <String > buildAllPackagesAtomFeedContent () async {
3555 final versions = await packageBackend.latestPackageVersions (limit: 100 );
36- final feed = _feedFromPackageVersions (versions);
56+ versions.removeWhere ((pv) => pv.isModerated || pv.isRetracted);
57+ final feed = _allPackagesFeed (versions);
58+ return feed.toXmlDocument ();
59+ }
60+
61+ /// Builds the content of the `/packages/<package>/feed.atom` endpoint.
62+ Future <String > buildPackageAtomFeedContent (String package) async {
63+ if (isSoftRemoved (package) ||
64+ ! await packageBackend.isPackageVisible (package)) {
65+ throw NotFoundException .resource (package);
66+ }
67+ final versions = await packageBackend
68+ .streamVersionsOfPackage (
69+ package,
70+ order: '-created' ,
71+ limit: 10 ,
72+ )
73+ .toList ();
74+ versions.removeWhere ((pv) => pv.isModerated || pv.isRetracted);
75+ final feed = _packageFeed (package, versions);
3776 return feed.toXmlDocument ();
3877}
3978
@@ -46,8 +85,15 @@ class FeedEntry {
4685 final String alternateUrl;
4786 final String ? alternateTitle;
4887
49- FeedEntry (this .id, this .title, this .updated, this .publisherId, this .content,
50- this .alternateUrl, this .alternateTitle);
88+ FeedEntry ({
89+ required this .id,
90+ required this .title,
91+ required this .updated,
92+ this .publisherId,
93+ required this .content,
94+ required this .alternateUrl,
95+ required this .alternateTitle,
96+ });
5197
5298 d.Node toNode () {
5399 return d.element (
@@ -75,27 +121,33 @@ class FeedEntry {
75121class Feed {
76122 final String id;
77123 final String title;
78- final String subTitle;
124+ final String ? subTitle;
79125 final DateTime updated;
80- final String author;
126+ final String ? author;
81127 final String alternateUrl;
82128 final String selfUrl;
83129 final String generator;
84130 final String generatorVersion;
85131
86132 final List <FeedEntry > entries;
87133
88- Feed (
89- this .id,
90- this .title,
91- this .subTitle,
92- this .updated,
93- this .author,
94- this .alternateUrl,
95- this .selfUrl,
96- this .generator,
97- this .generatorVersion,
98- this .entries);
134+ Feed ({
135+ required this .title,
136+ this .subTitle,
137+ this .author,
138+ required this .alternateUrl,
139+ required this .selfUrl,
140+ this .generator = 'Pub Feed Generator' ,
141+ this .generatorVersion = '0.1.0' ,
142+ required this .entries,
143+ }) : id = selfUrl,
144+ // Set the updated timestamp to the latest version timestamp. This prevents
145+ // unnecessary updates in the exported API bucket and makes tests consistent.
146+ updated = entries.isNotEmpty
147+ ? entries
148+ .map ((v) => v.updated)
149+ .reduce ((a, b) => a.isAfter (b) ? a : b)
150+ : clock.now ().toUtc ();
99151
100152 String toXmlDocument () {
101153 final buffer = StringBuffer ();
@@ -112,7 +164,8 @@ class Feed {
112164 d.element ('id' , text: id),
113165 d.element ('title' , text: title),
114166 d.element ('updated' , text: updated.toIso8601String ()),
115- d.element ('author' , child: d.element ('name' , text: author)),
167+ if (author != null )
168+ d.element ('author' , child: d.element ('name' , text: author)),
116169 d.element (
117170 'link' ,
118171 attributes: {'href' : alternateUrl, 'rel' : 'alternate' },
@@ -123,14 +176,14 @@ class Feed {
123176 attributes: {'version' : generatorVersion},
124177 text: generator,
125178 ),
126- d.element ('subtitle' , text: subTitle),
179+ if (subTitle != null ) d.element ('subtitle' , text: subTitle),
127180 ...entries.map ((e) => e.toNode ()),
128181 ],
129182 );
130183 }
131184}
132185
133- Feed _feedFromPackageVersions (List <PackageVersion > versions) {
186+ Feed _allPackagesFeed (List <PackageVersion > versions) {
134187 final entries = < FeedEntry > [];
135188 for (var i = 0 ; i < versions.length; i++ ) {
136189 final version = versions[i];
@@ -145,25 +198,59 @@ Feed _feedFromPackageVersions(List<PackageVersion> versions) {
145198 final id = createUuid (hash.bytes.sublist (0 , 16 ));
146199 final title = 'v${version .version } of ${version .package }' ;
147200 final content = version.ellipsizedDescription ?? '[no description]' ;
148- entries.add (FeedEntry (id, title, version.created! , version.publisherId,
149- content, alternateUrl, alternateTitle));
201+ entries.add (FeedEntry (
202+ id: id,
203+ title: title,
204+ updated: version.created! ,
205+ publisherId: version.publisherId,
206+ content: content,
207+ alternateUrl: alternateUrl,
208+ alternateTitle: alternateTitle,
209+ ));
150210 }
151211
152- final id =
153- activeConfiguration.primarySiteUri.resolve ('/feed.atom' ).toString ();
154- final selfUrl = id;
155-
156- final title = 'Pub Packages for Dart' ;
157- final subTitle = 'Last Updated Packages' ;
158- final alternateUrl =
159- activeConfiguration.primarySiteUri.resolve ('/' ).toString ();
160- final author = 'Dart Team' ;
161- // Set the updated timestamp to the latest version timestamp. This prevents
162- // unnecessary updates in the exported API bucket and makes tests consistent.
163- final updated = versions.isNotEmpty
164- ? versions.map ((v) => v.created! ).reduce ((a, b) => a.isAfter (b) ? a : b)
165- : clock.now ().toUtc ();
166-
167- return Feed (id, title, subTitle, updated, author, alternateUrl, selfUrl,
168- 'Pub Feed Generator' , '0.1.0' , entries);
212+ return Feed (
213+ title: 'Pub Packages for Dart' ,
214+ subTitle: 'Last Updated Packages' ,
215+ author: 'Dart Team' ,
216+ alternateUrl: activeConfiguration.primarySiteUri.resolve ('/' ).toString (),
217+ selfUrl:
218+ activeConfiguration.primarySiteUri.resolve ('/feed.atom' ).toString (),
219+ entries: entries,
220+ );
221+ }
222+
223+ Feed _packageFeed (String package, List <PackageVersion > versions) {
224+ return Feed (
225+ title: 'Most recently published versions for package $package ' ,
226+ alternateUrl: activeConfiguration.primarySiteUri
227+ .resolve (urls.pkgPageUrl (package))
228+ .toString (),
229+ subTitle: versions.firstOrNull? .ellipsizedDescription,
230+ selfUrl: activeConfiguration.primarySiteUri
231+ .resolve (urls.pkgFeedUrl (package))
232+ .toString (),
233+ author: versions.firstOrNull? .publisherId,
234+ entries: versions.map ((v) {
235+ final hash =
236+ sha512.convert (utf8.encode ('package-feed/$package /${v .version }' ));
237+ final id = createUuid (hash.bytes.sublist (0 , 16 ));
238+ final alternateUrl = activeConfiguration.primarySiteUri
239+ .replace (
240+ path: urls.pkgPageUrl (
241+ package,
242+ version: v.version,
243+ ))
244+ .toString ();
245+ return FeedEntry (
246+ id: id,
247+ title: 'v${v .version } of $package ' ,
248+ alternateUrl: alternateUrl,
249+ alternateTitle: v.version,
250+ content:
251+ '${v .version } was published on ${shortDateFormat .format (v .created !)}.' ,
252+ updated: v.created! ,
253+ );
254+ }).toList (),
255+ );
169256}
0 commit comments