11package codespace
22
3+ // This file defines the 'gh cs ssh' and 'gh cs cp' subcommands.
4+
35import (
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
2429func 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