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>
100 lines
2.2 KiB
Go
100 lines
2.2 KiB
Go
// Package shares manages the persistent multi-share configuration stored in shares.json.
|
|
// Shares are additive to the existing single-dir ShareDir mechanism.
|
|
package shares
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
// Share describes one shared directory entry.
|
|
type Share struct {
|
|
Path string `json:"path"`
|
|
Networks []string `json:"networks"` // ["*"] = global (all networks); otherwise list of network IDs
|
|
}
|
|
|
|
// Store manages the shares.json file.
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
path string
|
|
shares []Share
|
|
}
|
|
|
|
// Load reads shares.json from dataDir. Returns an empty store if the file doesn't exist.
|
|
func Load(dataDir string) (*Store, error) {
|
|
s := &Store{path: filepath.Join(dataDir, "shares.json")}
|
|
data, err := os.ReadFile(s.path)
|
|
if os.IsNotExist(err) {
|
|
return s, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(data, &s.shares); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Store) save() error {
|
|
data, err := json.MarshalIndent(s.shares, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(s.path, data, 0600)
|
|
}
|
|
|
|
// All returns a copy of all shares.
|
|
func (s *Store) All() []Share {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]Share, len(s.shares))
|
|
copy(out, s.shares)
|
|
return out
|
|
}
|
|
|
|
// ForNetwork returns all shares visible on networkID (global + network-specific).
|
|
func (s *Store) ForNetwork(networkID string) []Share {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
var out []Share
|
|
for _, sh := range s.shares {
|
|
for _, n := range sh.Networks {
|
|
if n == "*" || n == networkID {
|
|
out = append(out, sh)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Add adds a share (no-op if path already present). Persists immediately.
|
|
func (s *Store) Add(sh Share) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for i, existing := range s.shares {
|
|
if existing.Path == sh.Path {
|
|
s.shares[i].Networks = sh.Networks
|
|
return s.save()
|
|
}
|
|
}
|
|
s.shares = append(s.shares, sh)
|
|
return s.save()
|
|
}
|
|
|
|
// Remove removes the share with the given path. Persists immediately.
|
|
func (s *Store) Remove(path string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for i, sh := range s.shares {
|
|
if sh.Path == path {
|
|
s.shares = append(s.shares[:i], s.shares[i+1:]...)
|
|
return s.save()
|
|
}
|
|
}
|
|
return nil
|
|
}
|