forked from toolshed/abra
		
	
		
			
				
	
	
		
			390 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			390 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package app
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"path"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"coopcloud.tech/abra/cli/internal"
 | 
						|
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
						|
	"coopcloud.tech/abra/pkg/client"
 | 
						|
	containerPkg "coopcloud.tech/abra/pkg/container"
 | 
						|
	"coopcloud.tech/abra/pkg/formatter"
 | 
						|
	"coopcloud.tech/abra/pkg/i18n"
 | 
						|
	"coopcloud.tech/abra/pkg/log"
 | 
						|
	"coopcloud.tech/abra/pkg/upstream/container"
 | 
						|
	"github.com/docker/cli/cli/command"
 | 
						|
	containertypes "github.com/docker/docker/api/types/container"
 | 
						|
	dockerClient "github.com/docker/docker/client"
 | 
						|
	"github.com/docker/docker/errdefs"
 | 
						|
	"github.com/docker/docker/pkg/archive"
 | 
						|
	"github.com/spf13/cobra"
 | 
						|
)
 | 
						|
 | 
						|
// translators: `abra app cp` aliases. use a comma separated list of aliases with
 | 
						|
// no spaces in between
 | 
						|
var appCpAliases = i18n.G("c")
 | 
						|
 | 
						|
var AppCpCommand = &cobra.Command{
 | 
						|
	// translators: `app cp` command
 | 
						|
	Use:     i18n.G("cp <domain> <src> <dst> [flags]"),
 | 
						|
	Aliases: strings.Split(appCpAliases, ","),
 | 
						|
	// translators: Short description for `app cp` command
 | 
						|
	Short: i18n.G("Copy files to/from a deployed app service"),
 | 
						|
	Example: i18n.G(`  # copy myfile.txt to the root of the app service
 | 
						|
  abra app cp 1312.net myfile.txt app:/
 | 
						|
 | 
						|
  # copy that file back to your current working directory locally
 | 
						|
  abra app cp 1312.net app:/myfile.txt ./`),
 | 
						|
	Args: cobra.ExactArgs(3),
 | 
						|
	ValidArgsFunction: func(
 | 
						|
		cmd *cobra.Command,
 | 
						|
		args []string,
 | 
						|
		toComplete string) ([]string, cobra.ShellCompDirective) {
 | 
						|
		switch l := len(args); l {
 | 
						|
		case 0:
 | 
						|
			return autocomplete.AppNameComplete()
 | 
						|
		default:
 | 
						|
			return nil, cobra.ShellCompDirectiveDefault
 | 
						|
		}
 | 
						|
	},
 | 
						|
	Run: func(cmd *cobra.Command, args []string) {
 | 
						|
		app := internal.ValidateApp(args)
 | 
						|
 | 
						|
		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		src := args[1]
 | 
						|
		dst := args[2]
 | 
						|
		srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		cl, err := client.New(app.Server)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
		log.Debug(i18n.G("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server))
 | 
						|
 | 
						|
		if toContainer {
 | 
						|
			err = CopyToContainer(cl, container.ID, srcPath, dstPath)
 | 
						|
		} else {
 | 
						|
			err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
 | 
						|
		}
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
	},
 | 
						|
}
 | 
						|
 | 
						|
var errServiceMissing = errors.New(i18n.G("one of <src>/<dest> arguments must take $SERVICE:$PATH form"))
 | 
						|
 | 
						|
// parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH
 | 
						|
func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service string, toContainer bool, err error) {
 | 
						|
	parsedSrc := strings.SplitN(src, ":", 2)
 | 
						|
	parsedDst := strings.SplitN(dst, ":", 2)
 | 
						|
	if len(parsedSrc)+len(parsedDst) != 3 {
 | 
						|
		return "", "", "", false, errServiceMissing
 | 
						|
	}
 | 
						|
	if len(parsedSrc) == 2 {
 | 
						|
		return parsedSrc[1], dst, parsedSrc[0], false, nil
 | 
						|
	}
 | 
						|
	if len(parsedDst) == 2 {
 | 
						|
		return src, parsedDst[1], parsedDst[0], true, nil
 | 
						|
	}
 | 
						|
	return "", "", "", false, errServiceMissing
 | 
						|
}
 | 
						|
 | 
						|
// CopyToContainer copies a file or directory from the local file system to the container.
 | 
						|
// See the possible copy modes and their documentation.
 | 
						|
func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
 | 
						|
	srcStat, err := os.Stat(srcPath)
 | 
						|
	if err != nil {
 | 
						|
		return errors.New(i18n.G("local %s ", err))
 | 
						|
	}
 | 
						|
 | 
						|
	dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
 | 
						|
	dstExists := true
 | 
						|
	if err != nil {
 | 
						|
		if errdefs.IsNotFound(err) {
 | 
						|
			dstExists = false
 | 
						|
		} else {
 | 
						|
			return errors.New(i18n.G("remote path: %s", err))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	mode, err := copyMode(srcPath, dstPath, srcStat.Mode(), dstStat.Mode, dstExists)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	movePath := ""
 | 
						|
	switch mode {
 | 
						|
	case CopyModeDirToDir:
 | 
						|
		// Add the src directory to the destination path
 | 
						|
		_, srcDir := path.Split(srcPath)
 | 
						|
		dstPath = path.Join(dstPath, srcDir)
 | 
						|
 | 
						|
		// Make sure the dst directory exits.
 | 
						|
		dcli, err := command.NewDockerCli()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
 | 
						|
			AttachStderr: true,
 | 
						|
			AttachStdin:  true,
 | 
						|
			AttachStdout: true,
 | 
						|
			Cmd:          []string{"mkdir", "-p", dstPath},
 | 
						|
			Detach:       false,
 | 
						|
			Tty:          true,
 | 
						|
		}); err != nil {
 | 
						|
			return errors.New(i18n.G("create remote directory: %s", err))
 | 
						|
		}
 | 
						|
	case CopyModeFileToFile:
 | 
						|
		// Remove the file component from the path, since docker can only copy
 | 
						|
		// to a directory.
 | 
						|
		dstPath, _ = path.Split(dstPath)
 | 
						|
	case CopyModeFileToFileRename:
 | 
						|
		// Copy the file to the temp directory and move it to its dstPath
 | 
						|
		// afterwards.
 | 
						|
		movePath = dstPath
 | 
						|
		dstPath = "/tmp"
 | 
						|
	}
 | 
						|
 | 
						|
	toTarOpts := &archive.TarOptions{IncludeSourceDir: true, NoOverwriteDirNonDir: true, Compression: archive.Gzip}
 | 
						|
	content, err := archive.TarWithOptions(srcPath, toTarOpts)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug(i18n.G("copy %s from local to %s on container", srcPath, dstPath))
 | 
						|
	copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
 | 
						|
	if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if movePath != "" {
 | 
						|
		_, srcFile := path.Split(srcPath)
 | 
						|
		dcli, err := command.NewDockerCli()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
 | 
						|
			AttachStderr: true,
 | 
						|
			AttachStdin:  true,
 | 
						|
			AttachStdout: true,
 | 
						|
			Cmd:          []string{"mv", path.Join("/tmp", srcFile), movePath},
 | 
						|
			Detach:       false,
 | 
						|
			Tty:          true,
 | 
						|
		}); err != nil {
 | 
						|
			return errors.New(i18n.G("create remote directory: %s", err))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// CopyFromContainer copies a file or directory from the given container to the local file system.
 | 
						|
// See the possible copy modes and their documentation.
 | 
						|
func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
 | 
						|
	srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath)
 | 
						|
	if err != nil {
 | 
						|
		if errdefs.IsNotFound(err) {
 | 
						|
			return errors.New(i18n.G("remote: %s does not exist", srcPath))
 | 
						|
		} else {
 | 
						|
			return errors.New(i18n.G("remote path: %s", err))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	dstStat, err := os.Stat(dstPath)
 | 
						|
	dstExists := true
 | 
						|
	var dstMode os.FileMode
 | 
						|
	if err != nil {
 | 
						|
		if os.IsNotExist(err) {
 | 
						|
			dstExists = false
 | 
						|
		} else {
 | 
						|
			return errors.New(i18n.G("remote path: %s", err))
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		dstMode = dstStat.Mode()
 | 
						|
	}
 | 
						|
 | 
						|
	mode, err := copyMode(srcPath, dstPath, srcStat.Mode, dstMode, dstExists)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	moveDstDir := ""
 | 
						|
	moveDstFile := ""
 | 
						|
	switch mode {
 | 
						|
	case CopyModeFileToFile:
 | 
						|
		// Remove the file component from the path, since docker can only copy
 | 
						|
		// to a directory.
 | 
						|
		dstPath, _ = path.Split(dstPath)
 | 
						|
	case CopyModeFileToFileRename:
 | 
						|
		// Copy the file to the temp directory and move it to its dstPath
 | 
						|
		// afterwards.
 | 
						|
		moveDstFile = dstPath
 | 
						|
		dstPath = "/tmp"
 | 
						|
	case CopyModeFilesToDir:
 | 
						|
		// Copy the directory to the temp directory and move it to its
 | 
						|
		// dstPath afterwards.
 | 
						|
		moveDstDir = path.Join(dstPath, "/")
 | 
						|
		dstPath = "/tmp"
 | 
						|
 | 
						|
		// Make sure the temp directory always gets removed
 | 
						|
		defer os.Remove(path.Join("/tmp"))
 | 
						|
	}
 | 
						|
 | 
						|
	content, _, err := cl.CopyFromContainer(context.Background(), containerID, srcPath)
 | 
						|
	if err != nil {
 | 
						|
		return errors.New(i18n.G("copy: %s", err))
 | 
						|
	}
 | 
						|
	defer content.Close()
 | 
						|
	if err := archive.Untar(content, dstPath, &archive.TarOptions{
 | 
						|
		NoOverwriteDirNonDir: true,
 | 
						|
		Compression:          archive.Gzip,
 | 
						|
		NoLchown:             true,
 | 
						|
	}); err != nil {
 | 
						|
		return errors.New(i18n.G("untar: %s", err))
 | 
						|
	}
 | 
						|
 | 
						|
	if moveDstFile != "" {
 | 
						|
		_, srcFile := path.Split(strings.TrimSuffix(srcPath, "/"))
 | 
						|
		if err := moveFile(path.Join("/tmp", srcFile), moveDstFile); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if moveDstDir != "" {
 | 
						|
		_, srcDir := path.Split(strings.TrimSuffix(srcPath, "/"))
 | 
						|
		if err := moveDir(path.Join("/tmp", srcDir), moveDstDir); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
var (
 | 
						|
	ErrCopyDirToFile  = errors.New(i18n.G("can't copy dir to file"))
 | 
						|
	ErrDstDirNotExist = errors.New(i18n.G("destination directory does not exist"))
 | 
						|
)
 | 
						|
 | 
						|
type CopyMode int
 | 
						|
 | 
						|
const (
 | 
						|
	// Copy a src file to a dest file. The src and dest file names are the same.
 | 
						|
	//  <dir_src>/<file> + <dir_dst>/<file> -> <dir_dst>/<file>
 | 
						|
	CopyModeFileToFile = CopyMode(iota)
 | 
						|
	// Copy a src file to a dest file. The src and dest file names are  not the same.
 | 
						|
	//  <dir_src>/<file_src> + <dir_dst>/<file_dst> -> <dir_dst>/<file_dst>
 | 
						|
	CopyModeFileToFileRename
 | 
						|
	// Copy a src file to dest directory. The dest file gets created in the dest
 | 
						|
	// folder with the src filename.
 | 
						|
	//  <dir_src>/<file> + <dir_dst> -> <dir_dst>/<file>
 | 
						|
	CopyModeFileToDir
 | 
						|
	// Copy a src directory to dest directory.
 | 
						|
	//  <dir_src> + <dir_dst> -> <dir_dst>/<dir_src>
 | 
						|
	CopyModeDirToDir
 | 
						|
	// Copy all files in the src directory to the dest directory. This works recursively.
 | 
						|
	//  <dir_src>/ + <dir_dst> -> <dir_dst>/<files_from_dir_src>
 | 
						|
	CopyModeFilesToDir
 | 
						|
)
 | 
						|
 | 
						|
// copyMode takes a src and dest path and file mode to determine the copy mode.
 | 
						|
// See the possible copy modes and their documentation.
 | 
						|
func copyMode(srcPath, dstPath string, srcMode os.FileMode, dstMode os.FileMode, dstExists bool) (CopyMode, error) {
 | 
						|
	_, srcFile := path.Split(srcPath)
 | 
						|
	_, dstFile := path.Split(dstPath)
 | 
						|
	if srcMode.IsDir() {
 | 
						|
		if !dstExists {
 | 
						|
			return -1, ErrDstDirNotExist
 | 
						|
		}
 | 
						|
		if dstMode.IsDir() {
 | 
						|
			if strings.HasSuffix(srcPath, "/") {
 | 
						|
				return CopyModeFilesToDir, nil
 | 
						|
			}
 | 
						|
			return CopyModeDirToDir, nil
 | 
						|
		}
 | 
						|
		return -1, ErrCopyDirToFile
 | 
						|
	}
 | 
						|
 | 
						|
	if dstMode.IsDir() {
 | 
						|
		return CopyModeFileToDir, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if srcFile != dstFile {
 | 
						|
		return CopyModeFileToFileRename, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return CopyModeFileToFile, nil
 | 
						|
}
 | 
						|
 | 
						|
// moveDir moves all files from a source path to the destination path recursively.
 | 
						|
func moveDir(sourcePath, destPath string) error {
 | 
						|
	return filepath.Walk(sourcePath, func(p string, info os.FileInfo, err error) error {
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		newPath := path.Join(destPath, strings.TrimPrefix(p, sourcePath))
 | 
						|
		if info.IsDir() {
 | 
						|
			err := os.Mkdir(newPath, info.Mode())
 | 
						|
			if err != nil {
 | 
						|
				if os.IsExist(err) {
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if info.Mode().IsRegular() {
 | 
						|
			return moveFile(p, newPath)
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// moveFile moves a file from a source path to a destination path.
 | 
						|
func moveFile(sourcePath, destPath string) error {
 | 
						|
	inputFile, err := os.Open(sourcePath)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	outputFile, err := os.Create(destPath)
 | 
						|
	if err != nil {
 | 
						|
		inputFile.Close()
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer outputFile.Close()
 | 
						|
	_, err = io.Copy(outputFile, inputFile)
 | 
						|
	inputFile.Close()
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Remove file after succesfull copy.
 | 
						|
	err = os.Remove(sourcePath)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func init() {
 | 
						|
	AppCpCommand.Flags().BoolVarP(
 | 
						|
		&internal.Chaos,
 | 
						|
		i18n.G("chaos"),
 | 
						|
		i18n.G("C"),
 | 
						|
		false,
 | 
						|
		i18n.G("ignore uncommitted recipes changes"),
 | 
						|
	)
 | 
						|
}
 |