Rsync.net provides highly reliable offsite backup storage over SSH/SFTP, along with powerful ZFS filesystem snapshots exposed under the hidden path ~/.zfs/. These snapshots give per-directory, immutable historical versions of the filesystem but are not directly usable by S3-compatible backup tools such as restic, kopia, rclone, MinIO clients, or cloud-native S3 tooling.
The goal of this project is to build a complete, self-hosted S3-compatible storage gateway in front of rsync.net, combined with a browser UI that exposes .zfs snapshots as historical object versions, enabling:
- Using rsync.net as S3-compatible storage for backup tools
- Browsing buckets, objects, and historical versions through a web UI
- Downloading any past version directly from rsync.net
- Running everything in a lightweight Docker Compose stack
- Zero modification of rsync.net account; uses only standard SFTP access
This stack simulates versioning for humans (UI-level versioning) but does not attempt to re-implement S3 API versioning, because rsync.net snapshots do not follow S3 semantics. All S3 clients see a normal, non-versioned S3 endpoint.
- Implemented using:
rclone serve s3 rsyncnet:s3root - Backend: rclone SFTP remote to rsync.net
- Behavior:
- Each directory under
~/s3rootbecomes an S3 bucket - Files inside buckets are exposed as S3 objects
- No S3 versioning support (standard rclone behavior)
- Each directory under
- Implemented using Python FastAPI (or equivalent)
- Connects to two backends:
- S3 → talking to the rclone gateway
- SFTP → directly reading rsync.net
.zfssnapshots
- Provides:
- Bucket and object listing (via S3)
- Discovery of historical versions (by scanning
~/.zfs/*/s3root/...) - Download of any historical version (via SFTP streaming)
- Clean, web-friendly UI with breadcrumbs
- nginx / Caddy / Traefik
- Handles:
- TLS termination
- Authentication for the web UI
- Optionally protect S3 endpoint
~/s3root/
bucket-a/
foo/bar/file1.txt
bucket-b/
...
~/.zfs/daily_2025-12-01/
s3root/
bucket-a/foo/bar/file1.txt
~/.zfs/daily_2025-11-30/
s3root/
bucket-a/foo/bar/file1.txt
~/.zfs/custom_monthly_2025-12-01/
...
For S3 bucket bucket-a, key foo/bar/file1.txt:
| Version ID | Source | Path | How Downloaded |
|---|---|---|---|
current |
Live S3 | ~/s3root/bucket-a/foo/bar/file1.txt |
via S3 |
daily_2025-12-01 |
ZFS snapshot | ~/.zfs/daily_2025-12-01/s3root/bucket-a/foo/bar/file1.txt |
via SFTP |
| … | … | … | … |
→ HTTP → rclone serve s3 → SFTP → rsync.net
- Bucket/Object listing via S3 client (boto3)
- Version discovery + version download via SFTP client
Separate responsibilities:
- S3 for current view
- SFTP for historical versions
+-------------------------------+
| Browser (FastAPI) |
| http://host:8080 |
+-------------------------------+
|
| S3 (HTTP)
v
+-------------------------------+
| rclone S3 Gateway |
| http://host:9000 |
+-------------------------------+
|
| SFTP (SSH)
v
+-------------------------------+
| rsync.net server |
| with ZFS + .zfs snapshots |
+-------------------------------+
Optional:
Reverse Proxy (TLS/auth)
/ui → Browser
/s3 → rclone serve s3
- Decide on final ports, TLS, auth strategy
- Generate SSH key for rsync.net and store in
./secrets/ - Set up
.envwith:-
RSYNC_HOST -
RSYNC_USER -
S3_ACCESS_KEY,S3_SECRET_KEY
-
-
Create repo folders:
-
docker-compose.yml -
/browser/app/ -
/browser/Dockerfile -
/config/ -
/docs/
-
-
Add
README.mddescribing project goals and architecture (based on this document)
- Configure Rclone SFTP remote (environment variables)
- Create
/secrets/rsync_id_ed25519volume - Add Docker compose service:
- Image
rclone/rclone - Command
serve s3 rsyncnet:s3root - Environment with rclone config
- Port
9000:9000
- Image
- Confirm buckets appear using
aws s3 ls --endpoint-url=http://localhost:9000
- Implement
Settings(Pydantic) containing:- S3 endpoint/creds
- rsync.net host/user/key path
- snapshot directory
.zfs - S3 root prefix (
s3root)
- Add boto3/aioboto3 client
- Implement:
-
list_buckets() -
list_objects(bucket, prefix) -
head_object(bucket, key) - Streaming
get_object_content(bucket, key)
-
- Add
asyncsshdependency - Implement SFTP connectivity:
- One-shot SFTP connect helper
-
list_snapshots()= list directories under.zfs -
snapshot_root(snap)=.zfs/<snap>/s3root -
stat_snapshot_object() -
open_snapshot_file_stream()
- Create
VersionInfoclass - Implement
list_object_versions(bucket, key):- Add current version (via S3 head)
- For each snapshot:
- Check existence via SFTP
- Collect snapshot metadata
- Ensure versions are sorted by timestamp descending
- Write unit tests for version mapping logic
-
GET /buckets -
GET /b/{bucket} -
GET /b/{bucket}/o/{path:path} -
GET /b/{bucket}/o/{path:path}/downloadfor current -
GET /b/{bucket}/o/{path:path}/download?version={snap}for snapshot
- Add Jinja2 templates:
-
buckets.html -
objects.html -
object_detail.html
-
- Implement breadcrumb navigation
- Add version history table with download links
- Add CSS styling
-
browser/Dockerfilewith Uvicorn entrypoint - Add service in
docker-compose.yml:- Ports
8080:8080 - Environment mapping to Settings
- Mount ssh key
- Ports
- Confirm UI loads and can list buckets
- Implement Basic Auth in FastAPI OR
- Add reverse proxy with:
- TLS
- Auth
- Rate limiting (optional)
- Optionally restrict rclone gateway to LAN-only
- Add structured logging in FastAPI
- Add error handlers
- Expose simple metrics (optional)
- Version mapping logic
- Snapshot path resolution
- Mock SFTP server container mimicking rsync.net
- Start rclone gateway in compose
- Browser connects to mock S3 + mock SFTP
- End-to-end object + version listing tests
- Upload sample files
- Wait for auto snapshots
- Verify
.zfsstructure - Validate UI version view
- Validate SFTP version download
- Confirm integrity with checksums
- Deploy on target host
- Configure reverse proxy (TLS + auth)
- Lock down firewall rules
- Enable automated restarts
- Configure monitoring/alerts
- Write:
-
docs/setup.md -
docs/architecture.md -
docs/usage.md
-
- Add diagrams
- Describe assumptions and limitations
- "Restore version" button (snapshot → upload back to S3)
- JSON API for versions
- Snapshot diffing (size/time)
- Support for multiple rsync.net accounts
- Indexing + search