Skip to content

Commit

Permalink
Add koyeb instance cp cmd (2)
Browse files Browse the repository at this point in the history
Add feature which enable copying from koyeb instances
(smilar functionality as kubectl cp)
  • Loading branch information
pawelbeza committed Dec 1, 2023
1 parent 1db7561 commit 34fe4df
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 17 deletions.
76 changes: 61 additions & 15 deletions pkg/koyeb/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,23 @@ func NewCopyManager(src, dst *FileSpec) (*CopyManager, error) {
}

func (manager *CopyManager) Copy(ctx *CLIContext) error {
if len(manager.Src.InstanceID) == 0 {
return manager.copyToInstance(ctx)
}
return manager.copyFromInstance()
}
isCopyToInstance := len(manager.Src.InstanceID) == 0

func (manager *CopyManager) copyToInstance(ctx *CLIContext) error {
if err := manager.Src.Validate(ctx); err != nil {
if err := manager.Src.Validate(ctx, isCopyToInstance); err != nil {
return err
}

if err := manager.Dst.Validate(ctx); err != nil {
if err := manager.Dst.Validate(ctx, isCopyToInstance); err != nil {
return err
}

if isCopyToInstance {
return manager.copyToInstance(ctx)
}
return manager.copyFromInstance(ctx)
}

func (manager *CopyManager) copyToInstance(ctx *CLIContext) error {
reader, writer := io.Pipe()

go func(srcPath string, writer io.WriteCloser) {
Expand Down Expand Up @@ -79,8 +81,43 @@ func (manager *CopyManager) copyToInstance(ctx *CLIContext) error {
return nil
}

func (manager *CopyManager) copyFromInstance() error {
return errors.New("not implemented")
func (manager *CopyManager) copyFromInstance(ctx *CLIContext) error {
reader, writer := io.Pipe()

go func(dstPath string, reader io.ReadCloser) {
defer reader.Close()

err := Untar(dstPath, reader)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to tar file: %s\n", err)
os.Exit(1)
}
}(manager.Dst.FilePath, reader)

pathDir := path.Dir(manager.Src.FilePath)
pathBase := path.Base(manager.Src.FilePath)
retCode, err := ctx.ExecClient.ExecWithStreams(
ctx.Context,
&StdStreams{
Stdin: bytes.NewReader([]byte{}),
Stdout: writer,
Stderr: os.Stderr,
},
ExecId{
Id: manager.Src.InstanceID,
Type: koyeb.EXECCOMMANDREQUESTIDTYPE_INSTANCE_ID,
},
[]string{"tar", "-C", pathDir, "-czf", "-", pathBase},
)
if err != nil {
return fmt.Errorf("failed to copy path %s: %w", manager.Src.FilePath, err)
}

if retCode != 0 {
os.Exit(retCode)
}

return nil
}

func ValidateTargets(srcSpec, dstSpec *FileSpec) error {
Expand Down Expand Up @@ -115,7 +152,14 @@ func (spec *FileSpec) validateLocalPath() error {
return nil
}

func (spec *FileSpec) validateRemotePath(ctx *CLIContext) error {
func (spec *FileSpec) validateRemotePath(ctx *CLIContext, isDir bool) error {
// if isDir is true, we check if the path is a directory
// otherwise we allow path to be a file or a directory
testFlag := "-d"
if !isDir {
testFlag = "-e"
}

retCode, err := ctx.ExecClient.ExecWithStreams(
ctx.Context,
&StdStreams{
Expand All @@ -127,22 +171,24 @@ func (spec *FileSpec) validateRemotePath(ctx *CLIContext) error {
Id: spec.InstanceID,
Type: koyeb.EXECCOMMANDREQUESTIDTYPE_INSTANCE_ID,
},
[]string{"test", "-d", spec.FilePath},
[]string{"test", testFlag, spec.FilePath},
)
if err != nil {
return fmt.Errorf("remote filepath %s doesn't exist %v", spec.FilePath, err)
return fmt.Errorf("failed to check if path %s exist %v", spec.FilePath, err)
}

if retCode != 0 {
fmt.Fprintf(os.Stdout, "Remote path %s doesn't exist", spec.FilePath)
os.Exit(retCode)
}

return err
}

func (spec *FileSpec) Validate(ctx *CLIContext) error {
// isDir is used to determine if we should check existence of a remote directory or a path in general
func (spec *FileSpec) Validate(ctx *CLIContext, isDir bool) error {
if len(spec.InstanceID) == 0 {
return spec.validateLocalPath()
}
return spec.validateRemotePath(ctx)
return spec.validateRemotePath(ctx, isDir)
}
69 changes: 67 additions & 2 deletions pkg/koyeb/tar.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// modified version of https://gist.github.com/sdomino/e6bc0c98f87843bc26bb#file-targz-go

package koyeb

import (
Expand All @@ -12,6 +10,8 @@ import (
"strings"
)

// Slightly modified version of https://gist.github.com/sdomino/e6bc0c98f87843bc26bb#file-targz-go

// Tar takes a source and variable writers and walks 'source' writing each file
// found to the tar writer; the purpose for accepting multiple writers is to allow
// for multiple outputs (for example a file, or md5 hash)
Expand Down Expand Up @@ -78,3 +78,68 @@ func Tar(src string, writers ...io.Writer) error {
return nil
})
}

// https://gist.github.com/sdomino/635a5ed4f32c93aad131#file-untargz-go
func Untar(dst string, r io.Reader) error {
gzr, err := gzip.NewReader(r)
if err != nil {
return err
}
defer gzr.Close()

tr := tar.NewReader(gzr)

for {
header, err := tr.Next()

switch {

// if no more files are found return
case err == io.EOF:
return nil

// return any other error
case err != nil:
return err

// if the header is nil, just skip it (not sure how this happens)
case header == nil:
continue
}

// the target location where the dir/file should be created
target := filepath.Join(dst, header.Name)

// the following switch could also be done using fi.Mode(), not sure if there
// a benefit of using one vs. the other.
// fi := header.FileInfo()

// check the file type
switch header.Typeflag {

// if its a dir and it doesn't exist create it
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
}

// if it's a file create it
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return err
}

// copy over contents
if _, err := io.Copy(f, tr); err != nil {
return err
}

// manually close here after each file operation; defering would cause each file close
// to wait until all operations have completed.
f.Close()
}
}
}

0 comments on commit 34fe4df

Please sign in to comment.