blob: ab7cd6c0d28eedd7d5bef3697d4a75ec3eb3f1ee [file] [log] [blame]
// 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.
import 'dart:async';
import 'package:http/http.dart' as http;
import '../ascii_tree.dart' as tree;
import '../command.dart';
import '../exit_codes.dart' as exit_codes;
import '../http.dart';
import '../io.dart';
import '../log.dart' as log;
import '../oauth2.dart' as oauth2;
import '../utils.dart';
import '../validator.dart';
/// Handles the `lish` and `publish` pub commands.
class LishCommand extends PubCommand {
String get name => 'publish';
String get description => 'Publish the current package to';
String get argumentsDescription => '[options]';
String get docUrl => '';
bool get takesArguments => false;
/// The URL of the server to which to upload the package.
Uri get server {
// An explicit argument takes precedence.
if (argResults.wasParsed('server')) {
return Uri.parse(argResults['server']);
// Otherwise, use the one specified in the pubspec.
if (entrypoint.root.pubspec.publishTo != null) {
return Uri.parse(entrypoint.root.pubspec.publishTo);
// Otherwise, use the default.
return Uri.parse(cache.sources.hosted.defaultUrl);
/// Whether the publish is just a preview.
bool get dryRun => argResults['dry-run'];
/// Whether the publish requires confirmation.
bool get force => argResults['force'];
LishCommand() {
abbr: 'n',
negatable: false,
help: 'Validate but do not publish the package.');
abbr: 'f',
negatable: false,
help: 'Publish without confirmation if there are no errors.');
help: 'The package server to which to upload this package.',
hide: true);
Future<void> _publish(List<int> packageBytes) async {
Uri cloudStorageUrl;
try {
await oauth2.withClient(cache, (client) {
return log.progress('Uploading', () async {
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var newUri = server.resolve('/api/packages/versions/new');
var response = await client.get(newUri, headers: pubApiHeaders);
var parameters = parseJsonResponse(response);
var url = _expectField(parameters, 'url', response);
if (url is! String) invalidServerResponse(response);
cloudStorageUrl = Uri.parse(url);
var request = 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(http.MultipartFile.fromBytes('file', packageBytes,
filename: 'package.tar.gz'));
var postResponse =
await http.Response.fromStream(await client.send(request));
var location = postResponse.headers['location'];
if (location == null) throw PubHttpException(postResponse);
handleJsonSuccess(await client.get(location, headers: pubApiHeaders));
} on PubHttpException catch (error) {
var url = error.response.request.url;
if (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.
fail('Failed to upload the package.'));
} else if (Uri.parse(url.origin) == Uri.parse(server.origin)) {
} else {
Future runProtected() async {
if (argResults.wasParsed('server')) {
await log.warningsOnlyUnlessTerminal(() {
The --server option is deprecated. Use `publish_to` in your pubspec.yaml or set
the \$PUB_HOSTED_URL environment variable.''',
if (force && dryRun) {
usageException('Cannot use both --force and --dry-run.');
if (entrypoint.root.pubspec.isPrivate) {
dataError('A private package cannot be published.\n'
'You can enable this by changing the "publish_to" field in your '
var files = entrypoint.root.listFiles(useGitIgnore: true);
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.version} to $server:\n'
'${tree.fromFiles(files, baseDir: entrypoint.root.dir)}');
var packageBytesFuture =
createTarGz(files, baseDir: entrypoint.root.dir).toBytes();
// Validate the package.
var isValid =
await _validate(packageBytesFuture.then((bytes) => bytes.length));
if (!isValid) {
} else if (dryRun) {
} else {
await _publish(await packageBytesFuture);
/// Returns the value associated with [key] in [map]. Throws a user-friendly
/// error if [map] doesn't contain [key].
dynamic _expectField(Map map, String key, http.Response response) {
if (map.containsKey(key)) return map[key];
/// Validates the package. Completes to false if the upload should not
/// proceed.
Future<bool> _validate(Future<int> packageSize) async {
final hints = <String>[];
final warnings = <String>[];
final errors = <String>[];
await Validator.runAll(entrypoint, packageSize, server.toString(),
hints: hints, warnings: warnings, errors: errors);
if (errors.isNotEmpty) {
log.error('Sorry, your package is missing '
"${(errors.length > 1) ? 'some requirements' : 'a requirement'} "
"and can't be published yet.\nFor more information, see: "
return false;
if (force) return true;
String formatWarningCount() {
final hs = hints.length == 1 ? '' : 's';
final hintText = hints.isEmpty ? '' : ' and ${hints.length} hint$hs.';
final ws = warnings.length == 1 ? '' : 's';
return '\nPackage has ${warnings.length} warning$ws$hintText.';
if (dryRun) {
return warnings.isEmpty;
log.message('\nPublishing is forever; packages cannot be unpublished.'
'\nPolicy details are available at');
final package = entrypoint.root;
var message = 'Do you want to publish ${} ${package.version}';
if (warnings.isNotEmpty || hints.isNotEmpty) {
final warning = formatWarningCount();
message = '${log.bold(}. $message';
var confirmed = await confirm('\n$message');
if (!confirmed) {
log.error('Package upload canceled.');
return false;
return true;