feat: persistent multi-share configuration (shares.json + localStorage)

Daemon side:
- internal/shares: new package reads/writes shares.json next to identity
- proto: add ShareEntry type, add_share/remove_share/list_shares IPC commands,
  shares_list event, path field on FileEntry
- netmgr: load shares.json on startup; ScanAllShares combines legacy ShareDir
  with shares.json entries (recursive walk with relative paths)
- mesh: ScanFiles callback lets manager inject multi-share scanning without
  mesh knowing about shares.json
- ipc: handle add_share, remove_share, list_shares commands

Browser side:
- ShareManager component replaces FolderPicker: shows list of named shares
  with remove/re-pick buttons; persists share records to waste_shares in
  localStorage (name, global flag, networkId)
- FolderPicker retained for internal use; Sidebar now uses ShareManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-26 20:47:11 +02:00
parent 0f54f3bbad
commit e0704f210c
10 changed files with 366 additions and 4 deletions

View File

@@ -23,6 +23,7 @@ import (
"github.com/waste-go/internal/invite"
"github.com/waste-go/internal/netmgr"
"github.com/waste-go/internal/proto"
"github.com/waste-go/internal/shares"
)
// RunWS starts a WebSocket IPC server on 127.0.0.1:wsPort.
@@ -246,7 +247,7 @@ func handleClient(conn net.Conn, mgr *netmgr.Manager) {
Type: proto.EvtFileList,
NetworkID: n.ID,
PeerID: ptr(n.Identity.PeerID()),
Files: n.Mesh.ScanShareDir(),
Files: mgr.ScanAllShares(n.ID),
})
} else {
req, err := json.Marshal(proto.PeerMessage{Type: proto.MsgFileListReq})
@@ -258,6 +259,35 @@ func handleClient(conn net.Conn, mgr *netmgr.Manager) {
}
}
case proto.CmdAddShare:
if cmd.Path == "" {
send(errMsg("add_share: path is required"))
continue
}
networks := cmd.ShareNetworks
if len(networks) == 0 {
networks = []string{"*"}
}
if err := mgr.Shares.Add(shares.Share{Path: cmd.Path, Networks: networks}); err != nil {
send(errMsg(fmt.Sprintf("add_share: %v", err)))
continue
}
send(sharesListMsg(mgr))
case proto.CmdRemoveShare:
if cmd.Path == "" {
send(errMsg("remove_share: path is required"))
continue
}
if err := mgr.Shares.Remove(cmd.Path); err != nil {
send(errMsg(fmt.Sprintf("remove_share: %v", err)))
continue
}
send(sharesListMsg(mgr))
case proto.CmdListShares:
send(sharesListMsg(mgr))
case proto.CmdGenerateInvite:
n := mgr.Resolve(cmd.NetworkID)
if n == nil {
@@ -389,6 +419,18 @@ func errMsg(s string) proto.IpcMessage {
return proto.IpcMessage{Type: proto.EvtError, ErrorMessage: s}
}
func sharesListMsg(mgr *netmgr.Manager) proto.IpcMessage {
all := mgr.Shares.All()
entries := make([]proto.ShareEntry, len(all))
for i, sh := range all {
entries[i] = proto.ShareEntry{Path: sh.Path, Networks: sh.Networks}
}
return proto.IpcMessage{Type: proto.EvtSharesList, Shares: entries}
}
// ensure shares import is used
var _ = shares.Share{}
func ptr[T any](v T) *T { return &v }
func randomHex(n int) string {