// 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 { if (this.connected) return; await new Promise((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 { 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 { await this.connect(); return new Promise((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 { 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((resolve, reject) => { sftp.fastPut(localFilePath, remoteFilePath, (err) => (err ? reject(err) : resolve())); }); } async downloadFile(remoteFilePath: string, localPath: string): Promise { logger.info(`downloading remoteFilePath=${remoteFilePath} to localPath=${localPath}`) await this.connect(); const sftp = await this.getSFTP(); await new Promise((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, });