gosh-lan-transfer
A Rust library for peer-to-peer file transfers over LAN, VPN, or Tailscale networks.
This crate provides the core transfer engine without any GUI dependencies, making it suitable for integration into CLI tools, desktop applications, mobile apps, or headless services. Files transfer directly between devices with no cloud intermediary, keeping your data private and your transfers fast.
Why gosh-lan-transfer?
Sharing files between devices on the same network shouldn't require uploading to the cloud, installing platform-specific software, or configuring SSH keys. Yet existing solutions each come with significant limitations.
AirDrop and Nearby Share work seamlessly within their ecosystems, but they lock you into Apple or Google platforms respectively. If you have a mix of devices a MacBook, a Windows desktop, a Linux server, and an Android phone these solutions leave you stranded.
Cloud services like Dropbox, Google Drive, or WeTransfer solve the cross platform problem, but they route your files through external servers. For a 10GB video file sitting on your laptop that you want on your desktop three feet away, uploading to the cloud and downloading again wastes time and bandwidth. It also means your files pass through third-party infrastructure, which may be unacceptable for sensitive data.
SCP and rsync are powerful and cross platform, but they require SSH access, key management, and command line knowledge. They're tools for sysadmins, not for quickly sending a folder to a colleague.
LocalSend offers a good user experience for casual file sharing, but it's an end-user application, not a library. If you're building your own file sharing feature into an application, you can't easily integrate it.
gosh-lan-transfer takes a different approach. It's a library first, designed to be embedded into whatever application you're building. The HTTP-based protocol works across any platform that supports TCP. There's no proprietary discovery mechanism that might break across network boundaries, you simply specify the target device's IP or hostname. It works equally well on traditional LANs, corporate VPNs, and Tailscale networks where mDNS discovery often fails.
The library handles the complexity of file transfer progress tracking, approval workflows, retry logic, directory transfers, while staying out of your way on everything else. You provide the UI, the storage backend, and the user experience. gosh-lan-transfer provides reliable, fast, direct transfers.
Core Capabilities
The engine supports sending individual files or entire directory trees with their structure preserved. When receiving, transfers require explicit approval unless the sender is in your trusted hosts list, preventing unwanted file pushes. Progress updates stream in real-time with transfer speeds calculated on the fly. If a network hiccup interrupts the connection, automatic retry with exponential backoff handles transient failures gracefully.
The event-driven architecture means your application stays responsive. Whether you're building a GUI that needs to update a progress bar, a CLI that prints status to the terminal, or a headless service that logs to a file, the same event stream powers all of them. Three built-in event handlers cover common cases, and implementing your own takes just a few lines.
For applications that need to remember transfer history or save frequently used peers, the library defines persistence traits that you implement with whatever storage backend fits your needs SQLite, JSON files, or a full database. In memory implementations ship with the library for testing and simple use cases.
Installation
Add to your Cargo.toml:
[]
= "0.2"
= { = "1", = ["full"] }
Quick Start
use ;
use PathBuf;
async
Architecture
The library centers on GoshTransferEngine, which coordinates all operations. Internally, it manages an HTTP server for receiving files and an HTTP client for sending them. The engine exposes a high-level API while handling connection management, progress tracking, and error recovery behind the scenes.
┌─────────────────────────────────────────────────────────────┐
│ GoshTransferEngine │
│ - Coordinates all operations │
│ - Manages server lifecycle │
│ - Provides high-level API │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ TransferClient │ │ TransferServer │ │
│ │ (Sending) │ │ (Receiving) │ │
│ │ │ │ │ │
│ │ - DNS resolve │ │ - HTTP server │ │
│ │ - File upload │ │ - File receive │ │
│ │ - Progress │ │ - Approval │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ protocol (boundary types) │ events (infrastructure) │
│ - EngineEvent │ - EventHandler trait │
│ - TransferRequest/Response │ - ChannelEventHandler │
│ - TransferProgress │ - CallbackEventHandler │
│ - PendingTransfer │ - NoopEventHandler │
└─────────────────────────────────────────────────────────────┘
Types are organized with a clear separation between what crosses the engine boundary and what stays internal. The protocol module contains wire protocol types and event payloads anything sent over HTTP or emitted as an event lives here. The types module holds domain entities like favorites and transfer records that don't leave the local process.
Module Organization
src/
├── lib.rs # Public API and GoshTransferEngine
├── protocol.rs # Boundary-crossing types (wire + events)
├── types.rs # Domain entities (favorites, history)
├── events.rs # EventHandler trait and implementations
├── config.rs # EngineConfig
├── error.rs # EngineError
├── client.rs # TransferClient (internal)
├── server.rs # HTTP server (internal)
├── favorites.rs # FavoritesPersistence trait
└── history.rs # HistoryPersistence trait
API Reference
GoshTransferEngine
The main entry point for all operations. You create an engine with a configuration and an event handler, then use it to send files, receive files, and manage transfers.
Creating an Engine
The library offers several ways to create an engine depending on how you want to handle events. Channel based events work best for async applications where you want to process events in a separate task. Callback based events suit simpler use cases or FFI scenarios. The no-op handler discards all events, useful for batch operations or testing.
use ;
use Arc;
// Option 1: With channel-based events (recommended for async apps)
let config = default;
let = with_channel_events;
// Option 2: With callback-based events
let handler = callback_handler;
let mut engine = new;
// Option 3: With no-op handler (discard events)
use noop_handler;
let mut engine = new;
Server Operations
The HTTP server listens for incoming transfer requests. Starting and stopping the server is straightforward, and you can change the port at runtime if needed. When you change the port, the server gracefully shuts down and restarts on the new port. If binding to the new port fails, it automatically rolls back to the previous port.
// Start the HTTP server
engine.start_server.await?;
// Check if server is running
if engine.is_server_running
// Stop the server
engine.stop_server.await?;
// Get current port
let current_port = engine.port;
// Change port at runtime (gracefully restarts server)
engine.change_port.await?;
// Change port without rollback on failure
engine.change_port_with_options.await?;
Sending Files
Sending files involves specifying the target peer's address and port, along with the files to send. The engine handles DNS resolution, negotiates the transfer with the peer, waits for approval, and streams the files with progress updates.
use PathBuf;
let files = vec!;
engine.send_files.await?;
The send operation first transmits file metadata to the peer, then waits up to two minutes for approval. Once approved, it streams each file while emitting progress events. If the network connection drops, it automatically retries with exponential backoff. When all files are sent, a TransferComplete event fires; if something goes wrong, you get a TransferFailed event with the error details.
Sending Directories
For directory transfers, the library recursively enumerates all files and sends them with their relative paths preserved. The receiver automatically recreates the directory structure under its download directory.
engine.send_directory.await?;
Receiving Files
When a transfer request arrives, your application receives a TransferRequest event containing the sender's IP, device name, file list, and total size. You then decide whether to accept or reject the transfer. Accepted transfers receive a unique token that authorizes the sender to upload files.
match event
Batch Operations
When multiple transfers are pending, you can accept or reject them all at once. The batch methods return results for each transfer, so you can handle individual failures.
// Accept all pending transfers
let results = engine.accept_all_transfers.await;
for in results
// Reject all pending transfers
let results = engine.reject_all_transfers.await;
Cancelling Transfers
You can cancel an in-progress transfer at any time. Cancellation emits a TransferFailed event and causes subsequent upload attempts to be rejected.
engine.cancel_transfer.await?;
Network Utilities
The library includes utilities for DNS resolution, network interface enumeration, and peer health checks. These help you build features like peer discovery or connection status indicators.
// Resolve hostname to IPs
let result = resolve_address;
if result.success
// Get all network interfaces
let interfaces = get_network_interfaces;
for iface in interfaces
// Check if a peer is reachable
match engine.check_peer.await
// Get peer device info
let info = engine.get_peer_info.await?;
println!;
Configuration Management
Configuration can be updated at runtime. Trusted hosts determine which peers can send files without requiring manual approval.
// Get current config
let config = engine.config;
println!;
// Update config
let new_config = builder
.port
.device_name
.build;
engine.update_config.await;
// Manage trusted hosts
engine.add_trusted_host.await;
engine.remove_trusted_host.await;
EngineConfig
The configuration uses a builder pattern with sensible defaults. You can create a working configuration with just EngineConfig::default(), or customize specific fields as needed.
use EngineConfig;
use PathBuf;
let config = builder
.port // HTTP server port
.device_name // Name shown to peers
.download_dir // Where to save files
.trusted_hosts // Auto-accept from these
.receive_only // Allow sending
.max_retries // Retry failed transfers
.retry_delay_ms // Delay between retries
.build;
The defaults use port 53317, the system hostname as the device name, the current directory for downloads, no trusted hosts, and three retry attempts with a one-second base delay.
Events
The engine emits events for all significant state changes. Your application subscribes to these events to update UI, log activity, or trigger other actions.
use EngineEvent;
match event
Event Handlers
Three built-in handlers cover common scenarios. The channel handler uses Tokio broadcast channels and supports multiple subscribers, making it ideal for async applications. The callback handler wraps a closure, suitable for simple cases or FFI. The no-op handler discards events silently.
use channel_handler;
// Channel-based: multiple subscribers, async-friendly
let = channel_handler;
spawn;
// Additional subscribers can be created
let mut receiver2 = handler.subscribe;
For custom handling, implement the EventHandler trait:
use ;
Persistence
The library doesn't impose a storage backend. Instead, it defines traits for favorites and history persistence that you implement with whatever storage fits your application.
Favorites
Favorites let users save frequently-used peers for quick access. The FavoritesPersistence trait defines CRUD operations, and InMemoryFavorites provides a simple in-memory implementation.
use ;
let store = new;
let fav = store.add?;
store.update?;
store.delete?;
History
Transfer history records completed and failed transfers automatically when you provide a HistoryPersistence implementation. The InMemoryHistory implementation optionally limits how many records it keeps.
use ;
use Arc;
let history = new;
let config = default;
let = with_channel_events_and_history;
// Later: query history
let records = history.list?;
let page = history.list_paginated?;
Error Handling
All operations return EngineResult<T>, which is Result<T, EngineError>. The error type covers network issues, file I/O problems, protocol errors, and configuration mistakes.
use EngineError;
match engine.send_files.await
Transfer Protocol
The engine uses HTTP for all transfers, ensuring compatibility across firewalls and NAT. No custom binary protocol means standard tools can inspect traffic for debugging.
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/info |
GET | Device name and version |
/transfer |
POST | Initiate transfer request |
/transfer/status |
GET | Check approval status |
/chunk |
POST | Upload file data |
/events |
GET | SSE stream for real-time events |
Transfer Flow
SENDER RECEIVER
│ │
│── POST /transfer ──────────────────────▶│
│ {transfer_id, files[], total_size} │
│ │
│◀── 200 {accepted: false} ───────────────│
│ (or accepted: true if trusted) │
│ │
│ │ User approves
│ │
│── GET /transfer/status ────────────────▶│
│◀── {status: "accepted", token: "..."} ──│
│ │
│── POST /chunk?token=...&file_id=... ───▶│
│ [binary file data] │
│◀── 200 OK ──────────────────────────────│
│ │
│ (repeat for each file) │
│ │
Security
Each approved transfer receives a unique UUID token that must accompany all file uploads, preventing unauthorized data injection. Received filenames are sanitized to prevent path traversal attacks only the filename component is used, and parent directory references are stripped. Files exceeding their declared size are rejected and deleted.
The library is designed for trusted networks and does not implement user authentication. If you need transfers over untrusted networks, layer TLS on top or use a VPN.
Examples
CLI File Sender
use ;
use ;
async
CLI File Receiver
use ;
use PathBuf;
async
Types Reference
Protocol Types
These types cross the engine boundary they're either sent over HTTP or emitted as events.
TransferFile represents a single file in a transfer, with an ID, name, size, optional MIME type, and optional relative path for directory transfers.
TransferRequest is the wire format for initiating a transfer, containing the transfer ID, sender name, file list, and total size.
TransferResponse comes back from the receiver, indicating whether the transfer was accepted and providing an upload token if so.
PendingTransfer is the event payload for incoming transfer requests, adding the sender's IP and timestamp to the request data.
TransferProgress carries progress updates: transfer ID, current file name, bytes transferred, total bytes, and speed in bytes per second.
Domain Types
These types stay within the local process.
Favorite represents a saved peer with ID, display name, address, cached IP, and last used timestamp.
TransferRecord captures completed or failed transfer history: direction, status, peer address, file list, sizes, timestamps, and error message if applicable.
NetworkInterface describes a local network interface with its name, IP address, and loopback flag.
ResolveResult holds DNS resolution results: the original hostname, resolved IPs, success flag, and error message if resolution failed.
Disclaimer
This project is independent and is not sponsored by, endorsed by, or affiliated with LocalSend or GitHub, Inc.
It is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability or fitness for a particular purpose. Use at your own risk.
License
MIT - See LICENSE for details.
Contributing
Contributions are welcome. Please feel free to submit issues and pull requests.