Promise-based SFTP client for Node.js that wraps the ssh2 module
—
File management operations including deletion, renaming, permission changes, and remote file copying with advanced features like atomic operations.
Deletes a file from the remote server with optional error suppression for missing files.
/**
* Delete a file on the remote SFTP server
* @param remotePath - Path to file to delete
* @param notFoundOK - If true, ignore "file not found" errors (default: false)
* @param addListeners - Whether to add event listeners (default: true)
* @returns Promise resolving to success message
*/
delete(remotePath, notFoundOK = false, addListeners = true): Promise<String>;Usage Examples:
// Delete existing file
await sftp.delete('/remote/old-file.txt');
// Delete file, ignore if it doesn't exist
await sftp.delete('/remote/maybe-exists.txt', true);
// Safe deletion with existence check
const exists = await sftp.exists('/remote/temp-file.dat');
if (exists === '-') {
await sftp.delete('/remote/temp-file.dat');
console.log('Temporary file deleted');
}
// Bulk deletion with error handling
const filesToDelete = ['/remote/file1.txt', '/remote/file2.txt', '/remote/file3.txt'];
for (const file of filesToDelete) {
try {
await sftp.delete(file, true); // Ignore missing files
console.log(`Deleted: ${file}`);
} catch (err) {
console.error(`Failed to delete ${file}: ${err.message}`);
}
}Renames or moves a file/directory on the remote server.
/**
* Rename a file or directory on the remote server
* @param fromPath - Current path of file/directory
* @param toPath - New path for file/directory
* @param addListeners - Whether to add event listeners (default: true)
* @returns Promise resolving to success message
*/
rename(fromPath, toPath, addListeners = true): Promise<String>;Usage Examples:
// Simple file rename
await sftp.rename('/remote/old-name.txt', '/remote/new-name.txt');
// Move file to different directory
await sftp.rename('/remote/temp/file.dat', '/remote/archive/file.dat');
// Rename directory
await sftp.rename('/remote/old-folder', '/remote/new-folder');
// Safe rename with existence checks
const source = '/remote/source.txt';
const destination = '/remote/destination.txt';
const sourceExists = await sftp.exists(source);
const destExists = await sftp.exists(destination);
if (sourceExists === '-' && destExists === false) {
await sftp.rename(source, destination);
console.log('File renamed successfully');
} else if (destExists !== false) {
console.error('Destination already exists');
} else {
console.error('Source file does not exist');
}Performs atomic rename using the POSIX rename extension (SSH 4.8+).
/**
* Rename using POSIX atomic rename extension
* @param fromPath - Current path of file/directory
* @param toPath - New path for file/directory
* @param addListeners - Whether to add event listeners (default: true)
* @returns Promise resolving to success message
*/
posixRename(fromPath, toPath, addListeners = true): Promise<String>;Important Note: This method requires SSH server version 4.8 or higher with the posix-rename@openssh.com extension.
Usage Examples:
// Atomic file rename (overwrites destination if exists)
await sftp.posixRename('/remote/temp.txt', '/remote/final.txt');
// Atomic directory rename
await sftp.posixRename('/remote/temp-dir', '/remote/final-dir');
// Safe atomic rename with server capability check
try {
await sftp.posixRename('/remote/source.dat', '/remote/target.dat');
console.log('Atomic rename completed');
} catch (err) {
if (err.message.includes('not supported')) {
console.log('Server does not support POSIX rename, using regular rename');
await sftp.rename('/remote/source.dat', '/remote/target.dat');
} else {
throw err;
}
}Changes file or directory permissions on the remote server.
/**
* Change permissions of remote file or directory
* @param rPath - Path to remote file/directory
* @param mode - New permissions (octal number or string)
* @param addListeners - Whether to add event listeners (default: true)
* @returns Promise resolving to success message
*/
chmod(rPath, mode, addListeners = true): Promise<String>;Usage Examples:
// Set file permissions using octal notation
await sftp.chmod('/remote/script.sh', 0o755); // rwxr-xr-x
// Set permissions using string notation
await sftp.chmod('/remote/data.txt', '644'); // rw-r--r--
// Make file executable
await sftp.chmod('/remote/program', 0o755);
// Remove write permissions for group and others
await sftp.chmod('/remote/private.txt', 0o600); // rw-------
// Bulk permission changes
const scripts = await sftp.list('/remote/scripts');
for (const file of scripts) {
if (file.type === '-' && file.name.endsWith('.sh')) {
await sftp.chmod(`/remote/scripts/${file.name}`, 0o755);
console.log(`Made ${file.name} executable`);
}
}Creates a copy of a remote file on the remote server without downloading/uploading.
/**
* Create a remote copy of a remote file
* @param src - Source file path on remote server
* @param dst - Destination file path on remote server
* @returns Promise resolving to success message
*/
rcopy(src, dst): Promise<String>;Usage Examples:
// Create backup copy
await sftp.rcopy('/remote/important.txt', '/remote/important.txt.backup');
// Copy to different directory
await sftp.rcopy('/remote/config.json', '/remote/backup/config.json');
// Copy with timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `/remote/data-${timestamp}.json`;
await sftp.rcopy('/remote/data.json', backupName);
// Safe copy with checks
const source = '/remote/master.conf';
const backup = '/remote/master.conf.bak';
const sourceExists = await sftp.exists(source);
const backupExists = await sftp.exists(backup);
if (sourceExists === '-') {
if (backupExists !== false) {
// Remove old backup first
await sftp.delete(backup);
}
await sftp.rcopy(source, backup);
console.log('Backup created successfully');
}Common permission patterns:
// File permissions
0o644 // rw-r--r-- (readable by all, writable by owner)
0o600 // rw------- (readable/writable by owner only)
0o755 // rwxr-xr-x (executable by all, writable by owner)
0o700 // rwx------ (full access by owner only)
// Directory permissions
0o755 // rwxr-xr-x (standard directory permissions)
0o750 // rwxr-x--- (accessible by owner and group)
0o700 // rwx------ (accessible by owner only)// Check current permissions before changing
const stats = await sftp.stat('/remote/file.txt');
const currentMode = stats.mode & parseInt('777', 8);
console.log(`Current permissions: ${currentMode.toString(8)}`);
// Set restrictive permissions for sensitive files
if (stats.isFile && currentMode !== 0o600) {
await sftp.chmod('/remote/file.txt', 0o600);
console.log('Set restrictive permissions');
}// Safe file update pattern using temporary file and rename
const targetFile = '/remote/config.json';
const tempFile = `/remote/config.json.tmp.${Date.now()}`;
try {
// Upload new content to temporary file
await sftp.put('/local/new-config.json', tempFile);
// Verify upload succeeded
const exists = await sftp.exists(tempFile);
if (exists === '-') {
// Atomically replace original file
await sftp.posixRename(tempFile, targetFile);
console.log('File updated atomically');
}
} catch (err) {
// Clean up temporary file on error
await sftp.delete(tempFile, true);
throw err;
}// Create backup before modifying important files
const originalFile = '/remote/database.conf';
const backupFile = `/remote/database.conf.backup.${Date.now()}`;
// Create backup
await sftp.rcopy(originalFile, backupFile);
try {
// Modify original file
await sftp.put('/local/new-database.conf', originalFile);
console.log('Configuration updated successfully');
} catch (err) {
// Restore from backup on error
console.log('Update failed, restoring backup');
await sftp.rename(backupFile, originalFile);
throw err;
}// Clean up old backup files
const backupPattern = /\.backup\.\d+$/;
const files = await sftp.list('/remote/backups');
const oldBackups = files.filter(file => {
if (file.type !== '-') return false;
if (!backupPattern.test(file.name)) return false;
// Keep backups newer than 7 days
const weekOld = Date.now() - (7 * 24 * 60 * 60 * 1000);
return file.modifyTime < weekOld;
});
for (const backup of oldBackups) {
await sftp.delete(`/remote/backups/${backup.name}`);
console.log(`Deleted old backup: ${backup.name}`);
}File management errors provide detailed context:
ENOENT: File or directory does not existEACCES: Permission deniedEEXIST: Destination already exists (rename)EISDIR: Operation not supported on directoryENOTDIR: Parent is not a directoryEXDEV: Cross-device operation not supportedInstall with Tessl CLI
npx tessl i tessl/npm-ssh2-sftp-client