|
1 | 1 | package client |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/base64" |
| 5 | + "encoding/json" |
4 | 6 | "fmt" |
5 | 7 | "io" |
| 8 | + "net/http" |
| 9 | + "net/url" |
| 10 | + "os" |
| 11 | + "path/filepath" |
6 | 12 | "strings" |
7 | 13 |
|
8 | 14 | "github.com/docker/docker/api/types" |
9 | 15 | "github.com/docker/docker/pkg/archive" |
10 | 16 | flag "github.com/docker/docker/pkg/mflag" |
11 | 17 | ) |
12 | 18 |
|
13 | | -// CmdCp copies files/folders from a path on the container to a directory on the host running the command. |
| 19 | +type copyDirection int |
| 20 | + |
| 21 | +const ( |
| 22 | + fromContainer copyDirection = (1 << iota) |
| 23 | + toContainer |
| 24 | + acrossContainers = fromContainer | toContainer |
| 25 | +) |
| 26 | + |
| 27 | +// CmdCp copies files/folders to or from a path in a container. |
14 | 28 | // |
15 | | -// If HOSTDIR is '-', the data is written as a tar file to STDOUT. |
| 29 | +// When copying from a container, if LOCALPATH is '-' the data is written as a |
| 30 | +// tar archive file to STDOUT. |
16 | 31 | // |
17 | | -// Usage: docker cp CONTAINER:PATH HOSTDIR |
| 32 | +// When copying to a container, if LOCALPATH is '-' the data is read as a tar |
| 33 | +// archive file from STDIN, and the destination CONTAINER:PATH, must specify |
| 34 | +// a directory. |
| 35 | +// |
| 36 | +// Usage: |
| 37 | +// docker cp CONTAINER:PATH LOCALPATH|- |
| 38 | +// docker cp LOCALPATH|- CONTAINER:PATH |
18 | 39 | func (cli *DockerCli) CmdCp(args ...string) error { |
19 | | - cmd := cli.Subcmd("cp", []string{"CONTAINER:PATH HOSTDIR|-"}, "Copy files/folders from a container's PATH to a HOSTDIR on the host\nrunning the command. Use '-' to write the data as a tar file to STDOUT.", true) |
20 | | - cmd.Require(flag.Exact, 2) |
| 40 | + cmd := cli.Subcmd( |
| 41 | + "cp", |
| 42 | + []string{"CONTAINER:PATH LOCALPATH|-", "LOCALPATH|- CONTAINER:PATH"}, |
| 43 | + strings.Join([]string{ |
| 44 | + "Copy files/folders between a container and your host.\n", |
| 45 | + "Use '-' as the source to read a tar archive from stdin\n", |
| 46 | + "and extract it to a directory destination in a container.\n", |
| 47 | + "Use '-' as the destination to stream a tar archive of a\n", |
| 48 | + "container source to stdout.", |
| 49 | + }, ""), |
| 50 | + true, |
| 51 | + ) |
21 | 52 |
|
| 53 | + cmd.Require(flag.Exact, 2) |
22 | 54 | cmd.ParseFlags(args, true) |
23 | 55 |
|
24 | | - // deal with path name with `:` |
25 | | - info := strings.SplitN(cmd.Arg(0), ":", 2) |
| 56 | + if cmd.Arg(0) == "" { |
| 57 | + return fmt.Errorf("source can not be empty") |
| 58 | + } |
| 59 | + if cmd.Arg(1) == "" { |
| 60 | + return fmt.Errorf("destination can not be empty") |
| 61 | + } |
| 62 | + |
| 63 | + srcContainer, srcPath := splitCpArg(cmd.Arg(0)) |
| 64 | + dstContainer, dstPath := splitCpArg(cmd.Arg(1)) |
| 65 | + |
| 66 | + var direction copyDirection |
| 67 | + if srcContainer != "" { |
| 68 | + direction |= fromContainer |
| 69 | + } |
| 70 | + if dstContainer != "" { |
| 71 | + direction |= toContainer |
| 72 | + } |
| 73 | + |
| 74 | + switch direction { |
| 75 | + case fromContainer: |
| 76 | + return cli.copyFromContainer(srcContainer, srcPath, dstPath) |
| 77 | + case toContainer: |
| 78 | + return cli.copyToContainer(srcPath, dstContainer, dstPath) |
| 79 | + case acrossContainers: |
| 80 | + // Copying between containers isn't supported. |
| 81 | + return fmt.Errorf("copying between containers is not supported") |
| 82 | + default: |
| 83 | + // User didn't specify any container. |
| 84 | + return fmt.Errorf("must specify at least one container source") |
| 85 | + } |
| 86 | +} |
26 | 87 |
|
27 | | - if len(info) != 2 { |
28 | | - return fmt.Errorf("Error: Path not specified") |
| 88 | +// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be |
| 89 | +// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by |
| 90 | +// requiring a LOCALPATH with a `:` to be made explicit with a relative or |
| 91 | +// absolute path: |
| 92 | +// `/path/to/file:name.txt` or `./file:name.txt` |
| 93 | +// |
| 94 | +// This is apparently how `scp` handles this as well: |
| 95 | +// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/ |
| 96 | +// |
| 97 | +// We can't simply check for a filepath separator because container names may |
| 98 | +// have a separator, e.g., "host0/cname1" if container is in a Docker cluster, |
| 99 | +// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows |
| 100 | +// client, a `:` could be part of an absolute Windows path, in which case it |
| 101 | +// is immediately proceeded by a backslash. |
| 102 | +func splitCpArg(arg string) (container, path string) { |
| 103 | + if filepath.IsAbs(arg) { |
| 104 | + // Explicit local absolute path, e.g., `C:\foo` or `/foo`. |
| 105 | + return "", arg |
29 | 106 | } |
30 | 107 |
|
31 | | - cfg := &types.CopyConfig{ |
32 | | - Resource: info[1], |
| 108 | + parts := strings.SplitN(arg, ":", 2) |
| 109 | + |
| 110 | + if len(parts) == 1 || strings.HasPrefix(parts[0], ".") { |
| 111 | + // Either there's no `:` in the arg |
| 112 | + // OR it's an explicit local relative path like `./file:name.txt`. |
| 113 | + return "", arg |
33 | 114 | } |
34 | | - serverResp, err := cli.call("POST", "/containers/"+info[0]+"/copy", cfg, nil) |
35 | | - if serverResp.body != nil { |
36 | | - defer serverResp.body.Close() |
| 115 | + |
| 116 | + return parts[0], parts[1] |
| 117 | +} |
| 118 | + |
| 119 | +func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) { |
| 120 | + var stat types.ContainerPathStat |
| 121 | + |
| 122 | + query := make(url.Values, 1) |
| 123 | + query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. |
| 124 | + |
| 125 | + urlStr := fmt.Sprintf("/containers/%s/archive?%s", containerName, query.Encode()) |
| 126 | + |
| 127 | + response, err := cli.call("HEAD", urlStr, nil, nil) |
| 128 | + if err != nil { |
| 129 | + return stat, err |
37 | 130 | } |
38 | | - if serverResp.statusCode == 404 { |
39 | | - return fmt.Errorf("No such container: %v", info[0]) |
| 131 | + defer response.body.Close() |
| 132 | + |
| 133 | + if response.statusCode != http.StatusOK { |
| 134 | + return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) |
40 | 135 | } |
| 136 | + |
| 137 | + return getContainerPathStatFromHeader(response.header) |
| 138 | +} |
| 139 | + |
| 140 | +func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) { |
| 141 | + var stat types.ContainerPathStat |
| 142 | + |
| 143 | + encodedStat := header.Get("X-Docker-Container-Path-Stat") |
| 144 | + statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat)) |
| 145 | + |
| 146 | + err := json.NewDecoder(statDecoder).Decode(&stat) |
| 147 | + if err != nil { |
| 148 | + err = fmt.Errorf("unable to decode container path stat header: %s", err) |
| 149 | + } |
| 150 | + |
| 151 | + return stat, err |
| 152 | +} |
| 153 | + |
| 154 | +func resolveLocalPath(localPath string) (absPath string, err error) { |
| 155 | + if absPath, err = filepath.Abs(localPath); err != nil { |
| 156 | + return |
| 157 | + } |
| 158 | + |
| 159 | + return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil |
| 160 | +} |
| 161 | + |
| 162 | +func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (err error) { |
| 163 | + if dstPath != "-" { |
| 164 | + // Get an absolute destination path. |
| 165 | + dstPath, err = resolveLocalPath(dstPath) |
| 166 | + if err != nil { |
| 167 | + return err |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + query := make(url.Values, 1) |
| 172 | + query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. |
| 173 | + |
| 174 | + urlStr := fmt.Sprintf("/containers/%s/archive?%s", srcContainer, query.Encode()) |
| 175 | + |
| 176 | + response, err := cli.call("GET", urlStr, nil, nil) |
41 | 177 | if err != nil { |
42 | 178 | return err |
43 | 179 | } |
| 180 | + defer response.body.Close() |
| 181 | + |
| 182 | + if response.statusCode != http.StatusOK { |
| 183 | + return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) |
| 184 | + } |
| 185 | + |
| 186 | + if dstPath == "-" { |
| 187 | + // Send the response to STDOUT. |
| 188 | + _, err = io.Copy(os.Stdout, response.body) |
| 189 | + |
| 190 | + return err |
| 191 | + } |
| 192 | + |
| 193 | + // In order to get the copy behavior right, we need to know information |
| 194 | + // about both the source and the destination. The response headers include |
| 195 | + // stat info about the source that we can use in deciding exactly how to |
| 196 | + // copy it locally. Along with the stat info about the local destination, |
| 197 | + // we have everything we need to handle the multiple possibilities there |
| 198 | + // can be when copying a file/dir from one location to another file/dir. |
| 199 | + stat, err := getContainerPathStatFromHeader(response.header) |
| 200 | + if err != nil { |
| 201 | + return fmt.Errorf("unable to get resource stat from response: %s", err) |
| 202 | + } |
| 203 | + |
| 204 | + // Prepare source copy info. |
| 205 | + srcInfo := archive.CopyInfo{ |
| 206 | + Path: srcPath, |
| 207 | + Exists: true, |
| 208 | + IsDir: stat.Mode.IsDir(), |
| 209 | + } |
44 | 210 |
|
45 | | - hostPath := cmd.Arg(1) |
46 | | - if serverResp.statusCode == 200 { |
47 | | - if hostPath == "-" { |
48 | | - _, err = io.Copy(cli.out, serverResp.body) |
49 | | - } else { |
50 | | - err = archive.Untar(serverResp.body, hostPath, &archive.TarOptions{NoLchown: true}) |
| 211 | + // See comments in the implementation of `archive.CopyTo` for exactly what |
| 212 | + // goes into deciding how and whether the source archive needs to be |
| 213 | + // altered for the correct copy behavior. |
| 214 | + return archive.CopyTo(response.body, srcInfo, dstPath) |
| 215 | +} |
| 216 | + |
| 217 | +func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (err error) { |
| 218 | + if srcPath != "-" { |
| 219 | + // Get an absolute source path. |
| 220 | + srcPath, err = resolveLocalPath(srcPath) |
| 221 | + if err != nil { |
| 222 | + return err |
51 | 223 | } |
| 224 | + } |
| 225 | + |
| 226 | + // In order to get the copy behavior right, we need to know information |
| 227 | + // about both the source and destination. The API is a simple tar |
| 228 | + // archive/extract API but we can use the stat info header about the |
| 229 | + // destination to be more informed about exactly what the destination is. |
| 230 | + |
| 231 | + // Prepare destination copy info by stat-ing the container path. |
| 232 | + dstInfo := archive.CopyInfo{Path: dstPath} |
| 233 | + dstStat, err := cli.statContainerPath(dstContainer, dstPath) |
| 234 | + // Ignore any error and assume that the parent directory of the destination |
| 235 | + // path exists, in which case the copy may still succeed. If there is any |
| 236 | + // type of conflict (e.g., non-directory overwriting an existing directory |
| 237 | + // or vice versia) the extraction will fail. If the destination simply did |
| 238 | + // not exist, but the parent directory does, the extraction will still |
| 239 | + // succeed. |
| 240 | + if err == nil { |
| 241 | + dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() |
| 242 | + } |
| 243 | + |
| 244 | + var content io.Reader |
| 245 | + if srcPath == "-" { |
| 246 | + // Use STDIN. |
| 247 | + content = os.Stdin |
| 248 | + if !dstInfo.IsDir { |
| 249 | + return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath)) |
| 250 | + } |
| 251 | + } else { |
| 252 | + srcArchive, err := archive.TarResource(srcPath) |
| 253 | + if err != nil { |
| 254 | + return err |
| 255 | + } |
| 256 | + defer srcArchive.Close() |
| 257 | + |
| 258 | + // With the stat info about the local source as well as the |
| 259 | + // destination, we have enough information to know whether we need to |
| 260 | + // alter the archive that we upload so that when the server extracts |
| 261 | + // it to the specified directory in the container we get the disired |
| 262 | + // copy behavior. |
| 263 | + |
| 264 | + // Prepare source copy info. |
| 265 | + srcInfo, err := archive.CopyInfoStatPath(srcPath, true) |
| 266 | + if err != nil { |
| 267 | + return err |
| 268 | + } |
| 269 | + |
| 270 | + // See comments in the implementation of `archive.PrepareArchiveCopy` |
| 271 | + // for exactly what goes into deciding how and whether the source |
| 272 | + // archive needs to be altered for the correct copy behavior when it is |
| 273 | + // extracted. This function also infers from the source and destination |
| 274 | + // info which directory to extract to, which may be the parent of the |
| 275 | + // destination that the user specified. |
| 276 | + dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) |
52 | 277 | if err != nil { |
53 | 278 | return err |
54 | 279 | } |
| 280 | + defer preparedArchive.Close() |
| 281 | + |
| 282 | + dstPath = dstDir |
| 283 | + content = preparedArchive |
| 284 | + } |
| 285 | + |
| 286 | + query := make(url.Values, 2) |
| 287 | + query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API. |
| 288 | + // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. |
| 289 | + query.Set("noOverwriteDirNonDir", "true") |
| 290 | + |
| 291 | + urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode()) |
| 292 | + |
| 293 | + response, err := cli.stream("PUT", urlStr, &streamOpts{in: content}) |
| 294 | + if err != nil { |
| 295 | + return err |
55 | 296 | } |
| 297 | + defer response.body.Close() |
| 298 | + |
| 299 | + if response.statusCode != http.StatusOK { |
| 300 | + return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) |
| 301 | + } |
| 302 | + |
56 | 303 | return nil |
57 | 304 | } |
0 commit comments