2022-10-02 14:39:55 -04:00
|
|
|
package storage
|
|
|
|
|
|
|
|
import (
|
2022-10-25 00:16:29 -04:00
|
|
|
"encoding/json"
|
2022-11-13 09:05:09 -05:00
|
|
|
"errors"
|
2022-10-25 00:16:29 -04:00
|
|
|
"fmt"
|
2022-10-02 14:39:55 -04:00
|
|
|
"io/fs"
|
2022-11-13 09:05:09 -05:00
|
|
|
"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
|
2022-10-02 14:39:55 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
type Data interface {
|
|
|
|
}
|
|
|
|
|
2022-11-13 09:05:09 -05:00
|
|
|
type item struct {
|
2022-10-02 14:39:55 -04:00
|
|
|
Version int `json:"version"`
|
|
|
|
MinorVersion *int `json:"minor_version,omitempty"`
|
|
|
|
Key string `json:"key"`
|
2022-11-13 09:05:09 -05:00
|
|
|
Data interface{} `json:"data"`
|
|
|
|
|
|
|
|
fmode os.FileMode
|
|
|
|
dirty bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type Item interface {
|
|
|
|
Dirty()
|
|
|
|
IsDirty() bool
|
|
|
|
GetData() interface{}
|
|
|
|
SetData(interface{})
|
|
|
|
ItemKey() string
|
2022-10-02 14:39:55 -04:00
|
|
|
}
|
|
|
|
|
2022-11-13 09:05:09 -05:00
|
|
|
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 {
|
2022-10-02 14:39:55 -04:00
|
|
|
fs.FS
|
2022-11-13 09:05:09 -05:00
|
|
|
storeRoot string
|
|
|
|
s map[string]*item
|
2022-10-02 14:39:55 -04:00
|
|
|
}
|
|
|
|
|
2022-10-25 00:16:29 -04:00
|
|
|
type Store interface {
|
2022-11-13 09:05:09 -05:00
|
|
|
GetItem(key string, data interface{}) (Item, error)
|
2022-10-25 00:16:29 -04:00
|
|
|
Get(key string, data interface{}) error
|
2022-11-13 09:05:09 -05:00
|
|
|
Put(key string, version, minorVersion int, secretMode bool, data interface{}) (Item, error)
|
|
|
|
FlushAll() []error
|
|
|
|
Flush(key string) error
|
|
|
|
Shutdown()
|
2022-10-25 00:16:29 -04:00
|
|
|
}
|
|
|
|
|
2022-11-13 09:05:09 -05:00
|
|
|
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())
|
2022-10-25 00:16:29 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
2022-11-13 09:05:09 -05:00
|
|
|
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{
|
2022-10-25 00:16:29 -04:00
|
|
|
Data: data,
|
|
|
|
}
|
|
|
|
d := json.NewDecoder(f)
|
2022-11-13 09:05:09 -05:00
|
|
|
err = d.Decode(item)
|
2022-10-25 00:16:29 -04:00
|
|
|
if err != nil {
|
2022-11-13 09:05:09 -05:00
|
|
|
return nil, err
|
2022-10-25 00:16:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if item.Key != key {
|
2022-11-13 09:05:09 -05:00
|
|
|
return nil, fmt.Errorf("key mismatch '%s' != '%s'", item.Key, key)
|
2022-10-25 00:16:29 -04:00
|
|
|
}
|
|
|
|
|
2022-11-13 09:05:09 -05:00
|
|
|
s.s[key] = item
|
|
|
|
|
|
|
|
return item, nil
|
2022-10-25 00:16:29 -04:00
|
|
|
}
|
|
|
|
|
2022-11-13 09:05:09 -05:00
|
|
|
func OpenFileStore(configRoot string) (*fsStore, error) {
|
|
|
|
storeRoot := path.Join(configRoot, ".storage")
|
|
|
|
stor := os.DirFS(storeRoot)
|
2022-10-02 14:39:55 -04:00
|
|
|
|
2022-11-13 09:05:09 -05:00
|
|
|
return &fsStore{
|
|
|
|
FS: stor,
|
|
|
|
storeRoot: storeRoot,
|
|
|
|
s: make(map[string]*item),
|
|
|
|
}, nil
|
2022-10-02 14:39:55 -04:00
|
|
|
}
|