Chromium Code Reviews| Index: utils/pub/command_lish.dart |
| diff --git a/utils/pub/command_lish.dart b/utils/pub/command_lish.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..0fb79fa4cdca7a44bbc7cfd3d066807af9fd4a38 |
| --- /dev/null |
| +++ b/utils/pub/command_lish.dart |
| @@ -0,0 +1,164 @@ |
| +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +library command_lish; |
| + |
| +import 'dart:json'; |
| +import 'dart:uri'; |
| + |
| +import '../../pkg/args/lib/args.dart'; |
| +import '../../pkg/http/lib/http.dart' as http; |
| +import 'pub.dart'; |
| +import 'io.dart'; |
| +import 'git.dart' as git; |
| +import 'oauth2.dart' as oauth2; |
| + |
| +// TODO(nweiz): make "publish" the primary name for this command. |
| +/// Handles the `lish` and `publish` pub commands. |
| +class LishCommand extends PubCommand { |
| + String get description => "publish the current package to pub.dartlang.org"; |
| + String get usage => "pub lish [options]"; |
| + |
| + ArgParser get commandParser { |
| + var parser = new ArgParser(); |
| + parser.addOption('server', defaultsTo: 'http://pub.dartlang.org', |
| + help: 'The package server to which to upload this package'); |
| + return parser; |
| + } |
| + |
| + /// The URL of the server to which to upload the package. |
|
Bob Nystrom
2012/11/26 23:39:52
Don't fear the dangling preposition. "The URL of t
nweiz
2012/11/27 20:15:54
Never. :D
Bob Nystrom
2012/11/27 21:00:53
Dangling prepositions are a grammatical offense up
|
| + Uri get server => new Uri.fromString(commandOptions['server']); |
| + |
| + Future onRun() { |
| + return oauth2.withClient(cache, (client) { |
| + // TODO(nweiz): Better error-handling. There are a few cases we need to |
| + // handle better: |
| + // |
| + // * The server can tell us we need new credentials (a 401 error). The |
| + // oauth2 package should throw an AuthorizationException in this case |
| + // (contingent on issue 6813 and 6275). We should have the user |
| + // re-authorize the client, then restart the command. We should also do |
| + // this in case of an ExpirationException. |
| + // |
| + // * Cloud Storage can provide an XML-formatted error. We should report |
| + // that error and exit. |
| + return Futures.wait([ |
| + client.get(server.resolve("/packages/versions/new.json")), |
| + _filesToPublish.transform(createTarGz).chain(consumeInputStream) |
| + ]).chain((results) { |
| + var response = results[0]; |
| + var packageBytes = results[1]; |
| + var parameters = _parseJson(response); |
| + if (response.statusCode != 200) _serverError(parameters, response); |
| + |
| + var url = _expectField(parameters, 'url', response); |
| + if (url is! String) _invalidServerResponse(response); |
|
Bob Nystrom
2012/11/26 23:39:52
How about moving the condition into the function,
nweiz
2012/11/27 20:15:54
It doesn't really seem worthwhile to move a one-li
Bob Nystrom
2012/11/27 21:00:53
Fair enough. Part of my motivation here is that I'
|
| + var request = new http.MultipartRequest( |
| + 'POST', new Uri.fromString(url)); |
| + |
| + var fields = _expectField(parameters, 'fields', response); |
| + if (fields is! Map) _invalidServerResponse(response); |
| + fields.forEach((key, value) { |
| + if (value is! String) _invalidServerResponse(response); |
| + request.fields[key] = value; |
| + }); |
| + |
| + request.followRedirects = false; |
| + request.files.add(new http.MultipartFile.fromBytes( |
| + 'file', packageBytes, filename: 'package.tar.gz')); |
| + return client.send(request); |
| + }).chain(http.Response.fromStream).chain((response) { |
| + var location = response.headers['location']; |
| + if (location == null) { |
| + // TODO(nweiz): the response may have XML-formatted information about |
| + // the error. Try to parse that out once we have an easily-accessible |
| + // XML parser. |
| + throw 'Failed to upload the package.'; |
| + } |
| + return client.get(location); |
| + }).transform((response) { |
| + var parsed = _parseJson(response); |
| + if (parsed.containsKey('error')) _serverError(parsed, response); |
| + if (parsed['success'] is! Map || |
| + !parsed['success'].containsKey('message') || |
| + parsed['success']['message'] is! String) { |
| + _invalidServerResponse(response); |
| + } |
| + print(parsed['success']['message']); |
| + }); |
| + }).transformException((e) { |
| + if (e is! oauth2.ExpirationException) throw e; |
| + |
| + printError("Pub's authorization to upload packages has expired and can't " |
| + "be automatically refreshed."); |
|
Bob Nystrom
2012/11/26 23:39:52
Can we tell the user what they need to do to solve
nweiz
2012/11/27 20:15:54
We restart the command immediately afterwards, whi
|
| + return onRun(); |
| + }); |
| + } |
| + |
| + /// Returns a list of files that should be included in the published package. |
| + /// If this is a Git repository, this will respect .gitignore; otherwise, it |
|
Bob Nystrom
2012/11/26 23:39:52
Brilliant.
|
| + /// will return all non-hidden files. |
| + Future<List<String>> get _filesToPublish { |
| + var rootDir = entrypoint.root.dir; |
| + return Futures.wait([ |
| + dirExists(join(rootDir, '.git')), |
| + git.isInstalled |
| + ]).chain((results) { |
| + if (results[0] && results[1]) { |
| + // List all files that aren't gitignored, including those not checked in |
| + // to Git. |
|
Bob Nystrom
2012/11/26 23:39:52
Should we warn if they are about to publish files
nweiz
2012/11/27 20:15:54
I don't think so. It seems like that would get rea
|
| + return git.run(["ls-files", "--cached", "--others"]); |
| + } |
| + |
| + return listDir(rootDir, recursive: true).chain((entries) { |
| + return Futures.wait(entries.map((entry) { |
| + return fileExists(entry).transform((isFile) => isFile ? entry : null); |
| + })); |
| + }).transform((files) => files.filter((file) => file != null)); |
| + }).transform((files) { |
| + var prefix = '$rootDir/'; |
| + return files.map((file) { |
|
Bob Nystrom
2012/11/26 23:39:52
What happens on Windows here?
nweiz
2012/11/27 20:15:54
As far as I can tell it should just work.
|
| + if (!file.startsWith(prefix)) return file; |
| + return file.substring(prefix.length); |
| + }); |
| + }); |
|
Bob Nystrom
2012/11/27 21:00:53
Oh, I forgot to mention this in the original revie
nweiz
2012/11/27 22:07:34
Done.
|
| + } |
| + |
| + /// Parses a response body, assuming it's JSON-formatted. Throws a |
| + /// user-friendly error if the response body is invalid JSON, or if it's not a |
| + /// map. |
| + Map _parseJson(http.Response response) { |
| + var value; |
| + try { |
| + value = JSON.parse(response.body); |
| + } catch (e) { |
|
Bob Nystrom
2012/11/26 23:39:52
Bare catches make me break out in hives. This is b
nweiz
2012/11/27 20:15:54
Done.
|
| + _invalidServerResponse(response); |
| + } |
| + if (value is! Map) _invalidServerResponse(response); |
| + return value; |
| + } |
| + |
| + /// Returns the value associated with [key] in [map]. Throws a user-friendly |
| + /// error if [map] doens't contain [key]. |
| + _expectField(Map map, String key, http.Response response) { |
| + if (map.containsKey(key)) return map[key]; |
| + _invalidServerResponse(response); |
| + } |
| + |
| + /// Extracts the error message from a JSON error sent from the server. Throws |
| + /// an appropriate error if the error map is improperly formatted. |
| + void _serverError(Map errorMap, http.Response response) { |
| + if (errorMap['error'] is! Map || |
| + !errorMap['error'].containsKey('message') || |
| + errorMap['error']['message'] is! String) { |
| + _invalidServerResponse(response); |
| + } |
| + throw errorMap['error']['message']; |
| + } |
| + |
| + /// Throws an error describing an invalid response from the server. |
| + void _invalidServerResponse(http.Response response) { |
| + throw 'Invalid server response:\n${response.body}'; |
| + } |
| +} |