blob: 5719383431be7bda8b36dc8b396c423876ecb368 [file] [log] [blame]
// Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
nassh.exe.scp = function(cx) {
var scp = new nassh.Scp(cx);
scp.run().then(function(value) {
cx.closeOk(0);
}, function(err) {
cx.closeError(err);
});
return cx.ephemeralPromise;
};
nassh.exe.scp.signature = {
'help|h': '?',
'r': '?',
'p': '?',
'v': '?',
"_": '@'
};
nassh.Scp = function(cx) {
this.resolveRead_ = null;
this.readBuffer_ = '';
this.errors_ = 0;
this.remoteClosed_ = false;
this.mtimeSec_ = 0;
this.mtimeUsec_ = 0;
this.atimeSec_ = 0;
this.atimeUsec_ = 0;
this.fileMode_ = 0;
this.fileSize_ = 0;
this.fileName_ = '';
this.destIsDir_ = false;
this.destFile_ = '';
this.dirStack_ = [];
this.verbose_ = false;
this.isRecursive_ = false;
this.withTimestamp_ = false;
this.cx = cx;
};
nassh.Scp.prototype.run = function() {
var CMD_USAGE_STRING =
'usage: scp [[user]@host1:]file1 ... [[user@]host2:]file2';
this.cx.ready();
var list = this.cx.getArg('_', []);
if (list.length < 2 || this.cx.getArg('help')) {
this.cx.stderr.write(CMD_USAGE_STRING + '\n');
return this.cx.closeOk();
}
// TODO(binji): remove this code until HERE. Just a hack to support flags.
var nonFlags = [];
var i;
var j;
for (i = 0; i < list.length; ++i) {
var item = list[i];
if (item.lastIndexOf('-', 0) == 0) {
if (item[1] != '-') {
for (j = 1; j < item.length; ++j) {
if (item[j] == 'r') {
this.isRecursive_ = true;
}
if (item[j] == 'p') {
this.withTimestamp_ = true;
}
if (item[j] == 'v') {
this.verbose_ = true;
}
}
} else {
// Long flag name.
}
} else {
nonFlags.push(item);
}
}
list = nonFlags;
// HERE
if (this.cx.getArg('r')) {
this.isRecursive_ = true;
}
if (this.cx.getArg('p')) {
this.withTimestamp_ = true;
}
if (this.cx.getArg('v')) {
this.verbose_ = true;
}
var sources = list.slice(0, list.length - 1);
var dest = list[list.length - 1];
var destHostFile = this.parseHostFile_(dest);
this.destFile_ = destHostFile.file;
if (destHostFile.host != '') {
return this.toRemote_(destHostFile, sources);
} else {
return this.toLocal_(sources);
}
};
nassh.Scp.prototype.parseHostFile_ = function(arg) {
var flag = 0;
var i;
var user = '';
var host = '';
var file = arg;
if (arg[i] == ':') { // leading colon is part of file name.
return {host: host, user: user, file: file};
}
if (arg[i] == '[') {
flag = 1;
}
for (i = 0; i < arg.length; ++i) {
if (arg[i] == '@' && arg[i + 1] == '[') {
flag = 1;
}
if (arg[i] == ']' && arg[i + 1] == ':' && flag) {
host = arg.slice(0, i + 1);
file = arg.slice(i + 2);
break;
}
if (arg[i] == ':' && !flag) {
host = arg.slice(0, i);
file = arg.slice(i + 1);
break;
}
if (arg[i] == '/') {
return {host: host, user: user, file: file};
}
}
if (host == '') {
return {host: host, user: user, file: file};
}
// Check whether "host" is a fileSystem name. If so, assume that the user
// actually wants to write to a local fileSystem, and not a server with that
// name. It is always valid to wrap the hostname in (e.g. "[hostname]") if
// the other behavior is desired.
var fsm = this.cx.fileSystemManager;
var fss = fsm.getFileSystems();
for (i = 0; i < fss.length; ++i) {
if (fss[i].name == host) {
return {host: '', user: '', file: arg};
}
}
var at = host.lastIndexOf('@');
if (at != -1) {
user = host.slice(0, at);
host = host.slice(at + 1);
}
// Clean the host (i.e. remove square brackets, if any).
if (host[0] == '[' && host[host.length - 1] == ']') {
host = host.slice(1, host.length - 1);
}
return {host: host, user: user, file: file};
}
nassh.Scp.prototype.toRemote_ = function(destHostFile, sources) {
var p = this.doCmd_(this.makeCommand_(destHostFile, false))
.then(this.readResponseAndHandleError_.bind(this));
sources.forEach(function(source) {
var sourceHostFile = this.parseHostFile_(source);
if (sourceHostFile.host != '') {
// Remote to remote.
// TODO(binji): implement?
this.cx.stderr.write('Failed to copy ' + source +
'. Remote to remote not supported.\n');
return;
} else {
// Local to remote.
var sourcePath = new axiom.fs.path.Path(sourceHostFile.file);
p = p.then(function() {
return this.getPathInfo_(sourcePath);
}.bind(this))
.then(function(result) {
if (!result.exists) {
return this.errorExit('File not found: ' + sourceHostFile.file);
}
if (result.isDir && !this.isRecursive_) {
// TODO(binji): support
return this.errorExit(sourceHostFile.file +
': not a regular file');
}
if (result.isDir) {
return this.sendRemoteDirectory_(sourcePath, result.stat);
} else {
return this.sendRemoteFile_(sourcePath, result.stat);
}
}.bind(this));
}
}.bind(this));
return p;
};
nassh.Scp.prototype.makeCommand_ = function(hostFile, remoteIsSource) {
var command = [
'-x',
'-oForwardAgent=no',
'-oPermitLocalCommand=no',
'-oClearAllForwardings=yes',
];
if (hostFile.user != '') {
command.push('-l');
command.push(hostFile.user);
}
command.push('--');
command.push(hostFile.host);
command.push('scp');
command.push('-q');
if (remoteIsSource) {
command.push('-f'); // from
} else {
command.push('-t'); // to
}
if (this.verbose_) {
command.push('-v');
}
if (this.isRecursive_) {
command.push('-r');
}
if (this.withTimestamp_) {
command.push('-p');
}
command.push(hostFile.file);
return command;
};
nassh.Scp.prototype.getPathInfo_ = function(path) {
return this.cx.fileSystemManager.stat(path).then(
// stat success
function(statResult) {
return {
exists: true,
isDir: (statResult.mode & axiom.fs.path.Path.Mode.D) != 0,
stat: statResult
};
},
// stat fail
function(error) {
return {exists: false, isDir: false, stat: null};
});
};
nassh.Scp.prototype.sendRemoteDirectory_ = function(sourcePath, sourceStat) {
var p;
if (this.withTimestamp_) {
// T<mtime> 0 <atime> 0\n
// TODO(binji): The second value should be atime, but that isn't supported
// in axiom (yet?)
var timestamp = 'T' + sourceStat.mtime + ' 0 ' + sourceStat.mtime + ' 0\n';
this.remoteWrite_(timestamp);
p = this.readResponseAndHandleError_();
} else {
p = Promise.resolve();
}
return p.then(function() {
// D<mode in octal> 0 <file name>\n
// TODO(binji): read file mode when/if supported by axiom.
var control = 'D0755 0 ' + sourcePath.getBaseName() + '\n';
this.remoteWrite_(control);
return this.readResponseAndHandleError_();
}.bind(this))
.then(function() {
return this.cx.fileSystemManager.list(sourcePath);
}.bind(this))
.then(function(listResult) {
var names = Object.keys(listResult).sort();
var p = Promise.resolve();
names.forEach(function(name) {
var stat = listResult[name];
var path = sourcePath.combine(name);
if ((stat.mode & axiom.fs.path.Path.Mode.D) != 0) {
p = p.then(function() {
return this.sendRemoteDirectory_(path, stat);
}.bind(this));
} else {
p = p.then(function() {
return this.sendRemoteFile_(path, stat);
}.bind(this));
}
}.bind(this));
p = p.then(function() {
this.remoteWrite_('E\n');
return this.readResponseAndHandleError_();
}.bind(this));
return p;
}.bind(this));
};
nassh.Scp.prototype.remoteWrite_ = function(value) {
this.stdioSource.stdin.write(value);
};
nassh.Scp.prototype.readResponseAndHandleError_ = function() {
return this.readBytes_(1).then(this.handleError_.bind(this));
};
nassh.Scp.prototype.readBytes_ = function(numBytes) {
var onRead = function(success) {
if (!success) {
return Promise.resolve(null);
}
if (this.readBuffer_.length < numBytes) {
return this.read_().then(onRead);
}
var data = this.readBuffer_.slice(0, numBytes);
this.readBuffer_ = this.readBuffer_.slice(numBytes);
return Promise.resolve(data);
}.bind(this);
// Try reading from the buffer first.
return onRead(true);
};
nassh.Scp.prototype.read_ = function() {
if (this.remoteClosed_) {
if (this.readBuffer_.length > 0) {
console.error('Buffer has extra unparsed data. length ' +
this.readBuffer_.length + '\n' + this.readBuffer_);
}
return Promise.resolve(false);
}
if (this.resolveRead_ != null) {
// TODO(binji): better assert?
throw new Error('resolveRead_ not null when calling read_.');
}
return new Promise(function(resolve, reject) {
this.resolveRead_ = function(value) {
this.resolveRead_ = null;
resolve(value);
}.bind(this);
}.bind(this));
};
nassh.Scp.prototype.sendRemoteFile_ = function(sourcePath, sourceStat) {
var dataType = axiom.fs.data_type.DataType.UTF8String;
var openMode = axiom.fs.open_mode.OpenMode.fromString('r');
var readResult;
var p;
if (this.withTimestamp_) {
// T<mtime> 0 <atime> 0\n
// TODO(binji): The second value should be atime, but that isn't supported
// in axiom (yet?)
var timestamp = 'T' + sourceStat.mtime + ' 0 ' + sourceStat.mtime + ' 0\n';
this.remoteWrite_(timestamp);
p = this.readResponseAndHandleError_();
} else {
p = Promise.resolve();
}
return p.then(function() {
return this.cx.fileSystemManager.readFile(sourcePath, dataType,
openMode);
}.bind(this))
.then(function(result) {
readResult = result;
var control = 'C0644 ' + readResult.data.length + ' ' +
sourcePath.getBaseName() + '\n';
this.remoteWrite_(control);
return this.readResponseAndHandleError_();
}.bind(this))
.then(function() {
// TODO(binji): Write the file data to the plugin in smaller chunks.
// TODO(binji): Should print an error here (instead of \0) if the write
// failed.
this.remoteWrite_(readResult.data + '\0');
return this.readResponseAndHandleError_();
}.bind(this));
};
nassh.Scp.prototype.handleError_ = function(errorCode) {
if (errorCode == '\0') {
return;
}
return this.readLine_()
.then(function(line) {
line += '\n';
if (errorCode == '\u0002') {
this.cx.stderr.write(line);
return Promise.reject(line);
} else {
this.cx.stderr.write(line);
}
}.bind(this));
};
nassh.Scp.prototype.toLocal_ = function(sources) {
var destPath = new axiom.fs.path.Path(this.destFile_);
var p;
p = this.getPathInfo_(destPath)
.then(function(result) {
this.destIsDir_ = result.isDir;
if (sources.length > 1 && !this.destIsDir_) {
return Promise.reject('Expected ' + this.destFile_ +
' to be directory.');
}
}.bind(this));
sources.forEach(function(source) {
var sourceHostFile = this.parseHostFile_(source);
if (sourceHostFile.host == '') {
// Local to local.
// TODO(binji): implement via cp
this.cx.stderr.write('Failed to copy ' + sourceHostFile.file +
'. Local to local not supported.\n');
return;
} else {
// Remote to local.
p = p.then(function() {
return this.doCmd_(this.makeCommand_(sourceHostFile, true));
}.bind(this))
.then(function() {
this.remoteWrite_('\0');
}.bind(this))
.then(this.readAndParseLine_.bind(this));
}
}.bind(this));
return p;
};
nassh.Scp.prototype.readAndParseLine_ = function() {
return this.readLine_().then(this.parseLine_.bind(this));
};
nassh.Scp.prototype.doCmd_ = function(args) {
var nassh = new axiom.fs.path.Path('jsfs:/exe/nassh');
this.stdioSource = new axiom.fs.stdio_source.StdioSource();
return this.cx.fileSystemManager.createExecuteContext(
nassh, this.stdioSource.stdio, args)
.then(function(cx) {
this.sshcx = cx;
cx.dependsOn(this.cx);
cx.onClose.addListener(function(reason, value) {
this.remoteClosed_ = true;
if (this.resolveRead_) {
this.resolveRead_(false);
}
}.bind(this));
this.stdioSource.stdout.onData.addListener(function(value) {
this.readBuffer_ += value;
if (this.resolveRead_) {
this.resolveRead_(true);
}
}.bind(this));
this.stdioSource.stdout.resume();
this.stdioSource.stderr.onData.addListener(
this.cx.stderr.write.bind(this.cx.stderr));
this.stdioSource.stderr.resume();
this.stdioSource.ttyout.onData.addListener(
this.cx.stderr.write.bind(this.cx.stderr));
this.stdioSource.ttyout.resume();
this.cx.stdin.onData.addListener(
this.stdioSource.ttyin.write.bind(this.stdioSource.ttyin));
cx.execute();
}.bind(this));
};
nassh.Scp.prototype.readLine_ = function() {
var onRead = function(success) {
if (!success) {
return Promise.resolve(null);
}
var nl = this.readBuffer_.indexOf('\n');
if (nl == -1) {
return this.read_().then(onRead);
}
var line = this.readBuffer_.slice(0, nl);
this.readBuffer_ = this.readBuffer_.slice(nl + 1);
return Promise.resolve(line);
}.bind(this);
// Try reading from the buffer first.
return onRead(true);
};
nassh.Scp.prototype.parseLine_ = function(line) {
if (line == null) {
// No more data, the remote end must have closed.
return Promise.resolve();
}
var c = line[0];
var msg;
var match;
switch (c) {
case '\u0001':
case '\u0002':
// Error.
this.errors_++;
msg = line.slice(1);
this.cx.stderr.write(msg + '\n');
if (c == '\u0002') {
// Fatal error.
return Promise.reject(msg);
}
break;
case 'T':
// Timestamp
match = /T(\d+) (\d+) (\d+) (\d+)/.exec(line);
if (!match) {
return this.errorExit('unable to parse timestamp: ' + line);
}
this.mtimeSec_ = parseInt(match[1], 10);
this.mtimeUsec_ = parseInt(match[2], 10);
this.atimeSec_ = parseInt(match[3], 10);
this.atimeUsec_ = parseInt(match[4], 10);
this.remoteWrite_('\0');
break;
case 'C':
case 'D':
match = /(C|D)([0-7]+) (\d+) (.*)/.exec(line);
if (!match) {
return this.errorExit('unable to parse control line: ' + line);
}
this.fileMode_ = parseInt(match[2], 8);
this.fileSize_ = parseInt(match[3], 10);
this.fileName_ = match[4];
this.remoteWrite_('\0');
if (c == 'C') {
// Regular file
// TODO(binji): read/write file at the same time, rather than reading
// it all in at once.
return this.readBytes_(this.fileSize_)
.then(this.writeLocalFile_.bind(this))
.then(this.readResponseAndHandleError_.bind(this))
.then(function() {
this.remoteWrite_('\0');
}.bind(this))
.then(this.readAndParseLine_.bind(this));
} else {
// Directory
var path = this.makePath_();
return this.getPathInfo_(path)
.then(function(result) {
if (result.exists) {
// File exists, make sure it is a directory.
if (!result.isDir) {
return this.errorExit('expected \"' + path +
'\" to be directory');
}
} else {
// Directory doesn't exist? Try to create it.
return this.cx.fileSystemManager.mkdir(path);
}
}.bind(this))
.then(function() {
this.dirStack_.push(this.fileName_);
}.bind(this))
.then(this.readAndParseLine_.bind(this));
}
break;
case 'E':
this.dirStack_.pop();
this.remoteWrite_('\0');
break;
default:
return this.errorExit('expected control record');
}
return this.readAndParseLine_();
};
nassh.Scp.prototype.makePath_ = function() {
var path = new axiom.fs.path.Path(this.destFile_);
if (this.destIsDir_) {
if (this.dirStack_) {
path = path.combine(this.dirStack_.join('/'));
}
path = path.combine(this.fileName_);
}
return path;
};
nassh.Scp.prototype.writeLocalFile_ = function(data) {
var path = this.makePath_();
var dataType = axiom.fs.data_type.DataType.UTF8String;
var openMode = axiom.fs.open_mode.OpenMode.fromString('ctw');
// TODO(binji): Set timestamp and file mode...?
return this.cx.fileSystemManager.writeFile(path, dataType, data, openMode);
};
nassh.Scp.prototype.error = function(msg) {
msg += '\n';
this.remoteWrite_('\u0001' + msg);
this.cx.stderr.write(msg);
};
nassh.Scp.prototype.errorExit = function(msg) {
msg += '\n';
this.remoteWrite_('\u0002' + msg);
this.cx.stderr.write(msg);
// TODO(binji): Wait until msg is sent to remote end? How to do this?
return Promise.reject(msg);
};