filefive
Version:
SFTP/FTP/Amazon S3 client and dual-panel file manager for macOS and Linux
272 lines (271 loc) • 10.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ATTRIBUTES = void 0;
const node_path_1 = require("node:path");
const ssh2_1 = require("ssh2");
const FileSystem_1 = require("../FileSystem");
// https://github.com/mscdex/ssh2/blob/master/SFTP.md
// https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4.3
// see also https://github.com/theophilusx/ssh2-sftp-client
exports.ATTRIBUTES = [
{
name: "name",
type: FileSystem_1.FileAttributeType.String,
title: "Name"
},
{
name: "size",
type: FileSystem_1.FileAttributeType.Number,
title: "Size"
},
{
name: "modified",
type: FileSystem_1.FileAttributeType.Date,
title: "Last Modified"
},
{
name: "rights",
type: FileSystem_1.FileAttributeType.String,
title: "Rights"
}
];
class SFtp extends FileSystem_1.FileSystem {
constructor(host, user, password, privateKey = null, port = 22, onError, onClose = () => { }) {
super();
this.host = host;
this.user = user;
this.password = password;
this.privateKey = privateKey;
this.port = port;
this.onError = onError;
this.onClose = onClose;
this.connection = new ssh2_1.Client();
}
async open() {
if (this.connected === undefined) {
let connecting = true;
this.connected = new Promise((resolve, reject) => {
this.connection
.on('close', () => {
if (this.connected) {
if (connecting) {
reject(new Error('Remote side unexpectedly closed network connection'));
}
else {
this.onClose();
}
}
this.connected = undefined;
connecting = false;
})
.on('ready', () => {
connecting = false;
this.connection.sftp((e, sftp) => {
if (e) {
reject(new Error(this.decodeError(e)));
return;
}
this.connection.on('error', this.onError);
this.extensions = sftp._extensions;
resolve(sftp);
});
})
.on('error', err => reject(err));
});
this.connection.connect({
host: this.host,
username: this.user,
port: this.port,
...(this.privateKey ?
{ privateKey: this.privateKey } :
{ password: this.password })
// debug: s => console.log('DEBUG', s)
});
}
return this.connected;
}
close() {
this.connection.end();
}
opened() {
return this.connected != undefined;
}
async pwd() {
const sftp = await this.open();
return new Promise((resolve, reject) => sftp.realpath('.', (e, current) => e ?
reject(new Error(this.decodeError(e))) :
resolve(current)));
}
async ls(dir) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
sftp.readdir(dir, async (e, list) => {
if (e) {
reject(new Error(`LS: Can't get contents of ${dir} ` + this.decodeError(e)));
return;
}
const files = [];
for (let f of list) {
if (f.attrs.isSymbolicLink()) {
let target = await this.readlink((0, node_path_1.join)(dir, f.filename));
if (!(0, node_path_1.isAbsolute)(target)) {
target = (0, node_path_1.normalize)((0, node_path_1.join)(dir, target));
}
const targetStat = await this.stat(target);
files.push({
path: (0, node_path_1.join)(dir, f.filename),
name: f.filename,
dir: f.attrs.isDirectory(),
size: targetStat.size,
modified: new Date(targetStat.mtime * 1000),
owner: targetStat.uid,
group: targetStat.gid,
rights: targetStat.mode,
target
});
}
else {
files.push({
path: (0, node_path_1.join)(dir, f.filename),
name: f.filename,
dir: f.attrs.isDirectory(),
size: f.attrs.size,
modified: new Date(f.attrs.mtime * 1000),
owner: f.attrs.uid,
group: f.attrs.gid,
rights: f.attrs.mode
});
}
}
resolve(files);
});
});
}
async readlink(path) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
sftp.readlink(path, (e, target) => e ? reject(new Error(this.decodeError(e))) : resolve(target));
});
}
async stat(path) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
sftp.stat(path, (e, stats) => e ? reject(new Error(this.decodeError(e))) : resolve(stats));
});
}
async get(fromRemote, toLocal) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
sftp.fastGet(fromRemote, toLocal, (e) => e ? reject(new Error(this.decodeError(e))) : resolve());
});
}
async put(fromLocal, toRemote) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
sftp.fastPut(fromLocal, toRemote, e => e ? reject(new Error(this.decodeError(e) + ` ${toRemote}`)) : resolve());
});
}
async rm(path, recursive) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
if (recursive) {
sftp.rmdir(path, e => e ? reject(new Error(this.decodeError(e))) : resolve());
}
else {
sftp.unlink(path, e => e ? reject(new Error(this.decodeError(e))) : resolve());
}
});
}
async mkdir(path) {
const sftp = await this.open();
return new Promise((resolve, reject) => sftp.mkdir(path, e => e ?
reject(new Error(`MKDIR: Can't create directory ${path} ` + this.decodeError(e))) :
resolve()));
}
async rename(from, to) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
sftp.rename(from, to, e => {
if (e) {
reject(new Error(this.decodeError(e)));
return;
}
resolve();
});
});
}
async mv(from, to) {
await this.exec(`rm -Rf '${to}'`);
await this.exec(`mkdir -p '${(0, node_path_1.dirname)(to)}'`);
return this.exec(`mv -f '${from}' '${to}'`);
}
async cp(from, to, recursive) {
await this.exec(`rm -Rf '${to}'`);
if (recursive) {
return this.exec(`cp -fR '${from}' '${to}'`);
}
await this.exec(`mkdir -p '${(0, node_path_1.dirname)(to)}'`);
return this.exec(`cp -f '${from}' '${to}'`);
}
async write(path, s) {
const sftp = await this.open();
return new Promise((resolve, reject) => {
sftp.open(path, 'w', (e, h) => {
if (e) {
reject(e);
return;
}
const data = Buffer.from(s);
sftp.write(h, data, 0, data.length, 0, (e) => e ? reject(new Error(this.decodeError(e))) : resolve());
});
});
}
async exec(cmd) {
await this.open();
return new Promise((resolve, reject) => this.connection.exec(cmd, (e, stream) => {
if (e) {
reject(new Error(cmd + ': ' + this.decodeError(e)));
return;
}
stream.on('exit', (code) => {
if (code) {
reject(new Error(`${cmd}: the process's return code is ${code}`));
}
resolve();
}).on('data', (data) => {
console.log(`${cmd}: ${data}`);
}).stderr.on('data', data => {
reject(new Error(`${cmd}: ${data}`));
});
}));
}
decodeError(e) {
const 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
};
const STATUS_CODE_STR = {
[STATUS_CODE.OK]: 'No error',
[STATUS_CODE.EOF]: 'End of file',
[STATUS_CODE.NO_SUCH_FILE]: 'No such file or directory',
[STATUS_CODE.PERMISSION_DENIED]: 'Permission denied',
[STATUS_CODE.FAILURE]: 'Failure',
[STATUS_CODE.BAD_MESSAGE]: 'Bad message',
[STATUS_CODE.NO_CONNECTION]: 'No connection',
[STATUS_CODE.CONNECTION_LOST]: 'Connection lost',
[STATUS_CODE.OP_UNSUPPORTED]: 'Operation unsupported',
};
let msg = e.message || (('code' in e) ?
(e['code'] in STATUS_CODE_STR ? STATUS_CODE_STR[e['code']] : `Code: ${e['code']}`) :
'Unknown error');
return msg;
}
}
exports.default = SFtp;