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

@@ -6,6 +6,8 @@ import (
"os"
"sync"
"github.com/pion/webrtc/v3"
"github.com/waste-go/internal/crypto"
"github.com/waste-go/internal/proto"
"github.com/waste-go/internal/store"
@@ -16,18 +18,26 @@ type PeerConn struct {
Info proto.PeerInfo
// Send a line of JSON to this peer (pre-encrypted by the sender goroutine).
Send chan<- []byte
// PC is the underlying PeerConnection, used to open additional DataChannels.
PC *webrtc.PeerConnection
}
// Mesh is the shared state of the local node.
// All methods are safe to call from multiple goroutines.
type Mesh struct {
Identity *crypto.Identity
Store *store.Store // may be nil if persistence is disabled
ShareDir string // directory whose contents are shared with peers; "" = no sharing
Identity *crypto.Identity
Store *store.Store // may be nil if persistence is disabled
ShareDir string // directory whose contents are shared with peers; "" = no sharing
DownloadDir string // directory where received files are saved
mu sync.RWMutex
peers map[proto.PeerID]*PeerConn
// file transfer state
transferMu sync.Mutex
outbound map[string]*outboundTransfer // xid → pending outbound
inbound map[string]*inboundTransfer // xid → pending inbound
// subscribers receive a copy of every event (fan-out to IPC clients)
subMu sync.Mutex
subs []chan proto.IpcMessage
@@ -40,6 +50,8 @@ func New(id *crypto.Identity, st *store.Store) *Mesh {
Identity: id,
Store: st,
peers: make(map[proto.PeerID]*PeerConn),
outbound: make(map[string]*outboundTransfer),
inbound: make(map[string]*inboundTransfer),
}
}