Implementing sz/rz Upload/Download in WebShell
I recently added upload/download support and explored
lrzsz
. Here are the notes.
Usage
First, understand what sz/rz do:
sz
supports multiple-file downloads.rz
supports multiple-file uploads. The upload target directory is the current working directory at the time the command is triggered.rz
does not support uploading folders.- For
rz
, when a file already exists, the UI will indicate it. - File size limit: cannot transfer files larger than
4 GB
. - Both download and upload support cancel.
System Support for sz/rz
- Servers typically don’t have lrzsz installed; install it manually.
- 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:
- Demo: https://github.com/FGasper/xterm.js/tree/zmodem
- Switch Node to
v8
and re-runnpm i
- 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
For upload, compute progress as sentSize/totalSize; for download, receivedSize/totalSize.
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));
For better UX, render a progress bar; avoid
\n
when callingxterm.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)}%`;
}