fp/services/our/src/utils/sftp.ts
CJ_Clippy c3da9e26bc
Some checks failed
ci / build (push) Failing after 12m34s
ci / test (push) Failing after 8m11s
add qbittorrent-nox docker image
2025-09-08 02:56:37 -08:00

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,
});