Persistance d'un état de composant dans un fichier, générique sur le type stocké : temp + chmod 0600 + sync + rename, répertoire en 0700. Chaque écriture remplace le fichier, donc un chmod externe est effacé, et un échec d'encodage ne tronque pas l'état précédent. Load crée le fichier absent et rend le zéro du type ; un fichier vide est un point de départ valide, un fichier corrompu est une erreur. Dans pkg/ plutôt que sous internal/dhcpd : une réutilisation par l'intel est envisagée. Écarte Badger pour cet usage — une instance par subnet coûterait ~74 Mio et 4 goroutines pour ~1 Kio d'état dans un tmpfs, plus un LOCK résiduel après un arrêt brutal. 11 tests, quatre mutations toutes détectées. Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
92 lines
1.9 KiB
Go
92 lines
1.9 KiB
Go
package statefile
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
const (
|
|
FileMode = 0o600
|
|
DirMode = 0o700
|
|
)
|
|
|
|
type File[T any] struct {
|
|
path string
|
|
}
|
|
|
|
func New[T any](path string) *File[T] {
|
|
return &File[T]{path: path}
|
|
}
|
|
|
|
func (f *File[T]) Path() string {
|
|
return f.path
|
|
}
|
|
|
|
func (f *File[T]) Load() (T, error) {
|
|
var value T
|
|
|
|
raw, err := os.ReadFile(f.path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return value, f.Save(value)
|
|
}
|
|
if err != nil {
|
|
return value, fmt.Errorf("read %s: %w", f.path, err)
|
|
}
|
|
if len(raw) == 0 {
|
|
return value, nil
|
|
}
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
var zero T
|
|
return zero, fmt.Errorf("parse %s: %w", f.path, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (f *File[T]) Save(value T) error {
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("encode state for %s: %w", f.path, err)
|
|
}
|
|
|
|
dir := filepath.Dir(f.path)
|
|
if err := os.MkdirAll(dir, DirMode); err != nil {
|
|
return fmt.Errorf("create %s: %w", dir, err)
|
|
}
|
|
|
|
tmp, err := os.CreateTemp(dir, filepath.Base(f.path)+".tmp")
|
|
if err != nil {
|
|
return fmt.Errorf("create temp state in %s: %w", dir, err)
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
|
|
if err := tmp.Chmod(FileMode); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("chmod %s: %w", tmp.Name(), err)
|
|
}
|
|
if _, err := tmp.Write(raw); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("write %s: %w", tmp.Name(), err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("sync %s: %w", tmp.Name(), err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("close %s: %w", tmp.Name(), err)
|
|
}
|
|
|
|
if err := os.Rename(tmp.Name(), f.path); err != nil {
|
|
return fmt.Errorf("rename %s to %s: %w", tmp.Name(), f.path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *File[T]) Remove() error {
|
|
if err := os.Remove(f.path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("remove %s: %w", f.path, err)
|
|
}
|
|
return nil
|
|
}
|