mirror of https://github.com/jkjoy/sunpeiwen.git
2968 lines
88 KiB
JavaScript
2968 lines
88 KiB
JavaScript
// TODO: support EXTENDED request packets
|
|
|
|
var TransformStream = require('stream').Transform;
|
|
var ReadableStream = require('stream').Readable;
|
|
var WritableStream = require('stream').Writable;
|
|
var constants = require('fs').constants || process.binding('constants');
|
|
var util = require('util');
|
|
var inherits = util.inherits;
|
|
var isDate = util.isDate;
|
|
var listenerCount = require('events').EventEmitter.listenerCount;
|
|
var fs = require('fs');
|
|
|
|
var readString = require('./utils').readString;
|
|
var readInt = require('./utils').readInt;
|
|
|
|
var ATTR = {
|
|
SIZE: 0x00000001,
|
|
UIDGID: 0x00000002,
|
|
PERMISSIONS: 0x00000004,
|
|
ACMODTIME: 0x00000008,
|
|
EXTENDED: 0x80000000
|
|
};
|
|
|
|
var STATUS_CODE = {
|
|
OK: 0,
|
|
EOF: 1,
|
|
NO_SUCH_FILE: 2,
|
|
PERMISSION_DENIED: 3,
|
|
FAILURE: 4,
|
|
BAD_MESSAGE: 5,
|
|
NO_CONNECTION: 6,
|
|
CONNECTION_LOST: 7,
|
|
OP_UNSUPPORTED: 8
|
|
};
|
|
Object.keys(STATUS_CODE).forEach(function(key) {
|
|
STATUS_CODE[STATUS_CODE[key]] = key;
|
|
});
|
|
var STATUS_CODE_STR = {
|
|
0: 'No error',
|
|
1: 'End of file',
|
|
2: 'No such file or directory',
|
|
3: 'Permission denied',
|
|
4: 'Failure',
|
|
5: 'Bad message',
|
|
6: 'No connection',
|
|
7: 'Connection lost',
|
|
8: 'Operation unsupported'
|
|
};
|
|
SFTPStream.STATUS_CODE = STATUS_CODE;
|
|
|
|
var REQUEST = {
|
|
INIT: 1,
|
|
OPEN: 3,
|
|
CLOSE: 4,
|
|
READ: 5,
|
|
WRITE: 6,
|
|
LSTAT: 7,
|
|
FSTAT: 8,
|
|
SETSTAT: 9,
|
|
FSETSTAT: 10,
|
|
OPENDIR: 11,
|
|
READDIR: 12,
|
|
REMOVE: 13,
|
|
MKDIR: 14,
|
|
RMDIR: 15,
|
|
REALPATH: 16,
|
|
STAT: 17,
|
|
RENAME: 18,
|
|
READLINK: 19,
|
|
SYMLINK: 20,
|
|
EXTENDED: 200
|
|
};
|
|
Object.keys(REQUEST).forEach(function(key) {
|
|
REQUEST[REQUEST[key]] = key;
|
|
});
|
|
|
|
var RESPONSE = {
|
|
VERSION: 2,
|
|
STATUS: 101,
|
|
HANDLE: 102,
|
|
DATA: 103,
|
|
NAME: 104,
|
|
ATTRS: 105,
|
|
EXTENDED: 201
|
|
};
|
|
Object.keys(RESPONSE).forEach(function(key) {
|
|
RESPONSE[RESPONSE[key]] = key;
|
|
});
|
|
|
|
var OPEN_MODE = {
|
|
READ: 0x00000001,
|
|
WRITE: 0x00000002,
|
|
APPEND: 0x00000004,
|
|
CREAT: 0x00000008,
|
|
TRUNC: 0x00000010,
|
|
EXCL: 0x00000020
|
|
};
|
|
SFTPStream.OPEN_MODE = OPEN_MODE;
|
|
|
|
var MAX_PKT_LEN = 34000;
|
|
var MAX_REQID = Math.pow(2, 32) - 1;
|
|
var CLIENT_VERSION_BUFFER = new Buffer([0, 0, 0, 5 /* length */,
|
|
REQUEST.INIT,
|
|
0, 0, 0, 3 /* version */]);
|
|
var SERVER_VERSION_BUFFER = new Buffer([0, 0, 0, 5 /* length */,
|
|
RESPONSE.VERSION,
|
|
0, 0, 0, 3 /* version */]);
|
|
/*
|
|
http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02:
|
|
|
|
The maximum size of a packet is in practice determined by the client
|
|
(the maximum size of read or write requests that it sends, plus a few
|
|
bytes of packet overhead). All servers SHOULD support packets of at
|
|
least 34000 bytes (where the packet size refers to the full length,
|
|
including the header above). This should allow for reads and writes
|
|
of at most 32768 bytes.
|
|
|
|
OpenSSH caps this to 256kb instead of the ~34kb as mentioned in the sftpv3
|
|
spec.
|
|
*/
|
|
var RE_OPENSSH = /^SSH-2.0-(?:OpenSSH|dropbear)/;
|
|
var OPENSSH_MAX_DATA_LEN = (256 * 1024) - (2 * 1024)/*account for header data*/;
|
|
|
|
function DEBUG_NOOP(msg) {}
|
|
|
|
function SFTPStream(cfg, remoteIdentRaw) {
|
|
if (typeof cfg === 'string' && !remoteIdentRaw) {
|
|
remoteIdentRaw = cfg;
|
|
cfg = undefined;
|
|
}
|
|
if (typeof cfg !== 'object' || !cfg)
|
|
cfg = {};
|
|
|
|
TransformStream.call(this, {
|
|
highWaterMark: (typeof cfg.highWaterMark === 'number'
|
|
? cfg.highWaterMark
|
|
: 32 * 1024)
|
|
});
|
|
|
|
this.debug = (typeof cfg.debug === 'function' ? cfg.debug : DEBUG_NOOP);
|
|
this.server = (cfg.server ? true : false);
|
|
this._isOpenSSH = (remoteIdentRaw && RE_OPENSSH.test(remoteIdentRaw));
|
|
this._needContinue = false;
|
|
this._state = {
|
|
// common
|
|
status: 'packet_header',
|
|
writeReqid: -1,
|
|
pktLeft: undefined,
|
|
pktHdrBuf: new Buffer(9), // room for pktLen + pktType + req id
|
|
pktBuf: undefined,
|
|
pktType: undefined,
|
|
version: undefined,
|
|
extensions: {},
|
|
|
|
// client
|
|
maxDataLen: (this._isOpenSSH ? OPENSSH_MAX_DATA_LEN : 32768),
|
|
requests: {}
|
|
};
|
|
|
|
var self = this;
|
|
this.on('end', function() {
|
|
self.readable = false;
|
|
}).on('finish', onFinish)
|
|
.on('prefinish', onFinish);
|
|
function onFinish() {
|
|
self.writable = false;
|
|
self._cleanup(false);
|
|
}
|
|
|
|
if (!this.server)
|
|
this.push(CLIENT_VERSION_BUFFER);
|
|
}
|
|
inherits(SFTPStream, TransformStream);
|
|
|
|
SFTPStream.prototype.__read = TransformStream.prototype._read;
|
|
SFTPStream.prototype._read = function(n) {
|
|
if (this._needContinue) {
|
|
this._needContinue = false;
|
|
this.emit('continue');
|
|
}
|
|
return this.__read(n);
|
|
};
|
|
SFTPStream.prototype.__push = TransformStream.prototype.push;
|
|
SFTPStream.prototype.push = function(chunk, encoding) {
|
|
if (!this.readable)
|
|
return false;
|
|
if (chunk === null)
|
|
this.readable = false;
|
|
var ret = this.__push(chunk, encoding);
|
|
this._needContinue = (ret === false);
|
|
return ret;
|
|
};
|
|
|
|
SFTPStream.prototype._cleanup = function(callback) {
|
|
var state = this._state;
|
|
|
|
state.pktBuf = undefined; // give GC something to do
|
|
|
|
var requests = state.requests;
|
|
var keys = Object.keys(requests);
|
|
var len = keys.length;
|
|
if (len) {
|
|
if (this.readable) {
|
|
var err = new Error('SFTP session ended early');
|
|
for (var i = 0, cb; i < len; ++i)
|
|
(cb = requests[keys[i]].cb) && cb(err);
|
|
}
|
|
state.requests = {};
|
|
}
|
|
|
|
if (this.readable)
|
|
this.push(null);
|
|
if (!this._readableState.endEmitted && !this._readableState.flowing) {
|
|
// Ugh!
|
|
this.resume();
|
|
}
|
|
if (callback !== false) {
|
|
this.debug('DEBUG[SFTP]: Parser: Malformed packet');
|
|
callback && callback(new Error('Malformed packet'));
|
|
}
|
|
};
|
|
|
|
SFTPStream.prototype._transform = function(chunk, encoding, callback) {
|
|
var state = this._state;
|
|
var server = this.server;
|
|
var status = state.status;
|
|
var pktType = state.pktType;
|
|
var pktBuf = state.pktBuf;
|
|
var pktLeft = state.pktLeft;
|
|
var version = state.version;
|
|
var pktHdrBuf = state.pktHdrBuf;
|
|
var requests = state.requests;
|
|
var debug = this.debug;
|
|
var chunkLen = chunk.length;
|
|
var chunkPos = 0;
|
|
var buffer;
|
|
var chunkLeft;
|
|
var id;
|
|
|
|
while (true) {
|
|
if (status === 'discard') {
|
|
chunkLeft = (chunkLen - chunkPos);
|
|
if (pktLeft <= chunkLeft) {
|
|
chunkPos += pktLeft;
|
|
pktLeft = 0;
|
|
status = 'packet_header';
|
|
buffer = pktBuf = undefined;
|
|
} else {
|
|
pktLeft -= chunkLeft;
|
|
break;
|
|
}
|
|
} else if (pktBuf !== undefined) {
|
|
chunkLeft = (chunkLen - chunkPos);
|
|
if (pktLeft <= chunkLeft) {
|
|
chunk.copy(pktBuf,
|
|
pktBuf.length - pktLeft,
|
|
chunkPos,
|
|
chunkPos + pktLeft);
|
|
chunkPos += pktLeft;
|
|
pktLeft = 0;
|
|
buffer = pktBuf;
|
|
pktBuf = undefined;
|
|
continue;
|
|
} else {
|
|
chunk.copy(pktBuf, pktBuf.length - pktLeft, chunkPos);
|
|
pktLeft -= chunkLeft;
|
|
break;
|
|
}
|
|
} else if (status === 'packet_header') {
|
|
if (!buffer) {
|
|
pktLeft = 5;
|
|
pktBuf = pktHdrBuf;
|
|
} else {
|
|
// here we read the right-most 5 bytes from buffer (pktHdrBuf)
|
|
pktLeft = buffer.readUInt32BE(4, true) - 1; // account for type byte
|
|
pktType = buffer[8];
|
|
|
|
if (server) {
|
|
if (version === undefined && pktType !== REQUEST.INIT) {
|
|
debug('DEBUG[SFTP]: Parser: Unexpected packet before init');
|
|
this._cleanup(false);
|
|
return callback(new Error('Unexpected packet before init'));
|
|
} else if (version !== undefined && pktType === REQUEST.INIT) {
|
|
debug('DEBUG[SFTP]: Parser: Unexpected duplicate init');
|
|
status = 'bad_pkt';
|
|
} else if (pktLeft > MAX_PKT_LEN) {
|
|
var msg = 'Packet length ('
|
|
+ pktLeft
|
|
+ ') exceeds max length ('
|
|
+ MAX_PKT_LEN
|
|
+ ')';
|
|
debug('DEBUG[SFTP]: Parser: ' + msg);
|
|
this._cleanup(false);
|
|
return callback(new Error(msg));
|
|
} else if (pktType === REQUEST.EXTENDED) {
|
|
status = 'bad_pkt';
|
|
} else if (REQUEST[pktType] === undefined) {
|
|
debug('DEBUG[SFTP]: Parser: Unsupported packet type: ' + pktType);
|
|
status = 'discard';
|
|
}
|
|
} else if (version === undefined && pktType !== RESPONSE.VERSION) {
|
|
debug('DEBUG[SFTP]: Parser: Unexpected packet before version');
|
|
this._cleanup(false);
|
|
return callback(new Error('Unexpected packet before version'));
|
|
} else if (version !== undefined && pktType === RESPONSE.VERSION) {
|
|
debug('DEBUG[SFTP]: Parser: Unexpected duplicate version');
|
|
status = 'bad_pkt';
|
|
} else if (RESPONSE[pktType] === undefined) {
|
|
status = 'discard';
|
|
}
|
|
|
|
if (status === 'bad_pkt') {
|
|
// copy original packet info
|
|
pktHdrBuf.writeUInt32BE(pktLeft, 0, true);
|
|
pktHdrBuf[4] = pktType;
|
|
|
|
pktLeft = 4;
|
|
pktBuf = pktHdrBuf;
|
|
} else {
|
|
pktBuf = new Buffer(pktLeft);
|
|
status = 'payload';
|
|
}
|
|
}
|
|
} else if (status === 'payload') {
|
|
if (pktType === RESPONSE.VERSION || pktType === REQUEST.INIT) {
|
|
/*
|
|
uint32 version
|
|
<extension data>
|
|
*/
|
|
version = state.version = readInt(buffer, 0, this, callback);
|
|
if (version === false)
|
|
return;
|
|
if (version < 3) {
|
|
this._cleanup(false);
|
|
return callback(new Error('Incompatible SFTP version: ' + version));
|
|
} else if (server)
|
|
this.push(SERVER_VERSION_BUFFER);
|
|
|
|
var buflen = buffer.length;
|
|
var extname;
|
|
var extdata;
|
|
buffer._pos = 4;
|
|
while (buffer._pos < buflen) {
|
|
extname = readString(buffer, buffer._pos, 'ascii', this, callback);
|
|
if (extname === false)
|
|
return;
|
|
extdata = readString(buffer, buffer._pos, 'ascii', this, callback);
|
|
if (extdata === false)
|
|
return;
|
|
if (state.extensions[extname])
|
|
state.extensions[extname].push(extdata);
|
|
else
|
|
state.extensions[extname] = [ extdata ];
|
|
}
|
|
|
|
this.emit('ready');
|
|
} else {
|
|
/*
|
|
All other packets (client and server) begin with a (client) request
|
|
id:
|
|
uint32 id
|
|
*/
|
|
id = readInt(buffer, 0, this, callback);
|
|
if (id === false)
|
|
return;
|
|
|
|
var filename;
|
|
var attrs;
|
|
var handle;
|
|
var data;
|
|
|
|
if (!server) {
|
|
var req = requests[id];
|
|
var cb = req && req.cb;
|
|
debug('DEBUG[SFTP]: Parser: Response: ' + RESPONSE[pktType]);
|
|
if (req && cb) {
|
|
if (pktType === RESPONSE.STATUS) {
|
|
/*
|
|
uint32 error/status code
|
|
string error message (ISO-10646 UTF-8)
|
|
string language tag
|
|
*/
|
|
var code = readInt(buffer, 4, this, callback);
|
|
if (code === false)
|
|
return;
|
|
if (code === STATUS_CODE.OK) {
|
|
cb();
|
|
} else {
|
|
// We borrow OpenSSH behavior here, specifically we make the
|
|
// message and language fields optional, despite the
|
|
// specification requiring them (even if they are empty). This
|
|
// helps to avoid problems with buggy implementations that do
|
|
// not fully conform to the SFTP(v3) specification.
|
|
var msg;
|
|
var lang = '';
|
|
if (buffer.length >= 12) {
|
|
msg = readString(buffer, 8, 'utf8', this, callback);
|
|
if (msg === false)
|
|
return;
|
|
if ((buffer._pos + 4) < buffer.length) {
|
|
lang = readString(buffer,
|
|
buffer._pos,
|
|
'ascii',
|
|
this,
|
|
callback);
|
|
if (lang === false)
|
|
return;
|
|
}
|
|
}
|
|
var err = new Error(msg
|
|
|| STATUS_CODE_STR[code]
|
|
|| 'Unknown status');
|
|
err.code = code;
|
|
err.lang = lang;
|
|
cb(err);
|
|
}
|
|
} else if (pktType === RESPONSE.HANDLE) {
|
|
/*
|
|
string handle
|
|
*/
|
|
handle = readString(buffer, 4, this, callback);
|
|
if (handle === false)
|
|
return;
|
|
cb(undefined, handle);
|
|
} else if (pktType === RESPONSE.DATA) {
|
|
/*
|
|
string data
|
|
*/
|
|
if (req.buffer) {
|
|
// we have already pre-allocated space to store the data
|
|
var dataLen = readInt(buffer, 4, this, callback);
|
|
if (dataLen === false)
|
|
return;
|
|
var reqBufLen = req.buffer.length;
|
|
if (dataLen > reqBufLen) {
|
|
// truncate response data to fit expected size
|
|
buffer.writeUInt32BE(reqBufLen, 4, true);
|
|
}
|
|
data = readString(buffer, 4, req.buffer, this, callback);
|
|
if (data === false)
|
|
return;
|
|
cb(undefined, data, dataLen);
|
|
} else {
|
|
data = readString(buffer, 4, this, callback);
|
|
if (data === false)
|
|
return;
|
|
cb(undefined, data);
|
|
}
|
|
} else if (pktType === RESPONSE.NAME) {
|
|
/*
|
|
uint32 count
|
|
repeats count times:
|
|
string filename
|
|
string longname
|
|
ATTRS attrs
|
|
*/
|
|
var namesLen = readInt(buffer, 4, this, callback);
|
|
if (namesLen === false)
|
|
return;
|
|
var names = [],
|
|
longname;
|
|
buffer._pos = 8;
|
|
for (var i = 0; i < namesLen; ++i) {
|
|
// we are going to assume UTF-8 for filenames despite the SFTPv3
|
|
// spec not specifying an encoding because the specs for newer
|
|
// versions of the protocol all explicitly specify UTF-8 for
|
|
// filenames
|
|
filename = readString(buffer,
|
|
buffer._pos,
|
|
'utf8',
|
|
this,
|
|
callback);
|
|
if (filename === false)
|
|
return;
|
|
// `longname` only exists in SFTPv3 and since it typically will
|
|
// contain the filename, we assume it is also UTF-8
|
|
longname = readString(buffer,
|
|
buffer._pos,
|
|
'utf8',
|
|
this,
|
|
callback);
|
|
if (longname === false)
|
|
return;
|
|
attrs = readAttrs(buffer, buffer._pos, this, callback);
|
|
if (attrs === false)
|
|
return;
|
|
names.push({
|
|
filename: filename,
|
|
longname: longname,
|
|
attrs: attrs
|
|
});
|
|
}
|
|
cb(undefined, names);
|
|
} else if (pktType === RESPONSE.ATTRS) {
|
|
/*
|
|
ATTRS attrs
|
|
*/
|
|
attrs = readAttrs(buffer, 4, this, callback);
|
|
if (attrs === false)
|
|
return;
|
|
cb(undefined, attrs);
|
|
} else if (pktType === RESPONSE.EXTENDED) {
|
|
if (req.extended) {
|
|
switch (req.extended) {
|
|
case 'statvfs@openssh.com':
|
|
case 'fstatvfs@openssh.com':
|
|
/*
|
|
uint64 f_bsize // file system block size
|
|
uint64 f_frsize // fundamental fs block size
|
|
uint64 f_blocks // number of blocks (unit f_frsize)
|
|
uint64 f_bfree // free blocks in file system
|
|
uint64 f_bavail // free blocks for non-root
|
|
uint64 f_files // total file inodes
|
|
uint64 f_ffree // free file inodes
|
|
uint64 f_favail // free file inodes for to non-root
|
|
uint64 f_fsid // file system id
|
|
uint64 f_flag // bit mask of f_flag values
|
|
uint64 f_namemax // maximum filename length
|
|
*/
|
|
var stats = {
|
|
f_bsize: undefined,
|
|
f_frsize: undefined,
|
|
f_blocks: undefined,
|
|
f_bfree: undefined,
|
|
f_bavail: undefined,
|
|
f_files: undefined,
|
|
f_ffree: undefined,
|
|
f_favail: undefined,
|
|
f_sid: undefined,
|
|
f_flag: undefined,
|
|
f_namemax: undefined
|
|
};
|
|
stats.f_bsize = readUInt64BE(buffer, 4, this, callback);
|
|
if (stats.f_bsize === false)
|
|
return;
|
|
stats.f_frsize = readUInt64BE(buffer, 12, this, callback);
|
|
if (stats.f_frsize === false)
|
|
return;
|
|
stats.f_blocks = readUInt64BE(buffer, 20, this, callback);
|
|
if (stats.f_blocks === false)
|
|
return;
|
|
stats.f_bfree = readUInt64BE(buffer, 28, this, callback);
|
|
if (stats.f_bfree === false)
|
|
return;
|
|
stats.f_bavail = readUInt64BE(buffer, 36, this, callback);
|
|
if (stats.f_bavail === false)
|
|
return;
|
|
stats.f_files = readUInt64BE(buffer, 44, this, callback);
|
|
if (stats.f_files === false)
|
|
return;
|
|
stats.f_ffree = readUInt64BE(buffer, 52, this, callback);
|
|
if (stats.f_ffree === false)
|
|
return;
|
|
stats.f_favail = readUInt64BE(buffer, 60, this, callback);
|
|
if (stats.f_favail === false)
|
|
return;
|
|
stats.f_sid = readUInt64BE(buffer, 68, this, callback);
|
|
if (stats.f_sid === false)
|
|
return;
|
|
stats.f_flag = readUInt64BE(buffer, 76, this, callback);
|
|
if (stats.f_flag === false)
|
|
return;
|
|
stats.f_namemax = readUInt64BE(buffer, 84, this, callback);
|
|
if (stats.f_namemax === false)
|
|
return;
|
|
cb(undefined, stats);
|
|
break;
|
|
}
|
|
}
|
|
// XXX: at least provide the raw buffer data to the callback in
|
|
// case of unexpected extended response?
|
|
cb();
|
|
}
|
|
}
|
|
if (req)
|
|
delete requests[id];
|
|
} else {
|
|
// server
|
|
var evName = REQUEST[pktType];
|
|
var offset;
|
|
var path;
|
|
|
|
debug('DEBUG[SFTP]: Parser: Request: ' + evName);
|
|
if (listenerCount(this, evName)) {
|
|
if (pktType === REQUEST.OPEN) {
|
|
/*
|
|
string filename
|
|
uint32 pflags
|
|
ATTRS attrs
|
|
*/
|
|
filename = readString(buffer, 4, 'utf8', this, callback);
|
|
if (filename === false)
|
|
return;
|
|
var pflags = readInt(buffer, buffer._pos, this, callback);
|
|
if (pflags === false)
|
|
return;
|
|
attrs = readAttrs(buffer, buffer._pos + 4, this, callback);
|
|
if (attrs === false)
|
|
return;
|
|
this.emit(evName, id, filename, pflags, attrs);
|
|
} else if (pktType === REQUEST.CLOSE
|
|
|| pktType === REQUEST.FSTAT
|
|
|| pktType === REQUEST.READDIR) {
|
|
/*
|
|
string handle
|
|
*/
|
|
handle = readString(buffer, 4, this, callback);
|
|
if (handle === false)
|
|
return;
|
|
this.emit(evName, id, handle);
|
|
} else if (pktType === REQUEST.READ) {
|
|
/*
|
|
string handle
|
|
uint64 offset
|
|
uint32 len
|
|
*/
|
|
handle = readString(buffer, 4, this, callback);
|
|
if (handle === false)
|
|
return;
|
|
offset = readUInt64BE(buffer, buffer._pos, this, callback);
|
|
if (offset === false)
|
|
return;
|
|
var len = readInt(buffer, buffer._pos, this, callback);
|
|
if (len === false)
|
|
return;
|
|
this.emit(evName, id, handle, offset, len);
|
|
} else if (pktType === REQUEST.WRITE) {
|
|
/*
|
|
string handle
|
|
uint64 offset
|
|
string data
|
|
*/
|
|
handle = readString(buffer, 4, this, callback);
|
|
if (handle === false)
|
|
return;
|
|
offset = readUInt64BE(buffer, buffer._pos, this, callback);
|
|
if (offset === false)
|
|
return;
|
|
data = readString(buffer, buffer._pos, this, callback);
|
|
if (data === false)
|
|
return;
|
|
this.emit(evName, id, handle, offset, data);
|
|
} else if (pktType === REQUEST.LSTAT
|
|
|| pktType === REQUEST.STAT
|
|
|| pktType === REQUEST.OPENDIR
|
|
|| pktType === REQUEST.REMOVE
|
|
|| pktType === REQUEST.RMDIR
|
|
|| pktType === REQUEST.REALPATH
|
|
|| pktType === REQUEST.READLINK) {
|
|
/*
|
|
string path
|
|
*/
|
|
path = readString(buffer, 4, 'utf8', this, callback);
|
|
if (path === false)
|
|
return;
|
|
this.emit(evName, id, path);
|
|
} else if (pktType === REQUEST.SETSTAT
|
|
|| pktType === REQUEST.MKDIR) {
|
|
/*
|
|
string path
|
|
ATTRS attrs
|
|
*/
|
|
path = readString(buffer, 4, 'utf8', this, callback);
|
|
if (path === false)
|
|
return;
|
|
attrs = readAttrs(buffer, buffer._pos, this, callback);
|
|
if (attrs === false)
|
|
return;
|
|
this.emit(evName, id, path, attrs);
|
|
} else if (pktType === REQUEST.FSETSTAT) {
|
|
/*
|
|
string handle
|
|
ATTRS attrs
|
|
*/
|
|
handle = readString(buffer, 4, this, callback);
|
|
if (handle === false)
|
|
return;
|
|
attrs = readAttrs(buffer, buffer._pos, this, callback);
|
|
if (attrs === false)
|
|
return;
|
|
this.emit(evName, id, handle, attrs);
|
|
} else if (pktType === REQUEST.RENAME
|
|
|| pktType === REQUEST.SYMLINK) {
|
|
/*
|
|
RENAME:
|
|
string oldpath
|
|
string newpath
|
|
SYMLINK:
|
|
string linkpath
|
|
string targetpath
|
|
*/
|
|
var str1;
|
|
var str2;
|
|
str1 = readString(buffer, 4, 'utf8', this, callback);
|
|
if (str1 === false)
|
|
return;
|
|
str2 = readString(buffer, buffer._pos, 'utf8', this, callback);
|
|
if (str2 === false)
|
|
return;
|
|
if (pktType === REQUEST.SYMLINK && this._isOpenSSH) {
|
|
// OpenSSH has linkpath and targetpath positions switched
|
|
this.emit(evName, id, str2, str1);
|
|
} else
|
|
this.emit(evName, id, str1, str2);
|
|
}
|
|
} else {
|
|
// automatically reject request if no handler for request type
|
|
this.status(id, STATUS_CODE.OP_UNSUPPORTED);
|
|
}
|
|
}
|
|
}
|
|
|
|
// prepare for next packet
|
|
status = 'packet_header';
|
|
buffer = pktBuf = undefined;
|
|
} else if (status === 'bad_pkt') {
|
|
if (server && buffer[4] !== REQUEST.INIT) {
|
|
var errCode = (buffer[4] === REQUEST.EXTENDED
|
|
? STATUS_CODE.OP_UNSUPPORTED
|
|
: STATUS_CODE.FAILURE);
|
|
|
|
// no request id for init/version packets, so we have no way to send a
|
|
// status response, so we just close up shop ...
|
|
if (buffer[4] === REQUEST.INIT || buffer[4] === RESPONSE.VERSION)
|
|
return this._cleanup(callback);
|
|
|
|
id = readInt(buffer, 5, this, callback);
|
|
if (id === false)
|
|
return;
|
|
this.status(id, errCode);
|
|
}
|
|
|
|
// by this point we have already read the type byte and the id bytes, so
|
|
// we subtract those from the number of bytes to skip
|
|
pktLeft = buffer.readUInt32BE(0, true) - 5;
|
|
|
|
status = 'discard';
|
|
}
|
|
|
|
if (chunkPos >= chunkLen)
|
|
break;
|
|
}
|
|
|
|
state.status = status;
|
|
state.pktType = pktType;
|
|
state.pktBuf = pktBuf;
|
|
state.pktLeft = pktLeft;
|
|
state.version = version;
|
|
|
|
callback();
|
|
};
|
|
|
|
// client
|
|
SFTPStream.prototype.createReadStream = function(path, options) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
return new ReadStream(this, path, options);
|
|
};
|
|
SFTPStream.prototype.createWriteStream = function(path, options) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
return new WriteStream(this, path, options);
|
|
};
|
|
SFTPStream.prototype.open = function(path, flags_, attrs, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
if (typeof attrs === 'function') {
|
|
cb = attrs;
|
|
attrs = undefined;
|
|
}
|
|
|
|
var flags = stringToFlags(flags_);
|
|
if (flags === null)
|
|
throw new Error('Unknown flags string: ' + flags_);
|
|
|
|
var attrFlags = 0;
|
|
var attrBytes = 0;
|
|
if (typeof attrs === 'string' || typeof attrs === 'number') {
|
|
attrs = { mode: attrs };
|
|
}
|
|
if (typeof attrs === 'object') {
|
|
attrs = attrsToBytes(attrs);
|
|
attrFlags = attrs.flags;
|
|
attrBytes = attrs.nbytes;
|
|
attrs = attrs.bytes;
|
|
}
|
|
|
|
/*
|
|
uint32 id
|
|
string filename
|
|
uint32 pflags
|
|
ATTRS attrs
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen + 4 + 4 + attrBytes);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.OPEN;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
buf.writeUInt32BE(flags, p += pathlen, true);
|
|
buf.writeUInt32BE(attrFlags, p += 4, true);
|
|
if (attrs && attrFlags) {
|
|
p += 4;
|
|
for (var i = 0, len = attrs.length; i < len; ++i)
|
|
for (var j = 0, len2 = attrs[i].length; j < len2; ++j)
|
|
buf[p++] = attrs[i][j];
|
|
}
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing OPEN');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.close = function(handle, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string handle
|
|
*/
|
|
var handlelen = handle.length;
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + handlelen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.CLOSE;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(handlelen, p, true);
|
|
handle.copy(buf, p += 4);
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing CLOSE');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.readData = function(handle, buf, off, len, position, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
else if (!Buffer.isBuffer(buf))
|
|
throw new Error('buffer is not a Buffer');
|
|
else if (off >= buf.length)
|
|
throw new Error('offset is out of bounds');
|
|
else if (off + len > buf.length)
|
|
throw new Error('length extends beyond buffer');
|
|
else if (position === null)
|
|
throw new Error('null position currently unsupported');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string handle
|
|
uint64 offset
|
|
uint32 len
|
|
*/
|
|
var handlelen = handle.length;
|
|
var p = 9;
|
|
var pos = position;
|
|
var out = new Buffer(4 + 1 + 4 + 4 + handlelen + 8 + 4);
|
|
|
|
out.writeUInt32BE(out.length - 4, 0, true);
|
|
out[4] = REQUEST.READ;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
out.writeUInt32BE(reqid, 5, true);
|
|
|
|
out.writeUInt32BE(handlelen, p, true);
|
|
handle.copy(out, p += 4);
|
|
p += handlelen;
|
|
for (var i = 7; i >= 0; --i) {
|
|
out[p + i] = pos & 0xFF;
|
|
pos /= 256;
|
|
}
|
|
out.writeUInt32BE(len, p += 8, true);
|
|
|
|
state.requests[reqid] = {
|
|
cb: function(err, data, nb) {
|
|
if (err && err.code !== STATUS_CODE.EOF)
|
|
return cb(err);
|
|
cb(undefined, nb || 0, data, position);
|
|
},
|
|
buffer: buf.slice(off, off + len)
|
|
};
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing READ');
|
|
return this.push(out);
|
|
};
|
|
SFTPStream.prototype.writeData = function(handle, buf, off, len, position, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
else if (!Buffer.isBuffer(buf))
|
|
throw new Error('buffer is not a Buffer');
|
|
else if (off > buf.length)
|
|
throw new Error('offset is out of bounds');
|
|
else if (off + len > buf.length)
|
|
throw new Error('length extends beyond buffer');
|
|
else if (position === null)
|
|
throw new Error('null position currently unsupported');
|
|
|
|
var self = this;
|
|
var state = this._state;
|
|
|
|
if (!len) {
|
|
cb && process.nextTick(function() { cb(undefined, 0); });
|
|
return;
|
|
}
|
|
|
|
var overflow = (len > state.maxDataLen
|
|
? len - state.maxDataLen
|
|
: 0);
|
|
var origPosition = position;
|
|
|
|
if (overflow)
|
|
len = state.maxDataLen;
|
|
|
|
/*
|
|
uint32 id
|
|
string handle
|
|
uint64 offset
|
|
string data
|
|
*/
|
|
var handlelen = handle.length;
|
|
var p = 9;
|
|
var out = new Buffer(4 + 1 + 4 + 4 + handlelen + 8 + 4 + len);
|
|
|
|
out.writeUInt32BE(out.length - 4, 0, true);
|
|
out[4] = REQUEST.WRITE;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
out.writeUInt32BE(reqid, 5, true);
|
|
|
|
out.writeUInt32BE(handlelen, p, true);
|
|
handle.copy(out, p += 4);
|
|
p += handlelen;
|
|
for (var i = 7; i >= 0; --i) {
|
|
out[p + i] = position & 0xFF;
|
|
position /= 256;
|
|
}
|
|
out.writeUInt32BE(len, p += 8, true);
|
|
buf.copy(out, p += 4, off, off + len);
|
|
|
|
state.requests[reqid] = {
|
|
cb: function(err) {
|
|
if (err)
|
|
cb && cb(err);
|
|
else if (overflow) {
|
|
self.writeData(handle,
|
|
buf,
|
|
off + len,
|
|
overflow,
|
|
origPosition + len,
|
|
cb);
|
|
} else
|
|
cb && cb(undefined, off + len);
|
|
}
|
|
};
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing WRITE');
|
|
return this.push(out);
|
|
};
|
|
function tryCreateBuffer(size) {
|
|
try {
|
|
return new Buffer(size);
|
|
} catch (ex) {
|
|
return ex;
|
|
}
|
|
}
|
|
function fastXfer(src, dst, srcPath, dstPath, opts, cb) {
|
|
var concurrency = 64;
|
|
var chunkSize = 32768;
|
|
//var preserve = false;
|
|
var onstep;
|
|
var mode;
|
|
|
|
if (typeof opts === 'function') {
|
|
cb = opts;
|
|
} else if (typeof opts === 'object') {
|
|
if (typeof opts.concurrency === 'number'
|
|
&& opts.concurrency > 0
|
|
&& !isNaN(opts.concurrency))
|
|
concurrency = opts.concurrency;
|
|
if (typeof opts.chunkSize === 'number'
|
|
&& opts.chunkSize > 0
|
|
&& !isNaN(opts.chunkSize))
|
|
chunkSize = opts.chunkSize;
|
|
if (typeof opts.step === 'function')
|
|
onstep = opts.step;
|
|
//preserve = (opts.preserve ? true : false);
|
|
if (typeof opts.mode === 'string' || typeof opts.mode === 'number')
|
|
mode = modeNum(opts.mode);
|
|
}
|
|
|
|
// internal state variables
|
|
var fsize;
|
|
var chunk;
|
|
var psrc = 0;
|
|
var pdst = 0;
|
|
var reads = 0;
|
|
var total = 0;
|
|
var hadError = false;
|
|
var srcHandle;
|
|
var dstHandle;
|
|
var readbuf;
|
|
var bufsize = chunkSize * concurrency;
|
|
|
|
function onerror(err) {
|
|
if (hadError)
|
|
return;
|
|
|
|
hadError = true;
|
|
|
|
var left = 0;
|
|
var cbfinal;
|
|
|
|
if (srcHandle || dstHandle) {
|
|
cbfinal = function() {
|
|
if (--left === 0)
|
|
cb(err);
|
|
};
|
|
if (srcHandle && (src === fs || src.writable))
|
|
++left;
|
|
if (dstHandle && (dst === fs || dst.writable))
|
|
++left;
|
|
if (srcHandle && (src === fs || src.writable))
|
|
src.close(srcHandle, cbfinal);
|
|
if (dstHandle && (dst === fs || dst.writable))
|
|
dst.close(dstHandle, cbfinal);
|
|
} else
|
|
cb(err);
|
|
}
|
|
|
|
src.open(srcPath, 'r', function(err, sourceHandle) {
|
|
if (err)
|
|
return onerror(err);
|
|
|
|
srcHandle = sourceHandle;
|
|
|
|
src.fstat(srcHandle, function tryStat(err, attrs) {
|
|
if (err) {
|
|
if (src !== fs) {
|
|
// Try stat() for sftp servers that may not support fstat() for
|
|
// whatever reason
|
|
src.stat(srcPath, function(err_, attrs_) {
|
|
if (err_)
|
|
return onerror(err);
|
|
tryStat(null, attrs_);
|
|
});
|
|
return;
|
|
}
|
|
return onerror(err);
|
|
}
|
|
fsize = attrs.size;
|
|
|
|
dst.open(dstPath, 'w', function(err, destHandle) {
|
|
if (err)
|
|
return onerror(err);
|
|
|
|
dstHandle = destHandle;
|
|
|
|
if (fsize <= 0)
|
|
return onerror();
|
|
|
|
// Use less memory where possible
|
|
while (bufsize > fsize) {
|
|
if (concurrency === 1) {
|
|
bufsize = fsize;
|
|
break;
|
|
}
|
|
bufsize -= chunkSize;
|
|
--concurrency;
|
|
}
|
|
|
|
readbuf = tryCreateBuffer(bufsize);
|
|
if (readbuf instanceof Error)
|
|
return onerror(readbuf);
|
|
|
|
if (mode !== undefined) {
|
|
dst.fchmod(dstHandle, mode, function tryAgain(err) {
|
|
if (err) {
|
|
// Try chmod() for sftp servers that may not support fchmod() for
|
|
// whatever reason
|
|
dst.chmod(dstPath, mode, function(err_) {
|
|
tryAgain();
|
|
});
|
|
return;
|
|
}
|
|
read();
|
|
});
|
|
} else {
|
|
read();
|
|
}
|
|
|
|
function onread(err, nb, data, dstpos, datapos) {
|
|
if (err)
|
|
return onerror(err);
|
|
|
|
if (src === fs)
|
|
dst.writeData(dstHandle, data, datapos || 0, nb, dstpos, writeCb);
|
|
else
|
|
dst.write(dstHandle, data, datapos || 0, nb, dstpos, writeCb);
|
|
|
|
function writeCb(err) {
|
|
if (err)
|
|
return onerror(err);
|
|
|
|
total += nb;
|
|
onstep && onstep(total, nb, fsize);
|
|
|
|
if (--reads === 0) {
|
|
if (total === fsize) {
|
|
dst.close(dstHandle, function(err) {
|
|
dstHandle = undefined;
|
|
if (err)
|
|
return onerror(err);
|
|
src.close(srcHandle, function(err) {
|
|
srcHandle = undefined;
|
|
if (err)
|
|
return onerror(err);
|
|
cb();
|
|
});
|
|
});
|
|
} else
|
|
read();
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeCb(psrc, pdst) {
|
|
return function(err, nb, data) {
|
|
onread(err, nb, data, pdst, psrc);
|
|
};
|
|
}
|
|
|
|
function read() {
|
|
while (pdst < fsize && reads < concurrency) {
|
|
chunk = (pdst + chunkSize > fsize ? fsize - pdst : chunkSize);
|
|
if (src === fs) {
|
|
src.read(srcHandle,
|
|
readbuf,
|
|
psrc,
|
|
chunk,
|
|
pdst,
|
|
makeCb(psrc, pdst));
|
|
} else
|
|
src.readData(srcHandle, readbuf, psrc, chunk, pdst, onread);
|
|
psrc += chunk;
|
|
pdst += chunk;
|
|
++reads;
|
|
}
|
|
psrc = 0;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
SFTPStream.prototype.fastGet = function(remotePath, localPath, opts, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
fastXfer(this, fs, remotePath, localPath, opts, cb);
|
|
};
|
|
SFTPStream.prototype.fastPut = function(localPath, remotePath, opts, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
fastXfer(fs, this, localPath, remotePath, opts, cb);
|
|
};
|
|
SFTPStream.prototype.readFile = function(path, options, callback_) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var callback;
|
|
if (typeof callback_ === 'function') {
|
|
callback = callback_;
|
|
} else if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
|
|
var self = this;
|
|
|
|
if (typeof options === 'string')
|
|
options = { encoding: options, flag: 'r' };
|
|
else if (!options)
|
|
options = { encoding: null, flag: 'r' };
|
|
else if (typeof options !== 'object')
|
|
throw new TypeError('Bad arguments');
|
|
|
|
var encoding = options.encoding;
|
|
if (encoding && !Buffer.isEncoding(encoding))
|
|
throw new Error('Unknown encoding: ' + encoding);
|
|
|
|
// first, stat the file, so we know the size.
|
|
var size;
|
|
var buffer; // single buffer with file data
|
|
var buffers; // list for when size is unknown
|
|
var pos = 0;
|
|
var handle;
|
|
|
|
// SFTPv3 does not support using -1 for read position, so we have to track
|
|
// read position manually
|
|
var bytesRead = 0;
|
|
|
|
var flag = options.flag || 'r';
|
|
this.open(path, flag, 438 /*=0666*/, function(er, handle_) {
|
|
if (er)
|
|
return callback && callback(er);
|
|
handle = handle_;
|
|
|
|
self.fstat(handle, function tryStat(er, st) {
|
|
if (er) {
|
|
// Try stat() for sftp servers that may not support fstat() for
|
|
// whatever reason
|
|
self.stat(path, function(er_, st_) {
|
|
if (er_) {
|
|
return self.close(handle, function() {
|
|
callback && callback(er);
|
|
});
|
|
}
|
|
tryStat(null, st_);
|
|
});
|
|
return;
|
|
}
|
|
|
|
size = st.size || 0;
|
|
if (size === 0) {
|
|
// the kernel lies about many files.
|
|
// Go ahead and try to read some bytes.
|
|
buffers = [];
|
|
return read();
|
|
}
|
|
|
|
buffer = new Buffer(size);
|
|
read();
|
|
});
|
|
});
|
|
|
|
function read() {
|
|
if (size === 0) {
|
|
buffer = new Buffer(8192);
|
|
self.readData(handle, buffer, 0, 8192, bytesRead, afterRead);
|
|
} else
|
|
self.readData(handle, buffer, pos, size - pos, bytesRead, afterRead);
|
|
}
|
|
|
|
function afterRead(er, nbytes) {
|
|
if (er) {
|
|
return self.close(handle, function() {
|
|
return callback && callback(er);
|
|
});
|
|
}
|
|
|
|
if (nbytes === 0)
|
|
return close();
|
|
|
|
bytesRead += nbytes;
|
|
pos += nbytes;
|
|
if (size !== 0) {
|
|
if (pos === size)
|
|
close();
|
|
else
|
|
read();
|
|
} else {
|
|
// unknown size, just read until we don't get bytes.
|
|
buffers.push(buffer.slice(0, nbytes));
|
|
read();
|
|
}
|
|
}
|
|
|
|
function close() {
|
|
self.close(handle, function(er) {
|
|
if (size === 0) {
|
|
// collected the data into the buffers list.
|
|
buffer = Buffer.concat(buffers, pos);
|
|
} else if (pos < size)
|
|
buffer = buffer.slice(0, pos);
|
|
|
|
if (encoding)
|
|
buffer = buffer.toString(encoding);
|
|
return callback && callback(er, buffer);
|
|
});
|
|
}
|
|
};
|
|
function writeAll(self, handle, buffer, offset, length, position, callback_) {
|
|
var callback = (typeof callback_ === 'function' ? callback_ : undefined);
|
|
|
|
self.writeData(handle,
|
|
buffer,
|
|
offset,
|
|
length,
|
|
position,
|
|
function(writeErr, written) {
|
|
if (writeErr) {
|
|
return self.close(handle, function() {
|
|
callback && callback(writeErr);
|
|
});
|
|
}
|
|
if (written === length)
|
|
self.close(handle, callback);
|
|
else {
|
|
offset += written;
|
|
length -= written;
|
|
position += written;
|
|
writeAll(self, handle, buffer, offset, length, position, callback);
|
|
}
|
|
});
|
|
}
|
|
SFTPStream.prototype.writeFile = function(path, data, options, callback_) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var callback;
|
|
if (typeof callback_ === 'function') {
|
|
callback = callback_;
|
|
} else if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
var self = this;
|
|
|
|
if (typeof options === 'string')
|
|
options = { encoding: options, mode: 438, flag: 'w' };
|
|
else if (!options)
|
|
options = { encoding: 'utf8', mode: 438 /*=0666*/, flag: 'w' };
|
|
else if (typeof options !== 'object')
|
|
throw new TypeError('Bad arguments');
|
|
|
|
if (options.encoding && !Buffer.isEncoding(options.encoding))
|
|
throw new Error('Unknown encoding: ' + options.encoding);
|
|
|
|
var flag = options.flag || 'w';
|
|
this.open(path, flag, options.mode, function(openErr, handle) {
|
|
if (openErr)
|
|
callback && callback(openErr);
|
|
else {
|
|
var buffer = (Buffer.isBuffer(data)
|
|
? data
|
|
: new Buffer('' + data, options.encoding || 'utf8'));
|
|
var position = (/a/.test(flag) ? null : 0);
|
|
|
|
// SFTPv3 does not support the notion of 'current position'
|
|
// (null position), so we just attempt to append to the end of the file
|
|
// instead
|
|
if (position === null) {
|
|
self.fstat(handle, function tryStat(er, st) {
|
|
if (er) {
|
|
// Try stat() for sftp servers that may not support fstat() for
|
|
// whatever reason
|
|
self.stat(path, function(er_, st_) {
|
|
if (er_) {
|
|
return self.close(handle, function() {
|
|
callback && callback(er);
|
|
});
|
|
}
|
|
tryStat(null, st_);
|
|
});
|
|
return;
|
|
}
|
|
writeAll(self, handle, buffer, 0, buffer.length, st.size, callback);
|
|
});
|
|
return;
|
|
}
|
|
writeAll(self, handle, buffer, 0, buffer.length, position, callback);
|
|
}
|
|
});
|
|
};
|
|
SFTPStream.prototype.appendFile = function(path, data, options, callback_) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var callback;
|
|
if (typeof callback_ === 'function') {
|
|
callback = callback_;
|
|
} else if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
|
|
if (typeof options === 'string')
|
|
options = { encoding: options, mode: 438, flag: 'a' };
|
|
else if (!options)
|
|
options = { encoding: 'utf8', mode: 438 /*=0666*/, flag: 'a' };
|
|
else if (typeof options !== 'object')
|
|
throw new TypeError('Bad arguments');
|
|
|
|
if (!options.flag)
|
|
options = util._extend({ flag: 'a' }, options);
|
|
this.writeFile(path, data, options, callback);
|
|
};
|
|
SFTPStream.prototype.exists = function(path, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
this.stat(path, function(err) {
|
|
cb && cb(err ? false : true);
|
|
});
|
|
};
|
|
SFTPStream.prototype.unlink = function(filename, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string filename
|
|
*/
|
|
var fnamelen = Buffer.byteLength(filename);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + fnamelen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.REMOVE;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(fnamelen, p, true);
|
|
buf.write(filename, p += 4, fnamelen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing REMOVE');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.rename = function(oldPath, newPath, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string oldpath
|
|
string newpath
|
|
*/
|
|
var oldlen = Buffer.byteLength(oldPath);
|
|
var newlen = Buffer.byteLength(newPath);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + oldlen + 4 + newlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.RENAME;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(oldlen, p, true);
|
|
buf.write(oldPath, p += 4, oldlen, 'utf8');
|
|
buf.writeUInt32BE(newlen, p += oldlen, true);
|
|
buf.write(newPath, p += 4, newlen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing RENAME');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.mkdir = function(path, attrs, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var flags = 0;
|
|
var attrBytes = 0;
|
|
var state = this._state;
|
|
|
|
if (typeof attrs === 'function') {
|
|
cb = attrs;
|
|
attrs = undefined;
|
|
}
|
|
if (typeof attrs === 'object') {
|
|
attrs = attrsToBytes(attrs);
|
|
flags = attrs.flags;
|
|
attrBytes = attrs.nbytes;
|
|
attrs = attrs.bytes;
|
|
}
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
ATTRS attrs
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen + 4 + attrBytes);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.MKDIR;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
buf.writeUInt32BE(flags, p += pathlen);
|
|
if (flags) {
|
|
p += 4;
|
|
for (var i = 0, len = attrs.length; i < len; ++i)
|
|
for (var j = 0, len2 = attrs[i].length; j < len2; ++j)
|
|
buf[p++] = attrs[i][j];
|
|
}
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing MKDIR');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.rmdir = function(path, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.RMDIR;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing RMDIR');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.readdir = function(where, opts, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
var doFilter;
|
|
|
|
if (typeof opts === 'function') {
|
|
cb = opts;
|
|
opts = {};
|
|
}
|
|
if (typeof opts !== 'object')
|
|
opts = {};
|
|
|
|
doFilter = (opts && opts.full ? false : true);
|
|
|
|
if (!Buffer.isBuffer(where) && typeof where !== 'string')
|
|
throw new Error('missing directory handle or path');
|
|
|
|
if (typeof where === 'string') {
|
|
var self = this;
|
|
var entries = [];
|
|
var e = 0;
|
|
|
|
return this.opendir(where, function reread(err, handle) {
|
|
if (err)
|
|
return cb(err);
|
|
|
|
self.readdir(handle, opts, function(err, list) {
|
|
var eof = (err && err.code === STATUS_CODE.EOF);
|
|
|
|
if (err && !eof) {
|
|
return self.close(handle, function() {
|
|
cb(err);
|
|
});
|
|
} else if (eof) {
|
|
return self.close(handle, function(err) {
|
|
if (err)
|
|
return cb(err);
|
|
cb(undefined, entries);
|
|
});
|
|
}
|
|
|
|
for (var i = 0, len = list.length; i < len; ++i, ++e)
|
|
entries[e] = list[i];
|
|
|
|
reread(undefined, handle);
|
|
});
|
|
});
|
|
}
|
|
|
|
/*
|
|
uint32 id
|
|
string handle
|
|
*/
|
|
var handlelen = where.length;
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + handlelen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.READDIR;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(handlelen, p, true);
|
|
where.copy(buf, p += 4);
|
|
|
|
state.requests[reqid] = {
|
|
cb: (doFilter
|
|
? function(err, list) {
|
|
if (err)
|
|
return cb(err);
|
|
|
|
for (var i = list.length - 1; i >= 0; --i) {
|
|
if (list[i].filename === '.' || list[i].filename === '..')
|
|
list.splice(i, 1);
|
|
}
|
|
|
|
cb(undefined, list);
|
|
}
|
|
: cb)
|
|
};
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing READDIR');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.fstat = function(handle, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string handle
|
|
*/
|
|
var handlelen = handle.length;
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + handlelen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.FSTAT;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(handlelen, p, true);
|
|
handle.copy(buf, p += 4);
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing FSTAT');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.stat = function(path, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.STAT;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing STAT');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.lstat = function(path, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.LSTAT;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing LSTAT');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.opendir = function(path, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.OPENDIR;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing OPENDIR');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.setstat = function(path, attrs, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var flags = 0;
|
|
var attrBytes = 0;
|
|
var state = this._state;
|
|
|
|
if (typeof attrs === 'object') {
|
|
attrs = attrsToBytes(attrs);
|
|
flags = attrs.flags;
|
|
attrBytes = attrs.nbytes;
|
|
attrs = attrs.bytes;
|
|
} else if (typeof attrs === 'function')
|
|
cb = attrs;
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
ATTRS attrs
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen + 4 + attrBytes);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.SETSTAT;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
buf.writeUInt32BE(flags, p += pathlen);
|
|
if (flags) {
|
|
p += 4;
|
|
for (var i = 0, len = attrs.length; i < len; ++i)
|
|
for (var j = 0, len2 = attrs[i].length; j < len2; ++j)
|
|
buf[p++] = attrs[i][j];
|
|
}
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing SETSTAT');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.fsetstat = function(handle, attrs, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
|
|
var flags = 0;
|
|
var attrBytes = 0;
|
|
var state = this._state;
|
|
|
|
if (typeof attrs === 'object') {
|
|
attrs = attrsToBytes(attrs);
|
|
flags = attrs.flags;
|
|
attrBytes = attrs.nbytes;
|
|
attrs = attrs.bytes;
|
|
} else if (typeof attrs === 'function')
|
|
cb = attrs;
|
|
|
|
/*
|
|
uint32 id
|
|
string handle
|
|
ATTRS attrs
|
|
*/
|
|
var handlelen = handle.length;
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + handlelen + 4 + attrBytes);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.FSETSTAT;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(handlelen, p, true);
|
|
handle.copy(buf, p += 4);
|
|
buf.writeUInt32BE(flags, p += handlelen);
|
|
if (flags) {
|
|
p += 4;
|
|
for (var i = 0, len = attrs.length; i < len; ++i)
|
|
for (var j = 0, len2 = attrs[i].length; j < len2; ++j)
|
|
buf[p++] = attrs[i][j];
|
|
}
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing FSETSTAT');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.futimes = function(handle, atime, mtime, cb) {
|
|
return this.fsetstat(handle, {
|
|
atime: toUnixTimestamp(atime),
|
|
mtime: toUnixTimestamp(mtime)
|
|
}, cb);
|
|
};
|
|
SFTPStream.prototype.utimes = function(path, atime, mtime, cb) {
|
|
return this.setstat(path, {
|
|
atime: toUnixTimestamp(atime),
|
|
mtime: toUnixTimestamp(mtime)
|
|
}, cb);
|
|
};
|
|
SFTPStream.prototype.fchown = function(handle, uid, gid, cb) {
|
|
return this.fsetstat(handle, {
|
|
uid: uid,
|
|
gid: gid
|
|
}, cb);
|
|
};
|
|
SFTPStream.prototype.chown = function(path, uid, gid, cb) {
|
|
return this.setstat(path, {
|
|
uid: uid,
|
|
gid: gid
|
|
}, cb);
|
|
};
|
|
SFTPStream.prototype.fchmod = function(handle, mode, cb) {
|
|
return this.fsetstat(handle, {
|
|
mode: mode
|
|
}, cb);
|
|
};
|
|
SFTPStream.prototype.chmod = function(path, mode, cb) {
|
|
return this.setstat(path, {
|
|
mode: mode
|
|
}, cb);
|
|
};
|
|
SFTPStream.prototype.readlink = function(path, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.READLINK;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
|
|
state.requests[reqid] = {
|
|
cb: function(err, names) {
|
|
if (err)
|
|
return cb(err);
|
|
else if (!names || !names.length)
|
|
return cb(new Error('Response missing link info'));
|
|
cb(undefined, names[0].filename);
|
|
}
|
|
};
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing READLINK');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.symlink = function(targetPath, linkPath, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string linkpath
|
|
string targetpath
|
|
*/
|
|
var linklen = Buffer.byteLength(linkPath);
|
|
var targetlen = Buffer.byteLength(targetPath);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + linklen + 4 + targetlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.SYMLINK;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
if (this._isOpenSSH) {
|
|
// OpenSSH has linkpath and targetpath positions switched
|
|
buf.writeUInt32BE(targetlen, p, true);
|
|
buf.write(targetPath, p += 4, targetlen, 'utf8');
|
|
buf.writeUInt32BE(linklen, p += targetlen, true);
|
|
buf.write(linkPath, p += 4, linklen, 'utf8');
|
|
} else {
|
|
buf.writeUInt32BE(linklen, p, true);
|
|
buf.write(linkPath, p += 4, linklen, 'utf8');
|
|
buf.writeUInt32BE(targetlen, p += linklen, true);
|
|
buf.write(targetPath, p += 4, targetlen, 'utf8');
|
|
}
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing SYMLINK');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.realpath = function(path, cb) {
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
|
|
var state = this._state;
|
|
|
|
/*
|
|
uint32 id
|
|
string path
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + pathlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.REALPATH;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
|
|
buf.writeUInt32BE(pathlen, p, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
|
|
state.requests[reqid] = {
|
|
cb: function(err, names) {
|
|
if (err)
|
|
return cb(err);
|
|
else if (!names || !names.length)
|
|
return cb(new Error('Response missing path info'));
|
|
cb(undefined, names[0].filename);
|
|
}
|
|
};
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing REALPATH');
|
|
return this.push(buf);
|
|
};
|
|
// extended requests
|
|
SFTPStream.prototype.ext_openssh_rename = function(oldPath, newPath, cb) {
|
|
var state = this._state;
|
|
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!state.extensions['posix-rename@openssh.com']
|
|
|| state.extensions['posix-rename@openssh.com'].indexOf('1') === -1)
|
|
throw new Error('Server does not support this extended request');
|
|
|
|
/*
|
|
uint32 id
|
|
string "posix-rename@openssh.com"
|
|
string oldpath
|
|
string newpath
|
|
*/
|
|
var oldlen = Buffer.byteLength(oldPath);
|
|
var newlen = Buffer.byteLength(newPath);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + 24 + 4 + oldlen + 4 + newlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.EXTENDED;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
buf.writeUInt32BE(24, p, true);
|
|
buf.write('posix-rename@openssh.com', p += 4, 24, 'ascii');
|
|
|
|
buf.writeUInt32BE(oldlen, p += 24, true);
|
|
buf.write(oldPath, p += 4, oldlen, 'utf8');
|
|
buf.writeUInt32BE(newlen, p += oldlen, true);
|
|
buf.write(newPath, p += 4, newlen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing posix-rename@openssh.com');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.ext_openssh_statvfs = function(path, cb) {
|
|
var state = this._state;
|
|
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!state.extensions['statvfs@openssh.com']
|
|
|| state.extensions['statvfs@openssh.com'].indexOf('2') === -1)
|
|
throw new Error('Server does not support this extended request');
|
|
|
|
/*
|
|
uint32 id
|
|
string "statvfs@openssh.com"
|
|
string path
|
|
*/
|
|
var pathlen = Buffer.byteLength(path);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + 19 + 4 + pathlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.EXTENDED;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
buf.writeUInt32BE(19, p, true);
|
|
buf.write('statvfs@openssh.com', p += 4, 19, 'ascii');
|
|
|
|
buf.writeUInt32BE(pathlen, p += 19, true);
|
|
buf.write(path, p += 4, pathlen, 'utf8');
|
|
|
|
state.requests[reqid] = {
|
|
extended: 'statvfs@openssh.com',
|
|
cb: cb
|
|
};
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing statvfs@openssh.com');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.ext_openssh_fstatvfs = function(handle, cb) {
|
|
var state = this._state;
|
|
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!state.extensions['fstatvfs@openssh.com']
|
|
|| state.extensions['fstatvfs@openssh.com'].indexOf('2') === -1)
|
|
throw new Error('Server does not support this extended request');
|
|
else if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
|
|
/*
|
|
uint32 id
|
|
string "fstatvfs@openssh.com"
|
|
string handle
|
|
*/
|
|
var handlelen = handle.length;
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + 20 + 4 + handlelen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.EXTENDED;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
buf.writeUInt32BE(20, p, true);
|
|
buf.write('fstatvfs@openssh.com', p += 4, 20, 'ascii');
|
|
|
|
buf.writeUInt32BE(handlelen, p += 20, true);
|
|
buf.write(handle, p += 4, handlelen, 'utf8');
|
|
|
|
state.requests[reqid] = {
|
|
extended: 'fstatvfs@openssh.com',
|
|
cb: cb
|
|
};
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing fstatvfs@openssh.com');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.ext_openssh_hardlink = function(oldPath, newPath, cb) {
|
|
var state = this._state;
|
|
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!state.extensions['hardlink@openssh.com']
|
|
|| state.extensions['hardlink@openssh.com'].indexOf('1') === -1)
|
|
throw new Error('Server does not support this extended request');
|
|
|
|
/*
|
|
uint32 id
|
|
string "hardlink@openssh.com"
|
|
string oldpath
|
|
string newpath
|
|
*/
|
|
var oldlen = Buffer.byteLength(oldPath);
|
|
var newlen = Buffer.byteLength(newPath);
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + 20 + 4 + oldlen + 4 + newlen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.EXTENDED;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
buf.writeUInt32BE(20, p, true);
|
|
buf.write('hardlink@openssh.com', p += 4, 20, 'ascii');
|
|
|
|
buf.writeUInt32BE(oldlen, p += 20, true);
|
|
buf.write(oldPath, p += 4, oldlen, 'utf8');
|
|
buf.writeUInt32BE(newlen, p += oldlen, true);
|
|
buf.write(newPath, p += 4, newlen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing hardlink@openssh.com');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.ext_openssh_fsync = function(handle, cb) {
|
|
var state = this._state;
|
|
|
|
if (this.server)
|
|
throw new Error('Client-only method called in server mode');
|
|
else if (!state.extensions['fsync@openssh.com']
|
|
|| state.extensions['fsync@openssh.com'].indexOf('1') === -1)
|
|
throw new Error('Server does not support this extended request');
|
|
else if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
|
|
/*
|
|
uint32 id
|
|
string "fsync@openssh.com"
|
|
string handle
|
|
*/
|
|
var handlelen = handle.length;
|
|
var p = 9;
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + 17 + 4 + handlelen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = REQUEST.EXTENDED;
|
|
var reqid = state.writeReqid = (state.writeReqid + 1) % MAX_REQID;
|
|
buf.writeUInt32BE(reqid, 5, true);
|
|
buf.writeUInt32BE(17, p, true);
|
|
buf.write('fsync@openssh.com', p += 4, 17, 'ascii');
|
|
|
|
buf.writeUInt32BE(handlelen, p += 17, true);
|
|
buf.write(handle, p += 4, handlelen, 'utf8');
|
|
|
|
state.requests[reqid] = { cb: cb };
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing fsync@openssh.com');
|
|
return this.push(buf);
|
|
};
|
|
|
|
// server
|
|
SFTPStream.prototype.status = function(id, code, message, lang) {
|
|
if (!this.server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
if (!STATUS_CODE[code] || typeof code !== 'number')
|
|
throw new Error('Bad status code: ' + code);
|
|
|
|
message || (message = '');
|
|
lang || (lang = '');
|
|
|
|
var msgLen = Buffer.byteLength(message);
|
|
var langLen = Buffer.byteLength(lang);
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + 4 + msgLen + 4 + langLen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = RESPONSE.STATUS;
|
|
buf.writeUInt32BE(id, 5, true);
|
|
|
|
buf.writeUInt32BE(code, 9, true);
|
|
|
|
buf.writeUInt32BE(msgLen, 13, true);
|
|
if (msgLen)
|
|
buf.write(message, 17, msgLen, 'utf8');
|
|
|
|
buf.writeUInt32BE(langLen, 17 + msgLen, true);
|
|
if (langLen)
|
|
buf.write(lang, 17 + msgLen + 4, langLen, 'ascii');
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing STATUS');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.handle = function(id, handle) {
|
|
if (!this.server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
if (!Buffer.isBuffer(handle))
|
|
throw new Error('handle is not a Buffer');
|
|
|
|
var handleLen = handle.length;
|
|
|
|
if (handleLen > 256)
|
|
throw new Error('handle too large (> 256 bytes)');
|
|
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + handleLen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = RESPONSE.HANDLE;
|
|
buf.writeUInt32BE(id, 5, true);
|
|
|
|
buf.writeUInt32BE(handleLen, 9, true);
|
|
if (handleLen)
|
|
handle.copy(buf, 13);
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing HANDLE');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.data = function(id, data, encoding) {
|
|
if (!this.server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
var isBuffer = Buffer.isBuffer(data);
|
|
|
|
if (!isBuffer && typeof data !== 'string')
|
|
throw new Error('data is not a Buffer or string');
|
|
|
|
if (!isBuffer)
|
|
encoding || (encoding = 'utf8');
|
|
|
|
var dataLen = (isBuffer ? data.length : Buffer.byteLength(data, encoding));
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + dataLen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = RESPONSE.DATA;
|
|
buf.writeUInt32BE(id, 5, true);
|
|
|
|
buf.writeUInt32BE(dataLen, 9, true);
|
|
if (dataLen) {
|
|
if (isBuffer)
|
|
data.copy(buf, 13);
|
|
else
|
|
buf.write(data, 13, dataLen, encoding);
|
|
}
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing DATA');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.name = function(id, names) {
|
|
if (!this.server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
if (!Array.isArray(names) && typeof names === 'object')
|
|
names = [ names ];
|
|
else if (!Array.isArray(names))
|
|
throw new Error('names is not an object or array');
|
|
|
|
var count = names.length;
|
|
var namesLen = 0;
|
|
var nameAttrs;
|
|
var attrs = [];
|
|
var name;
|
|
var filename;
|
|
var longname;
|
|
var attr;
|
|
var len;
|
|
var len2;
|
|
var buf;
|
|
var p;
|
|
var i;
|
|
var j;
|
|
var k;
|
|
|
|
for (i = 0; i < count; ++i) {
|
|
name = names[i];
|
|
filename = (!name || !name.filename || typeof name.filename !== 'string'
|
|
? ''
|
|
: name.filename);
|
|
namesLen += 4 + Buffer.byteLength(filename);
|
|
longname = (!name || !name.longname || typeof name.longname !== 'string'
|
|
? ''
|
|
: name.longname);
|
|
namesLen += 4 + Buffer.byteLength(longname);
|
|
|
|
if (typeof name.attrs === 'object') {
|
|
nameAttrs = attrsToBytes(name.attrs);
|
|
namesLen += 4 + nameAttrs.nbytes;
|
|
attrs.push(nameAttrs);
|
|
} else {
|
|
namesLen += 4;
|
|
attrs.push(null);
|
|
}
|
|
}
|
|
|
|
buf = new Buffer(4 + 1 + 4 + 4 + namesLen);
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = RESPONSE.NAME;
|
|
buf.writeUInt32BE(id, 5, true);
|
|
|
|
buf.writeUInt32BE(count, 9, true);
|
|
|
|
p = 13;
|
|
|
|
for (i = 0; i < count; ++i) {
|
|
name = names[i];
|
|
|
|
filename = (!name || !name.filename || typeof name.filename !== 'string'
|
|
? ''
|
|
: name.filename);
|
|
len = Buffer.byteLength(filename);
|
|
buf.writeUInt32BE(len, p, true);
|
|
p += 4;
|
|
if (len) {
|
|
buf.write(filename, p, len, 'utf8');
|
|
p += len;
|
|
}
|
|
|
|
longname = (!name || !name.longname || typeof name.longname !== 'string'
|
|
? ''
|
|
: name.longname);
|
|
len = Buffer.byteLength(longname);
|
|
buf.writeUInt32BE(len, p, true);
|
|
p += 4;
|
|
if (len) {
|
|
buf.write(longname, p, len, 'utf8');
|
|
p += len;
|
|
}
|
|
|
|
attr = attrs[i];
|
|
if (attr) {
|
|
buf.writeUInt32BE(attr.flags, p, true);
|
|
p += 4;
|
|
if (attr.flags && attr.bytes) {
|
|
var bytes = attr.bytes;
|
|
for (j = 0, len = bytes.length; j < len; ++j)
|
|
for (k = 0, len2 = bytes[j].length; k < len2; ++k)
|
|
buf[p++] = bytes[j][k];
|
|
}
|
|
} else {
|
|
buf.writeUInt32BE(0, p, true);
|
|
p += 4;
|
|
}
|
|
}
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing NAME');
|
|
return this.push(buf);
|
|
};
|
|
SFTPStream.prototype.attrs = function(id, attrs) {
|
|
if (!this.server)
|
|
throw new Error('Server-only method called in client mode');
|
|
|
|
if (typeof attrs !== 'object')
|
|
throw new Error('attrs is not an object');
|
|
|
|
var info = attrsToBytes(attrs);
|
|
var buf = new Buffer(4 + 1 + 4 + 4 + info.nbytes);
|
|
var p = 13;
|
|
|
|
buf.writeUInt32BE(buf.length - 4, 0, true);
|
|
buf[4] = RESPONSE.ATTRS;
|
|
buf.writeUInt32BE(id, 5, true);
|
|
|
|
buf.writeUInt32BE(info.flags, 9, true);
|
|
|
|
if (info.flags && info.bytes) {
|
|
var bytes = info.bytes;
|
|
for (var j = 0, len = bytes.length; j < len; ++j)
|
|
for (var k = 0, len2 = bytes[j].length; k < len2; ++k)
|
|
buf[p++] = bytes[j][k];
|
|
}
|
|
|
|
this.debug('DEBUG[SFTP]: Outgoing: Writing ATTRS');
|
|
return this.push(buf);
|
|
};
|
|
|
|
function readAttrs(buf, p, stream, callback) {
|
|
/*
|
|
uint32 flags
|
|
uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE
|
|
uint32 uid present only if flag SSH_FILEXFER_ATTR_UIDGID
|
|
uint32 gid present only if flag SSH_FILEXFER_ATTR_UIDGID
|
|
uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS
|
|
uint32 atime present only if flag SSH_FILEXFER_ACMODTIME
|
|
uint32 mtime present only if flag SSH_FILEXFER_ACMODTIME
|
|
uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED
|
|
string extended_type
|
|
string extended_data
|
|
... more extended data (extended_type - extended_data pairs),
|
|
so that number of pairs equals extended_count
|
|
*/
|
|
var flags = buf.readUInt32BE(p, true);
|
|
var attrs = new Stats();
|
|
|
|
p += 4;
|
|
|
|
if (flags & ATTR.SIZE) {
|
|
var size = readUInt64BE(buf, p, stream, callback);
|
|
if (size === false)
|
|
return false;
|
|
attrs.size = size;
|
|
p += 8;
|
|
}
|
|
if (flags & ATTR.UIDGID) {
|
|
var uid;
|
|
var gid;
|
|
uid = readInt(buf, p, this, callback);
|
|
if (uid === false)
|
|
return false;
|
|
attrs.uid = uid;
|
|
p += 4;
|
|
gid = readInt(buf, p, this, callback);
|
|
if (gid === false)
|
|
return false;
|
|
attrs.gid = gid;
|
|
p += 4;
|
|
}
|
|
if (flags & ATTR.PERMISSIONS) {
|
|
var mode = readInt(buf, p, this, callback);
|
|
if (mode === false)
|
|
return false;
|
|
attrs.mode = mode;
|
|
// backwards compatibility
|
|
attrs.permissions = mode;
|
|
p += 4;
|
|
}
|
|
if (flags & ATTR.ACMODTIME) {
|
|
var atime;
|
|
var mtime;
|
|
atime = readInt(buf, p, this, callback);
|
|
if (atime === false)
|
|
return false;
|
|
attrs.atime = atime;
|
|
p += 4;
|
|
mtime = readInt(buf, p, this, callback);
|
|
if (mtime === false)
|
|
return false;
|
|
attrs.mtime = mtime;
|
|
p += 4;
|
|
}
|
|
if (flags & ATTR.EXTENDED) {
|
|
// TODO: read/parse extended data
|
|
var extcount = readInt(buf, p, this, callback);
|
|
if (extcount === false)
|
|
return false;
|
|
p += 4;
|
|
for (var i = 0, len; i < extcount; ++i) {
|
|
len = readInt(buf, p, this, callback);
|
|
if (len === false)
|
|
return false;
|
|
p += 4 + len;
|
|
}
|
|
}
|
|
|
|
buf._pos = p;
|
|
|
|
return attrs;
|
|
}
|
|
|
|
function readUInt64BE(buffer, p, stream, callback) {
|
|
if ((buffer.length - p) < 8) {
|
|
stream && stream._cleanup(callback);
|
|
return false;
|
|
}
|
|
|
|
var val = 0;
|
|
|
|
for (var len = p + 8; p < len; ++p) {
|
|
val *= 256;
|
|
val += buffer[p];
|
|
}
|
|
|
|
buffer._pos = p;
|
|
|
|
return val;
|
|
}
|
|
|
|
function attrsToBytes(attrs) {
|
|
var flags = 0;
|
|
var attrBytes = 0;
|
|
var ret = [];
|
|
var i = 0;
|
|
|
|
if (typeof attrs.size === 'number') {
|
|
flags |= ATTR.SIZE;
|
|
attrBytes += 8;
|
|
var sizeBytes = new Array(8);
|
|
var val = attrs.size;
|
|
for (i = 7; i >= 0; --i) {
|
|
sizeBytes[i] = val & 0xFF;
|
|
val /= 256;
|
|
}
|
|
ret.push(sizeBytes);
|
|
}
|
|
if (typeof attrs.uid === 'number' && typeof attrs.gid === 'number') {
|
|
flags |= ATTR.UIDGID;
|
|
attrBytes += 8;
|
|
ret.push([(attrs.uid >> 24) & 0xFF, (attrs.uid >> 16) & 0xFF,
|
|
(attrs.uid >> 8) & 0xFF, attrs.uid & 0xFF]);
|
|
ret.push([(attrs.gid >> 24) & 0xFF, (attrs.gid >> 16) & 0xFF,
|
|
(attrs.gid >> 8) & 0xFF, attrs.gid & 0xFF]);
|
|
}
|
|
if (typeof attrs.permissions === 'number'
|
|
|| typeof attrs.permissions === 'string'
|
|
|| typeof attrs.mode === 'number'
|
|
|| typeof attrs.mode === 'string') {
|
|
var mode = modeNum(attrs.mode || attrs.permissions);
|
|
flags |= ATTR.PERMISSIONS;
|
|
attrBytes += 4;
|
|
ret.push([(mode >> 24) & 0xFF,
|
|
(mode >> 16) & 0xFF,
|
|
(mode >> 8) & 0xFF,
|
|
mode & 0xFF]);
|
|
}
|
|
if ((typeof attrs.atime === 'number' || isDate(attrs.atime))
|
|
&& (typeof attrs.mtime === 'number' || isDate(attrs.mtime))) {
|
|
var atime = toUnixTimestamp(attrs.atime);
|
|
var mtime = toUnixTimestamp(attrs.mtime);
|
|
|
|
flags |= ATTR.ACMODTIME;
|
|
attrBytes += 8;
|
|
ret.push([(atime >> 24) & 0xFF, (atime >> 16) & 0xFF,
|
|
(atime >> 8) & 0xFF, atime & 0xFF]);
|
|
ret.push([(mtime >> 24) & 0xFF, (mtime >> 16) & 0xFF,
|
|
(mtime >> 8) & 0xFF, mtime & 0xFF]);
|
|
}
|
|
// TODO: extended attributes
|
|
|
|
return { flags: flags, nbytes: attrBytes, bytes: ret };
|
|
}
|
|
|
|
function toUnixTimestamp(time) {
|
|
if (typeof time === 'number' && !isNaN(time))
|
|
return time;
|
|
else if (isDate(time))
|
|
return parseInt(time.getTime() / 1000, 10);
|
|
throw new Error('Cannot parse time: ' + time);
|
|
}
|
|
|
|
function modeNum(mode) {
|
|
if (typeof mode === 'number' && !isNaN(mode))
|
|
return mode;
|
|
else if (typeof mode === 'string')
|
|
return modeNum(parseInt(mode, 8));
|
|
throw new Error('Cannot parse mode: ' + mode);
|
|
}
|
|
|
|
var stringFlagMap = {
|
|
'r': OPEN_MODE.READ,
|
|
'r+': OPEN_MODE.READ | OPEN_MODE.WRITE,
|
|
'w': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE,
|
|
'wx': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
'xw': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
'w+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE,
|
|
'wx+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
| OPEN_MODE.EXCL,
|
|
'xw+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
| OPEN_MODE.EXCL,
|
|
'a': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE,
|
|
'ax': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
'xa': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL,
|
|
'a+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE,
|
|
'ax+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
| OPEN_MODE.EXCL,
|
|
'xa+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE
|
|
| OPEN_MODE.EXCL
|
|
};
|
|
var stringFlagMapKeys = Object.keys(stringFlagMap);
|
|
|
|
function stringToFlags(str) {
|
|
var flags = stringFlagMap[str];
|
|
if (flags !== undefined)
|
|
return flags;
|
|
return null;
|
|
}
|
|
SFTPStream.stringToFlags = stringToFlags;
|
|
|
|
function flagsToString(flags) {
|
|
for (var i = 0; i < stringFlagMapKeys.length; ++i) {
|
|
var key = stringFlagMapKeys[i];
|
|
if (stringFlagMap[key] === flags)
|
|
return key;
|
|
}
|
|
return null;
|
|
}
|
|
SFTPStream.flagsToString = flagsToString;
|
|
|
|
function Stats(initial) {
|
|
this.mode = (initial && initial.mode);
|
|
this.permissions = this.mode; // backwards compatiblity
|
|
this.uid = (initial && initial.uid);
|
|
this.gid = (initial && initial.gid);
|
|
this.size = (initial && initial.size);
|
|
this.atime = (initial && initial.atime);
|
|
this.mtime = (initial && initial.mtime);
|
|
}
|
|
Stats.prototype._checkModeProperty = function(property) {
|
|
return ((this.mode & constants.S_IFMT) === property);
|
|
};
|
|
Stats.prototype.isDirectory = function() {
|
|
return this._checkModeProperty(constants.S_IFDIR);
|
|
};
|
|
Stats.prototype.isFile = function() {
|
|
return this._checkModeProperty(constants.S_IFREG);
|
|
};
|
|
Stats.prototype.isBlockDevice = function() {
|
|
return this._checkModeProperty(constants.S_IFBLK);
|
|
};
|
|
Stats.prototype.isCharacterDevice = function() {
|
|
return this._checkModeProperty(constants.S_IFCHR);
|
|
};
|
|
Stats.prototype.isSymbolicLink = function() {
|
|
return this._checkModeProperty(constants.S_IFLNK);
|
|
};
|
|
Stats.prototype.isFIFO = function() {
|
|
return this._checkModeProperty(constants.S_IFIFO);
|
|
};
|
|
Stats.prototype.isSocket = function() {
|
|
return this._checkModeProperty(constants.S_IFSOCK);
|
|
};
|
|
SFTPStream.Stats = Stats;
|
|
|
|
|
|
// ReadStream-related
|
|
var kMinPoolSpace = 128;
|
|
var pool;
|
|
function allocNewPool(poolSize) {
|
|
pool = new Buffer(poolSize);
|
|
pool.used = 0;
|
|
}
|
|
|
|
function ReadStream(sftp, path, options) {
|
|
if (!(this instanceof ReadStream))
|
|
return new ReadStream(sftp, path, options);
|
|
|
|
var self = this;
|
|
|
|
if (options === undefined)
|
|
options = {};
|
|
else if (typeof options === 'string')
|
|
options = { encoding: options };
|
|
else if (options === null || typeof options !== 'object')
|
|
throw new TypeError('"options" argument must be a string or an object');
|
|
else
|
|
options = Object.create(options);
|
|
|
|
// a little bit bigger buffer and water marks by default
|
|
if (options.highWaterMark === undefined)
|
|
options.highWaterMark = 64 * 1024;
|
|
|
|
ReadableStream.call(this, options);
|
|
|
|
this.path = path;
|
|
this.handle = options.handle === undefined ? null : options.handle;
|
|
this.flags = options.flags === undefined ? 'r' : options.flags;
|
|
this.mode = options.mode === undefined ? 438/*0666*/ : options.mode;
|
|
|
|
this.start = options.start === undefined ? undefined : options.start;
|
|
this.end = options.end === undefined ? undefined : options.end;
|
|
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
|
|
this.pos = 0;
|
|
this.sftp = sftp;
|
|
|
|
if (this.start !== undefined) {
|
|
if (typeof this.start !== 'number')
|
|
throw new TypeError('start must be a Number');
|
|
if (this.end === undefined)
|
|
this.end = Infinity;
|
|
else if (typeof this.end !== 'number')
|
|
throw new TypeError('end must be a Number');
|
|
|
|
if (this.start > this.end)
|
|
throw new Error('start must be <= end');
|
|
else if (this.start < 0)
|
|
throw new Error('start must be >= zero');
|
|
|
|
this.pos = this.start;
|
|
}
|
|
|
|
this.on('end', function() {
|
|
if (self.autoClose) {
|
|
self.destroy();
|
|
}
|
|
});
|
|
|
|
if (!Buffer.isBuffer(this.handle))
|
|
this.open();
|
|
}
|
|
inherits(ReadStream, ReadableStream);
|
|
|
|
ReadStream.prototype.open = function() {
|
|
var self = this;
|
|
this.sftp.open(this.path, this.flags, this.mode, function(er, handle) {
|
|
if (er) {
|
|
self.emit('error', er);
|
|
this.destroyed = this.closed = true;
|
|
self.emit('close');
|
|
return;
|
|
}
|
|
|
|
self.handle = handle;
|
|
self.emit('open', handle);
|
|
// start the flow of data.
|
|
self.read();
|
|
});
|
|
};
|
|
|
|
ReadStream.prototype._read = function(n) {
|
|
if (!Buffer.isBuffer(this.handle)) {
|
|
return this.once('open', function() {
|
|
this._read(n);
|
|
});
|
|
}
|
|
|
|
if (this.destroyed)
|
|
return;
|
|
|
|
if (!pool || pool.length - pool.used < kMinPoolSpace) {
|
|
// discard the old pool.
|
|
pool = null;
|
|
allocNewPool(this._readableState.highWaterMark);
|
|
}
|
|
|
|
// Grab another reference to the pool in the case that while we're
|
|
// in the thread pool another read() finishes up the pool, and
|
|
// allocates a new one.
|
|
var thisPool = pool;
|
|
var toRead = Math.min(pool.length - pool.used, n);
|
|
var start = pool.used;
|
|
|
|
if (this.end !== undefined)
|
|
toRead = Math.min(this.end - this.pos + 1, toRead);
|
|
|
|
// already read everything we were supposed to read!
|
|
// treat as EOF.
|
|
if (toRead <= 0)
|
|
return this.push(null);
|
|
|
|
// the actual read.
|
|
var self = this;
|
|
this.sftp.readData(this.handle, pool, pool.used, toRead, this.pos, onread);
|
|
|
|
// move the pool positions, and internal position for reading.
|
|
this.pos += toRead;
|
|
pool.used += toRead;
|
|
|
|
function onread(er, bytesRead) {
|
|
if (er) {
|
|
if (self.autoClose)
|
|
self.destroy();
|
|
self.emit('error', er);
|
|
} else {
|
|
var b = null;
|
|
if (bytesRead > 0)
|
|
b = thisPool.slice(start, start + bytesRead);
|
|
|
|
self.push(b);
|
|
}
|
|
}
|
|
};
|
|
|
|
ReadStream.prototype.destroy = function() {
|
|
if (this.destroyed)
|
|
return;
|
|
this.destroyed = true;
|
|
if (Buffer.isBuffer(this.handle))
|
|
this.close();
|
|
};
|
|
|
|
|
|
ReadStream.prototype.close = function(cb) {
|
|
var self = this;
|
|
if (cb)
|
|
this.once('close', cb);
|
|
if (this.closed || !Buffer.isBuffer(this.handle)) {
|
|
if (!Buffer.isBuffer(this.handle)) {
|
|
this.once('open', close);
|
|
return;
|
|
}
|
|
return process.nextTick(this.emit.bind(this, 'close'));
|
|
}
|
|
this.closed = true;
|
|
close();
|
|
|
|
function close(handle) {
|
|
self.sftp.close(handle || self.handle, function(er) {
|
|
if (er)
|
|
self.emit('error', er);
|
|
else
|
|
self.emit('close');
|
|
});
|
|
self.handle = null;
|
|
}
|
|
};
|
|
|
|
|
|
function WriteStream(sftp, path, options) {
|
|
if (!(this instanceof WriteStream))
|
|
return new WriteStream(sftp, path, options);
|
|
|
|
if (options === undefined)
|
|
options = {};
|
|
else if (typeof options === 'string')
|
|
options = { encoding: options };
|
|
else if (options === null || typeof options !== 'object')
|
|
throw new TypeError('"options" argument must be a string or an object');
|
|
else
|
|
options = Object.create(options);
|
|
|
|
WritableStream.call(this, options);
|
|
|
|
this.path = path;
|
|
this.handle = options.handle === undefined ? null : options.handle;
|
|
this.flags = options.flags === undefined ? 'w' : options.flags;
|
|
this.mode = options.mode === undefined ? 438/*0666*/ : options.mode;
|
|
|
|
this.start = options.start === undefined ? undefined : options.start;
|
|
this.autoClose = options.autoClose === undefined ? true : options.autoClose;
|
|
this.pos = 0;
|
|
this.bytesWritten = 0;
|
|
this.sftp = sftp;
|
|
|
|
if (this.start !== undefined) {
|
|
if (typeof this.start !== 'number')
|
|
throw new TypeError('start must be a Number');
|
|
if (this.start < 0)
|
|
throw new Error('start must be >= zero');
|
|
|
|
this.pos = this.start;
|
|
}
|
|
|
|
if (options.encoding)
|
|
this.setDefaultEncoding(options.encoding);
|
|
|
|
if (!Buffer.isBuffer(this.handle))
|
|
this.open();
|
|
|
|
// dispose on finish.
|
|
this.once('finish', function onclose() {
|
|
if (this.autoClose)
|
|
this.close();
|
|
});
|
|
}
|
|
inherits(WriteStream, WritableStream);
|
|
|
|
WriteStream.prototype.open = function() {
|
|
var self = this;
|
|
this.sftp.open(this.path, this.flags, this.mode, function(er, handle) {
|
|
if (er) {
|
|
self.emit('error', er);
|
|
if (self.autoClose) {
|
|
self.destroyed = self.closed = true;
|
|
self.emit('close');
|
|
}
|
|
return;
|
|
}
|
|
|
|
self.handle = handle;
|
|
|
|
self.sftp.fchmod(handle, self.mode, function tryAgain(err) {
|
|
if (err) {
|
|
// Try chmod() for sftp servers that may not support fchmod() for
|
|
// whatever reason
|
|
self.sftp.chmod(self.path, self.mode, function(err_) {
|
|
tryAgain();
|
|
});
|
|
return;
|
|
}
|
|
|
|
// SFTPv3 requires absolute offsets, no matter the open flag used
|
|
if (self.flags[0] === 'a') {
|
|
self.sftp.fstat(handle, function tryStat(err, st) {
|
|
if (err) {
|
|
// Try stat() for sftp servers that may not support fstat() for
|
|
// whatever reason
|
|
self.sftp.stat(self.path, function(err_, st_) {
|
|
if (err_) {
|
|
self.destroy();
|
|
self.emit('error', err);
|
|
return;
|
|
}
|
|
tryStat(null, st_);
|
|
});
|
|
return;
|
|
}
|
|
|
|
self.pos = st.size;
|
|
self.emit('open', handle);
|
|
});
|
|
return;
|
|
}
|
|
self.emit('open', handle);
|
|
});
|
|
});
|
|
};
|
|
|
|
WriteStream.prototype._write = function(data, encoding, cb) {
|
|
if (!Buffer.isBuffer(data))
|
|
return this.emit('error', new Error('Invalid data'));
|
|
|
|
if (!Buffer.isBuffer(this.handle)) {
|
|
return this.once('open', function() {
|
|
this._write(data, encoding, cb);
|
|
});
|
|
}
|
|
|
|
var self = this;
|
|
this.sftp.writeData(this.handle,
|
|
data,
|
|
0,
|
|
data.length,
|
|
this.pos,
|
|
function(er, bytes) {
|
|
if (er) {
|
|
if (self.autoClose)
|
|
self.destroy();
|
|
return cb(er);
|
|
}
|
|
self.bytesWritten += bytes;
|
|
cb();
|
|
});
|
|
|
|
this.pos += data.length;
|
|
};
|
|
|
|
WriteStream.prototype._writev = function(data, cb) {
|
|
if (!Buffer.isBuffer(this.handle)) {
|
|
return this.once('open', function() {
|
|
this._writev(data, cb);
|
|
});
|
|
}
|
|
|
|
var sftp = this.sftp;
|
|
var handle = this.handle;
|
|
var writesLeft = data.length;
|
|
var self = this;
|
|
|
|
for (var i = 0; i < data.length; ++i) {
|
|
var chunk = data[i].chunk;
|
|
|
|
sftp.writeData(handle, chunk, 0, chunk.length, this.pos, onwrite);
|
|
this.pos += chunk.length;
|
|
}
|
|
|
|
function onwrite(er, bytes) {
|
|
if (er) {
|
|
self.destroy();
|
|
return cb(er);
|
|
}
|
|
self.bytesWritten += bytes;
|
|
if (--writesLeft === 0)
|
|
cb();
|
|
}
|
|
};
|
|
|
|
WriteStream.prototype.destroy = ReadStream.prototype.destroy;
|
|
WriteStream.prototype.close = ReadStream.prototype.close;
|
|
|
|
// There is no shutdown() for files.
|
|
WriteStream.prototype.destroySoon = WriteStream.prototype.end;
|
|
|
|
|
|
module.exports = SFTPStream;
|
|
|