@@ -9,18 +9,38 @@ import 'package:clock/clock.dart';
9
9
import 'package:crypto/crypto.dart' ;
10
10
import 'package:shelf/shelf.dart' as shelf;
11
11
12
+ import '../../admin/actions/actions.dart' ;
12
13
import '../../package/backend.dart' ;
13
14
import '../../package/models.dart' ;
15
+ import '../../package/overrides.dart' ;
14
16
import '../../shared/configuration.dart' ;
15
17
import '../../shared/redis_cache.dart' ;
16
18
import '../../shared/urls.dart' as urls;
17
19
import '../../shared/utils.dart' ;
18
20
import '../dom/dom.dart' as d;
19
21
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 {
22
24
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));
24
44
return shelf.Response .ok (
25
45
feedContent,
26
46
headers: {
@@ -33,7 +53,26 @@ Future<shelf.Response> atomFeedHandler(shelf.Request request) async {
33
53
/// Builds the content of the /feed.atom endpoint.
34
54
Future <String > buildAllPackagesAtomFeedContent () async {
35
55
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);
37
76
return feed.toXmlDocument ();
38
77
}
39
78
@@ -46,8 +85,15 @@ class FeedEntry {
46
85
final String alternateUrl;
47
86
final String ? alternateTitle;
48
87
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
+ });
51
97
52
98
d.Node toNode () {
53
99
return d.element (
@@ -75,27 +121,33 @@ class FeedEntry {
75
121
class Feed {
76
122
final String id;
77
123
final String title;
78
- final String subTitle;
124
+ final String ? subTitle;
79
125
final DateTime updated;
80
- final String author;
126
+ final String ? author;
81
127
final String alternateUrl;
82
128
final String selfUrl;
83
129
final String generator;
84
130
final String generatorVersion;
85
131
86
132
final List <FeedEntry > entries;
87
133
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 ();
99
151
100
152
String toXmlDocument () {
101
153
final buffer = StringBuffer ();
@@ -112,7 +164,8 @@ class Feed {
112
164
d.element ('id' , text: id),
113
165
d.element ('title' , text: title),
114
166
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)),
116
169
d.element (
117
170
'link' ,
118
171
attributes: {'href' : alternateUrl, 'rel' : 'alternate' },
@@ -123,14 +176,14 @@ class Feed {
123
176
attributes: {'version' : generatorVersion},
124
177
text: generator,
125
178
),
126
- d.element ('subtitle' , text: subTitle),
179
+ if (subTitle != null ) d.element ('subtitle' , text: subTitle),
127
180
...entries.map ((e) => e.toNode ()),
128
181
],
129
182
);
130
183
}
131
184
}
132
185
133
- Feed _feedFromPackageVersions (List <PackageVersion > versions) {
186
+ Feed _allPackagesFeed (List <PackageVersion > versions) {
134
187
final entries = < FeedEntry > [];
135
188
for (var i = 0 ; i < versions.length; i++ ) {
136
189
final version = versions[i];
@@ -145,25 +198,59 @@ Feed _feedFromPackageVersions(List<PackageVersion> versions) {
145
198
final id = createUuid (hash.bytes.sublist (0 , 16 ));
146
199
final title = 'v${version .version } of ${version .package }' ;
147
200
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
+ ));
150
210
}
151
211
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
+ );
169
256
}
0 commit comments