Skip to content

Commit c986f85

Browse files
author
Arnaud Porterie
committed
Merge pull request moby#13171 from jlhawn/archive_copy
docker cp to and from containers
2 parents 879f440 + e54b1e0 commit c986f85

21 files changed

+3582
-141
lines changed

api/client/cp.go

Lines changed: 269 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,304 @@
11
package client
22

33
import (
4+
"encoding/base64"
5+
"encoding/json"
46
"fmt"
57
"io"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"path/filepath"
612
"strings"
713

814
"github.com/docker/docker/api/types"
915
"github.com/docker/docker/pkg/archive"
1016
flag "github.com/docker/docker/pkg/mflag"
1117
)
1218

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.
1428
//
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.
1631
//
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
1839
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+
)
2152

53+
cmd.Require(flag.Exact, 2)
2254
cmd.ParseFlags(args, true)
2355

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+
}
2687

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
29106
}
30107

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
33114
}
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
37130
}
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)
40135
}
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)
41177
if err != nil {
42178
return err
43179
}
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+
}
44210

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
51223
}
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)
52277
if err != nil {
53278
return err
54279
}
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
55296
}
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+
56303
return nil
57304
}

0 commit comments

Comments
 (0)