380 lines
10 KiB
Go
380 lines
10 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"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/upstream/container"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/docker/api/types"
|
|
dockerClient "github.com/docker/docker/client"
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/docker/docker/pkg/archive"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
var appCpCommand = cli.Command{
|
|
Name: "cp",
|
|
Aliases: []string{"c"},
|
|
ArgsUsage: "<domain> <src> <dst>",
|
|
Flags: []cli.Flag{
|
|
internal.DebugFlag,
|
|
internal.NoInputFlag,
|
|
},
|
|
Before: internal.SubCommandBefore,
|
|
Usage: "Copy files to/from a deployed app service",
|
|
Description: `
|
|
Copy files to and from any app service file system.
|
|
|
|
If you want to copy a myfile.txt to the root of the app service:
|
|
|
|
abra app cp <domain> myfile.txt app:/
|
|
|
|
And if you want to copy that file back to your current working directory locally:
|
|
|
|
abra app cp <domain> app:/myfile.txt .
|
|
`,
|
|
BashComplete: autocomplete.AppNameComplete,
|
|
Action: func(c *cli.Context) error {
|
|
app := internal.ValidateApp(c)
|
|
|
|
src := c.Args().Get(1)
|
|
dst := c.Args().Get(2)
|
|
if src == "" {
|
|
logrus.Fatal("missing <src> argument")
|
|
}
|
|
if dst == "" {
|
|
logrus.Fatal("missing <dest> argument")
|
|
}
|
|
|
|
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
logrus.Debugf("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 {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var errServiceMissing = errors.New("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 fmt.Errorf("local %s ", err)
|
|
}
|
|
|
|
dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
|
|
dstExists := true
|
|
if err != nil {
|
|
if errdefs.IsNotFound(err) {
|
|
dstExists = false
|
|
} else {
|
|
return fmt.Errorf("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, &types.ExecConfig{
|
|
AttachStderr: true,
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
Cmd: []string{"mkdir", "-p", dstPath},
|
|
Detach: false,
|
|
Tty: true,
|
|
}); err != nil {
|
|
return fmt.Errorf("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
|
|
}
|
|
|
|
logrus.Debugf("copy %s from local to %s on container", srcPath, dstPath)
|
|
copyOpts := types.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, &types.ExecConfig{
|
|
AttachStderr: true,
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
Cmd: []string{"mv", path.Join("/tmp", srcFile), movePath},
|
|
Detach: false,
|
|
Tty: true,
|
|
}); err != nil {
|
|
return fmt.Errorf("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 fmt.Errorf("remote: %s does not exist", srcPath)
|
|
} else {
|
|
return fmt.Errorf("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 fmt.Errorf("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 fmt.Errorf("copy: %s", err)
|
|
}
|
|
defer content.Close()
|
|
if err := archive.Untar(content, dstPath, &archive.TarOptions{
|
|
NoOverwriteDirNonDir: true,
|
|
Compression: archive.Gzip,
|
|
NoLchown: true,
|
|
}); err != nil {
|
|
return fmt.Errorf("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 = fmt.Errorf("can't copy dir to file")
|
|
ErrDstDirNotExist = fmt.Errorf("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
|
|
}
|