| 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 |