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}'; |
+ } |
+} |