Ctrl K
ComunidadGuías…

internal/protect/store.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}