SFTP
SFTP (SSH File Transfer Protocol) via ssh2-sftp-client. Node-only. Connect-per-operation with an injectable client for batch work; url() needs an HTTP front.
Installation
ssh2-sftp-client is an optional peer dependency of files-sdk - install alongside the SDK so the adapter’s imports resolve at runtime.
npm install files-sdk ssh2-sftp-clientpnpm add files-sdk ssh2-sftp-clientyarn add files-sdk ssh2-sftp-clientbun add files-sdk ssh2-sftp-clientnub add files-sdk ssh2-sftp-clientaube add files-sdk ssh2-sftp-clientUsage
SFTP via the ssh2-sftp-client library (built on ssh2). Virtual keys map to paths under a configurable root on the remote server, with a .. traversal guard. Node-only — SFTP uses raw sockets, so this adapter does not run on edge/browser/Workers runtimes.
By default the adapter opens a fresh connection per operation and closes it afterwards. For batch work, connect once and pass the client so every call reuses the same connection — you own its lifecycle.
import { Files } from "files-sdk";
import { sftp } from "files-sdk/sftp";
const files = new Files({
adapter: sftp({
host: "files.example.com",
username: process.env.SFTP_USERNAME!,
// Password or private-key auth (privateKey takes precedence):
privateKey: process.env.SFTP_PRIVATE_KEY!,
// passphrase: process.env.SFTP_PASSPHRASE,
root: "/uploads", // virtual keys resolve under here; defaults to "."
}),
});
await files.upload("reports/q1.csv", csv, { contentType: "text/csv" });
const file = await files.download("reports/q1.csv");
Auth falls back to SFTP_HOST, SFTP_USERNAME, SFTP_PASSWORD, SFTP_PRIVATE_KEY, SFTP_PASSPHRASE, and SFTP_PORT (default 22) when the matching option is omitted. Pass connectOptions to forward anything else to ssh2 (e.g. hostVerifier, algorithms, agent).
Reusing a connection
import SftpClient from "ssh2-sftp-client";
const client = new SftpClient();
await client.connect({ host: "files.example.com", username, privateKey });
const files = new Files({ adapter: sftp({ client }) });
// ...many operations over the one connection...
await client.end();
Options
host?string
SFTP host. Falls back to `SFTP_HOST`.
stringport?number
Port. Falls back to `SFTP_PORT`, then `22`.
numberusername?string
Username. Falls back to `SFTP_USERNAME`.
stringpassword?string
Password authentication. Falls back to `SFTP_PASSWORD`. When both this and `privateKey` are set, ssh2 tries the key first, then the password.
stringprivateKey?string | Buffer
Private key (PEM) for key-based auth. Falls back to `SFTP_PRIVATE_KEY`.
string | Bufferpassphrase?string
Passphrase for an encrypted `privateKey`. Falls back to `SFTP_PASSPHRASE`.
stringroot?string
Remote base directory. Virtual keys resolve under it; keys that escape it (e.g. `../etc/passwd`) throw `Provider`. An absolute root (`/uploads`) yields absolute paths; the default (`"."`) keeps paths relative to the connection's login directory — the common chroot/home case.
stringpublicBaseUrl?string
Origin used to build URLs from `url()`. When set, `url(key)` returns `${publicBaseUrl}/${key}` — useful when an HTTP server fronts the same tree. When unset, `url()` throws: SFTP serves no HTTP and has no signing primitive.
stringreadyTimeout?number
Connection-ready timeout in milliseconds, passed through to ssh2.
numberconnectOptions?SftpClient.ConnectOptions
Extra ssh2 connect options merged into the resolved config (e.g. `hostVerifier`, `algorithms`, `agent`). Wins over the discrete fields.
SftpClient.ConnectOptionsclient?SftpClient
Pre-connected `ssh2-sftp-client` instance. When passed, the adapter reuses it for every call and never opens or closes a connection — the caller owns the socket lifecycle. This is the high-throughput path: connect once and inject, rather than paying a handshake per operation.
SftpClientLimitations
Connect-per-operation means a high call rate becomes a high connection rate, and SSH servers commonly cap sessions per IP - inject a pre-connected client (shown above) for batch jobs.
Resumable uploads (upload({ control })) append each chunk to a <key>.fls-part staging file next to the target and rename it over the key only when the upload completes, so an existing object stays readable until then and abort() removes just the partial. list() hides staging files, and writes to keys ending in .fls-part throw. Base SFTP rename refuses an existing target on most servers, so when one is in the way it is deleted and the rename retried: the key is briefly absent at that moment.
Compatibility
| Method | Status | Notes |
|---|---|---|
upload |
⚠️ | User metadata and cacheControl throw - SFTP files have no arbitrary-metadata or cache-header field. contentType is accepted for the return value but not stored (it’s inferred from the key’s extension on read). Stream bodies upload directly. Node-only (raw sockets). |
download |
✅ | |
delete |
✅ | |
list |
⚠️ | Walks the directory tree recursively on every call - SFTP has no native prefix scan or pagination - and skips symlinks. A subdirectory that disappears mid-walk is skipped; a missing root lists as empty. prefix/limit/cursor are applied client-side over the full walk, so they’re accurate but a large tree means a full traversal per call. Content type is inferred from each key’s extension; size/lastModified come from the listing. |
search |
⚠️ | Built on listAll — inherits this adapter’s list behavior above. Client-side key match (glob, regex, substring, exact). |
head |
⚠️ | SFTP stores no content type, etag, or user metadata - head() infers the type from the key’s extension (or application/octet-stream) and returns no etag. size and lastModified come from stat. |
exists |
✅ | |
copy |
⚠️ | Read-then-write - base SFTP has no portable server-side copy, so the source is downloaded and re-uploaded over one connection. The whole object is buffered in memory; not atomic. |
url |
❌ | Throws unless publicBaseUrl is set (an HTTP server fronting the same tree), in which case it returns <publicBaseUrl>/<key>. SFTP serves no HTTP and has no signing primitive. responseContentDisposition always throws because the HTTP-front URL cannot bind the override. |
signedUploadUrl |
❌ | Throws - SFTP has no presigned-upload concept. Use upload(), or inject a pre-connected client for batch transfers. |