-
-
Notifications
You must be signed in to change notification settings - Fork 501
/
Copy pathmove.go
307 lines (241 loc) · 8.65 KB
/
move.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package root
import (
"context"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/internal/store"
"github.com/gopasspw/gopass/internal/store/leaf"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/fsutil"
)
// Copy will copy one entry to another location. Multi-store copies are
// supported. Each entry has to be decoded and encoded for the destination
// to make sure it's encrypted for the right set of recipients.
func (r *Store) Copy(ctx context.Context, from, to string) error {
debug.Log("Copy %s to %s", from, to)
return r.move(ctx, from, to, false)
}
// Move will move one entry from one location to another. Cross-store moves are
// supported. Moving an entry will decode it from the old location, encode it
// for the destination store with the right set of recipients and remove it
// from the old location afterwards.
func (r *Store) Move(ctx context.Context, from, to string) error {
debug.Log("Move %s to %s", from, to)
return r.move(ctx, from, to, true)
}
// move handles both copy and move operations. Since the only difference is
// deleting the source entry after the copy, we can reuse the same code.
func (r *Store) move(ctx context.Context, from, to string, del bool) error {
subFrom, fromPrefix := r.getStore(from)
subTo, _ := r.getStore(to)
srcIsDir := r.IsDir(ctx, from)
dstIsDir := r.IsDir(ctx, to)
if srcIsDir && r.Exists(ctx, to) && !dstIsDir {
return fmt.Errorf("destination is a file")
}
if err := r.moveFromTo(ctx, subFrom, from, to, fromPrefix, srcIsDir, dstIsDir, del); err != nil {
return err
}
if err := subFrom.Storage().Commit(ctx, fmt.Sprintf("Move from %s to %s", from, to)); del && err != nil {
switch {
case errors.Is(err, store.ErrGitNotInit):
debug.Log("skipping git commit - git not initialized in %s", subFrom.Alias())
case errors.Is(err, store.ErrGitNothingToCommit):
debug.Log("skipping git commit - nothing to commit in %s", subFrom.Alias())
default:
return fmt.Errorf("failed to commit changes to git (%s): %w", subFrom.Alias(), err)
}
}
if !subFrom.Equals(subTo) {
if err := subTo.Storage().Commit(ctx, fmt.Sprintf("Move from %s to %s", from, to)); err != nil {
switch {
case errors.Is(err, store.ErrGitNotInit):
debug.Log("skipping git commit - git not initialized in %s", subTo.Alias())
case errors.Is(err, store.ErrGitNothingToCommit):
debug.Log("skipping git commit - nothing to commit in %s", subTo.Alias())
default:
return fmt.Errorf("failed to commit changes to git (%s): %w", subTo.Alias(), err)
}
}
}
if err := subFrom.Storage().Push(ctx, "", ""); err != nil {
if errors.Is(err, store.ErrGitNotInit) {
msg := "Warning: git is not initialized for this storage. Ignoring auto-push option\n" +
"Run: gopass git init"
debug.Log(msg)
return nil
}
if errors.Is(err, store.ErrGitNoRemote) {
msg := "Warning: git has no remote. Ignoring auto-push option\n" +
"Run: gopass git remote add origin ..."
debug.Log(msg)
return nil
}
return fmt.Errorf("failed to push change to git remote: %w", err)
}
if subFrom.Equals(subTo) {
return nil
}
if err := subTo.Storage().Push(ctx, "", ""); err != nil {
if errors.Is(err, store.ErrGitNotInit) {
msg := "Warning: git is not initialized for this storage. Ignoring auto-push option\n" +
"Run: gopass git init"
debug.Log(msg)
return nil
}
if errors.Is(err, store.ErrGitNoRemote) {
msg := "Warning: git has no remote. Ignoring auto-push option\n" +
"Run: gopass git remote add origin ..."
debug.Log(msg)
return nil
}
return fmt.Errorf("failed to push change to git remote: %w", err)
}
return nil
}
func (r *Store) moveFromTo(ctx context.Context, subFrom *leaf.Store, from, to, fromPrefix string, srcIsDir, dstIsDir, del bool) error {
ctx = ctxutil.WithGitCommit(ctx, false)
entries := []string{from}
// if the source is a directory we enumerate all it's children
// and move them one by one.
if r.IsDir(ctx, from) {
var err error
entries, err = subFrom.List(ctx, fromPrefix+"/")
if err != nil {
return err
}
}
if len(entries) < 1 {
debug.Log("Subtree %q has no entries", from)
return fmt.Errorf("no entries")
}
debug.Log("Moving (sub) tree %q to %q (entries: %+v)", from, to, entries)
var moved uint
for _, src := range entries {
dst := computeMoveDestination(src, from, to, srcIsDir, dstIsDir)
if src == dst {
debug.Log("skipping %q. src eq dst", src)
continue
}
debug.Log("Moving entry %q (%q) => %q (%q) (srcIsDir:%t, dstIsDir:%t, delete:%t)\n", src, from, dst, to, srcIsDir, dstIsDir, del)
err := r.directMove(ctx, src, dst, del)
if err == nil {
moved++
debug.Log("directly moved from %q to %q", src, dst)
continue
}
debug.Log("direct move failed to move entry %q to %q: %s. Falling back to get and set", src, dst, err)
content, err := r.Get(ctx, src)
if err != nil {
return fmt.Errorf("source %s does not exist in source store %s: %w", from, subFrom.Alias(), err)
}
if err := r.Set(ctxutil.WithCommitMessage(ctx, fmt.Sprintf("Move from %s to %s", src, dst)), dst, content); err != nil {
if !errors.Is(err, store.ErrMeaninglessWrite) {
return fmt.Errorf("failed to save secret %q to store: %w", to, err)
}
out.Warningf(ctx, "No need to write: the secret is already there and with the right value")
}
if del {
debug.Log("Deleting moved entry %q from source %q", from, src)
if err := r.Delete(ctx, src); err != nil {
return fmt.Errorf("failed to delete secret %q: %w", src, err)
}
}
moved++
}
if moved < 1 {
return fmt.Errorf("no entries moved")
}
debug.Log("Moved (sub) tree %q to %q", from, to)
return nil
}
func (r *Store) directMove(ctx context.Context, from, to string, del bool) error {
debug.Log("directMove from %q to %q", from, to)
// will also remove the store prefix, if applicable
subFrom, from := r.getStore(from)
subTo, to := r.getStore(to)
if subFrom.Equals(subTo) {
debug.Log("directMove from %q to %q: same store", from, to)
if del {
return subFrom.Move(ctx, from, to)
}
return subFrom.Copy(ctx, from, to)
}
debug.Log("cross mount direct move from %s%s to %s%s", subFrom.Alias(), from, subTo.Alias(), to)
// assemble source and destination paths, call fsutil.CopyFile(from, to), remove source
// if del is true and then git add and commit both stores.
sfn := filepath.Join(subFrom.Path(), subFrom.Passfile(from))
dfn := filepath.Join(subTo.Path(), subTo.Passfile(to))
if err := fsutil.CopyFile(sfn, dfn); err != nil {
return fmt.Errorf("failed to copy %q to %q: %w", from, to, err)
}
if del {
if err := os.Remove(sfn); err != nil {
return fmt.Errorf("failed to delete %q from %s: %w", sfn, subFrom.Alias(), err)
}
}
if err := subFrom.Storage().Add(ctx, sfn); err != nil {
debug.Log("failed to add %q to %s: %w", sfn, subFrom.Alias(), err)
}
if err := subTo.Storage().Add(ctx, dfn); err != nil {
debug.Log("failed to add %q to %s: %w", dfn, subTo.Alias(), err)
}
return nil
}
func computeMoveDestination(src, from, to string, srcIsDir, dstIsDir bool) string {
// special case: moving up to the root
if to == "." || to == "/" {
dstIsDir = false
to = ""
}
// are we moving into an existing directory? Then we just need to prepend
// it's name to the source.
// a -> b
// - a/f1 -> b/a/f1
// a -> b
// - a -> b/a
if dstIsDir {
if !srcIsDir {
return path.Join(to, path.Base(src))
}
return path.Join(to, src)
}
// are we moving a simple file? that's easy
if !srcIsDir {
// otherwise we just rename a file to another name
return to
}
// move a/ b, where a is a directory with a trailing slash and b
// does not exist, i.e. move a to b
if strings.HasSuffix(from, "/") {
return path.Join(to, strings.TrimPrefix(src, from))
}
// move a b, where a is a directory but not b, i.e. rename a to b.
// this is applied to every child of a, so we need to remove the
// old prefix (a) and add the new one (b).
return path.Join(to, strings.TrimPrefix(src, from))
}
// Delete will remove an single entry from the store.
func (r *Store) Delete(ctx context.Context, name string) error {
store, sn := r.getStore(name)
if sn == "" {
return fmt.Errorf("can not delete a mount point. Use `gopass mounts remove %s`", store.Alias())
}
return store.Delete(ctx, sn)
}
// Prune will remove a subtree from the Store.
func (r *Store) Prune(ctx context.Context, tree string) error {
for mp := range r.mounts {
if strings.HasPrefix(mp, tree) {
return fmt.Errorf("can not prune subtree with mounts. Unmount first: `gopass mounts remove %s`", mp)
}
}
store, tree := r.getStore(tree)
return store.Prune(ctx, tree)
}