package storage import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path" "strings" "github.com/rs/zerolog/log" ) var ( IndentStr = strings.Repeat(" ", 4) ErrNoSuchKey = errors.New("no such key in store") ) const ( SecretMode os.FileMode = 0600 DefaultMode os.FileMode = 0644 ) type Data interface { } type item struct { Version int `json:"version"` MinorVersion *int `json:"minor_version,omitempty"` Key string `json:"key"` Data interface{} `json:"data"` fmode os.FileMode dirty bool } type Item interface { Dirty() IsDirty() bool GetData() interface{} SetData(interface{}) ItemKey() string } func (i *item) Dirty() { i.dirty = true } func (i *item) IsDirty() bool { return i.dirty } func (i *item) GetData() interface{} { return i.Data } func (i *item) SetData(d interface{}) { i.Data = d; i.Dirty() } func (i *item) ItemKey() string { return i.Key } func (it *item) mode() os.FileMode { if it.fmode != 0 { return it.fmode } return SecretMode } type fsStore struct { fs.FS storeRoot string s map[string]*item } type Store interface { GetItem(key string, data interface{}) (Item, error) Get(key string, data interface{}) error Put(key string, version, minorVersion int, secretMode bool, data interface{}) (Item, error) FlushAll() []error Flush(key string) error Shutdown() } func (s *fsStore) persist(it *item) error { f, err := os.OpenFile(path.Join(s.storeRoot, it.Key), os.O_WRONLY|os.O_CREATE, it.mode()) if err != nil { return err } defer f.Close() enc := json.NewEncoder(f) enc.SetIndent("", IndentStr) err = enc.Encode(it) if err == nil { it.dirty = false } return err } func (s *fsStore) Dirty(key string) error { it, has := s.s[key] if !has { return ErrNoSuchKey } it.dirty = true return nil } func (s *fsStore) Flush(key string) error { it, exists := s.s[key] if !exists { return ErrNoSuchKey } return s.persist(it) } func (s *fsStore) FlushAll() []error { var errs []error for _, it := range s.s { err := s.persist(it) if err != nil { errs = append(errs, fmt.Errorf("store key %s: %w", it.Key, err)) } } return errs } func (s *fsStore) Shutdown() { errs := s.FlushAll() if errs != nil { log.Error().Errs("errors", errs).Msg("errors persisting store") } } // Put puts an item into the store. // NB: Any user of a previous item with this key will now have a dangling reference that will not be persisted. // It is up to consumers to coordinate against this case! func (s *fsStore) Put(key string, version, minorVersion int, secretMode bool, data interface{}) (Item, error) { var mv *int if minorVersion != 0 { mv = &minorVersion } mode := DefaultMode if secretMode { mode = SecretMode } it := &item{ Version: version, MinorVersion: mv, Key: key, Data: data, fmode: mode, dirty: true, } s.s[key] = it return it, s.persist(it) } func (s *fsStore) Get(key string, data interface{}) error { _, err := s.GetItem(key, data) return err } func (s *fsStore) GetItem(key string, data interface{}) (Item, error) { f, err := s.Open(key) if err != nil { return nil, err } defer f.Close() item := &item{ Data: data, } d := json.NewDecoder(f) err = d.Decode(item) if err != nil { return nil, err } if item.Key != key { return nil, fmt.Errorf("key mismatch '%s' != '%s'", item.Key, key) } s.s[key] = item return item, nil } func OpenFileStore(configRoot string) (*fsStore, error) { storeRoot := path.Join(configRoot, ".storage") stor := os.DirFS(storeRoot) return &fsStore{ FS: stor, storeRoot: storeRoot, s: make(map[string]*item), }, nil }