Merge pull request #4553 from cli/cs-cp
gh cs cp: copy files between local/remote file systems
This commit is contained in:
commit
b96ccb4d99
3 changed files with 99 additions and 1 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue