120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
// src/utils/sftp.ts
|
|
import { Client, ConnectConfig, SFTPWrapper } from 'ssh2';
|
|
import path from 'path';
|
|
import { env } from '../config/env';
|
|
import logger from './logger';
|
|
|
|
interface SSHClientOptions {
|
|
host: string;
|
|
port?: number;
|
|
username: string;
|
|
password?: string;
|
|
privateKey?: Buffer;
|
|
}
|
|
|
|
export class SSHClient {
|
|
private client = new Client();
|
|
private sftp?: SFTPWrapper;
|
|
private connected = false;
|
|
|
|
constructor(private options: SSHClientOptions) { }
|
|
|
|
async connect(): Promise<void> {
|
|
if (this.connected) return;
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.client
|
|
.on('ready', () => resolve())
|
|
.on('error', reject)
|
|
.connect({
|
|
host: this.options.host,
|
|
port: this.options.port || 22,
|
|
username: this.options.username,
|
|
password: this.options.password,
|
|
privateKey: this.options.privateKey,
|
|
} as ConnectConfig);
|
|
});
|
|
|
|
this.connected = true;
|
|
}
|
|
|
|
private async getSFTP(): Promise<SFTPWrapper> {
|
|
if (!this.sftp) {
|
|
this.sftp = await new Promise((resolve, reject) => {
|
|
this.client.sftp((err, sftp) => {
|
|
if (err) reject(err);
|
|
else resolve(sftp);
|
|
});
|
|
});
|
|
}
|
|
return this.sftp;
|
|
}
|
|
|
|
async exec(command: string): Promise<string> {
|
|
await this.connect();
|
|
return new Promise<string>((resolve, reject) => {
|
|
this.client.exec(command, (err, stream) => {
|
|
if (err) return reject(err);
|
|
let stdout = '';
|
|
let stderr = '';
|
|
stream
|
|
.on('close', (code: number) => {
|
|
if (code !== 0) reject(new Error(`Command failed: ${stderr}`));
|
|
else resolve(stdout.trim());
|
|
})
|
|
.on('data', (data: Buffer) => (stdout += data.toString()))
|
|
.stderr.on('data', (data: Buffer) => (stderr += data.toString()));
|
|
});
|
|
});
|
|
}
|
|
|
|
async uploadFile(localFilePath: string, remoteDir: string): Promise<void> {
|
|
logger.info(`Uploading localFilePath=${localFilePath} to remoteDir=${remoteDir}...`);
|
|
|
|
logger.debug('awaiting connect')
|
|
await this.connect();
|
|
|
|
logger.debug('getting sftp')
|
|
const sftp = await this.getSFTP();
|
|
|
|
logger.debug('getting fileName')
|
|
const fileName = path.basename(localFilePath);
|
|
|
|
logger.debug(`fileName=${fileName}`)
|
|
|
|
const remoteFilePath = path.posix.join(remoteDir, fileName);
|
|
logger.debug(`remoteFilePath=${remoteFilePath}`)
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
sftp.fastPut(localFilePath, remoteFilePath, (err) => (err ? reject(err) : resolve()));
|
|
});
|
|
}
|
|
|
|
async downloadFile(remoteFilePath: string, localPath: string): Promise<void> {
|
|
logger.info(`downloading remoteFilePath=${remoteFilePath} to localPath=${localPath}`)
|
|
await this.connect();
|
|
const sftp = await this.getSFTP();
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
sftp.fastGet(remoteFilePath, localPath, (err) => (err ? reject(err) : resolve()));
|
|
});
|
|
}
|
|
|
|
end(): void {
|
|
this.client.end();
|
|
this.connected = false;
|
|
}
|
|
}
|
|
|
|
// --- usage helper ---
|
|
const url = URL.parse(env.SEEDBOX_SFTP_URL);
|
|
const hostname = url?.hostname;
|
|
const port = url?.port;
|
|
|
|
export const sshClient = new SSHClient({
|
|
host: hostname!,
|
|
port: port ? parseInt(port) : 22,
|
|
username: env.SEEDBOX_SFTP_USERNAME,
|
|
password: env.SEEDBOX_SFTP_PASSWORD,
|
|
});
|