From 6199a5fe848fbe8bbecd0b5adf738618b9a38b62 Mon Sep 17 00:00:00 2001
From: Dominik Schulz <dominik.schulz@gauner.org>
Date: Fri, 16 Mar 2018 21:28:01 +0100
Subject: [PATCH] Introduce store interface

---
 pkg/action/sync.go        |  4 +--
 pkg/cui/recipients.go     |  3 ++
 pkg/store/root/git.go     | 10 +++----
 pkg/store/root/gpg.go     |  2 +-
 pkg/store/root/mount.go   |  6 ++--
 pkg/store/root/store.go   |  8 +++--
 pkg/store/store.go        | 62 +++++++++++++++++++++++++++++++++++++--
 pkg/store/sub/git.go      | 26 ----------------
 pkg/store/sub/git_test.go | 12 ++++----
 pkg/store/sub/store.go    | 14 +++++++--
 10 files changed, 97 insertions(+), 50 deletions(-)

diff --git a/pkg/action/sync.go b/pkg/action/sync.go
index 2e669ac371..fccd7b8885 100644
--- a/pkg/action/sync.go
+++ b/pkg/action/sync.go
@@ -86,7 +86,7 @@ func (s *Action) syncMount(ctx context.Context, mp string) error {
 	}
 
 	out.Print(ctxno, "\n   "+color.GreenString("git pull and push ... "))
-	if err := sub.GitPush(ctx, "", ""); err != nil {
+	if err := sub.RCS().Push(ctx, "", ""); err != nil {
 		if errors.Cause(err) == store.ErrGitNoRemote {
 			out.Yellow(ctx, "Skipped (no remote)")
 			out.Debug(ctx, "Failed to push '%s' to it's remote: %s", name, err)
@@ -132,7 +132,7 @@ func (s *Action) syncMount(ctx context.Context, mp string) error {
 
 	// only run second push if we did export any keys
 	if exported {
-		if err := sub.GitPush(ctx, "", ""); err != nil {
+		if err := sub.RCS().Push(ctx, "", ""); err != nil {
 			out.Red(ctx, "Failed to push '%s' to it's remote: %s", name, err)
 			return err
 		}
diff --git a/pkg/cui/recipients.go b/pkg/cui/recipients.go
index 92638ec2ae..1102a0a71a 100644
--- a/pkg/cui/recipients.go
+++ b/pkg/cui/recipients.go
@@ -184,6 +184,9 @@ func AskForPrivateKey(ctx context.Context, crypto backend.Crypto, name, prompt s
 	if !ctxutil.IsInteractive(ctx) {
 		return "", errors.New("no interaction without terminal")
 	}
+	if crypto == nil {
+		return "", errors.New("no key selection without valid crypto")
+	}
 
 	kl, err := crypto.ListPrivateKeyIDs(gpg.WithAlwaysTrust(ctx, false))
 	if err != nil {
diff --git a/pkg/store/root/git.go b/pkg/store/root/git.go
index 5ee8faa68c..0df92232a5 100644
--- a/pkg/store/root/git.go
+++ b/pkg/store/root/git.go
@@ -26,30 +26,30 @@ func (r *Store) GitInit(ctx context.Context, name, userName, userEmail string) e
 // GitInitConfig initializes the git repos local config
 func (r *Store) GitInitConfig(ctx context.Context, name, userName, userEmail string) error {
 	ctx, store, _ := r.getStore(ctx, name)
-	return store.GitInitConfig(ctx, userName, userEmail)
+	return store.RCS().InitConfig(ctx, userName, userEmail)
 }
 
 // GitVersion returns git version information
 func (r *Store) GitVersion(ctx context.Context) semver.Version {
-	return r.store.GitVersion(ctx)
+	return r.store.RCS().Version(ctx)
 }
 
 // GitAddRemote adds a git remote
 func (r *Store) GitAddRemote(ctx context.Context, name, remote, url string) error {
 	ctx, store, _ := r.getStore(ctx, name)
-	return store.GitAddRemote(ctx, remote, url)
+	return store.RCS().AddRemote(ctx, remote, url)
 }
 
 // GitPull performs a git pull
 func (r *Store) GitPull(ctx context.Context, name, origin, remote string) error {
 	ctx, store, _ := r.getStore(ctx, name)
-	return store.GitPush(ctx, origin, remote)
+	return store.RCS().Push(ctx, origin, remote)
 }
 
 // GitPush performs a git push
 func (r *Store) GitPush(ctx context.Context, name, origin, remote string) error {
 	ctx, store, _ := r.getStore(ctx, name)
-	return store.GitPush(ctx, origin, remote)
+	return store.RCS().Push(ctx, origin, remote)
 }
 
 // ListRevisions will list all revisions for the named entity
diff --git a/pkg/store/root/gpg.go b/pkg/store/root/gpg.go
index 0fd808e1a9..ff12cf4823 100644
--- a/pkg/store/root/gpg.go
+++ b/pkg/store/root/gpg.go
@@ -9,7 +9,7 @@ import (
 // Crypto returns the crypto backend
 func (r *Store) Crypto(ctx context.Context, name string) backend.Crypto {
 	_, sub, _ := r.getStore(ctx, name)
-	if sub == nil {
+	if !sub.Valid() {
 		return nil
 	}
 	return sub.Crypto()
diff --git a/pkg/store/root/mount.go b/pkg/store/root/mount.go
index aba512018e..48b2678f3e 100644
--- a/pkg/store/root/mount.go
+++ b/pkg/store/root/mount.go
@@ -29,7 +29,7 @@ func (r *Store) addMount(ctx context.Context, alias, path string, sc *config.Sto
 		return errors.Errorf("alias must not be empty")
 	}
 	if r.mounts == nil {
-		r.mounts = make(map[string]*sub.Store, 1)
+		r.mounts = make(map[string]store.Store, 1)
 	}
 	if _, found := r.mounts[alias]; found {
 		return errors.Errorf("%s is already mounted", alias)
@@ -141,7 +141,7 @@ func (r *Store) MountPoint(name string) string {
 // getStore returns the Store object at the most-specific mount point for the
 // given key
 // context with sub store options set, sub store reference, truncated path to secret
-func (r *Store) getStore(ctx context.Context, name string) (context.Context, *sub.Store, string) {
+func (r *Store) getStore(ctx context.Context, name string) (context.Context, store.Store, string) {
 	name = strings.TrimSuffix(name, "/")
 	mp := r.MountPoint(name)
 	if sub, found := r.mounts[mp]; found {
@@ -152,7 +152,7 @@ func (r *Store) getStore(ctx context.Context, name string) (context.Context, *su
 
 // GetSubStore returns an exact match for a mount point or an error if this
 // mount point does not exist
-func (r *Store) GetSubStore(name string) (*sub.Store, error) {
+func (r *Store) GetSubStore(name string) (store.Store, error) {
 	if name == "" {
 		return r.store, nil
 	}
diff --git a/pkg/store/root/store.go b/pkg/store/root/store.go
index dfd7526cf6..1f54d5984f 100644
--- a/pkg/store/root/store.go
+++ b/pkg/store/root/store.go
@@ -8,14 +8,16 @@ import (
 	"github.com/justwatchcom/gopass/pkg/agent/client"
 	"github.com/justwatchcom/gopass/pkg/backend"
 	"github.com/justwatchcom/gopass/pkg/config"
+	"github.com/justwatchcom/gopass/pkg/store"
 	"github.com/justwatchcom/gopass/pkg/store/sub"
 	"github.com/pkg/errors"
 )
 
 // Store is the public facing password store
 type Store struct {
-	cfg     *config.Config
-	mounts  map[string]*sub.Store
+	cfg *config.Config
+	//mounts  map[string]*sub.Store
+	mounts  map[string]store.Store
 	url     *backend.URL // url of the root store
 	store   *sub.Store
 	version string
@@ -32,7 +34,7 @@ func New(ctx context.Context, cfg *config.Config) (*Store, error) {
 	}
 	r := &Store{
 		cfg:     cfg,
-		mounts:  make(map[string]*sub.Store, len(cfg.Mounts)),
+		mounts:  make(map[string]store.Store, len(cfg.Mounts)),
 		url:     cfg.Root.Path,
 		version: cfg.Version,
 	}
diff --git a/pkg/store/store.go b/pkg/store/store.go
index f8f26a9035..ea0f2863bc 100644
--- a/pkg/store/store.go
+++ b/pkg/store/store.go
@@ -1,6 +1,12 @@
 package store
 
-import "context"
+import (
+	"context"
+	"fmt"
+
+	"github.com/justwatchcom/gopass/pkg/backend"
+	"github.com/justwatchcom/gopass/pkg/tree"
+)
 
 // RecipientCallback is a callback to verify the list of recipients
 type RecipientCallback func(context.Context, string, []string) ([]string, error)
@@ -13,4 +19,56 @@ type ImportCallback func(context.Context, string, []string) bool
 // corrective actions
 type FsckCallback func(context.Context, string) bool
 
-// TODO add store interface
+// TemplateStore is a store supporting templating operations
+type TemplateStore interface {
+	GetTemplate(context.Context, string) ([]byte, error)
+	HasTemplate(context.Context, string) bool
+	ListTemplates(context.Context, string) []string
+	LookupTemplate(context.Context, string) ([]byte, bool)
+	RemoveTemplate(context.Context, string) error
+	SetTemplate(context.Context, string, []byte) error
+	TemplateTree(context.Context) (tree.Tree, error)
+}
+
+// RecipientStore is a store supporting recipient operations
+type RecipientStore interface {
+	AddRecipient(context.Context, string) error
+	GetRecipients(context.Context, string) ([]string, error)
+	RemoveRecipient(context.Context, string) error
+	SaveRecipients(context.Context) error
+	Recipients(context.Context) []string
+	ImportMissingPublicKeys(context.Context) error
+	ExportMissingPublicKeys(context.Context, []string) (bool, error)
+}
+
+// Store is secrets store
+type Store interface {
+	fmt.Stringer
+
+	TemplateStore
+	RecipientStore
+
+	Fsck(context.Context, string) error
+	Path() string
+	URL() string
+	RCS() backend.RCS
+	Crypto() backend.Crypto
+	Storage() backend.Storage
+	GitInit(context.Context, string, string) error
+	Alias() string
+	Copy(context.Context, string, string) error
+	Delete(context.Context, string) error
+	Equals(Store) bool
+	Exists(context.Context, string) bool
+	Get(context.Context, string) (Secret, error)
+	GetRevision(context.Context, string, string) (Secret, error)
+	Init(context.Context, string, ...string) error
+	Initialized(context.Context) bool
+	IsDir(context.Context, string) bool
+	List(context.Context, string) ([]string, error)
+	ListRevisions(context.Context, string) ([]backend.Revision, error)
+	Move(context.Context, string, string) error
+	Set(context.Context, string, Secret) error
+	Prune(context.Context, string) error
+	Valid() bool
+}
diff --git a/pkg/store/sub/git.go b/pkg/store/sub/git.go
index 2cdb3e51fd..5de8890de5 100644
--- a/pkg/store/sub/git.go
+++ b/pkg/store/sub/git.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/blang/semver"
 	"github.com/justwatchcom/gopass/pkg/backend"
 	gitcli "github.com/justwatchcom/gopass/pkg/backend/rcs/git/cli"
 	"github.com/justwatchcom/gopass/pkg/backend/rcs/git/gogit"
@@ -45,31 +44,6 @@ func (s *Store) GitInit(ctx context.Context, un, ue string) error {
 	}
 }
 
-// GitInitConfig (re-)intializes the git config in an existing repo
-func (s *Store) GitInitConfig(ctx context.Context, un, ue string) error {
-	return s.rcs.InitConfig(ctx, un, ue)
-}
-
-// GitVersion returns the git version
-func (s *Store) GitVersion(ctx context.Context) semver.Version {
-	return s.rcs.Version(ctx)
-}
-
-// GitAddRemote adds a new remote
-func (s *Store) GitAddRemote(ctx context.Context, remote, url string) error {
-	return s.rcs.AddRemote(ctx, remote, url)
-}
-
-// GitPull performs a git pull
-func (s *Store) GitPull(ctx context.Context, origin, branch string) error {
-	return s.rcs.Pull(ctx, origin, branch)
-}
-
-// GitPush performs a git push
-func (s *Store) GitPush(ctx context.Context, origin, branch string) error {
-	return s.rcs.Push(ctx, origin, branch)
-}
-
 // ListRevisions will list all revisions for a secret
 func (s *Store) ListRevisions(ctx context.Context, name string) ([]backend.Revision, error) {
 	p := s.passfile(name)
diff --git a/pkg/store/sub/git_test.go b/pkg/store/sub/git_test.go
index ef7427a79b..45f22c318b 100644
--- a/pkg/store/sub/git_test.go
+++ b/pkg/store/sub/git_test.go
@@ -26,11 +26,11 @@ func TestGit(t *testing.T) {
 
 	assert.NotNil(t, s.RCS())
 	assert.Equal(t, "noop", s.RCS().Name())
-	assert.NoError(t, s.GitInitConfig(ctx, "foo", "bar@baz.com"))
-	assert.Equal(t, semver.Version{}, s.GitVersion(ctx))
-	assert.NoError(t, s.GitAddRemote(ctx, "foo", "bar"))
-	assert.NoError(t, s.GitPull(ctx, "origin", "master"))
-	assert.NoError(t, s.GitPush(ctx, "origin", "master"))
+	assert.NoError(t, s.RCS().InitConfig(ctx, "foo", "bar@baz.com"))
+	assert.Equal(t, semver.Version{}, s.RCS().Version(ctx))
+	assert.NoError(t, s.RCS().AddRemote(ctx, "foo", "bar"))
+	assert.NoError(t, s.RCS().Pull(ctx, "origin", "master"))
+	assert.NoError(t, s.RCS().Push(ctx, "origin", "master"))
 
 	assert.NoError(t, s.GitInit(ctx, "", ""))
 	assert.NoError(t, s.GitInit(backend.WithRCSBackend(ctx, backend.Noop), "", ""))
@@ -55,7 +55,7 @@ func TestGitRevisions(t *testing.T) {
 
 	assert.NotNil(t, s.RCS())
 	assert.Equal(t, "noop", s.RCS().Name())
-	assert.NoError(t, s.GitInitConfig(ctx, "foo", "bar@baz.com"))
+	assert.NoError(t, s.RCS().InitConfig(ctx, "foo", "bar@baz.com"))
 
 	_, err = s.ListRevisions(ctx, "foo")
 	assert.Error(t, err)
diff --git a/pkg/store/sub/store.go b/pkg/store/sub/store.go
index f21e436841..27f93728ff 100644
--- a/pkg/store/sub/store.go
+++ b/pkg/store/sub/store.go
@@ -99,11 +99,11 @@ func (s *Store) idFile(ctx context.Context, name string) string {
 }
 
 // Equals returns true if this.storage has the same on-disk path as the other
-func (s *Store) Equals(other *Store) bool {
+func (s *Store) Equals(other store.Store) bool {
 	if other == nil {
 		return false
 	}
-	return s.url.String() == other.url.String()
+	return s.URL() == other.URL()
 }
 
 // IsDir returns true if the entry is folder inside the store
@@ -235,7 +235,17 @@ func (s *Store) Alias() string {
 	return s.alias
 }
 
+// URL returns the store URL
+func (s *Store) URL() string {
+	return s.url.String()
+}
+
 // Storage returns the storage backend used by this.storage
 func (s *Store) Storage() backend.Storage {
 	return s.storage
 }
+
+// Valid returns true if this store is not nil
+func (s *Store) Valid() bool {
+	return s != nil
+}