import type { Client, SFTPWrapper, FileEntry, Stats } from 'ssh2';
import { createReadStream, createWriteStream } from 'fs';
import { stat } from 'fs/promises';
import type { SFTPFileInfo } from '../types.js';
export async function getSFTPClient(client: Client): Promise<SFTPWrapper> {
return new Promise((resolve, reject) => {
client.sftp((err, sftp) => {
if (err) {
reject(new Error(`Failed to create SFTP session: ${err.message}`));
return;
}
resolve(sftp);
});
});
}
export async function uploadFile(
client: Client,
localPath: string,
remotePath: string
): Promise<{ bytesTransferred: number }> {
const sftp = await getSFTPClient(client);
return new Promise((resolve, reject) => {
// Get local file stats
stat(localPath)
.then((stats) => {
const readStream = createReadStream(localPath);
const writeStream = sftp.createWriteStream(remotePath);
let bytesTransferred = 0;
readStream.on('data', (chunk: Buffer | string) => {
bytesTransferred += typeof chunk === 'string' ? chunk.length : chunk.length;
});
readStream.on('error', (err: Error) => {
sftp.end();
reject(new Error(`Failed to read local file: ${err.message}`));
});
writeStream.on('error', (err: Error) => {
sftp.end();
reject(new Error(`Failed to write remote file: ${err.message}`));
});
writeStream.on('close', () => {
sftp.end();
resolve({ bytesTransferred });
});
readStream.pipe(writeStream);
})
.catch((err) => {
reject(new Error(`Local file not found: ${localPath}`));
});
});
}
export async function downloadFile(
client: Client,
remotePath: string,
localPath: string
): Promise<{ bytesTransferred: number }> {
const sftp = await getSFTPClient(client);
return new Promise((resolve, reject) => {
const readStream = sftp.createReadStream(remotePath);
const writeStream = createWriteStream(localPath);
let bytesTransferred = 0;
readStream.on('data', (chunk: Buffer | string) => {
bytesTransferred += typeof chunk === 'string' ? chunk.length : chunk.length;
});
readStream.on('error', (err: Error) => {
sftp.end();
reject(new Error(`Failed to read remote file: ${err.message}`));
});
writeStream.on('error', (err) => {
sftp.end();
reject(new Error(`Failed to write local file: ${err.message}`));
});
writeStream.on('close', () => {
sftp.end();
resolve({ bytesTransferred });
});
readStream.pipe(writeStream);
});
}
export async function listDirectory(
client: Client,
remotePath: string
): Promise<SFTPFileInfo[]> {
const sftp = await getSFTPClient(client);
return new Promise((resolve, reject) => {
sftp.readdir(remotePath, (err, list) => {
sftp.end();
if (err) {
reject(new Error(`Failed to list directory: ${err.message}`));
return;
}
const files: SFTPFileInfo[] = list.map((entry: FileEntry) => {
const attrs = entry.attrs;
return {
filename: entry.filename,
longname: entry.longname,
size: attrs.size,
modifyTime: new Date(attrs.mtime * 1000),
accessTime: new Date(attrs.atime * 1000),
isDirectory: isDirectory(attrs.mode),
isFile: isFile(attrs.mode),
isSymbolicLink: isSymbolicLink(attrs.mode),
permissions: attrs.mode & 0o777,
owner: attrs.uid,
group: attrs.gid,
};
});
resolve(files);
});
});
}
export async function statRemote(
client: Client,
remotePath: string
): Promise<SFTPFileInfo> {
const sftp = await getSFTPClient(client);
return new Promise((resolve, reject) => {
sftp.stat(remotePath, (err, stats) => {
sftp.end();
if (err) {
reject(new Error(`Failed to stat remote path: ${err.message}`));
return;
}
resolve({
filename: remotePath.split('/').pop() || remotePath,
longname: '',
size: stats.size,
modifyTime: new Date(stats.mtime * 1000),
accessTime: new Date(stats.atime * 1000),
isDirectory: isDirectory(stats.mode),
isFile: isFile(stats.mode),
isSymbolicLink: isSymbolicLink(stats.mode),
permissions: stats.mode & 0o777,
owner: stats.uid,
group: stats.gid,
});
});
});
}
export async function mkdir(
client: Client,
remotePath: string,
recursive: boolean = false
): Promise<void> {
const sftp = await getSFTPClient(client);
if (recursive) {
const parts = remotePath.split('/').filter(Boolean);
let currentPath = remotePath.startsWith('/') ? '' : '.';
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
try {
await new Promise<void>((resolve, reject) => {
sftp.mkdir(currentPath, (err) => {
if (err && (err as NodeJS.ErrnoException).code !== 'EEXIST') {
// Ignore "already exists" errors
if (!err.message.includes('already exists') && !err.message.includes('EEXIST')) {
reject(err);
return;
}
}
resolve();
});
});
} catch (e) {
// Check if directory exists
try {
await new Promise<void>((resolve, reject) => {
sftp.stat(currentPath, (err) => {
if (err) reject(err);
else resolve();
});
});
} catch {
sftp.end();
throw new Error(`Failed to create directory: ${currentPath}`);
}
}
}
sftp.end();
} else {
return new Promise((resolve, reject) => {
sftp.mkdir(remotePath, (err) => {
sftp.end();
if (err) {
reject(new Error(`Failed to create directory: ${err.message}`));
return;
}
resolve();
});
});
}
}
export async function rm(
client: Client,
remotePath: string
): Promise<void> {
const sftp = await getSFTPClient(client);
return new Promise((resolve, reject) => {
sftp.unlink(remotePath, (err) => {
sftp.end();
if (err) {
reject(new Error(`Failed to delete file: ${err.message}`));
return;
}
resolve();
});
});
}
export async function rmdir(
client: Client,
remotePath: string
): Promise<void> {
const sftp = await getSFTPClient(client);
return new Promise((resolve, reject) => {
sftp.rmdir(remotePath, (err) => {
sftp.end();
if (err) {
reject(new Error(`Failed to delete directory: ${err.message}`));
return;
}
resolve();
});
});
}
// File mode helpers
function isDirectory(mode: number): boolean {
return (mode & 0o170000) === 0o040000;
}
function isFile(mode: number): boolean {
return (mode & 0o170000) === 0o100000;
}
function isSymbolicLink(mode: number): boolean {
return (mode & 0o170000) === 0o120000;
}