package storage import ( "encoding/json" "fmt" "io/fs" "os" "path" "strings" "sync" "github.com/rs/zerolog/log" ) var ( IndentStr = strings.Repeat(" ", 2) ) const ( SecretMode fs.FileMode = 0600 DefaultMode fs.FileMode = 0644 ) type item struct { sync.Mutex `json:"-"` Version int `json:"version"` MinorVersion *int `json:"minor_version,omitempty"` Key string `json:"key"` Data interface{} `json:"data"` fmode fs.FileMode dirty bool } func (i *item) Dirty() { i.Lock(); defer i.Unlock(); i.dirty = true } func (i *item) IsDirty() bool { i.Lock(); defer i.Unlock(); return i.dirty } func (i *item) GetData() interface{} { i.Lock(); defer i.Unlock(); return i.Data } func (i *item) SetData(d interface{}) { i.Lock(); defer i.Unlock(); i.Data = d; i.dirty = true } func (i *item) ItemKey() string { return i.Key /* key is immutable */ } func (it *item) mode() fs.FileMode { if it.fmode != 0 { return it.fmode } return SecretMode } type fsStore struct { sync.RWMutex fs fs.FS storeRoot string s map[string]*item } func (s *fsStore) get(key string) *item { s.RLock() defer s.RUnlock() i, ok := s.s[key] if !ok { return nil } return i } func (s *fsStore) put(key string, it *item) { s.Lock() defer s.Unlock() s.s[key] = it } func (s *fsStore) persist(it *item) error { it.Lock() defer it.Unlock() f, err := os.OpenFile(path.Join(s.storeRoot, it.Key), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 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 := s.get(key) if it == nil { return ErrNoSuchKey } it.Dirty() return nil } func (s *fsStore) Flush(key string) error { it := s.get(key) if it == nil { return ErrNoSuchKey } return s.persist(it) } func (s *fsStore) FlushAll() []error { s.RLock() defer s.RUnlock() 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) { exists := s.get(key) if exists != nil { return exists, ErrKeyExists } f, err := s.fs.Open(key) if err != nil { return nil, err } defer f.Close() fi, err := f.Stat() if err != nil { return nil, err } item := &item{ Data: data, fmode: fi.Mode(), } 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.put(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 }