package main
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/coreos/go-semver/semver"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/lima-vm/lima/v2/pkg/ioutilx"
"github.com/lima-vm/lima/v2/pkg/limatype"
"github.com/lima-vm/lima/v2/pkg/sshutil"
"github.com/lima-vm/lima/v2/pkg/store"
)
const copyHelp = `Copy files between host and guest
Prefix guest filenames with the instance name and a colon.
Example: limactl copy default:/etc/os-release .
Not to be confused with 'limactl clone'.
`
func newCopyCommand() *cobra.Command {
copyCommand := &cobra.Command{
Use: "copy SOURCE ... TARGET",
Aliases: []string{"cp"},
Short: "Copy files between host and guest",
Long: copyHelp,
Args: WrapArgsError(cobra.MinimumNArgs(2)),
RunE: copyAction,
GroupID: advancedCommand,
}
copyCommand.Flags().BoolP("recursive", "r", false, "Copy directories recursively")
copyCommand.Flags().BoolP("verbose", "v", false, "Enable verbose output")
return copyCommand
}
func copyAction(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
recursive, err := cmd.Flags().GetBool("recursive")
if err != nil {
return err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return err
}
arg0, err := exec.LookPath("scp")
if err != nil {
return err
}
instances := make(map[string]*limatype.Instance)
scpFlags := []string{}
scpArgs := []string{}
debug, err := cmd.Flags().GetBool("debug")
if err != nil {
return err
}
if debug {
verbose = true
}
if verbose {
scpFlags = append(scpFlags, "-v")
} else {
scpFlags = append(scpFlags, "-q")
}
if recursive {
scpFlags = append(scpFlags, "-r")
}
sshExe, err := sshutil.NewSSHExe()
if err != nil {
return err
}
legacySSH := sshutil.DetectOpenSSHVersion(ctx, sshExe).LessThan(*semver.New("8.0.0"))
for _, arg := range args {
if runtime.GOOS == "windows" {
if filepath.IsAbs(arg) {
arg, err = ioutilx.WindowsSubsystemPath(ctx, arg)
if err != nil {
return err
}
} else {
arg = filepath.ToSlash(arg)
}
}
path := strings.Split(arg, ":")
switch len(path) {
case 1:
scpArgs = append(scpArgs, arg)
case 2:
instName := path[0]
inst, err := store.Inspect(ctx, instName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
}
return err
}
if inst.Status == limatype.StatusStopped {
return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
}
if legacySSH {
scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort))
scpArgs = append(scpArgs, fmt.Sprintf("%[email protected]:%s", *inst.Config.User.Name, path[1]))
} else {
scpArgs = append(scpArgs, fmt.Sprintf("scp://%[email protected]:%d/%s", *inst.Config.User.Name, inst.SSHLocalPort, path[1]))
}
instances[instName] = inst
default:
return fmt.Errorf("path %q contains multiple colons", arg)
}
}
if legacySSH && len(instances) > 1 {
return errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
}
scpFlags = append(scpFlags, "-3", "--")
scpArgs = append(scpFlags, scpArgs...)
var sshOpts []string
if len(instances) == 1 {
for _, inst := range instances {
sshExe, err := sshutil.NewSSHExe()
if err != nil {
return err
}
sshOpts, err = sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
if err != nil {
return err
}
}
} else {
sshExe, err := sshutil.NewSSHExe()
if err != nil {
return err
}
sshOpts, err = sshutil.CommonOpts(ctx, sshExe, false)
if err != nil {
return err
}
}
sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
sshCmd := exec.CommandContext(ctx, arg0, append(sshArgs, scpArgs...)...)
sshCmd.Stdin = cmd.InOrStdin()
sshCmd.Stdout = cmd.OutOrStdout()
sshCmd.Stderr = cmd.ErrOrStderr()
logrus.Debugf("executing scp (may take a long time): %+v", sshCmd.Args)
return sshCmd.Run()
}