Files
wx-win-agent/control-plane/store.go
T
rogee 13c31fc902
Build web service image / build (push) Successful in 1m53s
feat: add remote control plane and whitelist reads
2026-09-12 09:46:05 +08:00

132 lines
3.2 KiB
Go

package controlplane
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
)
// Store is a small durable JSON store for the single-process control-plane MVP.
// The file is an implementation detail; callers only observe transactional methods.
type Store struct {
path string
mu sync.Mutex
state PersistedState
}
func OpenStore(path string) (*Store, error) {
if path == "" {
return nil, errors.New("data file is required")
}
store := &Store{path: filepath.Clean(path), state: PersistedState{Nodes: map[string]Node{}, Tasks: map[string]Task{}, Events: []StoredEvent{}, Audit: []AuditEntry{}}}
data, err := os.ReadFile(store.path)
if errors.Is(err, os.ErrNotExist) {
return store, nil
}
if err != nil {
return nil, fmt.Errorf("read data file: %w", err)
}
if len(data) == 0 {
return store, nil
}
if err := json.Unmarshal(data, &store.state); err != nil {
return nil, fmt.Errorf("decode data file: %w", err)
}
store.ensureMaps()
return store, nil
}
func (s *Store) Snapshot() PersistedState {
s.mu.Lock()
defer s.mu.Unlock()
return cloneState(s.state)
}
func (s *Store) Mutate(fn func(*PersistedState) error) error {
s.mu.Lock()
defer s.mu.Unlock()
s.ensureMaps()
if err := fn(&s.state); err != nil {
return err
}
return s.saveLocked()
}
func (s *Store) Read(fn func(PersistedState) error) error {
s.mu.Lock()
defer s.mu.Unlock()
return fn(cloneState(s.state))
}
func (s *Store) ensureMaps() {
if s.state.Nodes == nil {
s.state.Nodes = map[string]Node{}
}
if s.state.Tasks == nil {
s.state.Tasks = map[string]Task{}
}
if s.state.Events == nil {
s.state.Events = []StoredEvent{}
}
if s.state.Audit == nil {
s.state.Audit = []AuditEntry{}
}
}
func (s *Store) saveLocked() error {
if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil {
return fmt.Errorf("create data directory: %w", err)
}
data, err := json.MarshalIndent(s.state, "", " ")
if err != nil {
return fmt.Errorf("encode data file: %w", err)
}
temporary, err := os.CreateTemp(filepath.Dir(s.path), ".wxagent-control-plane-*")
if err != nil {
return fmt.Errorf("create temporary data file: %w", err)
}
temporaryName := temporary.Name()
defer os.Remove(temporaryName)
if err := temporary.Chmod(0o600); err != nil {
_ = temporary.Close()
return fmt.Errorf("protect temporary data file: %w", err)
}
if _, err := temporary.Write(data); err != nil {
_ = temporary.Close()
return fmt.Errorf("write data file: %w", err)
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return fmt.Errorf("sync data file: %w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("close data file: %w", err)
}
if err := os.Rename(temporaryName, s.path); err != nil {
return fmt.Errorf("replace data file: %w", err)
}
return nil
}
func cloneState(source PersistedState) PersistedState {
data, _ := json.Marshal(source)
var copy PersistedState
_ = json.Unmarshal(data, &copy)
if copy.Nodes == nil {
copy.Nodes = map[string]Node{}
}
if copy.Tasks == nil {
copy.Tasks = map[string]Task{}
}
if copy.Events == nil {
copy.Events = []StoredEvent{}
}
if copy.Audit == nil {
copy.Audit = []AuditEntry{}
}
return copy
}