Add file listing: share-dir flag, file_list_req/resp DataChannel messages
- proto: FileEntry, FileListResp types; MsgFileListReq/Resp msg types;
CmdGetFileList + EvtFileList IPC types; Files field on IpcMessage
- mesh: ShareDir field + ScanShareDir(); on DataChannel open, auto-send
MsgFileListReq to new peer; handle MsgFileListReq (scan + reply) and
MsgFileListResp (emit EvtFileList to IPC subscribers)
- ipc: get_file_list command — own list returned immediately; remote peer
list requested via DataChannel (response arrives as EvtFileList event)
- daemon: -share-dir flag wired to mesh.ShareDir
- test scripts: pass -share-dir /home/frejoh/Downloads/{alice,bob,charlie};
test-network.sh verifies each peer's own file list via get_file_list
- FUTURE.md: document per-network share directories and multi-network design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,26 @@ func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork
|
||||
Rooms: []string{"general"},
|
||||
})
|
||||
|
||||
case proto.CmdGetFileList:
|
||||
if cmd.PeerID == nil || *cmd.PeerID == m.Identity.PeerID() {
|
||||
// Local list — scan immediately.
|
||||
send(proto.IpcMessage{
|
||||
Type: proto.EvtFileList,
|
||||
PeerID: ptr(m.Identity.PeerID()),
|
||||
Files: m.ScanShareDir(),
|
||||
})
|
||||
} else {
|
||||
// Remote list — send a request over the DataChannel.
|
||||
// The response arrives as EvtFileList via the mesh event bus.
|
||||
req, err := json.Marshal(proto.PeerMessage{Type: proto.MsgFileListReq})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !m.SendTo(*cmd.PeerID, req) {
|
||||
send(errMsg(fmt.Sprintf("get_file_list: peer %s not connected", (*cmd.PeerID).Short())))
|
||||
}
|
||||
}
|
||||
|
||||
case proto.CmdGenerateInvite:
|
||||
net := currentNetwork()
|
||||
if net == "" {
|
||||
|
||||
@@ -3,6 +3,7 @@ package mesh
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/waste-go/internal/crypto"
|
||||
@@ -22,6 +23,7 @@ type PeerConn struct {
|
||||
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
|
||||
|
||||
mu sync.RWMutex
|
||||
peers map[proto.PeerID]*PeerConn
|
||||
@@ -41,6 +43,31 @@ func New(id *crypto.Identity, st *store.Store) *Mesh {
|
||||
}
|
||||
}
|
||||
|
||||
// ScanShareDir returns the list of files in the local share directory.
|
||||
// Returns an empty slice if ShareDir is unset or the directory is empty.
|
||||
func (m *Mesh) ScanShareDir() []proto.FileEntry {
|
||||
if m.ShareDir == "" {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(m.ShareDir)
|
||||
if err != nil {
|
||||
log.Printf("mesh: scan share dir %s: %v", m.ShareDir, err)
|
||||
return nil
|
||||
}
|
||||
var files []proto.FileEntry
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, proto.FileEntry{Name: e.Name(), SizeBytes: info.Size()})
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// ── Peer management ───────────────────────────────────────────────────────────
|
||||
|
||||
// AddPeer registers a connected peer and notifies subscribers.
|
||||
|
||||
@@ -56,6 +56,11 @@ func WireDataChannel(
|
||||
}
|
||||
m.AddPeer(peerConn)
|
||||
|
||||
// Request the peer's file list immediately after connect.
|
||||
if req, err := json.Marshal(proto.PeerMessage{Type: proto.MsgFileListReq}); err == nil {
|
||||
sendCh <- req
|
||||
}
|
||||
|
||||
go func() {
|
||||
for payload := range sendCh {
|
||||
if err := dc.SendText(string(payload)); err != nil {
|
||||
@@ -147,6 +152,28 @@ func handleDCMessage(data []byte, from proto.PeerID, localID *crypto.Identity, m
|
||||
|
||||
func dispatchPeerMessage(msg proto.PeerMessage, from proto.PeerID, m *Mesh) {
|
||||
switch msg.Type {
|
||||
case proto.MsgFileListReq:
|
||||
// Peer wants our file list — reply directly on their send channel.
|
||||
files := m.ScanShareDir()
|
||||
resp, err := json.Marshal(proto.PeerMessage{
|
||||
Type: proto.MsgFileListResp,
|
||||
FileListResp: &proto.FileListResp{Files: files},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m.SendTo(from, resp)
|
||||
|
||||
case proto.MsgFileListResp:
|
||||
// Received a remote peer's file list — forward to IPC subscribers.
|
||||
if msg.FileListResp != nil {
|
||||
m.Emit(proto.IpcMessage{
|
||||
Type: proto.EvtFileList,
|
||||
PeerID: peerIDPtr(from),
|
||||
Files: msg.FileListResp.Files,
|
||||
})
|
||||
}
|
||||
|
||||
case proto.MsgChat:
|
||||
if msg.Chat != nil {
|
||||
m.SaveMessage(msg.Chat)
|
||||
|
||||
@@ -34,13 +34,15 @@ type PeerInfo struct {
|
||||
type MsgType string
|
||||
|
||||
const (
|
||||
MsgChat MsgType = "chat"
|
||||
MsgPeerGossip MsgType = "peer_gossip"
|
||||
MsgFileOffer MsgType = "file_offer"
|
||||
MsgFileResp MsgType = "file_response"
|
||||
MsgFileDone MsgType = "file_done"
|
||||
MsgPing MsgType = "ping"
|
||||
MsgPong MsgType = "pong"
|
||||
MsgChat MsgType = "chat"
|
||||
MsgPeerGossip MsgType = "peer_gossip"
|
||||
MsgFileListReq MsgType = "file_list_req"
|
||||
MsgFileListResp MsgType = "file_list_resp"
|
||||
MsgFileOffer MsgType = "file_offer"
|
||||
MsgFileResp MsgType = "file_response"
|
||||
MsgFileDone MsgType = "file_done"
|
||||
MsgPing MsgType = "ping"
|
||||
MsgPong MsgType = "pong"
|
||||
)
|
||||
|
||||
// PeerMessage is the top-level container sent over the "yaw" DataChannel.
|
||||
@@ -49,12 +51,13 @@ type PeerMessage struct {
|
||||
Type MsgType `json:"type"`
|
||||
|
||||
// Only one of these will be set, depending on Type.
|
||||
Chat *ChatMessage `json:"chat,omitempty"`
|
||||
Gossip *PeerGossip `json:"gossip,omitempty"`
|
||||
FileOffer *FileOffer `json:"file_offer,omitempty"`
|
||||
FileResp *FileResponse `json:"file_response,omitempty"`
|
||||
FileDone *FileDone `json:"file_done,omitempty"`
|
||||
Seq *uint64 `json:"seq,omitempty"` // for ping/pong
|
||||
Chat *ChatMessage `json:"chat,omitempty"`
|
||||
Gossip *PeerGossip `json:"gossip,omitempty"`
|
||||
FileListResp *FileListResp `json:"file_list_resp,omitempty"`
|
||||
FileOffer *FileOffer `json:"file_offer,omitempty"`
|
||||
FileResp *FileResponse `json:"file_response,omitempty"`
|
||||
FileDone *FileDone `json:"file_done,omitempty"`
|
||||
Seq *uint64 `json:"seq,omitempty"` // for ping/pong
|
||||
}
|
||||
|
||||
// ChatMessage is a message to a room or a DM.
|
||||
@@ -80,6 +83,18 @@ type GossipEntry struct {
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
// FileEntry describes a single file in a peer's shared directory.
|
||||
type FileEntry struct {
|
||||
Name string `json:"name"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// FileListResp is the payload for MsgFileListResp.
|
||||
// MsgFileListReq carries no payload — it is a zero-field request.
|
||||
type FileListResp struct {
|
||||
Files []FileEntry `json:"files"`
|
||||
}
|
||||
|
||||
// FileOffer initiates a file transfer.
|
||||
type FileOffer struct {
|
||||
Mid string `json:"mid"` // dedup id
|
||||
@@ -187,6 +202,7 @@ const (
|
||||
CmdGetState IpcMsgType = "get_state"
|
||||
CmdSendFile IpcMsgType = "send_file"
|
||||
CmdGenerateInvite IpcMsgType = "generate_invite"
|
||||
CmdGetFileList IpcMsgType = "get_file_list"
|
||||
|
||||
// Events (daemon → UI)
|
||||
EvtMessageReceived IpcMsgType = "message_received"
|
||||
@@ -198,6 +214,7 @@ const (
|
||||
EvtStateSnapshot IpcMsgType = "state_snapshot"
|
||||
EvtError IpcMsgType = "error"
|
||||
EvtInviteGenerated IpcMsgType = "invite_generated"
|
||||
EvtFileList IpcMsgType = "file_list"
|
||||
)
|
||||
|
||||
// IpcMessage covers both commands and events.
|
||||
@@ -229,4 +246,5 @@ type IpcMessage struct {
|
||||
Rooms []string `json:"rooms,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
InviteString string `json:"invite,omitempty"`
|
||||
Files []FileEntry `json:"files,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user