"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;