From a9334dcc3d1e0577d5715c23637d8fe7b1e8def7 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Wed, 21 Oct 2020 23:30:21 -0500 Subject: [PATCH] Handle single file sync --- internal/cmd/sync.go | 22 ++++++++------ internal/sync/singlefile.go | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 internal/sync/singlefile.go diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go index 05d9cd00..4ff3f4ec 100644 --- a/internal/cmd/sync.go +++ b/internal/cmd/sync.go @@ -47,6 +47,7 @@ func rsyncVersion() string { func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var ( + ctx = cmd.Context() local = args[0] remote = args[1] ) @@ -56,17 +57,9 @@ func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { return err } - info, err := os.Stat(local) - if err != nil { - return err - } - if !info.IsDir() { - return xerrors.Errorf("%s must be a directory", local) - } - remoteTokens := strings.SplitN(remote, ":", 2) if len(remoteTokens) != 2 { - return xerrors.New("remote misformatted") + return xerrors.New("remote malformatted") } var ( envName = remoteTokens[0] @@ -78,6 +71,17 @@ func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { return err } + info, err := os.Stat(local) + if err != nil { + return err + } + if info.Mode().IsRegular() { + return sync.SingleFile(ctx, local, remoteDir, env, client) + } + if !info.IsDir() { + return xerrors.Errorf("local path must lead to a regular file or directory: %w", err) + } + absLocal, err := filepath.Abs(local) if err != nil { return xerrors.Errorf("make abs path out of %s, %s: %w", local, absLocal, err) diff --git a/internal/sync/singlefile.go b/internal/sync/singlefile.go new file mode 100644 index 00000000..d36a7372 --- /dev/null +++ b/internal/sync/singlefile.go @@ -0,0 +1,58 @@ +package sync + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/wsep" + "golang.org/x/xerrors" + "nhooyr.io/websocket" +) + +// SingleFile copies the given file into the remote dir or remote path of the given coder.Environment. +func SingleFile(ctx context.Context, local, remoteDir string, env *coder.Environment, client *coder.Client) error { + conn, err := client.DialWsep(ctx, env) + if err != nil { + return xerrors.Errorf("dial remote execer: %w", err) + } + defer func() { _ = conn.Close(websocket.StatusNormalClosure, "normal closure") }() + + if strings.HasSuffix(remoteDir, string(filepath.Separator)) { + remoteDir += filepath.Base(local) + } + + execer := wsep.RemoteExecer(conn) + cmd := fmt.Sprintf(`[ -d %s ] && cat > %s/%s || cat > %s`, remoteDir, remoteDir, filepath.Base(local), remoteDir) + process, err := execer.Start(ctx, wsep.Command{ + Command: "sh", + Args: []string{"-c", cmd}, + Stdin: true, + }) + if err != nil { + return xerrors.Errorf("start sync command: %w", err) + } + + sourceFile, err := os.Open(local) + if err != nil { + return xerrors.Errorf("open source file: %w", err) + } + + go func() { _, _ = io.Copy(ioutil.Discard, process.Stdout()) }() + go func() { _, _ = io.Copy(ioutil.Discard, process.Stderr()) }() + go func() { + stdin := process.Stdin() + defer stdin.Close() + _, _ = io.Copy(stdin, sourceFile) + }() + + if err := process.Wait(); err != nil { + return xerrors.Errorf("copy process: %w", err) + } + return nil +}