Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cmd/fs/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"

"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/cmdio"
Expand Down Expand Up @@ -126,6 +128,16 @@ func (c *copy) emitFileCopiedEvent(sourcePath, targetPath string) error {
return cmdio.RenderWithTemplate(c.ctx, event, "", template)
}

// hasTrailingDirSeparator checks if a path ends with a directory separator.
func hasTrailingDirSeparator(path string) bool {
return strings.HasSuffix(path, string(os.PathSeparator))
}

// trimTrailingDirSeparators removes all trailing directory separators from a path.
func trimTrailingDirSeparators(path string) string {
return strings.TrimRight(path, string(os.PathSeparator))
}

func newCpCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cp SOURCE_PATH TARGET_PATH",
Expand Down Expand Up @@ -190,6 +202,11 @@ func newCpCommand() *cobra.Command {
return c.cpDirToDir(sourcePath, targetPath)
}

// If target path has a trailing separator, trim it and let case 2 handle it
if hasTrailingDirSeparator(fullTargetPath) {
targetPath = trimTrailingDirSeparators(targetPath)
}

// case 2: source path is a file, and target path is a directory. In this case
// we copy the file to inside the directory
if targetInfo, err := targetFiler.Stat(ctx, targetPath); err == nil && targetInfo.IsDir() {
Expand Down
65 changes: 65 additions & 0 deletions integration/cmd/fs/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,68 @@ func TestFsCpSourceIsDirectoryButTargetIsFile(t *testing.T) {
})
}
}

func TestFsCpFileToNonExistentDir(t *testing.T) {
t.Parallel()

for _, testCase := range copyTests() {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

ctx := context.Background()
sourceFiler, sourceDir := testCase.setupSource(t)
targetFiler, targetDir := testCase.setupTarget(t)
setupSourceFile(t, ctx, sourceFiler)

// Create a directory in the target
err := targetFiler.Mkdir(ctx, "existingdir")
require.NoError(t, err)

// Copy file to existing directory with trailing slash - should succeed
existingDir := path.Join(targetDir, "existingdir") + "/"
testcli.RequireSuccessfulRun(t, ctx, "fs", "cp", path.Join(sourceDir, "foo.txt"), existingDir)
assertTargetFile(t, ctx, targetFiler, "existingdir/foo.txt")

// Try to copy file to a non-existent directory with trailing slash - should fail
nonExistentDir := path.Join(targetDir, "nonexistent", "mydir") + "/"
_, _, err = testcli.RequireErrorRun(t, ctx, "fs", "cp", path.Join(sourceDir, "foo.txt"), nonExistentDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "directory")
assert.Contains(t, err.Error(), "does not exist")
})
}
}

func TestFsCpFileToNonExistentDirWindowsPaths(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Skipping test on non-windows OS")
}

ctx := context.Background()
sourceFiler, sourceDir := setupLocalFiler(t)
targetFiler, targetDir := setupLocalFiler(t)
setupSourceFile(t, ctx, sourceFiler)

// Create a directory in the target
err := targetFiler.Mkdir(ctx, "existingdir")
require.NoError(t, err)

// Copy file to existing directory with trailing backslash (Windows style) - should succeed
windowsExistingDir := filepath.Join(filepath.FromSlash(targetDir), "existingdir") + "\\"
testcli.RequireSuccessfulRun(t, ctx, "fs", "cp", filepath.Join(filepath.FromSlash(sourceDir), "foo.txt"), windowsExistingDir)
assertTargetFile(t, ctx, targetFiler, "existingdir/foo.txt")

// Try to copy file to a non-existent directory with trailing backslash - should fail
windowsNonExistentDir := filepath.Join(filepath.FromSlash(targetDir), "nonexistent", "mydir") + "\\"
_, _, err = testcli.RequireErrorRun(t, ctx, "fs", "cp", filepath.Join(filepath.FromSlash(sourceDir), "foo.txt"), windowsNonExistentDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "directory")
assert.Contains(t, err.Error(), "does not exist")

// Also test with forward slash on Windows (should also work)
forwardSlashDir := path.Join(targetDir, "nonexistent2", "mydir2") + "/"
_, _, err = testcli.RequireErrorRun(t, ctx, "fs", "cp", path.Join(sourceDir, "foo.txt"), forwardSlashDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "directory")
assert.Contains(t, err.Error(), "does not exist")
}