internal/protect/store.go
Rama main · réplica de lectura del repositorio original.
package protect
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
"net/url"
"os"
"path/filepath"
"sync"
"github.com/sophotechlabs/spinoza/internal/api"
"github.com/sophotechlabs/spinoza/internal/atomicfile"
"github.com/sophotechlabs/spinoza/internal/clusterid"
"github.com/sophotechlabs/spinoza/internal/filetx"
)
const maxFileBytes = 1 << 20
type state struct {
Clusters map[string]bool `json:"clusters"`
}
type Store struct {
mu sync.Mutex
ctx context.Context
path string
clusters map[string]bool
}
func DefaultPath() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("protected clusters: %w", err)
}
return filepath.Join(dir, "spinoza", "protected.json"), nil
}
func Open(ctx context.Context, path string) (*Store, error) {
store := &Store{ctx: ctx, path: path, clusters: map[string]bool{}}
clusters, err := read(path)
if err != nil {
return store, fmt.Errorf("protected clusters %s: %w", path, err)
}
store.clusters = clusters
return store, nil
}
func adopt(into, saved map[string]bool) {
for server, protected := range saved {
key := clusterid.Normalize(server)
if key == "" {
continue
}
if into[key] {
continue
}
into[key] = protected
}
}
func (s *Store) Verdict(server string) string {
key := clusterid.Normalize(server)
if key == "" {
return api.ProtectionUnknown
}
protected, decided := s.decision(key)
if decided && protected {
return api.ProtectionProtected
}
if decided {
return api.ProtectionOpen
}
if Local(key) {
return api.ProtectionOpen
}
return api.ProtectionUnknown
}
func (s *Store) decision(key string) (bool, bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.refresh()
protected, decided := s.clusters[key]
return protected, decided
}
func (s *Store) Set(server string, protected bool) error {
key := clusterid.Normalize(server)
if key == "" {
return errors.New("spinoza is not connected to a cluster")
}
s.mu.Lock()
defer s.mu.Unlock()
if s.path == "" {
if s.clusters == nil {
s.clusters = map[string]bool{}
}
s.clusters[key] = protected
return nil
}
err := filetx.Exclusive(s.ctx, s.path, func() error {
next, readErr := read(s.path)
if readErr != nil {
return fmt.Errorf("%s: %w", s.path, readErr)
}
next[key] = protected
if err := s.write(next); err != nil {
return err
}
s.clusters = next
return nil
})
if err != nil {
return fmt.Errorf("protected clusters: %w", err)
}
return nil
}
func read(path string) (map[string]bool, error) {
body, err := filetx.Read(path, maxFileBytes)
if errors.Is(err, fs.ErrNotExist) {
return map[string]bool{}, nil
}
if err != nil {
return nil, err
}
var saved state
if err := json.Unmarshal(body, &saved); err != nil {
return nil, err
}
found := map[string]bool{}
adopt(found, saved.Clusters)
return found, nil
}
func (s *Store) refresh() {
if s.path == "" {
return
}
found, err := read(s.path)
if err == nil {
s.clusters = found
}
}
func Local(server string) bool {
parsed, err := url.Parse(server)
if err != nil {
return false
}
host := parsed.Hostname()
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
return ip.IsLoopback()
}
func (s *Store) write(clusters map[string]bool) error {
if s.path == "" {
return nil
}
body, err := json.MarshalIndent(state{Clusters: clusters}, "", " ")
if err != nil {
return fmt.Errorf("protected clusters: %w", err)
}
if len(body)+1 > maxFileBytes {
return fmt.Errorf("protected clusters are larger than %d bytes", maxFileBytes)
}
saveErr := atomicfile.Save(s.path, "protected-*.json", append(body, '\n'))
if saveErr != nil {
return fmt.Errorf("protected clusters: %w", saveErr)
}
return nil
}