unnbound-sftp-sdk
v1.0.28
Published
An SFTP client with structured logging.
Readme
Unnbound SFTP Client
A TypeScript SFTP client with structured logging and distributed tracing capabilities, built on top of ssh2-sftp-client and integrated with the Unnbound logging system.
Features
- Full SFTP Operations: Complete set of SFTP operations including file transfer, directory management, and file system operations
- Structured Logging: Built-in integration with Unnbound Logger for comprehensive operation tracking
- Distributed Tracing: Automatic span creation for all SFTP operations with detailed payload information
- Type Safety: Full TypeScript support with comprehensive type definitions
- Performance Monitoring: Built-in metrics collection for file sizes, operation counts, and timing
- Flexible Configuration: Support for various authentication methods and connection options
Installation
npm install unnbound-sftp-sdk
# or
bun add unnbound-sftp-sdk
# or
yarn add unnbound-sftp-sdkQuick Start
import { UnnboundSftpClient } from 'unnbound-sftp-sdk';
import { logger } from 'unnbound-logger-sdk';
const sftp = new UnnboundSftpClient({
host: 'your-sftp-server.com',
port: 22,
username: 'your-username',
privateKey: 'your-private-key', // or use password
});
// Connect to the SFTP server
await sftp.connect();
// Get current working directory
const currentPath = await sftp.cwd();
console.log('Current path:', currentPath);
// Create a directory
await sftp.mkdir('./uploads', true); // recursive
// Upload a file
const fileContent = Buffer.from('Hello, World!');
await sftp.put(fileContent, './uploads/hello.txt');
// Download a file
const downloadedContent = await sftp.get('./uploads/hello.txt');
console.log('Downloaded:', downloadedContent.toString());
// List files in a directory
const files = await sftp.list('./uploads');
console.log(
'Files:',
files.map((f) => f.name),
);
// Close the connection
await sftp.close();API Reference
Constructor
new UnnboundSftpClient(options: UnnboundSftpClientOptions)Options
| Property | Type | Required | Description |
| ------------------- | ---------------- | -------- | ----------------------------------------------------- |
| host | string | ✅ | SFTP server hostname |
| port | number | ❌ | SFTP server port (default: 22) |
| username | string | ❌ | Username for authentication |
| password | string | ❌ | Password for authentication |
| privateKey | string | ❌ | Private key for key-based authentication |
| passphrase | string | ❌ | Passphrase for encrypted private keys |
| localHostname | string | ❌ | Local hostname for hostbased authentication |
| localUsername | string | ❌ | Local username for hostbased authentication |
| keepaliveInterval | number | ❌ | SSH keepalive interval in milliseconds (0 to disable) |
| keepaliveCountMax | number | ❌ | Max unanswered keepalive packets before disconnection |
| readyTimeout | number | ❌ | SSH handshake timeout in milliseconds |
| localAddress | string | ❌ | Local network interface IP address |
| localPort | number | ❌ | Local port number |
| timeout | number | ❌ | Socket timeout in milliseconds |
| ident | string | ❌ | Server software identifier (default: 'unnbound') |
| logger | UnnboundLogger | ❌ | Custom logger instance (uses default if not provided) |
Connection Management
connect(): Promise<void>
Establishes a connection to the SFTP server.
await sftp.connect();close(): Promise<boolean>
Closes the SFTP connection.
const closed = await sftp.close();File Operations
Use downloadFile, uploadFile, downloadDir, and uploadDir for anything larger than a few megabytes. They stream between disk and the server with up to 64 requests in flight per file. With the defaults, downloadDir and uploadDir move up to 12 files at once (three per session on four sessions), so a server can see up to 768 outstanding requests. get and put use one request at a time and hold the whole file in memory. Against a server about 85 ms away, that is about 0.4 MB/s, and their logs include the full file content.
downloadFile(remotePath: string, localPath: string, options?: { useFastget?: boolean }): Promise<number>
Downloads one file to a local path with pipelined reads. Resolves to the byte count. Logs carry paths and size, not content. { useFastget: false } streams the file with get instead, for a server that mishandles parallel requests.
const bytes = await sftp.downloadFile('/inbound/orders.csv', '/tmp/orders.csv');uploadFile(localPath: string, remotePath: string, options?: { useFastput?: boolean }): Promise<number>
Uploads one local file with pipelined writes. Resolves to the byte count. Logs carry paths and size, not content. { useFastput: false } streams the file with put instead, for a server that mishandles parallel requests.
await sftp.uploadFile('/tmp/result.csv', '/outbound/result.csv');get(path: string): Promise<Buffer>
Downloads a small file into memory.
const content = await sftp.get('/remote/path/file.txt');put(input: Buffer, path: string): Promise<string>
Uploads a buffer to the remote server.
const data = Buffer.from('file content');
await sftp.put(data, '/remote/path/file.txt');append(input: Buffer | NodeJS.ReadableStream, path: string): Promise<string>
Appends data to an existing file.
const additionalData = Buffer.from('more content');
await sftp.append(additionalData, '/remote/path/file.txt');Directory Operations
list(path: string, filter?: ListFilterFunction): Promise<FileInfo[]>
Lists files and directories in the specified path.
const files = await sftp.list('/remote/directory');
const onlyFiles = await sftp.list('/remote/directory', (item) => item.type === '-');mkdir(path: string, recursive?: boolean): Promise<string>
Creates a directory.
await sftp.mkdir('/remote/new-directory');
await sftp.mkdir('/remote/nested/path', true); // recursivermdir(path: string, recursive?: boolean): Promise<string>
Removes a directory.
await sftp.rmdir('/remote/empty-directory');
await sftp.rmdir('/remote/directory-with-files', true); // recursiveuploadDir(sourcePath: string, destinationPath: string, options?: UploadDirOptions): Promise<string>
Uploads an entire directory. Files move with pipelined writes over up to four SFTP sessions, three files per session at a time; nested directories are created first. Set connections to change the session count. When the server refuses an extra session, the transfer continues on the sessions it has. { useFastput: false } streams files with put on one session, up to 10 at once. Use it for a server that mishandles parallel requests: ssh2-sftp-client warns that such servers can truncate or corrupt pipelined transfers.
await sftp.uploadDir('./local/directory', '/remote/destination');downloadDir(sourcePath: string, destinationPath: string, options?: DownloadDirOptions): Promise<string>
Downloads an entire directory. Files move with pipelined reads over up to four SFTP sessions, three files per session at a time; a symlinked file downloads from its target. Set connections to change the session count. When the server refuses an extra session, the transfer continues on the sessions it has. { useFastget: false } streams files with get on one session, up to 10 at once. Use it for a server that mishandles parallel requests: ssh2-sftp-client warns that such servers can truncate or corrupt pipelined transfers.
await sftp.downloadDir('/remote/directory', './local/destination');File System Operations
exists(path: string): Promise<false | FileInfoType>
Checks if a file or directory exists.
const exists = await sftp.exists('/remote/path/file.txt');
// Returns: false, 'd' (directory), '-' (file), or 'l' (symlink)stat(path: string): Promise<FileStats>
Gets file or directory statistics.
const stats = await sftp.stat('/remote/path/file.txt');
console.log('Size:', stats.size);
console.log('Modified:', stats.modifyTime);realPath(path: string): Promise<string>
Resolves the real path of a file or directory.
const realPath = await sftp.realPath('/remote/symlink');cwd(): Promise<string>
Gets the current working directory.
const currentDir = await sftp.cwd();delete(path: string, noErrorOK?: boolean): Promise<string>
Deletes a file.
await sftp.delete('/remote/path/file.txt');rename(sourcePath: string, destinationPath: string): Promise<string>
Renames or moves a file or directory.
await sftp.rename('/remote/old-name.txt', '/remote/new-name.txt');chmod(path: string, mode: number | string): Promise<string>
Changes file or directory permissions.
await sftp.chmod('/remote/path/file.txt', 0o644);
await sftp.chmod('/remote/path/file.txt', '644');Type Definitions
FileInfo
interface FileInfo {
type: FileInfoType; // 'd' | '-' | 'l' (directory, file, symlink)
name: string;
size: number;
modifyTime: number;
accessTime: number;
rights: {
user: string;
group: string;
other: string;
};
owner: number;
group: number;
}FileStats
interface FileStats {
mode: number;
uid: number;
gid: number;
size: number;
accessTime: number;
modifyTime: number;
isDirectory: boolean;
isFile: boolean;
isBlockDevice: boolean;
isCharacterDevice: boolean;
isSymbolicLink: boolean;
isFIFO: boolean;
isSocket: boolean;
}Directory Options
interface DirOptions {
filter?: DirFilterFunction;
}
interface UploadDirOptions extends DirOptions {
useFastput?: boolean; // default true
connections?: number; // default 4
}
interface DownloadDirOptions extends DirOptions {
useFastget?: boolean; // default true
connections?: number; // default 4
}Logging and Tracing
The Unnbound SFTP Client automatically creates spans for all operations and logs detailed information including:
- Operation type and timing
- File paths
- Host information
- Error details (if any)
- Byte counts for single-file transfers (
get,put,append,downloadFile,uploadFile);downloadDiranduploadDirlog paths only - Full file contents for
get,put, andappend(content);downloadFile,uploadFile,downloadDir, anduploadDirlog no content
Synced file content is part of the observability product: get, put, and append spans carry the complete file body in sftp.content, the same contract as HTTP request/response body logging.
Custom Logger
You can provide a custom logger instance:
import { UnnboundSftpClient } from 'unnbound-sftp-sdk';
import { logger } from 'unnbound-logger-sdk';
const sftp = new UnnboundSftpClient({
host: 'sftp.example.com',
username: 'user',
password: 'pass',
logger: logger, // Custom logger instance
});Example Log Output
{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"sftp","sftp":{"host":"sftp.domain.com","operation":"connect"},"message":"SFTP connect started."}
{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"sftp","sftp":{"host":"sftp.domain.com","operation":"connect"},"duration":2771,"message":"SFTP connect completed."}
{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"sftp","sftp":{"host":"sftp.domain.com","operation":"put","path":"/uploads/file.txt","bytes":1024,"content":"Hello World"},"message":"SFTP put started."}
{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"sftp","sftp":{"host":"sftp.domain.com","operation":"put","path":"/uploads/file.txt","bytes":1024,"content":"Hello World"},"duration":150,"message":"SFTP put completed."}Error Handling
All methods return promises that will reject with detailed error information:
try {
await sftp.get('/non-existent-file.txt');
} catch (error) {
console.error('SFTP operation failed:', error.message);
// Error details are automatically logged with the span
}Authentication Methods
Password Authentication
const sftp = new UnnboundSftpClient({
host: 'sftp.example.com',
username: 'myuser',
password: 'mypassword',
});Private Key Authentication
const sftp = new UnnboundSftpClient({
host: 'sftp.example.com',
username: 'myuser',
privateKey: `-----BEGIN OPENSSH PRIVATE KEY-----
...your private key content...
-----END OPENSSH PRIVATE KEY-----`,
});Private Key with Passphrase
const sftp = new UnnboundSftpClient({
host: 'sftp.example.com',
username: 'myuser',
privateKey: 'your-private-key',
passphrase: 'your-passphrase',
});Private Key from Environment Variables
The client automatically normalizes private keys from environment variables by converting \n escape sequences to actual newlines:
// Environment variable: PRIVATE_KEY="-----BEGIN OPENSSH PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END OPENSSH PRIVATE KEY-----"
const sftp = new UnnboundSftpClient({
host: 'sftp.example.com',
username: 'myuser',
privateKey: process.env.PRIVATE_KEY, // Automatically normalized
});Requirements
- Node.js >= 22.0.0
- TypeScript (for TypeScript projects)
Dependencies
ssh2-sftp-client: Core SFTP functionalityunnbound-logger-sdk: Structured logging and tracing
Support
For support, please open an issue on the GitHub repository.
