Getting Started with ssh2-sftp-client

· 3 min read · 598 Words · -Views -Comments

While building a WebShell, I needed more than sz/rz uploads. I wanted a GUI that could list files and handle uploads/downloads. After some research I chose ssh2-sftp-client, which wraps SFTP/SSH. Here are the issues I ran into and how I solved them.

Enabling or Disabling the Service

Most Linux distributions ship with SFTP enabled because it’s part of the SSH stack. Administrators can disable it, though, so your code must handle unavailable services.

# vi /etc/ssh/sshd_config

# override default of no subsystems
Subsystem       sftp    /usr/libexec/openssh/sftp-server


After
# override default of no subsystems
# Subsystem       sftp    /usr/libexec/openssh/sftp-server

service sshd restart

If SFTP is disabled, ssh2-sftp-client throws ERR_GENERIC_CLIENT.

https://static.1991421.cn/2022/2022-06-12-230743.jpeg

list

Use list() to retrieve directory contents, but keep these in mind:

  1. ~ is not supported. Paths must be absolute or relative—not aliases.

    async function main() {
      try {
        await sftp.connect(config);
        const fileList = await sftp.list(remotePath);
        console.log(fileList);
        await sftp.end();
      } catch (err) {
        console.error(err);
      }
    }
    
  2. The type field describes the entry:

    • d: directory
    • -: regular file
    • l: symlink

    For symlinks, you must query again (e.g., via stat) to determine whether the target is a file or directory—isDirectory / isFile clarify the real type.

  3. size is in bytes, similar to standard shell output. Directories also have a size.

  4. rights expose permissions. The response also includes the owning UID (owner) and GID (group). To determine the current user’s access, you need that user’s UID/GID. Run id <username> over SSH (with the ssh2 client, not the SFTP wrapper) to retrieve it:

    const { Client: SSH2Client } = require('ssh2');
    
    const ssh2Client = new SSH2Client();
    
    await new Promise((resolve) =>
      ssh2Client.connect(config).on('ready', () =>
        ssh2Client.exec(`id ${config.username}`, (err, stream) => {
          stream.on('data', (buf) => {
            const idRes = buf.toString();
            console.log(idRes); // uid=0(root) gid=0(root) groups=0(root)
            const matches = idRes.match(/\d+/g);
            console.log({
              uid: +matches[0],
              gid: +matches[1],
            });
            resolve(idRes);
          });
        })
      )
    );
    

    Combine that with the list() response to decide whether the user can read/write.

filter

list(path, filter) accepts a regex-like filter. ^[^.] hides dotfiles. Note: Windows hidden files don’t follow the dot convention, so this method won’t hide them—you’d need to query attributes separately.

Response Payload

{
  "type": "-",
  "name": "admin1.pem",
  "size": 418,
  "modifyTime": 1651807713000,
  "accessTime": 1651807715000,
  "rights": {
    "user": "rw",
    "group": "r",
    "other": "r"
  },
  "owner": 0,
  "group": 0
}

put vs. fastPut

  1. Both upload files. fastPut always reads from disk—meaning temporary files if you’re proxying uploads through a Node server. That hurts speed and makes progress reporting tricky. For streaming scenarios (like a WebSocket upload piped through Node to the remote host), use put.
  2. put accepts any readable stream.
  3. Example: Convert chunks received over WebSocket into a readable stream and pipe directly to put. The Node server doesn’t persist anything locally.
  4. Set permissions explicitly—for example, 0o644, matching FileZilla’s SFTP defaults (rw-r--r--).

get vs. fastGet

Similar to uploads: get is better when you need to stream data to the frontend; fastGet writes files directly to disk.

Throttling

Use a throttling stream to limit bandwidth:

const Throttle = require('throttle');

const throttleStream = new Throttle(1024 * 1024); // 1 MB/s
this.conn.put(throttleStream.pipe(stream), data.path, {
  writeStreamOptions: {
    autoClose: false,
    mode: 0o644,
  },
  readStreamOptions: {
    autoClose: false,
  },
  pipeOptions: {
    end: false,
  },
});

Cancel Transfers

To abort an upload/download, close the SFTP connection and reconnect.

Resumable Transfers

  • Download: set readStreamOptions.start to the byte offset.

    sftp.get(remoteFile, fileWtr, {
      readStreamOptions: {
        start: 10,
      },
    });
    
  • Upload: use append() to resume.

downloadDir / uploadDir

These helpers work but can’t stream, so the server must store intermediate files and progress reporting is inaccurate. Instead, recursively iterate directories and use get/put. Create folders with mkdir as needed.

SFTP Primer

https://www.ssh.com/academy/ssh/sftp

Final Thoughts

That’s it—hope it saves you some time.

Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover