// Package store gère la persistance des hosts dans hosts.toml.
package store
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/pelletier/go-toml/v2"
"git.neuromancer.ovh/bastien-mrq/sssh/internal/host"
)
type fileFormat struct {
Hosts map[string]host.Host `toml:"hosts"`
}
// Store est le contenu chargé de hosts.toml.
type Store struct {
path string
Hosts map[string]host.Host
}
// Path rend le chemin du fichier de configuration, surchargé par $SSSH_CONFIG.
func Path() string {
if p := os.Getenv("SSSH_CONFIG"); p != "" {
return p
}
base, err := os.UserConfigDir()
if err != nil {
base = filepath.Join(os.Getenv("HOME"), ".config")
}
return filepath.Join(base, "sssh", "hosts.toml")
}
// Load lit hosts.toml ; un fichier absent donne un store vide.
func Load() (*Store, error) {
s := &Store{path: Path(), Hosts: map[string]host.Host{}}
data, err := os.ReadFile(s.path)
if os.IsNotExist(err) {
return s, nil
}
if err != nil {
return nil, err
}
var f fileFormat
if err := toml.Unmarshal(data, &f); err != nil {
return nil, fmt.Errorf("%s : %w", s.path, err)
}
if f.Hosts != nil {
s.Hosts = f.Hosts
}
return s, nil
}
// Save écrit le fichier de façon atomique (fichier temporaire puis rename).
func (s *Store) Save() error {
data, err := s.Marshal()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
return err
}
tmp := s.path + ".tmp"
if err := os.WriteFile(tmp, data, 0o600); err != nil {
return err
}
return os.Rename(tmp, s.path)
}
// Marshal sérialise le store au format hosts.toml.
func (s *Store) Marshal() ([]byte, error) {
return toml.Marshal(fileFormat{Hosts: s.Hosts})
}
// Get rend le host nommé, avec son Name renseigné.
func (s *Store) Get(name string) (host.Host, bool) {
h, ok := s.Hosts[name]
if ok {
h.Name = name
}
return h, ok
}
// Set ajoute ou remplace un host.
func (s *Store) Set(h host.Host) {
s.Hosts[h.Name] = h
}
// Remove supprime un host ; rend false s'il n'existait pas.
func (s *Store) Remove(name string) bool {
if _, ok := s.Hosts[name]; !ok {
return false
}
delete(s.Hosts, name)
return true
}
// List rend les hosts triés par nom.
func (s *Store) List() []host.Host {
hosts := make([]host.Host, 0, len(s.Hosts))
for name, h := range s.Hosts {
h.Name = name
hosts = append(hosts, h)
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
return hosts
}
// Resolve cherche name parmi hosts : correspondance exacte d'abord,
// sinon préfixe unique. En cas d'ambiguïté, l'erreur liste les candidats.
func Resolve(hosts []host.Host, name string) (host.Host, error) {
var candidates []host.Host
for _, h := range hosts {
if h.Name == name {
return h, nil
}
if strings.HasPrefix(h.Name, name) {
candidates = append(candidates, h)
}
}
switch len(candidates) {
case 1:
return candidates[0], nil
case 0:
return host.Host{}, fmt.Errorf("host inconnu : %q (voir sssh list)", name)
default:
names := make([]string, len(candidates))
for i, h := range candidates {
names[i] = h.Name
}
return host.Host{}, fmt.Errorf("%q est ambigu : %s", name, strings.Join(names, ", "))
}
}