1package action
2
3import (
4	"context"
5	"errors"
6	"fmt"
7
8	"github.com/gopasspw/gopass/internal/tree"
9
10	"github.com/gopasspw/gopass/internal/diff"
11	"github.com/gopasspw/gopass/internal/notify"
12	"github.com/gopasspw/gopass/internal/out"
13	"github.com/gopasspw/gopass/internal/store"
14	"github.com/gopasspw/gopass/pkg/ctxutil"
15	"github.com/gopasspw/gopass/pkg/debug"
16
17	"github.com/fatih/color"
18	"github.com/urfave/cli/v2"
19)
20
21// Sync all stores with their remotes
22func (s *Action) Sync(c *cli.Context) error {
23	return s.sync(ctxutil.WithGlobalFlags(c), c.String("store"))
24}
25
26func (s *Action) sync(ctx context.Context, store string) error {
27	out.Printf(ctx, "�� Syncing with all remotes ...")
28
29	numEntries := 0
30	if l, err := s.Store.Tree(ctx); err == nil {
31		numEntries = len(l.List(tree.INF))
32	}
33	numMPs := 0
34
35	mps := s.Store.MountPoints()
36	mps = append([]string{""}, mps...)
37
38	// sync all stores (root and all mounted sub stores)
39	for _, mp := range mps {
40		if store != "" {
41			if store != "root" && mp != store {
42				continue
43			}
44			if store == "root" && mp != "" {
45				continue
46			}
47		}
48
49		numMPs++
50		_ = s.syncMount(ctx, mp)
51	}
52	out.OKf(ctx, "All done")
53
54	// Calculate number of changed entries.
55	// This is a rough estimate as additions and deletions
56	// might cancel each other out.
57	if l, err := s.Store.Tree(ctx); err == nil {
58		numEntries = len(l.List(tree.INF)) - numEntries
59	}
60	diff := ""
61	if numEntries > 0 {
62		diff = fmt.Sprintf(" Added %d entries", numEntries)
63	} else if numEntries < 0 {
64		diff = fmt.Sprintf(" Removed %d entries", -1*numEntries)
65	}
66	_ = notify.Notify(ctx, "gopass - sync", fmt.Sprintf("Finished. Synced %d remotes.%s", numMPs, diff))
67
68	return nil
69}
70
71// syncMount syncs a single mount
72func (s *Action) syncMount(ctx context.Context, mp string) error {
73	ctxno := out.WithNewline(ctx, false)
74	name := mp
75	if mp == "" {
76		name = "<root>"
77	}
78	out.Printf(ctxno, color.GreenString("[%s] ", name))
79
80	sub, err := s.Store.GetSubStore(mp)
81	if err != nil {
82		out.Errorf(ctx, "Failed to get sub store %q: %s", name, err)
83		return fmt.Errorf("failed to get sub stores (%s)", err)
84	}
85
86	if sub == nil {
87		out.Errorf(ctx, "Failed to get sub stores '%s: nil'", name)
88		return fmt.Errorf("failed to get sub stores (nil)")
89	}
90
91	l, err := sub.List(ctx, "")
92	if err != nil {
93		out.Errorf(ctx, "Failed to list store: %s", err)
94	}
95
96	// TODO: Remove this hard coded check
97	if sub.Storage().Name() == "fs" {
98		out.Printf(ctxno, "\n   WARNING: Mount uses Storage backend 'fs'. Not syncing!\n")
99	} else {
100		out.Printf(ctxno, "\n   "+color.GreenString("git pull and push ... "))
101		if err := sub.Storage().Push(ctx, "", ""); err != nil {
102			if errors.Is(err, store.ErrGitNoRemote) {
103				out.Printf(ctx, "Skipped (no remote)")
104				debug.Log("Failed to push %q to its remote: %s", name, err)
105				return err
106			}
107
108			out.Errorf(ctx, "Failed to push %q to its remote: %s", name, err)
109			return err
110		}
111		out.Printf(ctxno, color.GreenString("OK"))
112
113		ln, err := sub.List(ctx, "")
114		if err != nil {
115			out.Errorf(ctx, "Failed to list store: %s", err)
116		}
117
118		added, removed := diff.List(l, ln)
119		debug.Log("diff - added: %d - removed: %d", added, removed)
120		if added > 0 {
121			out.Printf(ctxno, color.GreenString(" (Added %d entries)", added))
122		}
123		if removed > 0 {
124			out.Printf(ctxno, color.GreenString(" (Removed %d entries)", removed))
125		}
126		if added < 1 && removed < 1 {
127			out.Printf(ctxno, color.GreenString(" (no changes)"))
128		}
129	}
130
131	debug.Log("Syncing Mount %s. Exportkeys: %t", mp, ctxutil.IsExportKeys(ctx))
132	var exported bool
133	if ctxutil.IsExportKeys(ctx) {
134		// import keys
135		out.Printf(ctxno, "\n   "+color.GreenString("importing missing keys ... "))
136		if err := sub.ImportMissingPublicKeys(ctx); err != nil {
137			out.Errorf(ctx, "Failed to import missing public keys for %q: %s", name, err)
138			return err
139		}
140		out.Printf(ctxno, color.GreenString("OK"))
141
142		// export keys
143		out.Printf(ctxno, "\n   "+color.GreenString("exporting missing keys ... "))
144		rs, err := sub.GetRecipients(ctx, "")
145		if err != nil {
146			out.Errorf(ctx, "Failed to load recipients for %q: %s", name, err)
147			return err
148		}
149		exported, err = sub.ExportMissingPublicKeys(ctx, rs)
150		if err != nil {
151			out.Errorf(ctx, "Failed to export missing public keys for %q: %s", name, err)
152			return err
153		}
154	}
155
156	// only run second push if we did export any keys
157	if exported {
158		if err := sub.Storage().Push(ctx, "", ""); err != nil {
159			out.Errorf(ctx, "Failed to push %q to its remote: %s", name, err)
160			return err
161		}
162		out.Printf(ctxno, color.GreenString("OK"))
163	} else {
164		out.Printf(ctxno, color.GreenString("nothing to do"))
165	}
166	out.Printf(ctx, "\n   "+color.GreenString("done"))
167	return nil
168}
169