@@ -17,10 +17,27 @@ import '../../shared/urls.dart' as urls;
1717import '../../shared/utils.dart' ;
1818import '../dom/dom.dart' as d;
1919
20- /// Handles requests for /feed.atom
21- Future <shelf.Response > atomFeedHandler (shelf.Request request) async {
20+ /// Handles requests for ` /feed.atom`
21+ Future <shelf.Response > allPackagesAtomFeedhandler (shelf.Request request) async {
2222 final feedContent =
23- await cache.atomFeedXml ().get (buildAllPackagesAtomFeedContent);
23+ await cache.allPackagesAtomFeedXml ().get (buildAllPackagesAtomFeedContent);
24+ return shelf.Response .ok (
25+ feedContent,
26+ headers: {
27+ 'content-type' : 'application/atom+xml; charset="utf-8"' ,
28+ 'x-content-type-options' : 'nosniff' ,
29+ },
30+ );
31+ }
32+
33+ /// Handles requests for `/packages/<package>/feed.atom`
34+ Future <shelf.Response > packageAtomFeedhandler (
35+ shelf.Request request,
36+ String package,
37+ ) async {
38+ final feedContent = await cache
39+ .packageAtomFeedXml (package)
40+ .get (() => buildPackageAtomFeedContent (package));
2441 return shelf.Response .ok (
2542 feedContent,
2643 headers: {
@@ -33,7 +50,20 @@ Future<shelf.Response> atomFeedHandler(shelf.Request request) async {
3350/// Builds the content of the /feed.atom endpoint.
3451Future <String > buildAllPackagesAtomFeedContent () async {
3552 final versions = await packageBackend.latestPackageVersions (limit: 100 );
36- final feed = _feedFromPackageVersions (versions);
53+ final feed = _allPackagesFeed (versions);
54+ return feed.toXmlDocument ();
55+ }
56+
57+ /// Builds the content of the `/packages/<package>/feed.atom` endpoint.
58+ Future <String > buildPackageAtomFeedContent (String package) async {
59+ final versions = await packageBackend
60+ .streamVersionsOfPackage (
61+ package,
62+ order: '-created' ,
63+ limit: 10 ,
64+ )
65+ .toList ();
66+ final feed = _packageFeed (package, versions);
3767 return feed.toXmlDocument ();
3868}
3969
@@ -46,8 +76,15 @@ class FeedEntry {
4676 final String alternateUrl;
4777 final String ? alternateTitle;
4878
49- FeedEntry (this .id, this .title, this .updated, this .publisherId, this .content,
50- this .alternateUrl, this .alternateTitle);
79+ FeedEntry ({
80+ required this .id,
81+ required this .title,
82+ required this .updated,
83+ this .publisherId,
84+ required this .content,
85+ required this .alternateUrl,
86+ required this .alternateTitle,
87+ });
5188
5289 d.Node toNode () {
5390 return d.element (
@@ -75,27 +112,33 @@ class FeedEntry {
75112class Feed {
76113 final String id;
77114 final String title;
78- final String subTitle;
115+ final String ? subTitle;
79116 final DateTime updated;
80- final String author;
117+ final String ? author;
81118 final String alternateUrl;
82119 final String selfUrl;
83120 final String generator;
84121 final String generatorVersion;
85122
86123 final List <FeedEntry > entries;
87124
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);
125+ Feed ({
126+ required this .title,
127+ this .subTitle,
128+ this .author,
129+ required this .alternateUrl,
130+ required this .selfUrl,
131+ this .generator = 'Pub Feed Generator' ,
132+ this .generatorVersion = '0.1.0' ,
133+ required this .entries,
134+ }) : id = selfUrl,
135+ // Set the updated timestamp to the latest version timestamp. This prevents
136+ // unnecessary updates in the exported API bucket and makes tests consistent.
137+ updated = entries.isNotEmpty
138+ ? entries
139+ .map ((v) => v.updated)
140+ .reduce ((a, b) => a.isAfter (b) ? a : b)
141+ : clock.now ().toUtc ();
99142
100143 String toXmlDocument () {
101144 final buffer = StringBuffer ();
@@ -112,7 +155,8 @@ class Feed {
112155 d.element ('id' , text: id),
113156 d.element ('title' , text: title),
114157 d.element ('updated' , text: updated.toIso8601String ()),
115- d.element ('author' , child: d.element ('name' , text: author)),
158+ if (author != null )
159+ d.element ('author' , child: d.element ('name' , text: author)),
116160 d.element (
117161 'link' ,
118162 attributes: {'href' : alternateUrl, 'rel' : 'alternate' },
@@ -123,14 +167,14 @@ class Feed {
123167 attributes: {'version' : generatorVersion},
124168 text: generator,
125169 ),
126- d.element ('subtitle' , text: subTitle),
170+ if (subTitle != null ) d.element ('subtitle' , text: subTitle),
127171 ...entries.map ((e) => e.toNode ()),
128172 ],
129173 );
130174 }
131175}
132176
133- Feed _feedFromPackageVersions (List <PackageVersion > versions) {
177+ Feed _allPackagesFeed (List <PackageVersion > versions) {
134178 final entries = < FeedEntry > [];
135179 for (var i = 0 ; i < versions.length; i++ ) {
136180 final version = versions[i];
@@ -145,25 +189,59 @@ Feed _feedFromPackageVersions(List<PackageVersion> versions) {
145189 final id = createUuid (hash.bytes.sublist (0 , 16 ));
146190 final title = 'v${version .version } of ${version .package }' ;
147191 final content = version.ellipsizedDescription ?? '[no description]' ;
148- entries.add (FeedEntry (id, title, version.created! , version.publisherId,
149- content, alternateUrl, alternateTitle));
192+ entries.add (FeedEntry (
193+ id: id,
194+ title: title,
195+ updated: version.created! ,
196+ publisherId: version.publisherId,
197+ content: content,
198+ alternateUrl: alternateUrl,
199+ alternateTitle: alternateTitle,
200+ ));
150201 }
151202
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);
203+ return Feed (
204+ title: 'Pub Packages for Dart' ,
205+ subTitle: 'Last Updated Packages' ,
206+ author: 'Dart Team' ,
207+ alternateUrl: activeConfiguration.primarySiteUri.resolve ('/' ).toString (),
208+ selfUrl:
209+ activeConfiguration.primarySiteUri.resolve ('/feed.atom' ).toString (),
210+ entries: entries,
211+ );
212+ }
213+
214+ Feed _packageFeed (String package, List <PackageVersion > versions) {
215+ return Feed (
216+ title: 'Most recently published versions for package $package ' ,
217+ alternateUrl: activeConfiguration.primarySiteUri
218+ .resolve (urls.pkgPageUrl (package))
219+ .toString (),
220+ subTitle: versions.firstOrNull? .ellipsizedDescription,
221+ selfUrl: activeConfiguration.primarySiteUri
222+ .resolve (urls.pkgFeedUrl (package))
223+ .toString (),
224+ author: versions.firstOrNull? .publisherId,
225+ entries: versions.map ((v) {
226+ final hash =
227+ sha512.convert (utf8.encode ('package-feed/$package /${v .version }' ));
228+ final id = createUuid (hash.bytes.sublist (0 , 16 ));
229+ final alternateUrl = activeConfiguration.primarySiteUri
230+ .replace (
231+ path: urls.pkgPageUrl (
232+ package,
233+ version: v.version,
234+ ))
235+ .toString ();
236+ return FeedEntry (
237+ id: id,
238+ title: 'v${v .version } of $package ' ,
239+ alternateUrl: alternateUrl,
240+ alternateTitle: v.version,
241+ content:
242+ '${v .version } was published on ${shortDateFormat .format (v .created !)}.' ,
243+ updated: v.created! ,
244+ );
245+ }).toList (),
246+ );
169247}
0 commit comments