Implementing sz/rz Upload/Download in WebShell

· 2 min read · 373 Words · -Views -Comments

I recently added upload/download support and explored lrzsz. Here are the notes.

Usage

First, understand what sz/rz do:

  1. sz supports multiple-file downloads.
  2. rz supports multiple-file uploads. The upload target directory is the current working directory at the time the command is triggered.
  3. rz does not support uploading folders.
  4. For rz, when a file already exists, the UI will indicate it.
  5. File size limit: cannot transfer files larger than 4 GB.
  6. Both download and upload support cancel.

System Support for sz/rz

  1. Servers typically don’t have lrzsz installed; install it manually.
  2. Not every tool supports rz and sz—you must use a tool that supports the ZModem protocol. Installation scripts:
# macOS
brew install lrzsz
brew remove lrzsz

# CentOS
yum install lrzsz
yum remove lrzsz

## Cross-platform install script
sudo sh -c "$(curl -fsSL https://gist.githubusercontent.com/alanhe421/6a299b815f4dd3d242abc16b8be6b861/raw/dbe1497208f1d968ed8b67cad09c596e35c5be9c/install-package-lrzsz.sh)"

Official Demo

The zmodem.js author provides a demo. To run it:

  1. Demo: https://github.com/FGasper/xterm.js/tree/zmodem
  2. Switch Node to v8 and re-run npm i
  3. Start and visit http://127.0.0.1:3000/

Implementation Details

The author’s demo is minimal and omits some features. Here are key parts. Full example: link

Cancel sz download

When the system file picker is invoked for upload, canceling it isn’t observable in JS. The user must press Ctrl+C, then send an abort command to cancel.

activeZsession._skip();

Alternatively, build your own upload dialog and call abort when the user cancels.

Cancel rz upload

// Stop sending chunks (xfer.send(chunk))
await zsession.close()

Upload: file already exists

If xfer is falsy, the server rejected the upload—often because the file exists (other reasons are possible).

const xfer = await zsession.send_offer(curb);
if (!xfer) {
  showMessage(`${xfer.get_details().name} rejected.`);
}

Progress display

  1. For upload, compute progress as sentSize/totalSize; for download, receivedSize/totalSize.

  2. In upload loops, writing progress directly may not work because xterm output is async; add a small delay:

    await new Promise(resolve => window.setTimeout(resolve, 0));
    
  3. For better UX, render a progress bar; avoid \n when calling xterm.write to keep it on one line.

function getProgressBar(total, current) {
  if (total < current) {
    throw new Error('total must be greater than current');
  }
  const progressBarLength = 40;  // bar length
  const progress = Math.floor((current / total) * progressBarLength);
  const empty = progressBarLength - progress;
  const progressBar = '█'.repeat(progress) + '░'.repeat(empty);
  return `(${progressBar}) ${((current / total) * 100).toFixed(2)}%`;
}

Useful Resources

  1. https://juejin.cn/post/6935621453400244260
  2. https://wsgzao.github.io/post/lrzsz/
  3. https://qa.1r1g.com/sf/ask/672770031/
Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover