From a5be68580100f9f5a16f29180faa2e5fcce2b067 Mon Sep 17 00:00:00 2001 From: Vijay Kesanakurthi <104449500+vijay-kesanakurthi@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:39:14 +0530 Subject: [PATCH] [bugfix] Copy with trailing slash at destination. (#2966) * [bugfix] Copy with trailing slash at destination. Signed-off-by: Vijay Kesanakurthi * Copy with trailing slash at destination fix and test. Signed-off-by: Vijay Kesanakurthi --------- Signed-off-by: Vijay Kesanakurthi --- internal/action/copy.go | 40 ++++++++++++++++ internal/action/copy_test.go | 92 ++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/internal/action/copy.go b/internal/action/copy.go index 6af584715e..835b0b8293 100644 --- a/internal/action/copy.go +++ b/internal/action/copy.go @@ -3,6 +3,9 @@ package action import ( "context" "fmt" + "github.com/gopasspw/gopass/internal/tree" + "path/filepath" + "strings" "github.com/gopasspw/gopass/internal/action/exit" "github.com/gopasspw/gopass/pkg/ctxutil" @@ -30,6 +33,43 @@ func (s *Action) copy(ctx context.Context, from, to string, force bool) error { return exit.Error(exit.NotFound, nil, "%s does not exist", from) } + isSourceDir := s.Store.IsDir(ctx, from) + hasTrailingSlash := strings.HasSuffix(to, "/") + + if isSourceDir && hasTrailingSlash { + return s.copyFlattenDir(ctx, from, to, force) + } + + return s.copyRegular(ctx, from, to, force) +} + +func (s *Action) copyFlattenDir(ctx context.Context, from, to string, force bool) error { + entries, err := s.Store.List(ctx, tree.INF) + + if err != nil { + return exit.Error(exit.List, err, "failed to list entries in %q", from) + } + + fromPrefix := from + if !strings.HasSuffix(fromPrefix, "/") { + fromPrefix += "/" + } + + for _, entry := range entries { + if strings.HasPrefix(entry, fromPrefix) { + toPath := filepath.Join(to, filepath.Base(entry)) + + if err := s.copyRegular(ctx, entry, toPath, force); err != nil { + return err + } + } + } + + return nil +} + +func (s *Action) copyRegular(ctx context.Context, from, to string, force bool) error { + if !force { if s.Store.Exists(ctx, to) && !termio.AskForConfirmation(ctx, fmt.Sprintf("%s already exists. Overwrite it?", to)) { return exit.Error(exit.Aborted, nil, "not overwriting your current secret") diff --git a/internal/action/copy_test.go b/internal/action/copy_test.go index 2df73d26ce..d3858c6d18 100644 --- a/internal/action/copy_test.go +++ b/internal/action/copy_test.go @@ -180,3 +180,95 @@ func TestCopyGpg(t *testing.T) { assert.Equal(t, "barfoo", buf.String()) buf.Reset() } + +func TestCopyWithTrailingSlash(t *testing.T) { + u := gptest.NewUnitTester(t) + + ctx := config.NewContextInMemory() + ctx = ctxutil.WithInteractive(ctx, false) + ctx = ctxutil.WithAlwaysYes(ctx, true) + + act, err := newMock(ctx, u.StoreDir("")) + require.NoError(t, err) + require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) + + require.NoError(t, act.cfg.Set("", "generate.autoclip", "false")) + + buf := &bytes.Buffer{} + out.Stdout = buf + stdout = buf + defer func() { + stdout = os.Stdout + out.Stdout = os.Stdout + }() + + color.NoColor = true + + // generate foo + c := gptest.CliCtx(ctx, t, "foo") + require.NoError(t, act.Generate(c)) + buf.Reset() + + // copy foo bar + c = gptest.CliCtx(ctx, t, "foo", "bar") + require.NoError(t, act.Copy(c)) + buf.Reset() + + // copy foo bar (again, should fail) + { + ctx := ctxutil.WithAlwaysYes(ctx, false) + ctx = ctxutil.WithInteractive(ctx, false) + c.Context = ctx + require.Error(t, act.Copy(c)) + buf.Reset() + } + + // copy not-found still-not-there + c = gptest.CliCtx(ctx, t, "not-found", "still-not-there") + require.Error(t, act.Copy(c)) + buf.Reset() + + // copy + c = gptest.CliCtx(ctx, t) + require.Error(t, act.Copy(c)) + buf.Reset() + + // Create a directory structure + require.NoError(t, act.insertStdin(ctx, "secret/some/zab", []byte("secret"), false)) + require.NoError(t, act.insertStdin(ctx, "secret/baz", []byte("another"), false)) + + // Test copying a directory with trailing slash + c = gptest.CliCtx(ctx, t, "secret/", "new/") + require.NoError(t, act.Copy(c)) + buf.Reset() + + // Verify the result + require.NoError(t, act.List(gptest.CliCtx(ctx, t))) + want := `gopass +├── bar +├── foo +├── new/ +│ ├── baz +│ └── zab +└── secret/ + ├── baz + └── some/ + └── zab + +` + + assert.Equal(t, want, buf.String()) + buf.Reset() + + // Verify content of copied files + ctx = ctxutil.WithTerminal(ctx, false) + require.NoError(t, act.show(ctx, c, "new/zab", false)) + assert.Equal(t, "secret\n", buf.String()) + buf.Reset() + + require.NoError(t, act.show(ctx, c, "new/baz", false)) + assert.Equal(t, "another\n", buf.String()) + buf.Reset() + +}