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
subgraph Application
CLI["CLI Interface"]
API["REST API"]
end
subgraph Business Logic
Index["File Index Manager"]
Scheduler["Piece Download Scheduler"]
Verify["Verify & Reassemble"]
end
subgraph P2P Network
Discovery["Kad-DHT<br/>Peer Discovery + Metadata"]
Broadcast["GossipSub<br/>Message Broadcast"]
Transfer["Custom Protocol<br/>File Transfer"]
end
CLI --> Index
API --> Index
Index --> Scheduler
Scheduler --> Verify
Index --> Discovery
Scheduler --> Transfer
Broadcast -->|"New Peer Notification"| IndexCore 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 LR
S["Seeder Node"] -->|"1. Chunk and hash file"| FM["FileMetadata<br/>{filename, piece_size,<br/> piece_hashes[]}"]
FM -->|"2. hash(fileID) as DHT key<br/>store on K closest nodes"| DHT["Kademlia DHT"]
D["Downloader Node"] -->|"3. Query DHT with fileID"| DHT
DHT -->|"4. Return FileMetadata"| D
D -->|"5. Start per-Piece download"| SThe 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
KW["Peer has Piece set"] --> B["Count replicas of<br/>each Piece globally"]
B --> C{"Select rarest<br/>missing Piece"}
C --> D["Request Piece from<br/>a peer that has it"]
D --> E{"Download OK?"}
E -->|"Yes"| F["Verify SHA-256"]
E -->|"No"| G["Mark peer unavailable<br/>Pick another"]
F -->|"Passed"| H["Mark Complete<br/>Notify peers"]
F -->|"Failed"| I["Re-request Piece"]
H --> J{"All Pieces<br/>Complete?"}
J -->|"No"| A
J -->|"Yes"| K["Reassemble file"] | |
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/