mirror of
https://github.com/certd/certd.git
synced 2026-04-30 17:37:24 +08:00
perf: 上传到主机支持scp方式
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
export enum errorCode {
|
||||||
|
generic = "ERR_GENERIC_CLIENT",
|
||||||
|
connect = "ERR_NOT_CONNECTED",
|
||||||
|
badPath = "ERR_BAD_PATH",
|
||||||
|
permission = "EACCES",
|
||||||
|
notexist = "ENOENT",
|
||||||
|
notdir = "ENOTDIR",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum targetType {
|
||||||
|
writeFile = 1,
|
||||||
|
readFile = 2,
|
||||||
|
writeDir = 3,
|
||||||
|
readDir = 4,
|
||||||
|
readObj = 5,
|
||||||
|
writeObj = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CLIENT_EVENTS = new Set([
|
||||||
|
"banner",
|
||||||
|
"ready",
|
||||||
|
"tcp connection",
|
||||||
|
"x11",
|
||||||
|
"keyboard-interactive",
|
||||||
|
"change password",
|
||||||
|
"error",
|
||||||
|
"end",
|
||||||
|
"close",
|
||||||
|
"timeout",
|
||||||
|
"connect",
|
||||||
|
"greeting",
|
||||||
|
"handshake",
|
||||||
|
"hostkeys",
|
||||||
|
"unix connection",
|
||||||
|
]);
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { mkdirSync, readdirSync, existsSync } from "fs";
|
||||||
|
import { join, win32, posix } from "path";
|
||||||
|
import { Client as SSHClient, ConnectConfig, InputAttributes, SFTPWrapper, Stats, TransferOptions, WriteFileOptions, ParsedKey, UNIXConnectionDetails, AcceptConnection, RejectConnection } from "ssh2";
|
||||||
|
import { targetType } from "./constant.js";
|
||||||
|
import * as utils from "./utils.js";
|
||||||
|
import { ClientEvents } from "./types.js";
|
||||||
|
|
||||||
|
export type TScpOptions = ConnectConfig & {
|
||||||
|
remoteOsType?: "posix" | "win32";
|
||||||
|
events?: ClientEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ScpClient extends EventEmitter {
|
||||||
|
sftpWrapper: SFTPWrapper | null = null;
|
||||||
|
sshClient: SSHClient | null = null;
|
||||||
|
remotePathSep = posix.sep;
|
||||||
|
endCalled = false;
|
||||||
|
errorHandled = false;
|
||||||
|
|
||||||
|
constructor(options: TScpOptions) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const ssh = new SSHClient();
|
||||||
|
ssh
|
||||||
|
.on("connect", () => this.emit("connect"))
|
||||||
|
.on("ready", () => {
|
||||||
|
ssh.sftp((err, sftp) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
// save for reuse
|
||||||
|
this.sftpWrapper = sftp;
|
||||||
|
this.emit("ready");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", err => this.emit("error", err))
|
||||||
|
.on("end", () => this.emit("end"))
|
||||||
|
.on("close", () => {
|
||||||
|
if (!this.endCalled) {
|
||||||
|
this.sftpWrapper = null;
|
||||||
|
}
|
||||||
|
this.emit("close");
|
||||||
|
})
|
||||||
|
.on("keyboard-interactive", (name, instructions, instructionsLang, prompts, finish) => this.emit("keyboard-interactive", name, instructions, instructionsLang, prompts, finish))
|
||||||
|
.on("change password", (message, done) => this.emit("change password", message, done))
|
||||||
|
.on("tcp connection", (details, accept, reject) => this.emit("tcp connection", details, accept, reject))
|
||||||
|
.on("banner", message => this.emit("banner", message))
|
||||||
|
.on("greeting", greeting => this.emit("banner", greeting))
|
||||||
|
.on("handshake", negotiated => this.emit("handshake", negotiated))
|
||||||
|
.on("hostkeys", (keys: ParsedKey[]) => this.emit("hostkeys", keys))
|
||||||
|
.on("timeout", () => this.emit("timeout"))
|
||||||
|
.on("unix connection", (info: UNIXConnectionDetails, accept: AcceptConnection, reject: RejectConnection) => this.emit("unix connection", info, accept, reject))
|
||||||
|
.on("x11", message => this.emit("x11", message));
|
||||||
|
ssh.connect(options);
|
||||||
|
this.sshClient = ssh;
|
||||||
|
|
||||||
|
if (options.remoteOsType === "win32") {
|
||||||
|
this.remotePathSep = win32.sep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file from `localPath` to `remotePath` using parallel reads for faster throughput.
|
||||||
|
*/
|
||||||
|
public async uploadFile(localPath: string, remotePath: string, options: TransferOptions = {}): Promise<void> {
|
||||||
|
utils.haveConnection(this, "uploadFile");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.fastPut(localPath, remotePath, options, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a file at `remotePath` to `localPath` using parallel reads for faster throughput.
|
||||||
|
*/
|
||||||
|
public async downloadFile(remotePath: string, localPath: string, options: TransferOptions = {}): Promise<void> {
|
||||||
|
utils.haveConnection(this, "downloadFile");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.fastGet(remotePath, localPath, options, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean a directory in remote server
|
||||||
|
*/
|
||||||
|
public async emptyDir(dir: string): Promise<void> {
|
||||||
|
utils.haveConnection(this, "uploadDir");
|
||||||
|
try {
|
||||||
|
const isExist = await this.exists(dir);
|
||||||
|
|
||||||
|
if (!isExist) {
|
||||||
|
await this.mkdir(dir);
|
||||||
|
} else if (isExist === "d") {
|
||||||
|
await this.rmdir(dir);
|
||||||
|
await this.mkdir(dir);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async uploadDir(src: string, dest: string): Promise<void> {
|
||||||
|
utils.haveConnection(this, "uploadDir");
|
||||||
|
try {
|
||||||
|
const isExist = await this.exists(dest);
|
||||||
|
|
||||||
|
if (!isExist) {
|
||||||
|
await this.mkdir(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirEntries = readdirSync(src, {
|
||||||
|
encoding: "utf8",
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const e of dirEntries) {
|
||||||
|
if (e.isDirectory()) {
|
||||||
|
const newSrc = join(src, e.name);
|
||||||
|
const newDst = utils.joinRemote(this, dest, e.name);
|
||||||
|
await this.uploadDir(newSrc, newDst);
|
||||||
|
} else if (e.isFile()) {
|
||||||
|
const newSrc = join(src, e.name);
|
||||||
|
const newDst = utils.joinRemote(this, dest, e.name);
|
||||||
|
await this.uploadFile(newSrc, newDst);
|
||||||
|
|
||||||
|
// this.client.emit('upload', {source: src, destination: dst})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadDir(remotePath: string, localPath: string) {
|
||||||
|
utils.haveConnection(this, "downloadDir");
|
||||||
|
const remoteInfo: any = await utils.checkRemotePath(this, remotePath, targetType.readDir);
|
||||||
|
if (!remoteInfo.valid) {
|
||||||
|
throw new Error(remoteInfo.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(localPath)) {
|
||||||
|
mkdirSync(localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const localInfo = await utils.checkLocalPath(localPath, targetType.writeDir);
|
||||||
|
|
||||||
|
if (localInfo.valid && !localInfo.type) {
|
||||||
|
mkdirSync(localInfo.path, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localInfo.valid) {
|
||||||
|
throw new Error(localInfo.msg);
|
||||||
|
}
|
||||||
|
const fileList = await this.list(remoteInfo.path);
|
||||||
|
for (const f of fileList) {
|
||||||
|
if (f.type === "d") {
|
||||||
|
const newSrc = remoteInfo.path + this.remotePathSep + f.name;
|
||||||
|
const newDst = join(localInfo.path, f.name);
|
||||||
|
await this.downloadDir(newSrc, newDst);
|
||||||
|
} else if (f.type === "-") {
|
||||||
|
const src = remoteInfo.path + this.remotePathSep + f.name;
|
||||||
|
const dst = join(localInfo.path, f.name);
|
||||||
|
await this.downloadFile(src, dst);
|
||||||
|
this.sshClient!.emit("download", { source: src, destination: dst });
|
||||||
|
} else {
|
||||||
|
console.log(`downloadDir: File ignored: ${f.name} not regular file`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${remoteInfo.path} downloaded to ${localInfo.path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves attributes for `path`.
|
||||||
|
*/
|
||||||
|
public async stat(remotePath: string): Promise<Stats> {
|
||||||
|
utils.haveConnection(this, "stat");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.stat(remotePath, (err, stats) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(stats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the attributes defined in `attributes` for `path`.
|
||||||
|
*/
|
||||||
|
public async setstat(path: string, attributes: InputAttributes = {}): Promise<void> {
|
||||||
|
utils.haveConnection(this, "setstat");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.setstat(path, attributes, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the file/symlink at `path`.
|
||||||
|
*/
|
||||||
|
public async unlink(remotePath: string): Promise<void> {
|
||||||
|
utils.haveConnection(this, "unlink");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.unlink(remotePath, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// _rmdir - only works with an empty directory
|
||||||
|
async _rmdir(remotePath: string): Promise<void> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
this.sftpWrapper!.rmdir(remotePath, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async rmdir(remotePath: string): Promise<void> {
|
||||||
|
const files = await this.list(remotePath);
|
||||||
|
for (const file of files) {
|
||||||
|
const fullFilename = utils.joinRemote(this, remotePath, file.name);
|
||||||
|
if (file.type === "d") {
|
||||||
|
await this.rmdir(fullFilename);
|
||||||
|
} else {
|
||||||
|
await this.unlink(fullFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this._rmdir(remotePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new directory `path`.
|
||||||
|
*/
|
||||||
|
public async mkdir(remotePath: string, attributes: InputAttributes = {}): Promise<void> {
|
||||||
|
utils.haveConnection(this, "mkdir");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.mkdir(remotePath, attributes, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async exists(remotePath: string): Promise<string | boolean> {
|
||||||
|
utils.haveConnection(this, "exists");
|
||||||
|
try {
|
||||||
|
const stats = await this.stat(remotePath);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
return "d";
|
||||||
|
}
|
||||||
|
if (stats.isSymbolicLink()) {
|
||||||
|
return "l";
|
||||||
|
}
|
||||||
|
if (stats.isFile()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes data to a file
|
||||||
|
*/
|
||||||
|
public async writeFile(remotePath: string, data: string | Buffer, options: WriteFileOptions = {}): Promise<void> {
|
||||||
|
utils.haveConnection(this, "writeFile");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.writeFile(remotePath, data, options, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the access time and modified time for `path`.
|
||||||
|
*/
|
||||||
|
public async utimes(path: string, atime: number | Date, mtime: number | Date): Promise<void> {
|
||||||
|
utils.haveConnection(this, "utimes");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.utimes(path, atime, mtime, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a symlink at `linkPath` to `targetPath`.
|
||||||
|
*/
|
||||||
|
public async symlink(targetPath: string, linkPath: string): Promise<void> {
|
||||||
|
utils.haveConnection(this, "symlink");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.symlink(targetPath, linkPath, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames/moves `srcPath` to `destPath`.
|
||||||
|
*/
|
||||||
|
public async rename(srcPath: string, destPath: string): Promise<void> {
|
||||||
|
utils.haveConnection(this, "rename");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.rename(srcPath, destPath, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the target for a symlink at `path`.
|
||||||
|
*/
|
||||||
|
public async readlink(path: string): Promise<string> {
|
||||||
|
utils.haveConnection(this, "readlink");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.readlink(path, (err, target) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a file in memory and returns its contents
|
||||||
|
*/
|
||||||
|
public async readFile(remotePath: string): Promise<Buffer> {
|
||||||
|
utils.haveConnection(this, "readFile");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.readFile(remotePath, (err, handle) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(handle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves attributes for `path`. If `path` is a symlink, the link itself is stat'ed
|
||||||
|
* instead of the resource it refers to.
|
||||||
|
*/
|
||||||
|
public async lstat(path: string): Promise<Stats> {
|
||||||
|
utils.haveConnection(this, "lstat");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.lstat(path, (err, stats) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(stats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends data to a file
|
||||||
|
*/
|
||||||
|
public async appendFile(remotePath: string, data: string | Buffer, options: WriteFileOptions): Promise<void> {
|
||||||
|
utils.haveConnection(this, "appendFile");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.appendFile(remotePath, data, options, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the mode for `path`.
|
||||||
|
*/
|
||||||
|
public async chmod(path: string, mode: number | string): Promise<void> {
|
||||||
|
utils.haveConnection(this, "chmod");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.chmod(path, mode, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the owner for `path`.
|
||||||
|
*/
|
||||||
|
public async chown(path: string, uid: number, gid: number): Promise<void> {
|
||||||
|
utils.haveConnection(this, "chown");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.sftpWrapper!.chown(path, uid, gid, err => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close SSH connection
|
||||||
|
*/
|
||||||
|
public close() {
|
||||||
|
if (this.sshClient && this.sftpWrapper) {
|
||||||
|
this.sshClient.end();
|
||||||
|
this.sshClient = null;
|
||||||
|
this.sftpWrapper = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.endCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all files and directories at remotePath
|
||||||
|
*/
|
||||||
|
public async list(remotePath: string, pattern = /.*/): Promise<any> {
|
||||||
|
const _list = (aPath: string, filter: RegExp | string) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reg = /-/gi;
|
||||||
|
this.sftpWrapper!.readdir(aPath, (err, fileList) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
let newList: any = [];
|
||||||
|
// reset file info
|
||||||
|
if (fileList) {
|
||||||
|
newList = fileList.map(item => {
|
||||||
|
return {
|
||||||
|
type: item.longname.substr(0, 1),
|
||||||
|
name: item.filename,
|
||||||
|
size: item.attrs.size,
|
||||||
|
modifyTime: item.attrs.mtime * 1000,
|
||||||
|
accessTime: item.attrs.atime * 1000,
|
||||||
|
rights: {
|
||||||
|
user: item.longname.substr(1, 3).replace(reg, ""),
|
||||||
|
group: item.longname.substr(4, 3).replace(reg, ""),
|
||||||
|
other: item.longname.substr(7, 3).replace(reg, ""),
|
||||||
|
},
|
||||||
|
owner: item.attrs.uid,
|
||||||
|
group: item.attrs.gid,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// provide some compatibility for auxList
|
||||||
|
let regex: RegExp;
|
||||||
|
if (filter instanceof RegExp) {
|
||||||
|
regex = filter;
|
||||||
|
} else {
|
||||||
|
const newPattern = filter.replace(/\*([^*])*?/gi, ".*");
|
||||||
|
regex = new RegExp(newPattern);
|
||||||
|
}
|
||||||
|
resolve(newList.filter((item: any) => regex.test(item.name)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
utils.haveConnection(this, "list");
|
||||||
|
const pathInfo = await utils.checkRemotePath(this, remotePath, targetType.readDir);
|
||||||
|
if (!pathInfo.valid) {
|
||||||
|
throw new Error("Remote path is invalid");
|
||||||
|
}
|
||||||
|
return _list(pathInfo.path, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves `path` to an absolute path.
|
||||||
|
*/
|
||||||
|
public realPath(remotePath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const closeListener = utils.makeCloseListener(this, reject, "realPath");
|
||||||
|
this.sshClient!.prependListener("close", closeListener);
|
||||||
|
const errorListener = utils.makeErrorListener(reject, this, "realPath");
|
||||||
|
this.sshClient!.prependListener("error", errorListener);
|
||||||
|
if (utils.haveConnection(this, "realPath", reject)) {
|
||||||
|
this.sftpWrapper!.realpath(remotePath, (err, absPath) => {
|
||||||
|
if (err) {
|
||||||
|
reject(utils.formatError(`${err.message} ${remotePath}`, "realPath"));
|
||||||
|
}
|
||||||
|
resolve(absPath);
|
||||||
|
this.removeListener("error", errorListener);
|
||||||
|
this.removeListener("close", closeListener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Client(options: TScpOptions): Promise<ScpClient> {
|
||||||
|
const client = new ScpClient(options);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.on("ready", () => {
|
||||||
|
resolve(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("close", () => {
|
||||||
|
client.removeAllListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const event in options.events) {
|
||||||
|
client.on(event, (...args) => {
|
||||||
|
(options.events![event as keyof ClientEvents] as (...args: any[]) => void)(...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Client;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import type {
|
||||||
|
AcceptConnection,
|
||||||
|
ChangePasswordCallback,
|
||||||
|
ClientChannel,
|
||||||
|
ClientErrorExtensions,
|
||||||
|
KeyboardInteractiveCallback,
|
||||||
|
NegotiatedAlgorithms,
|
||||||
|
ParsedKey,
|
||||||
|
Prompt,
|
||||||
|
RejectConnection,
|
||||||
|
TcpConnectionDetails,
|
||||||
|
UNIXConnectionDetails,
|
||||||
|
X11Details,
|
||||||
|
} from "ssh2";
|
||||||
|
|
||||||
|
export class ErrorCustom extends Error {
|
||||||
|
custom?: boolean;
|
||||||
|
code?: string;
|
||||||
|
level?: string;
|
||||||
|
hostname?: string;
|
||||||
|
address?: string;
|
||||||
|
}
|
||||||
|
export interface CheckResult {
|
||||||
|
path: string;
|
||||||
|
type?: string;
|
||||||
|
valid?: boolean;
|
||||||
|
msg?: string;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientEvents {
|
||||||
|
/**
|
||||||
|
* Emitted when a notice was sent by the server upon connection.
|
||||||
|
*/
|
||||||
|
banner?: (message: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when authentication was successful.
|
||||||
|
*/
|
||||||
|
ready?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when an incoming forwarded TCP connection is being requested.
|
||||||
|
*
|
||||||
|
* Calling `accept()` accepts the connection and returns a `Channel` object.
|
||||||
|
* Calling `reject()` rejects the connection and no further action is needed.
|
||||||
|
*/
|
||||||
|
"tcp connection"?: (details: TcpConnectionDetails, accept: AcceptConnection<ClientChannel>, reject: RejectConnection) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when an incoming X11 connection is being requested.
|
||||||
|
*
|
||||||
|
* Calling `accept()` accepts the connection and returns a `Channel` object.
|
||||||
|
* Calling `reject()` rejects the connection and no further action is needed.
|
||||||
|
*/
|
||||||
|
x11?: (details: X11Details, accept: AcceptConnection<ClientChannel>, reject: RejectConnection) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the server is asking for replies to the given `prompts` for keyboard-
|
||||||
|
* interactive user authentication.
|
||||||
|
*
|
||||||
|
* * `name` is generally what you'd use as a window title (for GUI apps).
|
||||||
|
* * `prompts` is an array of `Prompt` objects.
|
||||||
|
*
|
||||||
|
* The answers for all prompts must be provided as an array of strings and passed to
|
||||||
|
* `finish` when you are ready to continue.
|
||||||
|
*
|
||||||
|
* NOTE: It's possible for the server to come back and ask more questions.
|
||||||
|
*/
|
||||||
|
"keyboard-interactive"?: (name: string, instructions: string, lang: string, prompts: Prompt[], finish: KeyboardInteractiveCallback) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the server has requested that the user's password be changed, if using
|
||||||
|
* password-based user authentication.
|
||||||
|
*
|
||||||
|
* Call `done` with the new password.
|
||||||
|
*/
|
||||||
|
"change password"?: (message: string, done: ChangePasswordCallback) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when an error occurred.
|
||||||
|
*/
|
||||||
|
error?: (err: Error & ClientErrorExtensions) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the socket was disconnected.
|
||||||
|
*/
|
||||||
|
end?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the socket was closed.
|
||||||
|
*/
|
||||||
|
close?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the socket has timed out.
|
||||||
|
*/
|
||||||
|
timeout?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the socket has connected.
|
||||||
|
*/
|
||||||
|
connect?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the server responds with a greeting message.
|
||||||
|
*/
|
||||||
|
greeting?: (greeting: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when a handshake has completed (either initial or rekey).
|
||||||
|
*/
|
||||||
|
handshake?: (negotiated: NegotiatedAlgorithms) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the server announces its available host keys.
|
||||||
|
*/
|
||||||
|
hostkeys?: (keys: ParsedKey[]) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An incoming forwarded UNIX socket connection is being requested.
|
||||||
|
*/
|
||||||
|
"unix connection"?: (info: UNIXConnectionDetails, accept: AcceptConnection, reject: RejectConnection) => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { errorCode, targetType } from "./constant.js";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { ScpClient } from ".";
|
||||||
|
import { CheckResult, ErrorCustom } from "./types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new Error object with a reformatted error message which
|
||||||
|
* is a little more informative and useful to users.
|
||||||
|
*
|
||||||
|
* @param {Error|string} err - The Error object the new error will be based on
|
||||||
|
* @param {number} retryCount - For those functions which use retry. Number of
|
||||||
|
* attempts to complete before giving up
|
||||||
|
* @returns {Error} New error with custom error message
|
||||||
|
*/
|
||||||
|
export function formatError(err: ErrorCustom | string, name = "sftp", eCode = errorCode.generic, retryCount?: number) {
|
||||||
|
let msg = "";
|
||||||
|
let code = "";
|
||||||
|
const retry = retryCount ? ` after ${retryCount} ${retryCount > 1 ? "attempts" : "attempt"}` : "";
|
||||||
|
|
||||||
|
if (err === undefined) {
|
||||||
|
msg = `${name}: Undefined error - probably a bug!`;
|
||||||
|
code = errorCode.generic;
|
||||||
|
} else if (typeof err === "string") {
|
||||||
|
msg = `${name}: ${err}${retry}`;
|
||||||
|
code = eCode;
|
||||||
|
} else if (err.custom) {
|
||||||
|
msg = `${name}->${err.message}${retry}`;
|
||||||
|
code = err.code!;
|
||||||
|
} else {
|
||||||
|
switch (err.code) {
|
||||||
|
case "ENOTFOUND":
|
||||||
|
msg = `${name}: ${err.level} error. ` + `Address lookup failed for host ${err.hostname}${retry}`;
|
||||||
|
break;
|
||||||
|
case "ECONNREFUSED":
|
||||||
|
msg = `${name}: ${err.level} error. Remote host at ` + `${err.address} refused connection${retry}`;
|
||||||
|
break;
|
||||||
|
case "ECONNRESET":
|
||||||
|
msg = `${name}: Remote host has reset the connection: ` + `${err.message}${retry}`;
|
||||||
|
break;
|
||||||
|
case "ENOENT":
|
||||||
|
msg = `${name}: ${err.message}${retry}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
msg = `${name}: ${err.message}${retry}`;
|
||||||
|
}
|
||||||
|
code = err.code ? err.code : eCode;
|
||||||
|
}
|
||||||
|
const newError = new ErrorCustom(msg);
|
||||||
|
newError.code = code;
|
||||||
|
newError.custom = true;
|
||||||
|
return newError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests an error to see if it is one which has already been customised
|
||||||
|
* by this module or not. If not, applies appropriate customisation.
|
||||||
|
*
|
||||||
|
* @param {Error} err - an Error object
|
||||||
|
* @param {String} name - name to be used in customised error message
|
||||||
|
* @param {Function} reject - If defined, call this function instead of
|
||||||
|
* throwing the error
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
export function handleError(err: ErrorCustom, name: string, reject: (e: any) => void) {
|
||||||
|
if (reject) {
|
||||||
|
if (err.custom) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
reject(formatError(err, name, undefined, undefined));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (err.custom) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
throw formatError(err, name, undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all ready, error and end listeners.
|
||||||
|
*
|
||||||
|
* @param {Emitter} emitter - The emitter object to remove listeners from
|
||||||
|
*/
|
||||||
|
// function removeListeners(emitter) {
|
||||||
|
// const listeners = emitter.eventNames()
|
||||||
|
// listeners.forEach((name) => {
|
||||||
|
// emitter.removeAllListeners(name)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple default error listener. Will reformat the error message and
|
||||||
|
* throw a new error.
|
||||||
|
*
|
||||||
|
* @param {Error} err - source for defining new error
|
||||||
|
* @throws {Error} Throws new error
|
||||||
|
*/
|
||||||
|
export function makeErrorListener(reject: (e: any) => void, client: ScpClient, name: string) {
|
||||||
|
return (err: Error) => {
|
||||||
|
client.errorHandled = true;
|
||||||
|
reject(formatError(err, name));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeEndListener(client: ScpClient) {
|
||||||
|
return () => {
|
||||||
|
if (!client.endCalled) {
|
||||||
|
console.error("End Listener: Connection ended unexpectedly");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeCloseListener(client: ScpClient, reject?: (e: any) => void, name?: string) {
|
||||||
|
return () => {
|
||||||
|
if (!client.endCalled) {
|
||||||
|
if (reject) {
|
||||||
|
reject(formatError("Connection closed unexpectedly", name));
|
||||||
|
} else {
|
||||||
|
console.error("Connection closed unexpectedly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.sftpWrapper = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @async
|
||||||
|
*
|
||||||
|
* Tests to see if a path identifies an existing item. Returns either
|
||||||
|
* 'd' = directory, 'l' = sym link or '-' regular file if item exists. Returns
|
||||||
|
* false if it does not
|
||||||
|
*
|
||||||
|
* @param {String} localPath
|
||||||
|
* @returns {Boolean | String}
|
||||||
|
*/
|
||||||
|
export function localExists(localPath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.stat(localPath, (err, stats) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
resolve("ENOENT");
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
resolve("d");
|
||||||
|
} else if (stats.isSymbolicLink()) {
|
||||||
|
resolve("l");
|
||||||
|
} else if (stats.isFile()) {
|
||||||
|
resolve("-");
|
||||||
|
} else {
|
||||||
|
resolve("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by checkRemotePath and checkLocalPath to help ensure consistent
|
||||||
|
* error messages.
|
||||||
|
*
|
||||||
|
* @param {Error} err - original error
|
||||||
|
* @param {String} testPath - path associated with the error
|
||||||
|
* @returns {Object} with properties of 'msg' and 'code'.
|
||||||
|
*/
|
||||||
|
export function classifyError(err: ErrorCustom, testPath: string) {
|
||||||
|
switch (err.code) {
|
||||||
|
case "EACCES":
|
||||||
|
return {
|
||||||
|
msg: `Permission denied: ${testPath}`,
|
||||||
|
code: errorCode.permission,
|
||||||
|
};
|
||||||
|
case "ENOENT":
|
||||||
|
return {
|
||||||
|
msg: `No such file: ${testPath}`,
|
||||||
|
code: errorCode.notexist,
|
||||||
|
};
|
||||||
|
case "ENOTDIR":
|
||||||
|
return {
|
||||||
|
msg: `Not a directory: ${testPath}`,
|
||||||
|
code: errorCode.notdir,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
msg: err.message,
|
||||||
|
code: err.code ? err.code : errorCode.generic,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localAccess(localPath: string, mode: number): Promise<CheckResult> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
fs.access(localPath, mode, err => {
|
||||||
|
if (err) {
|
||||||
|
const { msg, code } = classifyError(err, localPath);
|
||||||
|
resolve({
|
||||||
|
path: localPath,
|
||||||
|
valid: false,
|
||||||
|
msg,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
path: localPath,
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkLocalReadFile(localPath: string, localType: string) {
|
||||||
|
try {
|
||||||
|
const rslt: CheckResult = {
|
||||||
|
path: localPath,
|
||||||
|
type: localType,
|
||||||
|
};
|
||||||
|
if (localType === "d") {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = `Bad path: ${localPath} must be a file`;
|
||||||
|
rslt.code = errorCode.badPath;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
const access = await localAccess(localPath, fs.constants.R_OK);
|
||||||
|
if (access.valid) {
|
||||||
|
rslt.valid = true;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = access.msg;
|
||||||
|
rslt.code = access.code;
|
||||||
|
return rslt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw formatError(err as ErrorCustom, "checkLocalReadFile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkLocalReadDir(localPath: string, localType: string) {
|
||||||
|
try {
|
||||||
|
const rslt: CheckResult = {
|
||||||
|
path: localPath,
|
||||||
|
type: localType,
|
||||||
|
};
|
||||||
|
if (!localType) {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = `No such directory: ${localPath}`;
|
||||||
|
rslt.code = errorCode.notdir;
|
||||||
|
return rslt;
|
||||||
|
} else if (localType !== "d") {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = `Bad path: ${localPath} must be a directory`;
|
||||||
|
rslt.code = errorCode.badPath;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
const access = await localAccess(localPath, fs.constants.R_OK | fs.constants.X_OK);
|
||||||
|
if (!access.valid) {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = access.msg;
|
||||||
|
rslt.code = access.code;
|
||||||
|
return rslt;
|
||||||
|
}
|
||||||
|
rslt.valid = true;
|
||||||
|
return rslt;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw formatError(err as ErrorCustom, "checkLocalReadDir");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkLocalWriteFile(localPath: string, localType: string) {
|
||||||
|
try {
|
||||||
|
const rslt: CheckResult = {
|
||||||
|
path: localPath,
|
||||||
|
type: localType,
|
||||||
|
};
|
||||||
|
if (localType === "d") {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = `Bad path: ${localPath} must be a file`;
|
||||||
|
rslt.code = errorCode.badPath;
|
||||||
|
return rslt;
|
||||||
|
} else if (!localType) {
|
||||||
|
const dir = path.parse(localPath).dir;
|
||||||
|
const parent = await localAccess(dir, fs.constants.W_OK);
|
||||||
|
if (parent.valid) {
|
||||||
|
rslt.valid = true;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = parent.msg;
|
||||||
|
rslt.code = parent.code;
|
||||||
|
return rslt;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const access = await localAccess(localPath, fs.constants.W_OK);
|
||||||
|
if (access.valid) {
|
||||||
|
rslt.valid = true;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = access.msg;
|
||||||
|
rslt.code = access.code;
|
||||||
|
return rslt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw formatError(err as ErrorCustom, "checkLocalWriteFile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkLocalWriteDir(localPath: string, localType: string) {
|
||||||
|
try {
|
||||||
|
const rslt: CheckResult = {
|
||||||
|
path: localPath,
|
||||||
|
type: localType,
|
||||||
|
};
|
||||||
|
if (!localType) {
|
||||||
|
const parent = path.parse(localPath).dir;
|
||||||
|
const access = await localAccess(parent, fs.constants.W_OK);
|
||||||
|
if (access.valid) {
|
||||||
|
rslt.valid = true;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = access.msg;
|
||||||
|
rslt.code = access.code;
|
||||||
|
return rslt;
|
||||||
|
}
|
||||||
|
} else if (localType !== "d") {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = `Bad path: ${localPath} must be a directory`;
|
||||||
|
rslt.code = errorCode.badPath;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
const access = await localAccess(localPath, fs.constants.W_OK);
|
||||||
|
if (access.valid) {
|
||||||
|
rslt.valid = true;
|
||||||
|
return rslt;
|
||||||
|
} else {
|
||||||
|
rslt.valid = false;
|
||||||
|
rslt.msg = access.msg;
|
||||||
|
rslt.code = access.code;
|
||||||
|
return rslt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw formatError(err as ErrorCustom, "checkLocalWriteDir");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkLocalPath(lPath: string, target = targetType.readFile) {
|
||||||
|
const localPath = path.resolve(lPath);
|
||||||
|
const type = await localExists(localPath);
|
||||||
|
switch (target) {
|
||||||
|
case targetType.readFile:
|
||||||
|
return checkLocalReadFile(localPath, type);
|
||||||
|
case targetType.readDir:
|
||||||
|
return checkLocalReadDir(localPath, type);
|
||||||
|
case targetType.writeFile:
|
||||||
|
return checkLocalWriteFile(localPath, type);
|
||||||
|
case targetType.writeDir:
|
||||||
|
return checkLocalWriteDir(localPath, type);
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
path: localPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function normalizeRemotePath(client: ScpClient, aPath: string) {
|
||||||
|
try {
|
||||||
|
if (aPath.startsWith("..")) {
|
||||||
|
const root = await client.realPath("..");
|
||||||
|
return root + client.remotePathSep + aPath.substring(3);
|
||||||
|
} else if (aPath.startsWith(".")) {
|
||||||
|
const root = await client.realPath(".");
|
||||||
|
return root + client.remotePathSep + aPath.substring(2);
|
||||||
|
}
|
||||||
|
return aPath;
|
||||||
|
} catch (err) {
|
||||||
|
throw formatError(err as ErrorCustom, "normalizeRemotePath");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkReadObject(aPath: string, type: string) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: type ? true : false,
|
||||||
|
msg: type ? undefined : `No such file ${aPath}`,
|
||||||
|
code: type ? undefined : errorCode.notexist,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkReadFile(aPath: string, type: string) {
|
||||||
|
if (!type) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `No such file: ${aPath}`,
|
||||||
|
code: errorCode.notexist,
|
||||||
|
};
|
||||||
|
} else if (type === "d") {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${aPath} must be a file`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkReadDir(aPath: string, type: string) {
|
||||||
|
if (!type) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `No such directory: ${aPath}`,
|
||||||
|
code: errorCode.notdir,
|
||||||
|
};
|
||||||
|
} else if (type !== "d") {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${aPath} must be a directory`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkWriteFile(client: ScpClient, aPath: string, type: string) {
|
||||||
|
if (type && type === "d") {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${aPath} must be a regular file`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
} else if (!type) {
|
||||||
|
const { root, dir } = path.parse(aPath);
|
||||||
|
// let parentDir = path.parse(aPath).dir;
|
||||||
|
if (!dir) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type: false,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${aPath} cannot determine parent directory`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (root === dir) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const parentType = await client.exists(dir);
|
||||||
|
if (!parentType) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${dir} parent not exist`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
} else if (parentType !== "d") {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${dir} must be a directory`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkWriteDir(client: ScpClient, aPath: string, type: string) {
|
||||||
|
if (type && type !== "d") {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${aPath} must be a directory`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
} else if (!type) {
|
||||||
|
const { root, dir } = path.parse(aPath);
|
||||||
|
if (root === dir) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!dir) {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type: false,
|
||||||
|
valid: false,
|
||||||
|
msg: `Bad path: ${aPath} cannot determine directory parent`,
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const parentType = await client.exists(dir);
|
||||||
|
if (parentType && parentType !== "d") {
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: false,
|
||||||
|
msg: "Bad path: Parent Directory must be a directory",
|
||||||
|
code: errorCode.badPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// don't care if parent does not exist as it might be created
|
||||||
|
// via recursive call to mkdir.
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkWriteObject(aPath: string, type: string) {
|
||||||
|
// for writeObj, not much error checking we can do
|
||||||
|
// Just return path, type and valid indicator
|
||||||
|
return {
|
||||||
|
path: aPath,
|
||||||
|
type,
|
||||||
|
valid: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkRemotePath(client: ScpClient, rPath: string, target = targetType.readFile) {
|
||||||
|
const aPath = await normalizeRemotePath(client, rPath);
|
||||||
|
const type = await client.exists(aPath);
|
||||||
|
switch (target) {
|
||||||
|
case targetType.readObj:
|
||||||
|
return checkReadObject(aPath, type as string);
|
||||||
|
case targetType.readFile:
|
||||||
|
return checkReadFile(aPath, type as string);
|
||||||
|
case targetType.readDir:
|
||||||
|
return checkReadDir(aPath, type as string);
|
||||||
|
case targetType.writeFile:
|
||||||
|
return checkWriteFile(client, aPath, type as string);
|
||||||
|
case targetType.writeDir:
|
||||||
|
return checkWriteDir(client, aPath, type as string);
|
||||||
|
case targetType.writeObj:
|
||||||
|
return checkWriteObject(aPath, type as string);
|
||||||
|
default:
|
||||||
|
throw formatError(`Unknown target type: ${target}`, "checkRemotePath", errorCode.generic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if there is an active sftp connection
|
||||||
|
*
|
||||||
|
* @param {Object} client - current sftp object
|
||||||
|
* @param {String} name - name given to this connection
|
||||||
|
* @param {Function} reject - if defined, call this rather than throw
|
||||||
|
* an error
|
||||||
|
* @returns {Boolean} True if connection OK
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
export function haveConnection(client: ScpClient, name: string, reject?: (e: any) => void) {
|
||||||
|
if (!client.sftpWrapper) {
|
||||||
|
const newError = formatError("No SFTP connection available", name, errorCode.connect);
|
||||||
|
if (reject) {
|
||||||
|
reject(newError);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw newError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dumpListeners(emitter: EventEmitter) {
|
||||||
|
const eventNames = emitter.eventNames();
|
||||||
|
if (eventNames.length) {
|
||||||
|
console.log("Listener Data");
|
||||||
|
eventNames.map((n: any) => {
|
||||||
|
const listeners = emitter.listeners(n);
|
||||||
|
console.log(`${n}: ${emitter.listenerCount(n)}`);
|
||||||
|
console.dir(listeners);
|
||||||
|
listeners.map(l => {
|
||||||
|
console.log(`listener name = ${l.name}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasListener(emitter: EventEmitter, eventName: string, listenerName: string) {
|
||||||
|
const listeners = emitter.listeners(eventName);
|
||||||
|
const matches = listeners.filter(l => l.name === listenerName);
|
||||||
|
return matches.length === 0 ? false : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinRemote(client: ScpClient, ...args: string[]) {
|
||||||
|
if (client.remotePathSep === path.win32.sep) {
|
||||||
|
return path.win32.join(...args);
|
||||||
|
}
|
||||||
|
return path.posix.join(...args);
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import { SshAccess } from "./ssh-access.js";
|
|||||||
import stripAnsi from "strip-ansi";
|
import stripAnsi from "strip-ansi";
|
||||||
import { SocksClient } from "socks";
|
import { SocksClient } from "socks";
|
||||||
import { SocksProxy, SocksProxyType } from "socks/typings/common/constants.js";
|
import { SocksProxy, SocksProxyType } from "socks/typings/common/constants.js";
|
||||||
|
import { ScpClient } from "../scp/index.js";
|
||||||
|
import fs from "fs";
|
||||||
export type TransportItem = { localPath: string; remotePath: string };
|
export type TransportItem = { localPath: string; remotePath: string };
|
||||||
|
|
||||||
export class AsyncSsh2Client {
|
export class AsyncSsh2Client {
|
||||||
@@ -265,15 +267,15 @@ export class SshClient {
|
|||||||
}
|
}
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
async uploadFiles(options: { connectConf: SshAccess; transports: TransportItem[]; mkdirs: boolean; opts?: { mode?: string } }) {
|
async uploadFiles(options: { connectConf: SshAccess; transports: TransportItem[]; mkdirs: boolean; opts?: { mode?: string }; uploadType?: string }) {
|
||||||
const { connectConf, transports, mkdirs, opts } = options;
|
const { connectConf, transports, mkdirs, opts } = options;
|
||||||
await this._call({
|
await this._call({
|
||||||
connectConf,
|
connectConf,
|
||||||
callable: async (conn: AsyncSsh2Client) => {
|
callable: async (conn: AsyncSsh2Client) => {
|
||||||
const sftp = await conn.getSftp();
|
|
||||||
this.logger.info("开始上传");
|
this.logger.info("开始上传");
|
||||||
for (const transport of transports) {
|
if (mkdirs !== false) {
|
||||||
if (mkdirs !== false) {
|
this.logger.info("初始化父目录");
|
||||||
|
for (const transport of transports) {
|
||||||
const filePath = path.dirname(transport.remotePath);
|
const filePath = path.dirname(transport.remotePath);
|
||||||
let mkdirCmd = `mkdir -p ${filePath} `;
|
let mkdirCmd = `mkdir -p ${filePath} `;
|
||||||
if (conn.windows) {
|
if (conn.windows) {
|
||||||
@@ -291,13 +293,60 @@ export class SshClient {
|
|||||||
}
|
}
|
||||||
await conn.exec(mkdirCmd);
|
await conn.exec(mkdirCmd);
|
||||||
}
|
}
|
||||||
await conn.fastPut({ sftp, ...transport, opts });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.uploadType === "sftp") {
|
||||||
|
const sftp = await conn.getSftp();
|
||||||
|
for (const transport of transports) {
|
||||||
|
await conn.fastPut({ sftp, ...transport, opts });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//scp
|
||||||
|
for (const transport of transports) {
|
||||||
|
await this.scpUpload({ conn, ...transport, opts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info("文件全部上传成功");
|
this.logger.info("文件全部上传成功");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scpUpload(options: { conn: any; localPath: string; remotePath: string; opts?: { mode?: string } }) {
|
||||||
|
const { conn, localPath, remotePath, opts } = options;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 关键步骤:构造 SCP 命令
|
||||||
|
try {
|
||||||
|
this.logger.info(`开始上传:${localPath} => ${remotePath}`);
|
||||||
|
conn.conn.exec(
|
||||||
|
`scp -t ${remotePath}`, // -t 表示目标模式
|
||||||
|
(err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
// 准备 SCP 协议头
|
||||||
|
const fileStats = fs.statSync(localPath);
|
||||||
|
const fileName = path.basename(localPath);
|
||||||
|
|
||||||
|
// SCP 协议格式:C[权限] [文件大小] [文件名]\n
|
||||||
|
stream.write(`C0644 ${fileStats.size} ${fileName}\n`);
|
||||||
|
|
||||||
|
// 通过管道传输文件
|
||||||
|
fs.createReadStream(localPath)
|
||||||
|
.pipe(stream)
|
||||||
|
.on("finish", () => {
|
||||||
|
this.logger.info(`上传文件成功:${localPath} => ${remotePath}`);
|
||||||
|
resolve(true);
|
||||||
|
})
|
||||||
|
.on("error", reject);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async removeFiles(opts: { connectConf: SshAccess; files: string[] }) {
|
async removeFiles(opts: { connectConf: SshAccess; files: string[] }) {
|
||||||
const { connectConf, files } = opts;
|
const { connectConf, files } = opts;
|
||||||
await this._call({
|
await this._call({
|
||||||
|
|||||||
@@ -179,6 +179,21 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
})
|
})
|
||||||
accessId!: string;
|
accessId!: string;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: '上传方式',
|
||||||
|
helper: '选择上传方式,sftp或者scp',
|
||||||
|
value:"sftp",
|
||||||
|
component: {
|
||||||
|
name: 'a-select',
|
||||||
|
options: [
|
||||||
|
{ value: 'sftp', label: 'sftp' },
|
||||||
|
{ value: 'scp', label: 'scp' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
uploadType: string = 'sftp';
|
||||||
|
|
||||||
@TaskInput({
|
@TaskInput({
|
||||||
title: '自动创建远程目录',
|
title: '自动创建远程目录',
|
||||||
helper: '是否自动创建远程目录,如果关闭则你需要自己确保远程目录存在',
|
helper: '是否自动创建远程目录,如果关闭则你需要自己确保远程目录存在',
|
||||||
@@ -249,18 +264,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
|
|
||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
// copyFile(srcFile: string, destFile: string) {
|
|
||||||
// if (!srcFile || !destFile) {
|
|
||||||
// this.logger.warn(`srcFile:${srcFile} 或 destFile:${destFile} 为空,不复制`);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// const dir = destFile.substring(0, destFile.lastIndexOf('/'));
|
|
||||||
// if (!fs.existsSync(dir)) {
|
|
||||||
// fs.mkdirSync(dir, { recursive: true });
|
|
||||||
// }
|
|
||||||
// fs.copyFileSync(srcFile, destFile);
|
|
||||||
// this.logger.info(`复制文件:${srcFile} => ${destFile}`);
|
|
||||||
// }
|
|
||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
const { cert, accessId } = this;
|
const { cert, accessId } = this;
|
||||||
let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath, onePath } = this;
|
let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath, onePath } = this;
|
||||||
@@ -268,16 +272,6 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
|
|
||||||
const handle = async (opts: CertReaderHandleContext) => {
|
const handle = async (opts: CertReaderHandleContext) => {
|
||||||
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpJksPath, tmpPfxPath, tmpIcPath, tmpOnePath } = opts;
|
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpJksPath, tmpPfxPath, tmpIcPath, tmpOnePath } = opts;
|
||||||
// if (this.copyToThisHost) {
|
|
||||||
// this.logger.info('复制到目标路径');
|
|
||||||
// this.copyFile(tmpCrtPath, crtPath);
|
|
||||||
// this.copyFile(tmpKeyPath, keyPath);
|
|
||||||
// this.copyFile(tmpIcPath, this.icPath);
|
|
||||||
// this.copyFile(tmpPfxPath, this.pfxPath);
|
|
||||||
// this.copyFile(tmpDerPath, this.derPath);
|
|
||||||
// this.logger.warn('复制到当前主机功能已迁移到 “复制到本机”插件,请尽快换成复制到本机插件');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (accessId == null) {
|
if (accessId == null) {
|
||||||
this.logger.error('复制到当前主机功能已迁移到 “复制到本机”插件,请换成复制到本机插件');
|
this.logger.error('复制到当前主机功能已迁移到 “复制到本机”插件,请换成复制到本机插件');
|
||||||
@@ -355,7 +349,9 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||||||
connectConf,
|
connectConf,
|
||||||
transports,
|
transports,
|
||||||
mkdirs: this.mkdirs,
|
mkdirs: this.mkdirs,
|
||||||
|
uploadType: this.uploadType,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info('上传文件到服务器成功');
|
this.logger.info('上传文件到服务器成功');
|
||||||
//输出
|
//输出
|
||||||
this.hostCrtPath = crtPath;
|
this.hostCrtPath = crtPath;
|
||||||
|
|||||||
Reference in New Issue
Block a user