Hands-On: Building a Distributed File Sharing System
Combining theory with practice, we’ll build a real distributed file sharing system. This system will leverage technologies introduced in previous articles — Kademlia DHT for node discovery and metadata distribution, Gossipsub for broadcasting, and a custom file transfer protocol.
System Architecture
flowchart TD
APP["Application<br/>CLI / REST API"] --> LOGIC["Business Logic<br/>File index / download scheduler / verify"]
LOGIC --> NET["P2P Network<br/>Kad-DHT / Gossipsub / file transfer"]
NET -.->|"new peer notification"| LOGIC
style APP fill:#2196F3,color:#fff
style LOGIC fill:#FF9800,color:#fff
style NET fill:#4CAF50,color:#fffThree strictly separated layers: application faces outward, business logic handles scheduling and verification, and the P2P network layer handles peer discovery, message broadcast, and file transfer.
Core design principles:
- Layered abstraction: Business logic strictly separated from P2P network layer
- Modularity: Each component independently testable
- Fault tolerance: Node failures don’t affect overall system availability
Metadata Distribution
How does a downloader know which Pieces a file has and their hashes? This is done through DHT-based FileMetadata distribution:
flowchart TD
S["Seeder<br/>chunk + hash"] --> FM["FileMetadata<br/>store in DHT"]
FM --> DHT["Kademlia DHT<br/>key = hash(fileID)"]
D["Downloader"] -->|"query fileID"| DHT
DHT -->|"return metadata"| D
D -->|"download per Piece"| S
style S fill:#2196F3,color:#fff
style FM fill:#FF9800,color:#fff
style DHT fill:#9C27B0,color:#fff
style D fill:#4CAF50,color:#fffThe Seeder stores file metadata (including each Piece’s SHA-256 hash) in the DHT keyed by fileID. The Downloader only needs to know fileID to retrieve complete metadata and start downloading. fileID is typically the hash of file content, so identical files always map to the same ID.
Rust Core Module Implementation
Piece State Management
First, define the Piece status enum and thread-safe shared state:
| |
Why Arc<RwLock<>>? In async P2P programs, multiple Pieces may download simultaneously from different peers, each as an independent async task. These tasks need to concurrently update piece_status (marking Pieces as Downloading/Complete). RwLock allows multiple tasks to read state simultaneously (non-blocking) while exclusive access during writes prevents data races. Arc provides shared ownership across tasks.
File Chunking and Verification
| |
Rarest First Download Scheduling
flowchart TD
B["Count replicas of<br/>each Piece globally"] --> C{"Select rarest<br/>missing Piece"}
C --> D["Request from a peer<br/>that has it"]
D --> E{"SHA-256 verify<br/>passed?"}
E -->|"passed"| H["Mark Complete"]
E -->|"failed / timeout"| B
H -->|"still missing"| B
H -->|"all Complete"| K["Reassemble file"]
style B fill:#2196F3,color:#fff
style C fill:#9C27B0,color:#fff
style D fill:#FF9800,color:#fff
style E fill:#9C27B0,color:#fff
style H fill:#4CAF50,color:#fff
style K fill:#4CAF50,color:#fff | |
Seeder Side: Serving Piece Requests
The seeder node must listen for download requests and return data by Piece index:
| |
Running and Testing
Start a seeder node:
| |
Start a downloader node:
| |
Download Flow
sequenceDiagram
participant D as Downloader
participant T as DHT
participant S as Seeder
D->>T: Query fileID metadata
T-->>D: Return FileMetadata (piece_hashes[])
D->>S: Request Piece 0 (rarest first)
S-->>D: Send Piece 0 data
Note over D: Verify SHA-256 | Mark Complete
D->>S: Request Piece 1
S-->>D: Send Piece 1 data
Note over D: All Pieces Complete
Note over D: Reassemble in orderProduction Reference: Syncthing Sync Engine Design
Syncthing is a production-grade P2P file synchronization engine (85K+ stars), employing fundamentally different design strategies from the BitTorrent-style approach that this system is based on. The following analysis covers five key dimensions, offering design insights for custom implementations.
Variable Block Sizing
This system uses a fixed 256KB piece size. Syncthing dynamically selects the optimal block size based on file size:
| File Size | Block Size | Max Blocks |
|---|---|---|
| ≤ 250 MB | 128 KiB | ~2000 |
| 1 GB | 512 KiB | ~2000 |
| 4 GB | 2 MiB | ~2000 |
| > 16 GB | 16 MiB | ~2000 |
The design rule is: select the smallest block size that keeps the block count under 2000. This ensures fine granularity for small files (128 KiB blocks) while avoiding excessive metadata for large files (metadata grows with block count, not file size). The 2000-block limit stems from BEP’s Index message Protobuf encoding efficiency, ensuring a single Index transmission completes within a typical MTU.
Version Vector Conflict Resolution
When two devices modify the same file simultaneously, a conflict occurs — one of the hardest problems in distributed file synchronization. Syncthing uses Version Vectors to detect and resolve conflicts:
| |
Version vector comparison yields one of five relationships:
| |
flowchart TD
A["Device A<br/>{A:3} → {A:4}"]
B["Device B<br/>{B:2} → {B:3}"]
C{"Comparison?"}
A --> C
B --> C
C -->|"Concurrent"| Conflict["Keep both<br/>Create .conflict"]
C -->|"Greater/Lesser"| Sync["Direct overwrite"]
style Conflict fill:#FF9800,color:#fff
style Sync fill:#4CAF50,color:#fffUnlike the DHT single-key metadata distribution in this system, version vectors precisely describe the causal relationships of file changes: when two version vectors are neither Greater than the other, they are Concurrent — triggering conflict handling that preserves both versions (generating .conflict files) to ensure no data loss. This is the most critical data safety guarantee in P2P file synchronization.
Delta Index Exchange
This system uses DHT for full metadata storage with complete pull by the downloader. Syncthing employs delta index exchange instead:
When two devices connect, they first exchange IndexID (a UUID identifying the local index) and Sequence (index sequence number). If the remote device’s IndexID matches the locally recorded value, only changes after the Sequence need to be transmitted — this is the essence of incremental sync:
| |
This design is highly efficient for frequent reconnection scenarios: after device sleep/wake cycles, only recent changes need to be exchanged rather than a full sync. The same principle can be applied to custom systems — maintaining a sequence-driven delta channel alongside DHT.
Folder Type Semantics
Syncthing defines four folder types, each with different sync semantics:
| Type | Local Changes | Remote Changes | Use Case |
|---|---|---|---|
| Send & Receive | Send to cluster | Receive from cluster | Default bidirectional sync |
| Send Only | Send to cluster | Ignored | NVR recording storage (write-only) |
| Receive Only | Ignored | Receive from cluster | Edge devices, archive replicas |
| Receive Encrypted | Ignored | Receive but can’t decrypt | Untrusted device hosting |
The Receive Encrypted type is particularly noteworthy — a device can store encrypted data without being able to decrypt it, as the private key stays on authorized devices. This offers an “edge storage” design pattern for custom file sharing systems: trusted nodes handle reads and writes, while untrusted nodes only provide storage space.
Parallel Hash Computation
Syncthing’s file scanner uses a parallel hasher pool to accelerate large file processing:
| |
A key design detail: Stat() is called both before and after hash computation. If the file’s mtime or size changes during computation, the result is discarded — ensuring stale hashes are never used. This check can be applied equally well in Rust implementations.
In contrast, this system uses serial hash computation (single-threaded read → hash → next block). On multi-core systems, Syncthing’s parallel hasher pool approach can be adopted to accelerate large file scanning.
References
- Cohen, B. (2003). Incentives build robustness in BitTorrent. Workshop on Economics of Peer-to-Peer Systems.
- IPFS. https://ipfs.tech/
- anacrolix/torrent (Go BitTorrent). https://github.com/anacrolix/torrent
- rust-libp2p examples. https://github.com/libp2p/rust-libp2p/tree/master/examples
- Syncthing source. https://github.com/syncthing/syncthing
- BEP Protocol Spec. https://docs.syncthing.net/specs/bep-v1.html
- Syncthing Architecture Docs. https://docs.syncthing.net/dev/