feat: implement §9 file transfer over dedicated binary DataChannels

File chunks travel over a per-transfer "f:<xid>" WebRTC DataChannel —
direct DTLS-encrypted P2P, the anchor never sees file data. Once the
initial handshake is done the anchor can disappear and transfers continue.

Key design choices:
- Receiver sends "ok" on the file DC before sender streams, eliminating
  a race where tiny files could be fully sent/closed before the receiver's
  OnMessage handler is registered (open-race §6 analogue for data DCs).
- Auto-accept: receiver accepts every incoming offer immediately.
- Download dir: per-network at <data-dir>/downloads-<netid>/.
- Backpressure: bufferedAmountLowThreshold to avoid overwhelming the DC.
- SHA-256 verified on receive; mismatches emit EvtError and discard temp file.
- IPC: send_file {peer_id, path} → offers the named file from share dir.
- EvtFileComplete {transfer_id, path} emitted on success.

IPC command: {"type":"send_file","peer_id":"<hex>","path":"<filename>"}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-22 14:22:59 +02:00
parent 0051c8fdbf
commit 13b30ca0cb
8 changed files with 462 additions and 5 deletions

View File

@@ -230,6 +230,14 @@ func offer(peerID proto.PeerID, id *crypto.Identity, m *mesh.Mesh, s *sender) (*
mesh.WireDataChannel(dc, pc, peerID, id, m)
mesh.WireCandidateTrickle(pc, peerID, s)
// Handle file DataChannels opened by the remote peer (they are the file sender).
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
if strings.HasPrefix(dc.Label(), "f:") {
xid := strings.TrimPrefix(dc.Label(), "f:")
m.HandleInboundFileDC(dc, xid, peerID)
}
})
sdpOffer, err := pc.CreateOffer(nil)
if err != nil {
pc.Close()
@@ -248,8 +256,12 @@ func answer(payload proto.SignalingPayload, fromID proto.PeerID, id *crypto.Iden
return nil, err
}
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
if dc.Label() == "yaw" {
switch {
case dc.Label() == "yaw":
mesh.WireDataChannel(dc, pc, fromID, id, m)
case strings.HasPrefix(dc.Label(), "f:"):
xid := strings.TrimPrefix(dc.Label(), "f:")
m.HandleInboundFileDC(dc, xid, fromID)
}
})
mesh.WireCandidateTrickle(pc, fromID, s)