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: " ", 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 myfile.txt app:/ And if you want to copy that file back to your current working directory locally: abra app cp 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 argument") } if dst == "" { logrus.Fatal("missing 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 / 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. // / + / -> / CopyModeFileToFile = CopyMode(iota) // Copy a src file to a dest file. The src and dest file names are not the same. // / + / -> / CopyModeFileToFileRename // Copy a src file to dest directory. The dest file gets created in the dest // folder with the src filename. // / + -> / CopyModeFileToDir // Copy a src directory to dest directory. // + -> / CopyModeDirToDir // Copy all files in the src directory to the dest directory. This works recursively. // / + -> / 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 }