Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/copy.go
1645 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package main
5
6
import (
7
"errors"
8
"fmt"
9
"os"
10
"os/exec"
11
"path/filepath"
12
"runtime"
13
"strings"
14
15
"github.com/coreos/go-semver/semver"
16
"github.com/sirupsen/logrus"
17
"github.com/spf13/cobra"
18
19
"github.com/lima-vm/lima/v2/pkg/ioutilx"
20
"github.com/lima-vm/lima/v2/pkg/limatype"
21
"github.com/lima-vm/lima/v2/pkg/sshutil"
22
"github.com/lima-vm/lima/v2/pkg/store"
23
)
24
25
const copyHelp = `Copy files between host and guest
26
27
Prefix guest filenames with the instance name and a colon.
28
29
Example: limactl copy default:/etc/os-release .
30
31
Not to be confused with 'limactl clone'.
32
`
33
34
func newCopyCommand() *cobra.Command {
35
copyCommand := &cobra.Command{
36
Use: "copy SOURCE ... TARGET",
37
Aliases: []string{"cp"},
38
Short: "Copy files between host and guest",
39
Long: copyHelp,
40
Args: WrapArgsError(cobra.MinimumNArgs(2)),
41
RunE: copyAction,
42
GroupID: advancedCommand,
43
}
44
45
copyCommand.Flags().BoolP("recursive", "r", false, "Copy directories recursively")
46
copyCommand.Flags().BoolP("verbose", "v", false, "Enable verbose output")
47
48
return copyCommand
49
}
50
51
func copyAction(cmd *cobra.Command, args []string) error {
52
ctx := cmd.Context()
53
recursive, err := cmd.Flags().GetBool("recursive")
54
if err != nil {
55
return err
56
}
57
58
verbose, err := cmd.Flags().GetBool("verbose")
59
if err != nil {
60
return err
61
}
62
63
arg0, err := exec.LookPath("scp")
64
if err != nil {
65
return err
66
}
67
instances := make(map[string]*limatype.Instance)
68
scpFlags := []string{}
69
scpArgs := []string{}
70
debug, err := cmd.Flags().GetBool("debug")
71
if err != nil {
72
return err
73
}
74
75
if debug {
76
verbose = true
77
}
78
79
if verbose {
80
scpFlags = append(scpFlags, "-v")
81
} else {
82
scpFlags = append(scpFlags, "-q")
83
}
84
85
if recursive {
86
scpFlags = append(scpFlags, "-r")
87
}
88
// this assumes that ssh and scp come from the same place, but scp has no -V
89
sshExe, err := sshutil.NewSSHExe()
90
if err != nil {
91
return err
92
}
93
legacySSH := sshutil.DetectOpenSSHVersion(ctx, sshExe).LessThan(*semver.New("8.0.0"))
94
for _, arg := range args {
95
if runtime.GOOS == "windows" {
96
if filepath.IsAbs(arg) {
97
arg, err = ioutilx.WindowsSubsystemPath(ctx, arg)
98
if err != nil {
99
return err
100
}
101
} else {
102
arg = filepath.ToSlash(arg)
103
}
104
}
105
path := strings.Split(arg, ":")
106
switch len(path) {
107
case 1:
108
scpArgs = append(scpArgs, arg)
109
case 2:
110
instName := path[0]
111
inst, err := store.Inspect(ctx, instName)
112
if err != nil {
113
if errors.Is(err, os.ErrNotExist) {
114
return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
115
}
116
return err
117
}
118
if inst.Status == limatype.StatusStopped {
119
return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
120
}
121
if legacySSH {
122
scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort))
123
scpArgs = append(scpArgs, fmt.Sprintf("%[email protected]:%s", *inst.Config.User.Name, path[1]))
124
} else {
125
scpArgs = append(scpArgs, fmt.Sprintf("scp://%[email protected]:%d/%s", *inst.Config.User.Name, inst.SSHLocalPort, path[1]))
126
}
127
instances[instName] = inst
128
default:
129
return fmt.Errorf("path %q contains multiple colons", arg)
130
}
131
}
132
if legacySSH && len(instances) > 1 {
133
return errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
134
}
135
scpFlags = append(scpFlags, "-3", "--")
136
scpArgs = append(scpFlags, scpArgs...)
137
138
var sshOpts []string
139
if len(instances) == 1 {
140
// Only one (instance) host is involved; we can use the instance-specific
141
// arguments such as ControlPath. This is preferred as we can multiplex
142
// sessions without re-authenticating (MaxSessions permitting).
143
for _, inst := range instances {
144
sshExe, err := sshutil.NewSSHExe()
145
if err != nil {
146
return err
147
}
148
sshOpts, err = sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
149
if err != nil {
150
return err
151
}
152
}
153
} else {
154
// Copying among multiple hosts; we can't pass in host-specific options.
155
sshExe, err := sshutil.NewSSHExe()
156
if err != nil {
157
return err
158
}
159
sshOpts, err = sshutil.CommonOpts(ctx, sshExe, false)
160
if err != nil {
161
return err
162
}
163
}
164
sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
165
166
sshCmd := exec.CommandContext(ctx, arg0, append(sshArgs, scpArgs...)...)
167
sshCmd.Stdin = cmd.InOrStdin()
168
sshCmd.Stdout = cmd.OutOrStdout()
169
sshCmd.Stderr = cmd.ErrOrStderr()
170
logrus.Debugf("executing scp (may take a long time): %+v", sshCmd.Args)
171
172
// TODO: use syscall.Exec directly (results in losing tty?)
173
return sshCmd.Run()
174
}
175
176