@jonathonm. Ours is tied closely to our own customer area, so rather than a script you could drop in, here's how it works, with our hosts and keys left out.
Where the backups are
On the backup server each website has its own folder:
/backups/<website-uuid>/
current -> snapshot-<epoch-ms>
snapshot-<epoch-ms>/
home/ the site's home directory (the docroot is under here)
mysql/ one <database>.sql.gz per database
postgresql/
email/
website.json site config: vhosts, DB and FTP users, and so on
metadata.json includes home_dir_size and the backup description
Snapshots are rsync hardlink copies, so every snapshot is a complete tree you can read directly. To find a site's folder from its domain, grep the website.json files for it.
A locked-down SSH command on the backup server
The customer area connects with its own SSH key, and that key can only run one script:
from="<customer-area server IPs>",restrict,command="/usr/local/bin/backup-export" ssh-ed25519 AAAA... backup-export
restrict turns off shells, port forwarding and the rest. from= limits the key to the customer-area server. command= means the caller only ever supplies arguments, which arrive in $SSH_ORIGINAL_COMMAND. The script accepts three commands and validates everything, using regexes for the UUID and snapshot name plus a realpath check, so nothing can reach outside /backups:
list <website-uuid> JSON list of snapshots with file and database sizes
size <website-uuid> <snapshot> <what> exact byte length of the download
fetch <website-uuid> <snapshot> <what> uncompressed tar on stdout
what = files | db | both
fetch streams a tar of home/ and/or the database dumps straight to stdout. Nothing is written to disk, so there's nothing to clean up. The dumps are already gzipped, so we don't compress again.
size builds the same file list as fetch and adds up the tar headers and padding. That lets the customer area send an exact Content-Length, so the browser shows a real progress bar. If a file somehow changed between the two calls, fetch pads or trims it to the promised size, because a wrong Content-Length breaks the download.
The customer area side
- Only offer websites the logged-in customer owns. We look them up through the Enhance API (the customer's org, then its websites) and refuse any website ID that isn't theirs.
- Connect with phpseclib rather than calling the ssh binary, and pin the backup server's host key.
- Cache the snapshot list for a couple of minutes. Allow one download at a time per customer, with a small overall cap. We use file locks, so a worker that dies mid-download can't leave a slot stuck.
- Stream the SSH output straight to the browser with
Content-Type: application/x-tar, the Content-Length from size, and output buffering turned off.
Things to watch
website.json includes the site's database and FTP users, with password hashes, plus other config. Decide whether you want it in customer downloads.
- Enhance doesn't delete a site's backup folder when the website is deleted, so old folders stay. Only list folders for websites the customer currently has.
- Keep port 22 open on the backup server for the cluster nodes, because Enhance's own backups use rsync over port 22.
Happy to answer questions.