Easy File Transfer: Building a Desktop SSH/rsync App with Tauri

Why I built a native desktop file transfer app using Tauri, Rust, and rsync — and what I learned about bridging a Rust backend to a React frontend.

I spend a lot of time moving files between my local machine and remote servers. The usual workflow is a mix of scp, rsync incantations I half-remember, and occasionally dragging things through an SFTP client that looks like it was designed in 2003.

I wanted something better: a dual-pane file manager that understands SSH and rsync natively, runs as a real native app, and doesn’t cost $40/month.

That’s Easy File Transfer.

What it does

The app gives you a dual-pane file browser — local on the left, remote on the right. You can drag files between panes, queue multiple transfers, watch progress with speed and ETA readouts, pause, resume, or cancel in-flight, and manage SSH connection profiles so you never have to type user@host -i ~/.ssh/id_ed25519 again.

Under the hood, all transfers go through rsync via the Rust backend. That means delta sync, compression, and resumability for free.

Why Tauri?

I’d been following Tauri for a while as an Electron alternative. The pitch is simple: use the OS’s native webview instead of shipping Chromium, and write the system-level code in Rust. The result is a much smaller binary and dramatically better memory usage.

For a file transfer app, the Rust backend made immediate sense. SSH key handling, spawning rsync processes, reading directory listings from remote hosts over SFTP — these are all things you’d rather do in a language with proper system access than try to hack through Node.js.

The challenges

The Tauri IPC bridge

The trickiest part was the communication between the React frontend and the Rust backend. Tauri uses an IPC mechanism where the frontend calls Rust commands via invoke(), and Rust can emit events back to the window.

For simple operations like “list directory” or “connect to host,” this is straightforward. For transfers, it gets more complex — I needed a streaming progress channel that could report bytes transferred, current speed, and remaining time, and could also receive pause/cancel signals from the UI.

I ended up building a channel-per-transfer model in Rust, where each transfer gets its own tokio channel. The Rust side emits progress events tagged with a transfer ID, and the React side maps those to the right UI element. It works well, but getting the event typing right on both ends took longer than expected.

SSH key authentication

Supporting SSH key auth (rather than just passwords) meant I had to shell out to the OS’s SSH agent or read key files directly. On macOS, the Keychain adds another layer. I eventually settled on reading from ~/.ssh/config to pick up user-configured hosts and keys, which covered 90% of real usage.

The file browser UX

The dual-pane layout sounds simple but has a lot of edge cases: symlinks, hidden files, permission errors, long file names, binary vs text detection, and the ever-present question of “what happens if you drag a folder onto a file?” Getting the context menus, multi-select, and keyboard shortcuts to feel native was more work than the rsync integration.

What I’d do differently

I’d invest more time in the Tauri plugin ecosystem earlier. There are now official plugins for things like dialog boxes, clipboard, and the OS shell that handle a lot of the platform-specific edge cases I had to work around. Starting fresh today I’d lean on those much more.

Where it stands

The app has two releases on GitHub and works well enough for my daily use. macOS prebuilt binaries are available. Windows and Linux support is on the roadmap — the Rust backend is already cross-platform, it’s mostly the UI polish that needs work on those platforms.

If you find yourself constantly juggling rsync commands, give it a try.