Releases: aeroxy/drift
drift v0.4.2
Improved transfer atomicity, staging of all temporary files inside the hidden .drift/ directory, and enhanced end-to-end error propagation / atomic rollback for encrypted WebSocket transfers.
Highlights:
- Unified Temporary Staging: Staged all in-progress files (both regular files and directory archives) in the hidden
.drift/directory, preventing clutter in the user's browse directory during active transfers. - Redundant Rename Mitigation: Added path checking in
ChunkedWriter::finalize()to bypasstokio::fs::rename()calls when staging path and final destination coincide (e.g., directory archives). - Consolidated Error Routing: Designed a clean
TransferErrorrouting pipeline throughAppStateto propagate network/decompress/write failures back to client/server connection task handlers. - Atomic Rollback: Implemented all-or-nothing rollback in
TransferReceiverthat concurrently sweeps away partial archives and successfully finalized files on any decompression or writer failure. - Flattened Match Blocks: Simplified nested timeout pattern match constructs in
browser_transfer.rsto flatten channel receiver handling and significantly improve readability.
1. High-Level Summary (TL;DR)
- Impact: Medium - Significantly improves correctness, file system cleanliness, and failure safety across the transfer lifetime, with zero protocol-breaking changes.
- Key Changes:
- Staging & Storage: Migrated active file writes from the target directory to
.drift/staging files, introducingChunkedWriter::create_with_tempfor custom temp paths. - Reliability & Errors: Integrated error tracking into oneshot completion channels and introduced
AppState::handle_transfer_errorto handleTransferErrorcontrol messages. - Decomposition & Refactoring: Flattened nested result checks and consolidated file deletion into a robust concurrent helper
TransferReceiver::remove_files.
- Staging & Storage: Migrated active file writes from the target directory to
2. Visual Overview (Code & Logic Map)
3. Detailed Change Analysis
Staging & Writer Updates
- What Changed: Added
ChunkedWriter::create_with_tempto write to a temp file and rename on finalization. Restructuredfinalize()to skip the rename when the temp path matches the final path. - Source:
src/fileops/writer.rs
| Old Method | New Method | Reason |
|---|---|---|
ChunkedWriter::create(path) |
ChunkedWriter::create_with_temp(temp_path, final_path) |
Allows arbitrary temp directories (e.g., hidden .drift/) to stage writes away from final destination. |
Always call tokio::fs::rename |
Call only if temp_path != final_path |
Prevents redundant rename and permission/filesystem issues for directory archives located within .drift/. |
Robust Error Signaling & Rollback
- What Changed: Redesigned the oneshot completion mechanism to transmit
Resultinstances rather than empty(). Added a concurrentremove_fileshelper that performs a complete rollback of the transfer state (temp and finalized files) on failure. - Source:
src/server/transfer_receiver.rs
| Before | After | Reason |
|---|---|---|
completion_tx: Option<oneshot::Sender<()>> |
completion_tx: Option<oneshot::Sender<Result<u64, String>>> |
Signals error message and total bytes written to the waiting transfer handler. |
| Manual inline tokio futures for archive deletion | Concurrent remove_files(Vec<PathBuf>) |
Generalizes rollback across directory and regular multi-file transfers. |
Flattened Completion Checks
- What Changed: Flattened deep nesting within
handle_browser_transferandpush_entriestimeout completion handlers. - Source:
src/server/browser_transfer.rs
// Before (Nested Match)
match tokio::time::timeout(...) {
Ok(Ok(())) => { ... }
Ok(Err(_)) => { ... }
Err(_) => { ... }
}
// After (Flattened Match)
match wait_result {
Err(_) => { ... }
Ok(Err(_)) => { ... }
Ok(Ok(Err(error))) => { ... }
Ok(Ok(Ok(total_bytes))) => { ... }
}4. Impact & Risk Assessment
- Breaking Changes: None. The core wire control protocols and API definitions remain structurally backwards compatible.
- Testing Suggestions:
- Directory Rollback Failure Case: Initiate a directory transfer and inject a decompression error (e.g., corrupt tarball signature) halfway. Verify that
.drift/temp archives AND any already extracted regular files are deleted automatically. - Simultaneous Multi-file Write: Send multiple files concurrently over WebSocket. Kill the connection abruptly and ensure
signal_error()successfully drops all writers and cleans up.drift/files.
- Directory Rollback Failure Case: Initiate a directory transfer and inject a decompression error (e.g., corrupt tarball signature) halfway. Verify that
5. Known Issues
Missing cleanup of successful files on partial finalization failure
- Severity: Low
- Section:
src/server/transfer_receiver.rs-finalize_transfer - What's wrong: If a writer fails to finalize in the middle of the loop, successfully finalized files (prior index) are scheduled for deletion, but the current failing writer's final path is also added to
dest_paths. Since finalization failed, the file remains in temp_path, which is cleaned up. However, there's a tiny window where partial finalized destination paths are cleaned up correctly but could benefit from a defensive try-catch block. - Verdict: The implemented rollback logic is already very robust and handles this well. No immediate action required.
drift v0.4.1
Frontend enhancements
The file list now feels much more like a desktop file manager:
Sortable columns
- Click on Name, Size, or Date column headers to sort the list
- Click again to toggle between ascending and descending order
- Directories always stay at the top, regardless of the sort key
Resizable columns
- Hover over the divider between Size/Date headers to reveal a draggable handle
- Drag left or right to adjust column width
- Minimum and maximum width constraints keep the layout usable
Under the hood
- Added
useColumnResizecustom hook - No backend changes – the backend still sorts by directories-first alphabetical as a fallback, but the frontend now overrides sorting on the client side
drift v0.4.0
Multi-file transfers in a single request
The web panel's multi-select (Cmd/Ctrl-click, Shift-click) now completes in one round trip instead of one per file. A new V2 data frame format adds a file_index field so chunks for different files can be interleaved over the same WebSocket connection and routed to the correct writer on arrival.
Protocol versioning
Peers now advertise a protocol_version during the handshake. Drift 0.4.0 speaks version 2; it falls back to V1 frames automatically when talking to an older peer, so existing setups keep working without any config changes.
CLI tools no longer interrupt active connections
Running drift ls or drift pull against a server that already has a peer connected would previously clobber the server's connection state, causing the browser panel to lose its remote. The server now saves and restores the persistent connection around each transient CLI session.
Bug fixes
- Archive filenames for directory entries now include the file index, preventing corruption when multiple folders are sent in one transfer.
- The finalize log correctly reports the number of files written (was always 0).
- The connection fingerprint shown in the browser is preserved after a CLI tool disconnects from the same server.
recv_control_with_repliesis bounded to 100 iterations, preventing a hang if a peer spamsInfoRequestorPing.
drift v0.3.0
Multi-select in the web panel
Pick more than one file at a time.
The Copy buttons in the toolbar now show the running count, and a small "×" appears next to each side so you can clear the selection without touching the file list.
Behind the scenes nothing changes on the wire — drift's protocol has always taken entries: Vec<TransferEntry> per TransferRequest. The web panel now actually fills that array. Each folder in a multi-selection is still compressed to its own .drift/{name}.tar.gz and decompressed individually on the receiver, so per-folder progress and integrity stay intact (no monolithic top-level archive).
drift v0.2.0
Reconnect after remote drop
When the server-to-server connection drops unexpectedly, drift now shows a Reconnect button in the toolbar — no modal, no re-typing credentials.
# Machine A
drift --port 8000
# Machine B — target and password are remembered
drift --port 9000 --target 192.168.0.2:8000 --password secretIf Machine A restarts or the network blips, the Reconnect button appears on Machine B's UI. Click it to re-establish the encrypted session instantly.
Works for both CLI-launched sessions (--target at startup) and connections made through the "Connect to remote" modal — credentials are retained in the server process across drops. An explicit disconnect (the unplug button) clears them, so Reconnect only appears when it makes sense.
New endpoint for scripted use:
curl -X POST http://localhost:9000/api/reconnect
# {"success":true,"fingerprint":"a1b2c3"}GET /api/info now includes can_reconnect and last_target fields.
drift v0.1.7
drift v0.1.7
wss:// — Connect over TLS
drift now connects to remote servers over TLS. Pass a wss:// target to any command that takes --target:
drift --target wss://example.com/drift
drift ls --target wss://example.com/drift
drift pull --target wss://example.com/drift somefile.txtBare host:port still works as before (defaults to ws://). Path prefixes are supported for reverse-proxy subpath mounts — wss://example.com/drift automatically resolves to wss://example.com/drift/ws.
Add --allow-insecure-tls to skip certificate verification for self-signed or lab certs.
--disable-ui — Safe public exposure
drift --port 8000 --disable-uiStrips the REST API (/api/*) and embedded frontend from the router — only /ws is mounted. Use this when running drift behind a public reverse proxy. The encrypted WebSocket handshake is the auth boundary; no unauthenticated filesystem-listing endpoints are reachable.
Example caddy config:
handle_path /drift/* {
reverse_proxy localhost:8000
}--daemon — Background server
drift --port 8000 --daemon
# drift daemon started (PID: 12345)
# Logs: /path/to/cwd/drift.logSpawns the server in the background, detaches from the terminal's process group (so closing the shell doesn't kill it), and appends logs to ./drift.log in the current directory. Kill with kill <PID>.
Simplified CLI
The serve subcommand is removed. Start the server with just drift:
drift --port 8000
drift --port 8000 --target wss://remote.example.com/drift --disable-ui --daemondrift v0.1.6
UI
Connect to a remote from the browser.
You no longer need to restart the server with --target to establish a peer connection. Click "Connect to remote" in the toolbar, enter the address (and optional password), and drift performs the encrypted handshake in place. The connection fingerprint appears inline for MITM verification.
Disconnect or switch to a different remote at any time with the unplug button — no restart required.
Dynamic port.
drift serve now works without --port. When the flag is omitted, the OS assigns a free port and drift logs the full address on startup.
Distribution
Homebrew formula.
Formula/drift.rb is included in the repo. Once a tap is set up:
brew tap aeroxy/drift
brew install drift
Developer
make update-formula
After uploading a release zip, run make update-formula to fetch the live archive, recompute its SHA256, and patch Formula/drift.rb automatically. The existing bump-patch / bump-minor / bump-major targets now keep the formula version in sync alongside Cargo.toml and App.tsx.
drift v0.1.5
UI
Tab completion in the path bar.
Pressing Tab while the autocomplete dropdown is open fills the highlighted suggestion (or the first one if none is selected) into the input and appends a trailing /. The debounce effect fires immediately, so the dropdown refreshes to show the contents of the completed directory — ready for the next segment of the path.
This matches the behaviour of most address bars and shells: Tab completes without committing, Enter navigates.
Developer
make bump-patch / bump-minor / bump-major
Three new Makefile targets for version management. Each reads the current version from Cargo.toml, increments the appropriate component, and writes the new version to both Cargo.toml and the frontend version badge in App.tsx.
drift v0.1.4
UI
Editable path bar with autocomplete in the file browser.
The path bar in each pane now works like an address bar — click it to type a path directly instead of clicking through directories one by one.
Autocomplete
- Local pane: as you type, drift fetches the parent directory via
/api/browse(debounced 200ms) and shows matching subdirectories in a dropdown. Typing a trailing/shows all subdirectories at that level. - Remote pane: suggestions come from the directory listing already on screen — no extra round-trips over the WebSocket connection.
Both panes support full keyboard navigation: arrow keys to move through suggestions, Enter to navigate, Escape to cancel. Clicking a suggestion navigates immediately.
Error handling
Typing an invalid or non-existent path shows a red error toast and reverts the path bar to the previous directory — no broken state left behind.
- Local: uses the HTTP response code from
/api/browseto detect failure - Remote: listens for the
Errormessage from the WebSocket and reverts
Testing
- Four new integration tests: absolute-path browse via REST, non-existent path returns non-OK via REST, absolute-path
BrowseRequestvia WebSocket, non-existent path returnsErrorvia WebSocket
drift v0.1.3
Security
MITM protection via password authentication and connection fingerprints.
Pure X25519 ECDH is fast and private, but without identity verification any attacker who can intercept the WebSocket connection can complete independent handshakes with both sides. v0.1.3 addresses this with two complementary mechanisms.
Password authentication (--password)
When both sides are started with --password <secret>, the handshake includes a challenge-response step after key exchange:
- Server generates a random 32-byte nonce and sends
AuthChallenge { nonce } - Client computes
HMAC-SHA256(password, nonce || shared_secret)and sendsAuthResponse { proof } - Server verifies the proof before sending
HandshakeComplete
Because the proof covers the DH shared secret, an attacker doing MITM gets a different shared secret on each side and cannot forge a valid proof without knowing the password. Wrong or missing passwords are rejected with a clear error.
Connection fingerprint (always on)
After every handshake, both sides independently compute SHA-256(shared_secret)[0..3] — a 6-character hex string. It is:
- Logged in both terminals:
Handshake complete (fingerprint: a3f2b1) - Shown in the web UI toolbar in amber next to the connection status badge
Users can compare the fingerprint out-of-band (Telegram, phone call, etc.) to confirm no one is in the middle — even without a password.
Testing
- Added
passwordoption toDriftProcessintegration test helper - Three new test cases: correct password connects, wrong password is rejected, missing password is rejected