| // 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:async'; |
| import 'dart:io'; |
| import 'dart:json'; |
| import 'dart:uri'; |
| |
| import '../../pkg/args/lib/args.dart'; |
| import '../../pkg/http/lib/http.dart' as http; |
| import '../../pkg/pathos/lib/path.dart' as path; |
| import 'directory_tree.dart'; |
| import 'exit_codes.dart' as exit_codes; |
| import 'git.dart' as git; |
| import 'http.dart'; |
| import 'io.dart'; |
| import 'log.dart' as log; |
| import 'oauth2.dart' as oauth2; |
| import 'pub.dart'; |
| import 'utils.dart'; |
| import 'validator.dart'; |
| |
| /// Handles the `lish` and `publish` pub commands. |
| class LishCommand extends PubCommand { |
| final description = "Publish the current package to pub.dartlang.org."; |
| final usage = "pub publish [options]"; |
| final aliases = const ["lish", "lush"]; |
| |
| ArgParser get commandParser { |
| var parser = new ArgParser(); |
| // TODO(nweiz): Use HostedSource.defaultUrl as the default value once we use |
| // dart:io for HTTPS requests. |
| parser.addFlag('dry-run', abbr: 'n', negatable: false, |
| help: 'Validate but do not publish the package'); |
| parser.addFlag('force', abbr: 'f', negatable: false, |
| help: 'Publish without confirmation if there are no errors'); |
| parser.addOption('server', defaultsTo: 'https://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. |
| Uri get server => Uri.parse(commandOptions['server']); |
| |
| /// Whether the publish is just a preview. |
| bool get dryRun => commandOptions['dry-run']; |
| |
| /// Whether the publish requires confirmation. |
| bool get force => commandOptions['force']; |
| |
| Future _publish(packageBytes) { |
| var cloudStorageUrl; |
| return oauth2.withClient(cache, (client) { |
| // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We |
| // should report that error and exit. |
| var newUri = server.resolve("/packages/versions/new.json"); |
| return client.get(newUri).then((response) { |
| var parameters = parseJsonResponse(response); |
| |
| var url = _expectField(parameters, 'url', response); |
| if (url is! String) invalidServerResponse(response); |
| cloudStorageUrl = Uri.parse(url); |
| var request = new http.MultipartRequest('POST', cloudStorageUrl); |
| |
| 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); |
| }).then(http.Response.fromStream).then((response) { |
| var location = response.headers['location']; |
| if (location == null) throw new PubHttpException(response); |
| return location; |
| }).then((location) => client.get(location)) |
| .then(handleJsonSuccess); |
| }).catchError((asyncError) { |
| if (asyncError.error is! PubHttpException) throw asyncError; |
| var url = asyncError.error.response.request.url; |
| if (urisEqual(url, cloudStorageUrl)) { |
| // 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.'; |
| } else if (urisEqual(Uri.parse(url.origin), Uri.parse(server.origin))) { |
| handleJsonError(asyncError.error.response); |
| } else { |
| throw asyncError; |
| } |
| }); |
| } |
| |
| Future onRun() { |
| if (force && dryRun) { |
| log.error('Cannot use both --force and --dry-run.'); |
| this.printUsage(); |
| exit(exit_codes.USAGE); |
| } |
| |
| var packageBytesFuture = _filesToPublish.then((files) { |
| log.fine('Archiving and publishing ${entrypoint.root}.'); |
| |
| // Show the package contents so the user can verify they look OK. |
| var package = entrypoint.root; |
| log.message( |
| 'Publishing "${package.name}" ${package.version}:\n' |
| '${generateTree(files)}'); |
| |
| return createTarGz(files, baseDir: entrypoint.root.dir); |
| }).then((stream) => stream.toBytes()); |
| |
| // Validate the package. |
| return _validate(packageBytesFuture.then((bytes) => bytes.length)) |
| .then((isValid) { |
| if (isValid) return packageBytesFuture.then(_publish); |
| }); |
| } |
| |
| /// The basenames of files that are automatically excluded from archives. |
| final _BLACKLISTED_FILES = const ['pubspec.lock']; |
| |
| /// The basenames of directories that are automatically excluded from |
| /// archives. |
| final _BLACKLISTED_DIRS = const ['packages']; |
| |
| /// 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 |
| /// will return all non-hidden files. |
| Future<List<String>> get _filesToPublish { |
| var rootDir = entrypoint.root.dir; |
| |
| return git.isInstalled.then((gitInstalled) { |
| if (dirExists(path.join(rootDir, '.git')) && gitInstalled) { |
| // List all files that aren't gitignored, including those not checked |
| // in to Git. |
| return git.run(["ls-files", "--cached", "--others", |
| "--exclude-standard"]); |
| } |
| |
| return listDir(rootDir, recursive: true).then((entries) { |
| return entries |
| .where(fileExists) // Skip directories. |
| .map((entry) => path.relative(entry, from: rootDir)); |
| }); |
| }).then((files) => files.where(_shouldPublish).toList()); |
| } |
| |
| /// Returns `true` if [file] should be published. |
| bool _shouldPublish(String file) { |
| if (_BLACKLISTED_FILES.contains(path.basename(file))) return false; |
| return !path.split(file).any(_BLACKLISTED_DIRS.contains); |
| } |
| |
| /// 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); |
| } |
| |
| /// Validates the package. Completes to false if the upload should not |
| /// proceed. |
| Future<bool> _validate(Future<int> packageSize) { |
| return Validator.runAll(entrypoint, packageSize).then((pair) { |
| var errors = pair.first; |
| var warnings = pair.last; |
| |
| if (!errors.isEmpty) { |
| log.error("Sorry, your package is missing " |
| "${(errors.length > 1) ? 'some requirements' : 'a requirement'} " |
| "and can't be published yet.\nFor more information, see: " |
| "http://pub.dartlang.org/doc/pub-lish.html.\n"); |
| return false; |
| } |
| |
| if (force) return true; |
| |
| if (dryRun) { |
| var s = warnings.length == 1 ? '' : 's'; |
| log.warning("Package has ${warnings.length} warning$s."); |
| return false; |
| } |
| |
| var message = 'Looks great! Are you ready to upload your package'; |
| |
| if (!warnings.isEmpty) { |
| var s = warnings.length == 1 ? '' : 's'; |
| message = "Package has ${warnings.length} warning$s. Upload anyway"; |
| } |
| |
| return confirm(message).then((confirmed) { |
| if (!confirmed) { |
| log.error("Package upload canceled."); |
| return false; |
| } |
| return true; |
| }); |
| }); |
| } |
| } |