OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library pubserver.shelf_pubserver; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:convert'; | |
9 | |
10 import 'package:logging/logging.dart'; | |
11 import 'package:mime/mime.dart'; | |
12 import 'package:pub_semver/pub_semver.dart'; | |
13 import 'package:shelf/shelf.dart' as shelf; | |
14 import 'package:yaml/yaml.dart'; | |
15 | |
16 import 'repository.dart'; | |
17 | |
18 final Logger _logger = new Logger('pubserver.shelf_pubserver'); | |
19 | |
20 // TODO: Error handling from [PackageRepo] class. | |
21 // Distinguish between: | |
22 // - Unauthorized Error | |
23 // - Version Already Exists Error | |
24 // - Internal Server Error | |
25 /// A shelf handler for serving a pub [PackageRepository]. | |
26 /// | |
27 /// The following API endpoints are provided by this shelf handler: | |
28 /// | |
29 /// * Getting information about all versions of a package. | |
30 /// | |
31 /// GET /api/packages/<package-name> | |
32 /// [200 OK] [Content-Type: application/json] | |
33 /// { | |
34 /// "name" : "<package-name>", | |
35 /// "latest" : { ...}, | |
36 /// "versions" : [ | |
37 /// { | |
38 /// "version" : "<version>", | |
39 /// "archive_url" : "<download-url tar.gz>", | |
40 /// "pubspec" : { | |
41 /// "author" : ..., | |
42 /// "dependencies" : { ... }, | |
43 /// ... | |
44 /// }, | |
45 /// }, | |
46 /// ... | |
47 /// ], | |
48 /// } | |
49 /// or | |
50 /// [404 Not Found] | |
51 /// | |
52 /// * Getting information about a specific (package, version) pair. | |
53 /// | |
54 /// GET /api/packages/<package-name>/versions/<version-name> | |
55 /// [200 OK] [Content-Type: application/json] | |
56 /// { | |
57 /// "version" : "<version>", | |
58 /// "archive_url" : "<download-url tar.gz>", | |
59 /// "pubspec" : { | |
60 /// "author" : ..., | |
61 /// "dependencies" : { ... }, | |
62 /// ... | |
63 /// }, | |
64 /// } | |
65 /// or | |
66 /// [404 Not Found] | |
67 /// | |
68 /// * Downloading package. | |
69 /// | |
70 /// GET /api/packages/<package-name>/versions/<version-name>.tar.gz | |
71 /// [200 OK] [Content-Type: octet-stream ??? FIXME ???] | |
72 /// or | |
73 /// [302 Found / Temporary Redirect] | |
74 /// Location: <new-location> | |
75 /// or | |
76 /// [404 Not Found] | |
77 /// | |
78 /// * Uploading | |
79 /// | |
80 /// GET /api/packages/versions/new | |
81 /// Headers: | |
82 /// Authorization: Bearer <oauth2-token> | |
83 /// [200 OK] | |
84 /// { | |
85 /// "fields" : { | |
86 /// "a": "...", | |
87 /// "b": "...", | |
88 /// ... | |
89 /// }, | |
90 /// "url" : "https://storage.googleapis.com" | |
91 /// } | |
92 /// | |
93 /// POST "https://storage.googleapis.com" | |
94 /// Headers: | |
95 /// a: ... | |
96 /// b: ... | |
97 /// ... | |
98 /// <multipart> file package.tar.gz | |
99 /// [302 Found / Temporary Redirect] | |
100 /// Location: https://pub.dartlang.org/finishUploadUrl | |
101 /// | |
102 /// GET https://pub.dartlang.org/finishUploadUrl | |
103 /// [200 OK] | |
104 /// { | |
105 /// "success" : { | |
106 /// "message": "Successfully uploaded package.", | |
107 /// }, | |
108 /// } | |
109 /// | |
110 /// It will use the pub [PackageRepository] given in the constructor to provide | |
111 /// this HTTP endpoint. | |
112 class ShelfPubServer { | |
113 static final RegExp _packageRegexp = | |
114 new RegExp(r'^/api/packages/([^/]+)$'); | |
115 | |
116 static final RegExp _versionRegexp = | |
117 new RegExp(r'^/api/packages/([^/]+)/versions/([^/]+)$'); | |
118 | |
119 static final RegExp _downloadRegexp = | |
120 new RegExp(r'^/packages/([^/]+)/versions/([^/]+)\.tar\.gz$'); | |
121 | |
122 static final RegExp _boundaryRegExp = new RegExp(r'^.*boundary="([^"]+)"$'); | |
123 | |
124 | |
125 final PackageRepository repository; | |
126 | |
127 ShelfPubServer(this.repository); | |
128 | |
129 | |
130 Future<shelf.Response> requestHandler(shelf.Request request) { | |
131 String path = request.requestedUri.path; | |
132 if (request.method == 'GET') { | |
133 var downloadMatch = _downloadRegexp.matchAsPrefix(path); | |
134 if (downloadMatch != null) { | |
135 var package = Uri.decodeComponent(downloadMatch.group(1)); | |
136 var version = Uri.decodeComponent(downloadMatch.group(2)); | |
137 return _download(request.requestedUri, package, version); | |
138 } | |
139 | |
140 var packageMatch = _packageRegexp.matchAsPrefix(path); | |
141 if (packageMatch != null) { | |
142 var package = Uri.decodeComponent(packageMatch.group(1)); | |
143 return _listVersions(request.requestedUri, package); | |
144 } | |
145 | |
146 var versionMatch = _versionRegexp.matchAsPrefix(path); | |
147 if (versionMatch != null) { | |
148 var package = Uri.decodeComponent(versionMatch.group(1)); | |
149 var version = Uri.decodeComponent(versionMatch.group(2)); | |
150 return _showVersion(request.requestedUri, package, version); | |
151 } | |
152 | |
153 if (path == '/api/packages/versions/new') { | |
154 if (!repository.supportsUpload) { | |
155 return new Future.value(new shelf.Response.notFound(null)); | |
156 } | |
157 | |
158 if (repository.supportsAsyncUpload) { | |
159 return _startUploadAsync(request.requestedUri); | |
160 } else { | |
161 return _startUploadSimple(request.requestedUri); | |
162 } | |
163 } | |
164 | |
165 if (path == '/api/packages/versions/newUploadFinish') { | |
166 if (!repository.supportsUpload) { | |
167 return new Future.value(new shelf.Response.notFound(null)); | |
168 } | |
169 | |
170 if (repository.supportsAsyncUpload) { | |
171 return _finishUploadAsync(request.requestedUri); | |
172 } else { | |
173 return _finishUploadSimple(request.requestedUri); | |
174 } | |
175 } | |
176 } else if (request.method == 'POST') { | |
177 if (!repository.supportsUpload) { | |
178 return new Future.value(new shelf.Response.notFound(null)); | |
179 } | |
180 | |
181 if (path == '/api/packages/versions/newUpload') { | |
182 return _uploadSimple( | |
183 request.requestedUri, | |
184 request.headers['content-type'], | |
185 request.read()); | |
186 } | |
187 } | |
188 return new Future.value(new shelf.Response.notFound(null)); | |
189 } | |
190 | |
191 | |
192 // Metadata handlers. | |
193 | |
194 Future<shelf.Response> _listVersions(Uri uri, String package) { | |
195 return repository.versions(package).toList() | |
196 .then((List<PackageVersion> packageVersions) { | |
197 if (packageVersions.length == 0) { | |
198 return new shelf.Response.notFound(null); | |
199 } | |
200 | |
201 packageVersions.sort((a, b) => a.version.compareTo(b.version)); | |
202 | |
203 // TODO: Add legacy entries (if necessary), such as version_url. | |
204 Map packageVersion2Json(PackageVersion version) { | |
205 return { | |
206 'archive_url': | |
207 '${_downloadUrl( | |
208 uri, version.packageName, version.versionString)}', | |
209 'pubspec': loadYaml(version.pubspecYaml), | |
210 'version': version.versionString, | |
211 }; | |
212 } | |
213 | |
214 var latestVersion = packageVersions.last; | |
215 for (int i = packageVersions.length - 1; i >= 0; i--) { | |
216 if (!packageVersions[i].version.isPreRelease) { | |
217 latestVersion = packageVersions[i]; | |
218 break; | |
219 } | |
220 } | |
221 | |
222 // TODO: The 'latest' is something we should get rid of, since it's | |
223 // duplicated in 'versions'. | |
224 return _jsonResponse({ | |
225 'name' : package, | |
226 'latest' : packageVersion2Json(latestVersion), | |
227 'versions' : packageVersions.map(packageVersion2Json).toList(), | |
228 }); | |
229 }); | |
230 } | |
231 | |
232 Future<shelf.Response> _showVersion(Uri uri, String package, String version) { | |
233 return repository | |
234 .lookupVersion(package, version).then((PackageVersion version) { | |
235 if (version == null) { | |
236 return new shelf.Response.notFound(''); | |
237 } | |
238 | |
239 // TODO: Add legacy entries (if necessary), such as version_url. | |
240 return _jsonResponse({ | |
241 'archive_url': | |
242 '${_downloadUrl( | |
243 uri, version.packageName, version.versionString)}', | |
244 'pubspec': loadYaml(version.pubspecYaml), | |
245 'version': version.versionString, | |
246 }); | |
247 }); | |
248 } | |
249 | |
250 | |
251 // Download handlers. | |
252 | |
253 Future<shelf.Response> _download(Uri uri, String package, String version) { | |
254 if (repository.supportsDownloadUrl) { | |
255 return repository.downloadUrl(package, version).then((Uri url) { | |
256 // This is a redirect to [url] | |
257 return new shelf.Response.seeOther(url); | |
258 }); | |
259 } | |
260 return repository.download(package, version).then((stream) { | |
261 return new shelf.Response.ok(stream); | |
262 }); | |
263 } | |
264 | |
265 | |
266 // Upload async handlers. | |
267 | |
268 Future<shelf.Response> _startUploadAsync(Uri uri) { | |
269 return repository.startAsyncUpload(_finishUploadAsyncUrl(uri)) | |
270 .then((AsyncUploadInfo info) { | |
271 return _jsonResponse({ | |
272 'url' : '${info.uri}', | |
273 'fields' : info.fields, | |
274 }); | |
275 }); | |
276 } | |
277 | |
278 Future<shelf.Response> _finishUploadAsync(Uri uri) { | |
279 return repository.finishAsyncUpload(uri).then((_) { | |
280 return _jsonResponse({ | |
281 'success' : { | |
282 'message' : 'Successfully uploaded package.', | |
283 }, | |
284 }); | |
285 }).catchError((error, stack) { | |
286 return _jsonResponse({ | |
287 'error' : { | |
288 'message' : '$error.', | |
289 }, | |
290 }, status: 400); | |
291 }); | |
292 } | |
293 | |
294 | |
295 // Upload custom handlers. | |
296 | |
297 Future<shelf.Response> _startUploadSimple(Uri url) { | |
298 _logger.info('Start simple upload.'); | |
299 return _jsonResponse({ | |
300 'url' : '${_uploadSimpleUrl(url)}', | |
301 'fields' : {}, | |
302 }); | |
303 } | |
304 | |
305 Future<shelf.Response> _uploadSimple( | |
306 Uri uri, String contentType, Stream<List<int>> stream) { | |
307 _logger.info('Perform simple upload.'); | |
308 if (contentType.startsWith('multipart/form-data')) { | |
309 var match = _boundaryRegExp.matchAsPrefix(contentType); | |
310 if (match != null) { | |
311 var boundary = match.group(1); | |
312 return stream | |
313 .transform(new MimeMultipartTransformer(boundary)) | |
314 .first.then((MimeMultipart part) { | |
315 // TODO: Ensure that `part.headers['content-disposition']` is | |
316 // `form-data; name="file"; filename="package.tar.gz` | |
317 return repository.upload(part).then((_) { | |
318 return new shelf.Response.found(_finishUploadSimpleUrl(uri)); | |
319 }).catchError((error, stack) { | |
320 // TODO: Do error checking and return error codes? | |
321 return new shelf.Response.found( | |
322 _finishUploadSimpleUrl(uri, error: error)); | |
323 }); | |
324 }); | |
325 } | |
326 } | |
327 return | |
328 _badRequest('Upload must contain a multipart/form-data content type.'); | |
329 } | |
330 | |
331 Future<shelf.Response> _finishUploadSimple(Uri uri) { | |
332 var error = uri.queryParameters['error']; | |
333 _logger.info('Finish simple upload (error: $error).'); | |
334 if (error != null) { | |
335 return _jsonResponse( | |
336 { 'error' : { 'message' : error } }, status: 400); | |
337 } | |
338 return _jsonResponse( | |
339 { 'success' : { 'message' : 'Successfully uploaded package.' } }); | |
340 } | |
341 | |
342 | |
343 // Helper functions. | |
344 | |
345 Future<shelf.Response> _badRequest(String message) { | |
346 return new Future.value(new shelf.Response( | |
347 400, | |
348 body: JSON.encode({ 'error' : message }), | |
349 headers: {'content-type': 'application/json'})); | |
350 } | |
351 | |
352 Future<shelf.Response> _jsonResponse(Map json, {int status: 200}) { | |
353 return new Future.sync(() { | |
354 return new shelf.Response(status, | |
355 body: JSON.encode(json), | |
356 headers: {'content-type': 'application/json'}); | |
357 }); | |
358 } | |
359 | |
360 | |
361 // Metadata urls. | |
362 | |
363 Uri _packageUrl(Uri url, String package) { | |
364 var encode = Uri.encodeComponent; | |
365 return url.resolve('/api/packages/${encode(package)}'); | |
366 } | |
367 | |
368 | |
369 // Download urls. | |
370 | |
371 Uri _downloadUrl(Uri url, String package, String version) { | |
372 var encode = Uri.encodeComponent; | |
373 return url.resolve( | |
374 '/packages/${encode(package)}/versions/${encode(version)}.tar.gz'); | |
375 } | |
376 | |
377 | |
378 // Upload async urls. | |
379 | |
380 Uri _startUploadAsyncUrl(Uri url) { | |
381 var encode = Uri.encodeComponent; | |
382 return url.resolve('/api/packages/versions/new'); | |
383 } | |
384 | |
385 Uri _finishUploadAsyncUrl(Uri url) { | |
386 var encode = Uri.encodeComponent; | |
387 return url.resolve('/api/packages/versions/newUploadFinish'); | |
388 } | |
389 | |
390 | |
391 // Upload custom urls. | |
392 | |
393 Uri _uploadSimpleUrl(Uri url) { | |
394 return url.resolve('/api/packages/versions/newUpload'); | |
395 } | |
396 | |
397 Uri _finishUploadSimpleUrl(Uri url, {String error}) { | |
398 var postfix = error == null ? '' : '?error=${Uri.encodeComponent(error)}'; | |
399 return url.resolve('/api/packages/versions/newUploadFinish$postfix'); | |
400 } | |
401 } | |
OLD | NEW |