2023-10-03 11:14:36 +08:00
|
|
|
"use strict";
|
|
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
|
|
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
|
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
|
|
});
|
|
|
|
};
|
|
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
|
const ssh2_1 = require("ssh2");
|
|
|
|
const path = require("path");
|
|
|
|
const fs = require("fs");
|
|
|
|
const util_1 = require("./util");
|
|
|
|
const queuifiedSftp_1 = require("./queuifiedSftp");
|
|
|
|
const syncTable_1 = require("./syncTable");
|
|
|
|
/**
|
|
|
|
* Creates a new SftpDeploy instance
|
|
|
|
* @class
|
|
|
|
*/
|
|
|
|
class SftpSync {
|
|
|
|
/**
|
|
|
|
* Constructor
|
|
|
|
*/
|
|
|
|
constructor(config, options) {
|
|
|
|
/**
|
|
|
|
* Whether a SSH2 connection has been made or not
|
|
|
|
*/
|
|
|
|
this.connected = false;
|
|
|
|
this.config = config;
|
|
|
|
this.options = Object.assign({ dryRun: false, exclude: [], excludeMode: 'remove', concurrency: 100 }, options);
|
|
|
|
this.client = new ssh2_1.Client;
|
|
|
|
this.localRoot = util_1.chomp(path.resolve(this.config.localDir), path.sep);
|
|
|
|
this.remoteRoot = util_1.chomp(this.config.remoteDir, path.posix.sep);
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Make SSH2 connection
|
|
|
|
*/
|
|
|
|
connect() {
|
|
|
|
let privKeyRaw;
|
|
|
|
if (this.config.privateKey) {
|
|
|
|
try {
|
|
|
|
privKeyRaw = fs.readFileSync(this.config.privateKey);
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
throw new Error(`Local Error: Private key file not found ${this.config.privateKey}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.client
|
|
|
|
.on('ready', () => {
|
|
|
|
this.connected = true;
|
|
|
|
resolve();
|
|
|
|
})
|
|
|
|
.on('error', err_1 => {
|
|
|
|
reject(new Error(`Connection Error: ${err_1.message}`));
|
|
|
|
})
|
|
|
|
.connect({
|
|
|
|
host: this.config.host,
|
|
|
|
port: this.config.port || 22,
|
|
|
|
username: this.config.username,
|
|
|
|
password: this.config.password,
|
|
|
|
passphrase: this.config.passphrase,
|
|
|
|
privateKey: privKeyRaw,
|
|
|
|
agent: this.config.agent
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Close SSH2 connection
|
|
|
|
*/
|
|
|
|
close() {
|
|
|
|
this.connected = false;
|
|
|
|
this.client.end();
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Sync with specified path
|
|
|
|
*/
|
|
|
|
sync(relativePath = '', isRootTask = true) {
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
const table = yield this.buildSyncTable(relativePath);
|
|
|
|
yield Promise.all(table.all.map((entry) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
const task = entry.getTask();
|
|
|
|
const args = [entry.path, false];
|
|
|
|
if (this.options.dryRun) {
|
|
|
|
entry.dryRunLog();
|
|
|
|
if (task.method === 'sync') {
|
|
|
|
return this.sync(entry.path, false);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (task.removeRemote) {
|
|
|
|
yield this.removeRemote(entry.path, false);
|
|
|
|
}
|
|
|
|
if (task.method === 'sync' && entry.remoteStat !== 'dir') {
|
|
|
|
yield this.createRemoteDirectory(entry.path);
|
|
|
|
}
|
|
|
|
yield this[task.method].apply(this, args);
|
|
|
|
entry.liveRunLog();
|
|
|
|
})));
|
|
|
|
if (isRootTask)
|
|
|
|
this.close();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Upload file/directory
|
|
|
|
*/
|
|
|
|
upload(relativePath, isRootTask = true) {
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
if (!this.queuifiedSftp) {
|
|
|
|
yield this.initQueuifiedSftp();
|
|
|
|
return this.upload(relativePath, isRootTask);
|
|
|
|
}
|
|
|
|
const localPath = this.localFullPath(relativePath);
|
|
|
|
const remotePath = this.remoteFullPath(relativePath);
|
|
|
|
const stat = fs.lstatSync(localPath);
|
|
|
|
if (stat.isDirectory()) {
|
|
|
|
const files = fs.readdirSync(localPath);
|
|
|
|
if (files && files.length) {
|
|
|
|
yield Promise.all(files.map(filename => this.upload(path.posix.join(relativePath, filename), false)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
try {
|
|
|
|
// const buffer = fs.readFileSync(localPath);
|
|
|
|
// const handle = await this.queuifiedSftp.open(remotePath, 'r+');
|
|
|
|
// await this.queuifiedSftp.writeData(handle, buffer, 0, buffer.length, 0);
|
|
|
|
// await this.queuifiedSftp.close(handle);
|
|
|
|
yield this.queuifiedSftp.fastPut(localPath, remotePath);
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
switch (err.code) {
|
|
|
|
case ssh2_1.SFTP_STATUS_CODE.NO_SUCH_FILE: {
|
|
|
|
throw new Error(`Remote Error: Cannot upload file ${remotePath}`);
|
|
|
|
}
|
|
|
|
case ssh2_1.SFTP_STATUS_CODE.PERMISSION_DENIED: {
|
|
|
|
throw new Error(`Remote Error: Cannot upload file. Permission denied ${remotePath}`);
|
|
|
|
}
|
|
|
|
case ssh2_1.SFTP_STATUS_CODE.FAILURE: {
|
|
|
|
throw new Error(`Remote Error: Unknown error while uploading file ${remotePath}`);
|
|
|
|
}
|
|
|
|
default: throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isRootTask)
|
|
|
|
this.close();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Remove a remote file or directory
|
|
|
|
*/
|
|
|
|
removeRemote(relativePath, isRootTask = true) {
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
if (!this.queuifiedSftp) {
|
|
|
|
yield this.initQueuifiedSftp();
|
|
|
|
return this.removeRemote(relativePath, isRootTask);
|
|
|
|
}
|
|
|
|
const remotePath = this.remoteFullPath(relativePath);
|
|
|
|
const stat = yield this.queuifiedSftp.lstat(remotePath);
|
|
|
|
if (stat.isDirectory()) {
|
|
|
|
const files = yield this.queuifiedSftp.readdir(remotePath);
|
|
|
|
yield Promise.all(files.map(file => this.removeRemote(path.posix.join(relativePath, file.filename), false)));
|
|
|
|
yield this.queuifiedSftp.rmdir(remotePath);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return this.queuifiedSftp.unlink(remotePath);
|
|
|
|
}
|
|
|
|
if (isRootTask)
|
|
|
|
this.close();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* No operation
|
|
|
|
*/
|
|
|
|
noop() {
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Create a directory on a remote host
|
|
|
|
*/
|
|
|
|
createRemoteDirectory(relativePath) {
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
if (!this.queuifiedSftp) {
|
|
|
|
yield this.initQueuifiedSftp();
|
|
|
|
return this.createRemoteDirectory(relativePath);
|
|
|
|
}
|
|
|
|
const remotePath = this.remoteFullPath(relativePath);
|
|
|
|
try {
|
|
|
|
yield this.queuifiedSftp.mkdir(remotePath);
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
switch (err.code) {
|
|
|
|
case ssh2_1.SFTP_STATUS_CODE.NO_SUCH_FILE: {
|
|
|
|
throw new Error(`Remote Error: Cannot create directory ${remotePath}`);
|
|
|
|
}
|
|
|
|
case ssh2_1.SFTP_STATUS_CODE.PERMISSION_DENIED: {
|
|
|
|
throw new Error(`Remote Error: Cannot create directory. Permission denied ${remotePath}`);
|
|
|
|
}
|
|
|
|
case ssh2_1.SFTP_STATUS_CODE.FAILURE: {
|
|
|
|
throw new Error(`Remote Error: Unknown error while creating directory ${remotePath}`);
|
|
|
|
}
|
|
|
|
default: throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Build a local and remote files status report for the specified path
|
|
|
|
*/
|
|
|
|
buildSyncTable(relativePath) {
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
if (!this.queuifiedSftp) {
|
|
|
|
yield this.initQueuifiedSftp();
|
|
|
|
return this.buildSyncTable(relativePath);
|
|
|
|
}
|
|
|
|
const localPath = this.localFullPath(relativePath);
|
|
|
|
const remotePath = this.remoteFullPath(relativePath);
|
|
|
|
const table = new syncTable_1.SyncTable(relativePath, this.options);
|
|
|
|
const readLocal = () => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
let files;
|
|
|
|
try {
|
|
|
|
files = fs.readdirSync(localPath);
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
switch (err.code) {
|
|
|
|
case 'ENOENT': throw new Error(`Local Error: No such directory ${localPath}`);
|
|
|
|
case 'ENOTDIR': throw new Error(`Local Error: Not a directory ${localPath}`);
|
|
|
|
case 'EPERM': throw new Error(`Local Error: Cannot read directory. Permission denied ${localPath}`);
|
|
|
|
default: throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!files || !files.length)
|
|
|
|
return;
|
|
|
|
yield Promise.all(files.map((filename) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
const fullPath = path.join(localPath, filename);
|
|
|
|
let stat;
|
|
|
|
try {
|
|
|
|
fs.accessSync(fullPath, fs.constants.R_OK);
|
|
|
|
stat = fs.lstatSync(fullPath);
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
|
|
table.set(filename, { localStat: 'error', localTimestamp: null });
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const mtime = Math.floor(new Date(stat.mtime).getTime() / 1000);
|
|
|
|
table.set(filename, {
|
|
|
|
localStat: stat.isDirectory() ? 'dir' : 'file',
|
|
|
|
localTimestamp: mtime
|
|
|
|
});
|
|
|
|
})));
|
|
|
|
});
|
|
|
|
const readRemote = () => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
let files;
|
|
|
|
try {
|
|
|
|
files = yield this.queuifiedSftp.readdir(remotePath);
|
|
|
|
}
|
|
|
|
catch (err) { }
|
|
|
|
if (!files || !files.length)
|
|
|
|
return;
|
|
|
|
yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
|
|
|
|
const fullPath = path.posix.join(remotePath, file.filename);
|
|
|
|
let stat;
|
|
|
|
try {
|
|
|
|
stat = yield this.queuifiedSftp.lstat(fullPath);
|
|
|
|
if (stat.isDirectory()) {
|
|
|
|
yield this.queuifiedSftp.readdir(fullPath);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
const buffer = yield this.queuifiedSftp.open(fullPath, 'r+');
|
|
|
|
yield this.queuifiedSftp.close(buffer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (err) {
|
|
|
|
if (err.code === ssh2_1.SFTP_STATUS_CODE.PERMISSION_DENIED) {
|
|
|
|
table.set(file.filename, { remoteStat: 'error', remoteTimestamp: null });
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (stat) {
|
|
|
|
table.set(file.filename, {
|
|
|
|
remoteStat: stat.isDirectory() ? 'dir' : 'file',
|
|
|
|
remoteTimestamp: stat.mtime
|
|
|
|
});
|
|
|
|
}
|
|
|
|
})));
|
|
|
|
});
|
|
|
|
yield Promise.all([readLocal(), readRemote()]);
|
|
|
|
return table.forEach(entry => entry.detectExclusion());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Get an async version of sftp stream
|
|
|
|
*/
|
|
|
|
initQueuifiedSftp(concurrency = this.options.concurrency) {
|
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
|
if (this.queuifiedSftp) {
|
|
|
|
return this.queuifiedSftp;
|
|
|
|
}
|
|
|
|
if (!this.connected) {
|
|
|
|
yield this.connect();
|
|
|
|
return this.initQueuifiedSftp(concurrency);
|
|
|
|
}
|
|
|
|
this.queuifiedSftp = yield queuifiedSftp_1.QueuifiedSFTP.init(this.client, concurrency);
|
|
|
|
return this.queuifiedSftp;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Get a full path of a local file or directory
|
|
|
|
*/
|
|
|
|
localFullPath(relativePath) {
|
|
|
|
return path.join(this.localRoot, relativePath);
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Get a full path of a remote file or directory
|
|
|
|
*/
|
|
|
|
remoteFullPath(relativePath) {
|
|
|
|
return path.posix.join(this.remoteRoot, relativePath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
exports.SftpSync = SftpSync;
|