Merge pull request #4553 from cli/cs-cp

gh cs cp: copy files between local/remote file systems
This commit is contained in:
Alan Donovan 2021-10-18 17:32:08 -04:00 committed by GitHub
commit b96ccb4d99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 1 deletions

View file

@ -25,6 +25,31 @@ func Shell(ctx context.Context, log logger, sshArgs []string, port int, destinat
return cmd.Run()
}
// 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 "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.
cmd := exec.CommandContext(ctx, "scp",
"-P", strconv.Itoa(port),
"-o", "NoHostAuthenticationForLocalhost=yes",
"-C", // compression
)
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
return cmd.Run()
}
// NewRemoteCommand returns an exec.Cmd that will securely run a shell
// command on the remote machine.
func NewRemoteCommand(ctx context.Context, tunnelPort int, destination string, sshArgs ...string) (*exec.Cmd, error) {

View file

@ -27,6 +27,7 @@ func NewRootCmd(app *App) *cobra.Command {
root.AddCommand(newLogsCmd(app))
root.AddCommand(newPortsCmd(app))
root.AddCommand(newSSHCmd(app))
root.AddCommand(newCpCmd(app))
root.AddCommand(newStopCmd(app))
return root

View file

@ -1,5 +1,7 @@
package codespace
// This file defines the 'gh cs ssh' and 'gh cs cp' subcommands.
import (
"context"
"fmt"
@ -7,6 +9,8 @@ import (
"log"
"net"
"os"
"path/filepath"
"strings"
"github.com/cli/cli/v2/internal/codespaces"
"github.com/cli/cli/v2/pkg/liveshare"
@ -19,6 +23,7 @@ type sshOptions struct {
serverPort int
debug bool
debugFile string
scpArgs []string // scp arguments, for 'cs cp' (nil for 'cs ssh')
}
func newSSHCmd(app *App) *cobra.Command {
@ -117,7 +122,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
shellClosed := make(chan error, 1)
go func() {
shellClosed <- codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort)
var err error
if opts.scpArgs != nil {
err = codespaces.Copy(ctx, opts.scpArgs, localSSHServerPort, connectDestination)
} else {
err = codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort)
}
shellClosed <- err
}()
select {
@ -131,6 +142,67 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
}
}
type cpOptions struct {
sshOptions
recursive bool // -r
}
func newCpCmd(app *App) *cobra.Command {
var opts cpOptions
cpCmd := &cobra.Command{
Use: "cp [-r] srcs... dest",
Short: "Copy files between local and remote file systems",
Long: `
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. 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,
if the destination is a directory.
The -r (recursive) flag is required if any source is a directory.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return app.Copy(cmd.Context(), args, opts)
},
}
// We don't expose all sshOptions.
cpCmd.Flags().BoolVarP(&opts.recursive, "recursive", "r", false, "Recursively copy directories")
cpCmd.Flags().StringVarP(&opts.codespace, "codespace", "c", "", "Name of the codespace")
return cpCmd
}
// Copy copies files between the local and remote file systems.
// The mechanics are similar to 'ssh' but using 'scp'.
func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err error) {
if len(args) < 2 {
return fmt.Errorf("cp requires source and destination arguments")
}
if opts.recursive {
opts.scpArgs = append(opts.scpArgs, "-r")
}
opts.scpArgs = append(opts.scpArgs, "--")
for _, arg := range args {
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.
const sep = string(os.PathSeparator)
first := strings.Split(filepath.ToSlash(arg), sep)[0]
if strings.Contains(first, ":") {
arg = "." + sep + arg
}
}
opts.scpArgs = append(opts.scpArgs, arg)
}
return a.SSH(ctx, nil, opts.sshOptions)
}
// fileLogger is a wrapper around an log.Logger configured to write
// to a file. It exports two additional methods to get the log file name
// and close the file handle when the operation is finished.