diff --git a/internal/codespaces/ssh.go b/internal/codespaces/ssh.go index b54dff0dc..1807c87fa 100644 --- a/internal/codespaces/ssh.go +++ b/internal/codespaces/ssh.go @@ -27,16 +27,23 @@ func Shell(ctx context.Context, log logger, sshArgs []string, port int, destinat // Copy runs an scp command over the specified port. The arguments may // include flags and non-flags, optionally separated by "--". -// Remote files are indicated by a "user@host:" prefix. -func Copy(ctx context.Context, scpArgs []string, port int) error { +// Remote files are indicated by a "remote:" prefix, and are resolved +// relative to the remote user's home directory. +func Copy(ctx context.Context, scpArgs []string, port int, destination string) error { // Beware: invalid syntax causes scp to exit 1 with // no error message, so don't let that happen. - scpArgs = append([]string{ + cmd := exec.CommandContext(ctx, "scp", "-P", strconv.Itoa(port), "-o", "NoHostAuthenticationForLocalhost=yes", "-C", // compression - }, scpArgs...) - cmd := exec.CommandContext(ctx, "scp", scpArgs...) + ) + for _, arg := range scpArgs { + // Replace "remote:" prefix with (e.g.) "root@localhost:". + if rest := strings.TrimPrefix(arg, "remote:"); rest != arg { + arg = destination + ":" + rest + } + cmd.Args = append(cmd.Args, arg) + } cmd.Stdin = nil cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 91b8a2e7e..d0d3e49fe 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -124,7 +124,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e go func() { var err error if opts.scpArgs != nil { - err = codespaces.Copy(ctx, opts.scpArgs, localSSHServerPort) + err = codespaces.Copy(ctx, opts.scpArgs, localSSHServerPort, connectDestination) } else { err = codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort) } @@ -157,7 +157,8 @@ func newCpCmd(app *App) *cobra.Command { The cp command copies files between the local and remote file systems. A 'remote:' prefix on any file name argument indicates that it refers to -the file system of the remote (Codespace) machine. +the file system of the remote (Codespace) machine. It is resolved relative +to the home directory of the remote user. As with the UNIX cp command, the first argument specifies the source and the last specifies the destination; additional sources may be specified after the first, @@ -187,11 +188,7 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err erro } opts.scpArgs = append(opts.scpArgs, "--") for _, arg := range args { - if rest := strings.TrimPrefix(arg, "remote:"); rest != arg { - // TODO(adonovan): don't assume user=root: - // use value from session.StartSSHServer. - arg = "root@localhost:" + rest - } else if !filepath.IsAbs(arg) { + if !filepath.IsAbs(arg) && !strings.HasPrefix(arg, "remote:") { // scp treats a colon in the first path segment as a host identifier. // Escape it by prepending "./". // TODO(adonovan): test on Windows, including with a c:\\foo path.