--- description: "Upgrade from node-scp 0.x in two steps: change one import to node-scp/legacy, then move to connect() file by file, with a full method mapping." --- # Upgrading from node-scp 0.x node-scp 1.0 has a new API. The old one is still shipped, so upgrading is two steps: first a one line change, then moving to the new API whenever it suits you. ```mermaid flowchart LR A["node-scp 0.x
import from 'node-scp'"] -- "step 1: change the import
(minutes)" --> B["node-scp 1.x
'node-scp/legacy'"] B -- "step 2: file by file
(when it suits you)" --> C["node-scp 1.x
connect() from 'node-scp'"] ``` The same task in each stage: ::: code-group ```ts [0.x] import { Client } from 'node-scp'; const client = await Client({ host, username, privateKey }); await client.uploadDir('./dist', '/var/www/app'); client.close(); ``` ```ts [1.x, step 1: legacy] import { Client } from 'node-scp/legacy'; const client = await Client({ host, username, privateKey }); await client.uploadDir('./dist', '/var/www/app'); await client.close(); ``` ```ts [1.x, step 2: new API] import { connect } from 'node-scp'; await using client = await connect({ host, username, privateKey }); await client.upload('./dist', '/var/www/app', { recursive: true }); ``` ::: ## Step 1: keep the old API ```diff - import { Client } from 'node-scp'; + import { Client } from 'node-scp/legacy'; ``` or with CommonJS: ```diff - const { Client } = require('node-scp'); + const { Client } = require('node-scp/legacy'); ``` The default export works too (`import Client from 'node-scp/legacy'`). Every 0.x method is there with the same arguments and return values, and it still uses SFTP only. A few things behave slightly differently, all of them fixes: | 0.x | 1.x legacy layer | | --- | --- | | A connection error after `ready` could crash the process with an unhandled `error` event. | `error` is only emitted when you listen for it. | | An SFTP failure right after login was thrown inside an event handler and crashed. | `Client()` rejects instead. | | `close()` returned nothing. | It returns a promise that resolves once the connection is closed. You can ignore it. | | The `greeting` event was emitted as `banner`. | It is emitted as `greeting`. | | `uploadDir` and `downloadDir` copied one file at a time and skipped symlinks. | Four files in parallel, symlinks are followed. | | `downloadDir` printed skipped entries with `console.log`. | Nothing is printed. | | `uploadDir` and `downloadDir` created files with the default mode of the other side. | New files get the permission bits of the source, like `scp`. Existing files keep their mode. | | `list()` took the entry type from the first letter of the long listing, which is wrong on some servers. | The type comes from the file mode. | Node.js 20 or newer is required. ## Step 2: move to the new API ```ts import { connect } from 'node-scp'; const client = await connect({ host, username, privateKey }); ``` | 0.x | 1.x | | --- | --- | | `Client(options)` | `connect(options)` | | `remoteOsType: 'win32'` | `remoteOs: 'win32'` | | `events: { banner, ... }` | `beforeConnect: (ssh) => ssh.on('banner', ...)` | | `uploadFile(local, remote, opts)` | `upload(local, remote)` | | `downloadFile(remote, local, opts)` | `download(remote, local)` | | `uploadDir(src, dest)` | `upload(src, dest, { recursive: true })` | | `downloadDir(src, dest)` returning a message | `download(src, dest, { recursive: true })` returning `{ files, directories, bytes }` | | `writeFile(path, data)` | `writeFile(path, data)`, also accepts streams | | `readFile(path)` | `readFile(path)` | | `exists(path)` resolving `'d'`, `'-'`, `'l'` or `false` | `fs.exists(path)` resolving `'directory'`, `'file'`, `'symlink'`, `'other'` or `false` | | `stat(path)`, `lstat(path)` returning ssh2 `Stats` | `fs.stat(path)`, `fs.lstat(path)` returning `{ type, size, mode, uid, gid, atime, mtime }` with `Date` times | | `list(path, pattern)` | `fs.list(path)`, then filter the array | | `mkdir(path, attrs, { recursive })` | `fs.mkdir(path, { recursive, mode })` | | `unlink(path)` | `fs.rm(path)` | | `rmdir(path)` (always recursive) | `fs.rm(path, { recursive: true })` | | `emptyDir(path)` | `fs.rm(path, { recursive: true, force: true })` then `fs.mkdir(path)` | | `rename(from, to)` | `fs.rename(from, to)` | | `realPath(path)` | `fs.realpath(path)` | | `chmod`, `chown`, `utimes`, `setstat`, `symlink`, `readlink`, `appendFile` | the same methods on `client.sftp` (the raw ssh2 SFTP session) | | `close()` | `await close()` or `await using` | ### Errors 0.x passed ssh2 errors through, so you had to know that SFTP code `2` means "no such file". In 1.x every error is a `ScpError`: ```ts import { ErrorCode, isScpError } from 'node-scp'; try { await client.download('/missing.txt', './missing.txt'); } catch (err) { if (isScpError(err, ErrorCode.NotFound)) { // ... } } ``` The original error is still available as `err.cause`. ### Destinations are exact `upload('dist', '/srv/app', { recursive: true })` makes `/srv/app` a copy of `dist`, exactly like `uploadDir` did. The parent (`/srv`) has to exist. For single files the destination is the file path, never a directory to put the file into. ### New things you get - SCP support, so the same code works on servers without SFTP. - `onProgress`, `filter`, `concurrency`, `preserve` and `signal` for every transfer. - `await using` for automatic cleanup. - One shot `upload()` and `download()` helpers that take `user@host:path`.