package secrets import ( "context" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" ) type fsStore struct { dirPath string } type fsStorePayload[Body any] struct { Version int Body Body } // NewFSStore returns a Store which will store secrets to the given directory. func NewFSStore(dirPath string) (Store, error) { err := os.Mkdir(dirPath, 0700) if err != nil && !errors.Is(err, fs.ErrExist) { return nil, fmt.Errorf("making directory: %w", err) } return &fsStore{dirPath}, nil } func (s *fsStore) path(id ID) string { return filepath.Join(s.dirPath, string(id)) } func (s *fsStore) Set(_ context.Context, id ID, payload any) error { path := s.path(id) f, err := os.Create(path) if err != nil { return fmt.Errorf("creating file %q: %w", path, err) } defer f.Close() if err := json.NewEncoder(f).Encode(fsStorePayload[any]{ Version: 1, Body: payload, }); err != nil { return fmt.Errorf("writing JSON encoded payload to %q: %w", path, err) } return nil } func (s *fsStore) Get(_ context.Context, into any, id ID) error { path := s.path(id) f, err := os.Open(path) if errors.Is(err, fs.ErrNotExist) { return ErrNotFound } else if err != nil { return fmt.Errorf("creating file %q: %w", path, err) } defer f.Close() var fullPayload fsStorePayload[json.RawMessage] if err := json.NewDecoder(f).Decode(&fullPayload); err != nil { return fmt.Errorf("decoding JSON payload from %q: %w", path, err) } if fullPayload.Version != 1 { return fmt.Errorf( "unexpected JSON payload version %d", fullPayload.Version, ) } if err := json.Unmarshal(fullPayload.Body, into); err != nil { return fmt.Errorf( "decoding JSON payload body from %q into %T: %w", path, into, err, ) } return nil }