X Tutup
Skip to content

Commit b96ccb4

Browse files
authored
Merge pull request cli#4553 from cli/cs-cp
gh cs cp: copy files between local/remote file systems
2 parents a02d423 + 48ada6d commit b96ccb4

File tree

3 files changed

+99
-1
lines changed

3 files changed

+99
-1
lines changed

internal/codespaces/ssh.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ func Shell(ctx context.Context, log logger, sshArgs []string, port int, destinat
2525
return cmd.Run()
2626
}
2727

28+
// Copy runs an scp command over the specified port. The arguments may
29+
// include flags and non-flags, optionally separated by "--".
30+
// Remote files are indicated by a "remote:" prefix, and are resolved
31+
// relative to the remote user's home directory.
32+
func Copy(ctx context.Context, scpArgs []string, port int, destination string) error {
33+
// Beware: invalid syntax causes scp to exit 1 with
34+
// no error message, so don't let that happen.
35+
cmd := exec.CommandContext(ctx, "scp",
36+
"-P", strconv.Itoa(port),
37+
"-o", "NoHostAuthenticationForLocalhost=yes",
38+
"-C", // compression
39+
)
40+
for _, arg := range scpArgs {
41+
// Replace "remote:" prefix with (e.g.) "root@localhost:".
42+
if rest := strings.TrimPrefix(arg, "remote:"); rest != arg {
43+
arg = destination + ":" + rest
44+
}
45+
cmd.Args = append(cmd.Args, arg)
46+
}
47+
cmd.Stdin = nil
48+
cmd.Stdout = os.Stderr
49+
cmd.Stderr = os.Stderr
50+
return cmd.Run()
51+
}
52+
2853
// NewRemoteCommand returns an exec.Cmd that will securely run a shell
2954
// command on the remote machine.
3055
func NewRemoteCommand(ctx context.Context, tunnelPort int, destination string, sshArgs ...string) (*exec.Cmd, error) {

pkg/cmd/codespace/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func NewRootCmd(app *App) *cobra.Command {
2727
root.AddCommand(newLogsCmd(app))
2828
root.AddCommand(newPortsCmd(app))
2929
root.AddCommand(newSSHCmd(app))
30+
root.AddCommand(newCpCmd(app))
3031
root.AddCommand(newStopCmd(app))
3132

3233
return root

pkg/cmd/codespace/ssh.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package codespace
22

3+
// This file defines the 'gh cs ssh' and 'gh cs cp' subcommands.
4+
35
import (
46
"context"
57
"fmt"
68
"io/ioutil"
79
"log"
810
"net"
911
"os"
12+
"path/filepath"
13+
"strings"
1014

1115
"github.com/cli/cli/v2/internal/codespaces"
1216
"github.com/cli/cli/v2/pkg/liveshare"
@@ -19,6 +23,7 @@ type sshOptions struct {
1923
serverPort int
2024
debug bool
2125
debugFile string
26+
scpArgs []string // scp arguments, for 'cs cp' (nil for 'cs ssh')
2227
}
2328

2429
func newSSHCmd(app *App) *cobra.Command {
@@ -117,7 +122,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
117122

118123
shellClosed := make(chan error, 1)
119124
go func() {
120-
shellClosed <- codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort)
125+
var err error
126+
if opts.scpArgs != nil {
127+
err = codespaces.Copy(ctx, opts.scpArgs, localSSHServerPort, connectDestination)
128+
} else {
129+
err = codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort)
130+
}
131+
shellClosed <- err
121132
}()
122133

123134
select {
@@ -131,6 +142,67 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
131142
}
132143
}
133144

145+
type cpOptions struct {
146+
sshOptions
147+
recursive bool // -r
148+
}
149+
150+
func newCpCmd(app *App) *cobra.Command {
151+
var opts cpOptions
152+
153+
cpCmd := &cobra.Command{
154+
Use: "cp [-r] srcs... dest",
155+
Short: "Copy files between local and remote file systems",
156+
Long: `
157+
The cp command copies files between the local and remote file systems.
158+
159+
A 'remote:' prefix on any file name argument indicates that it refers to
160+
the file system of the remote (Codespace) machine. It is resolved relative
161+
to the home directory of the remote user.
162+
163+
As with the UNIX cp command, the first argument specifies the source and the last
164+
specifies the destination; additional sources may be specified after the first,
165+
if the destination is a directory.
166+
167+
The -r (recursive) flag is required if any source is a directory.
168+
`,
169+
RunE: func(cmd *cobra.Command, args []string) error {
170+
return app.Copy(cmd.Context(), args, opts)
171+
},
172+
}
173+
174+
// We don't expose all sshOptions.
175+
cpCmd.Flags().BoolVarP(&opts.recursive, "recursive", "r", false, "Recursively copy directories")
176+
cpCmd.Flags().StringVarP(&opts.codespace, "codespace", "c", "", "Name of the codespace")
177+
return cpCmd
178+
}
179+
180+
// Copy copies files between the local and remote file systems.
181+
// The mechanics are similar to 'ssh' but using 'scp'.
182+
func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err error) {
183+
if len(args) < 2 {
184+
return fmt.Errorf("cp requires source and destination arguments")
185+
}
186+
if opts.recursive {
187+
opts.scpArgs = append(opts.scpArgs, "-r")
188+
}
189+
opts.scpArgs = append(opts.scpArgs, "--")
190+
for _, arg := range args {
191+
if !filepath.IsAbs(arg) && !strings.HasPrefix(arg, "remote:") {
192+
// scp treats a colon in the first path segment as a host identifier.
193+
// Escape it by prepending "./".
194+
// TODO(adonovan): test on Windows, including with a c:\\foo path.
195+
const sep = string(os.PathSeparator)
196+
first := strings.Split(filepath.ToSlash(arg), sep)[0]
197+
if strings.Contains(first, ":") {
198+
arg = "." + sep + arg
199+
}
200+
}
201+
opts.scpArgs = append(opts.scpArgs, arg)
202+
}
203+
return a.SSH(ctx, nil, opts.sshOptions)
204+
}
205+
134206
// fileLogger is a wrapper around an log.Logger configured to write
135207
// to a file. It exports two additional methods to get the log file name
136208
// and close the file handle when the operation is finished.

0 commit comments

Comments
 (0)
X Tutup