Browse code

Add '-L' option for `cp`

Fixes #16555

Original docker `cp` always copy symbol link itself instead of target,
now we provide '-L' option to allow docker to follow symbol link to real
target.

Signed-off-by: Zhang Wei <zhangwei555@huawei.com>

Zhang Wei authored on 2015/10/01 16:56:39
Showing 7 changed files
... ...
@@ -26,22 +26,26 @@ const (
26 26
 	acrossContainers = fromContainer | toContainer
27 27
 )
28 28
 
29
+type cpConfig struct {
30
+	followLink bool
31
+}
32
+
29 33
 // CmdCp copies files/folders to or from a path in a container.
30 34
 //
31
-// When copying from a container, if LOCALPATH is '-' the data is written as a
35
+// When copying from a container, if DEST_PATH is '-' the data is written as a
32 36
 // tar archive file to STDOUT.
33 37
 //
34
-// When copying to a container, if LOCALPATH is '-' the data is read as a tar
35
-// archive file from STDIN, and the destination CONTAINER:PATH, must specify
38
+// When copying to a container, if SRC_PATH is '-' the data is read as a tar
39
+// archive file from STDIN, and the destination CONTAINER:DEST_PATH, must specify
36 40
 // a directory.
37 41
 //
38 42
 // Usage:
39
-// 	docker cp CONTAINER:PATH LOCALPATH|-
40
-// 	docker cp LOCALPATH|- CONTAINER:PATH
43
+// 	docker cp CONTAINER:SRC_PATH DEST_PATH|-
44
+// 	docker cp SRC_PATH|- CONTAINER:DEST_PATH
41 45
 func (cli *DockerCli) CmdCp(args ...string) error {
42 46
 	cmd := Cli.Subcmd(
43 47
 		"cp",
44
-		[]string{"CONTAINER:PATH LOCALPATH|-", "LOCALPATH|- CONTAINER:PATH"},
48
+		[]string{"CONTAINER:SRC_PATH DEST_PATH|-", "SRC_PATH|- CONTAINER:DEST_PATH"},
45 49
 		strings.Join([]string{
46 50
 			Cli.DockerCommands["cp"].Description,
47 51
 			"\nUse '-' as the source to read a tar archive from stdin\n",
... ...
@@ -52,6 +56,8 @@ func (cli *DockerCli) CmdCp(args ...string) error {
52 52
 		true,
53 53
 	)
54 54
 
55
+	followLink := cmd.Bool([]string{"L", "-follow-link"}, false, "Always follow symbol link in SRC_PATH")
56
+
55 57
 	cmd.Require(flag.Exact, 2)
56 58
 	cmd.ParseFlags(args, true)
57 59
 
... ...
@@ -73,11 +79,15 @@ func (cli *DockerCli) CmdCp(args ...string) error {
73 73
 		direction |= toContainer
74 74
 	}
75 75
 
76
+	cpParam := &cpConfig{
77
+		followLink: *followLink,
78
+	}
79
+
76 80
 	switch direction {
77 81
 	case fromContainer:
78
-		return cli.copyFromContainer(srcContainer, srcPath, dstPath)
82
+		return cli.copyFromContainer(srcContainer, srcPath, dstPath, cpParam)
79 83
 	case toContainer:
80
-		return cli.copyToContainer(srcPath, dstContainer, dstPath)
84
+		return cli.copyToContainer(srcPath, dstContainer, dstPath, cpParam)
81 85
 	case acrossContainers:
82 86
 		// Copying between containers isn't supported.
83 87
 		return fmt.Errorf("copying between containers is not supported")
... ...
@@ -161,7 +171,7 @@ func resolveLocalPath(localPath string) (absPath string, err error) {
161 161
 	return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
162 162
 }
163 163
 
164
-func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (err error) {
164
+func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
165 165
 	if dstPath != "-" {
166 166
 		// Get an absolute destination path.
167 167
 		dstPath, err = resolveLocalPath(dstPath)
... ...
@@ -170,6 +180,26 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (
170 170
 		}
171 171
 	}
172 172
 
173
+	// if client requests to follow symbol link, then must decide target file to be copied
174
+	var rebaseName string
175
+	if cpParam.followLink {
176
+		srcStat, err := cli.statContainerPath(srcContainer, srcPath)
177
+
178
+		// If the destination is a symbolic link, we should follow it.
179
+		if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
180
+			linkTarget := srcStat.LinkTarget
181
+			if !system.IsAbs(linkTarget) {
182
+				// Join with the parent directory.
183
+				srcParent, _ := archive.SplitPathDirEntry(srcPath)
184
+				linkTarget = filepath.Join(srcParent, linkTarget)
185
+			}
186
+
187
+			linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
188
+			srcPath = linkTarget
189
+		}
190
+
191
+	}
192
+
173 193
 	query := make(url.Values, 1)
174 194
 	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
175 195
 
... ...
@@ -205,18 +235,24 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (
205 205
 
206 206
 	// Prepare source copy info.
207 207
 	srcInfo := archive.CopyInfo{
208
-		Path:   srcPath,
209
-		Exists: true,
210
-		IsDir:  stat.Mode.IsDir(),
208
+		Path:       srcPath,
209
+		Exists:     true,
210
+		IsDir:      stat.Mode.IsDir(),
211
+		RebaseName: rebaseName,
211 212
 	}
212 213
 
214
+	preArchive := response.body
215
+	if len(srcInfo.RebaseName) != 0 {
216
+		_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
217
+		preArchive = archive.RebaseArchiveEntries(response.body, srcBase, srcInfo.RebaseName)
218
+	}
213 219
 	// See comments in the implementation of `archive.CopyTo` for exactly what
214 220
 	// goes into deciding how and whether the source archive needs to be
215 221
 	// altered for the correct copy behavior.
216
-	return archive.CopyTo(response.body, srcInfo, dstPath)
222
+	return archive.CopyTo(preArchive, srcInfo, dstPath)
217 223
 }
218 224
 
219
-func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (err error) {
225
+func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) {
220 226
 	if srcPath != "-" {
221 227
 		// Get an absolute source path.
222 228
 		srcPath, err = resolveLocalPath(srcPath)
... ...
@@ -271,7 +307,7 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
271 271
 		}
272 272
 	} else {
273 273
 		// Prepare source copy info.
274
-		srcInfo, err := archive.CopyInfoSourcePath(srcPath)
274
+		srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
275 275
 		if err != nil {
276 276
 			return err
277 277
 		}
... ...
@@ -10,81 +10,79 @@ parent = "smn_cli"
10 10
 
11 11
 # cp
12 12
 
13
-    Usage: docker cp [OPTIONS] CONTAINER:PATH LOCALPATH|-
14
-           docker cp [OPTIONS] LOCALPATH|- CONTAINER:PATH
13
+    Usage: docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH | -
14
+           docker cp [OPTIONS] SRC_PATH | - CONTAINER:DEST_PATH
15 15
 
16 16
     Copy files/folders between a container and the local filesystem
17 17
 
18
-      --help=false        Print usage
19
-
20
-In the first synopsis form, the `docker cp` utility copies the contents of
21
-`PATH` from the filesystem of `CONTAINER` to the `LOCALPATH` (or stream as
22
-a tar archive to `STDOUT` if `-` is specified).
23
-
24
-In the second synopsis form, the contents of `LOCALPATH` (or a tar archive
25
-streamed from `STDIN` if `-` is specified) are copied from the local machine to
26
-`PATH` in the filesystem of `CONTAINER`.
27
-
28
-You can copy to or from either a running or stopped container. The `PATH` can
29
-be a file or directory. The `docker cp` command assumes all `CONTAINER:PATH`
30
-values are relative to the `/` (root) directory of the container. This means
31
-supplying the initial forward slash is optional; The command sees
32
-`compassionate_darwin:/tmp/foo/myfile.txt` and
33
-`compassionate_darwin:tmp/foo/myfile.txt` as identical. If a `LOCALPATH` value
34
-is not absolute, is it considered relative to the current working directory.
35
-
36
-Behavior is similar to the common Unix utility `cp -a` in that directories are
18
+      -L, --follow-link=false    Always follow symbol link in SRC_PATH
19
+      --help=false               Print usage
20
+
21
+The `docker cp` utility copies the contents of `SRC_PATH` to the `DEST_PATH`.
22
+You can copy from the container's file system to the local machine or the
23
+reverse, from the local filesystem to the container. If `-` is specified for
24
+either the `SRC_PATH` or `DEST_PATH`, you can also stream a tar archive from
25
+`STDIN` or to `STDOUT`. The `CONTAINER` can be a running or stopped container.
26
+The `SRC_PATH` or `DEST_PATH` be a file or directory.
27
+
28
+The `docker cp` command assumes container paths are relative to the container's 
29
+`/` (root) directory. This means supplying the initial forward slash is optional;
30
+The command sees `compassionate_darwin:/tmp/foo/myfile.txt` and
31
+`compassionate_darwin:tmp/foo/myfile.txt` as identical. Local machine paths can
32
+be an absolute or relative value. The command interprets a local machine's
33
+relative paths as relative to the current working directory where `docker cp` is
34
+run.
35
+
36
+The `cp` command behaves like the Unix `cp -a` command in that directories are
37 37
 copied recursively with permissions preserved if possible. Ownership is set to
38
-the user and primary group on the receiving end of the transfer. For example,
39
-files copied to a container will be created with `UID:GID` of the root user.
40
-Files copied to the local machine will be created with the `UID:GID` of the
41
-user which invoked the `docker cp` command.
38
+the user and primary group at the destination. For example, files copied to a
39
+container are created with `UID:GID` of the root user. Files copied to the local
40
+machine are created with the `UID:GID` of the user which invoked the `docker cp`
41
+command.  If you specify the `-L` option, `docker cp` follows any symbolic link
42
+in the `SRC_PATH`.
42 43
 
43 44
 Assuming a path separator of `/`, a first argument of `SRC_PATH` and second
44
-argument of `DST_PATH`, the behavior is as follows:
45
+argument of `DEST_PATH`, the behavior is as follows:
45 46
 
46 47
 - `SRC_PATH` specifies a file
47
-    - `DST_PATH` does not exist
48
-        - the file is saved to a file created at `DST_PATH`
49
-    - `DST_PATH` does not exist and ends with `/`
48
+    - `DEST_PATH` does not exist
49
+        - the file is saved to a file created at `DEST_PATH`
50
+    - `DEST_PATH` does not exist and ends with `/`
50 51
         - Error condition: the destination directory must exist.
51
-    - `DST_PATH` exists and is a file
52
-        - the destination is overwritten with the contents of the source file
53
-    - `DST_PATH` exists and is a directory
52
+    - `DEST_PATH` exists and is a file
53
+        - the destination is overwritten with the source file's contents
54
+    - `DEST_PATH` exists and is a directory
54 55
         - the file is copied into this directory using the basename from
55 56
           `SRC_PATH`
56 57
 - `SRC_PATH` specifies a directory
57
-    - `DST_PATH` does not exist
58
-        - `DST_PATH` is created as a directory and the *contents* of the source
58
+    - `DEST_PATH` does not exist
59
+        - `DEST_PATH` is created as a directory and the *contents* of the source
59 60
            directory are copied into this directory
60
-    - `DST_PATH` exists and is a file
61
+    - `DEST_PATH` exists and is a file
61 62
         - Error condition: cannot copy a directory to a file
62
-    - `DST_PATH` exists and is a directory
63
+    - `DEST_PATH` exists and is a directory
63 64
         - `SRC_PATH` does not end with `/.`
64 65
             - the source directory is copied into this directory
65 66
         - `SRC_PATH` does end with `/.`
66 67
             - the *content* of the source directory is copied into this
67 68
               directory
68 69
 
69
-The command requires `SRC_PATH` and `DST_PATH` to exist according to the above
70
+The command requires `SRC_PATH` and `DEST_PATH` to exist according to the above
70 71
 rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not
71
-the target, is copied.
72
+the target, is copied by default. To copy the link target and not the link, specify 
73
+the `-L` option.
72 74
 
73
-A colon (`:`) is used as a delimiter between `CONTAINER` and `PATH`, but `:`
74
-could also be in a valid `LOCALPATH`, like `file:name.txt`. This ambiguity is
75
-resolved by requiring a `LOCALPATH` with a `:` to be made explicit with a
76
-relative or absolute path, for example:
75
+A colon (`:`) is used as a delimiter between `CONTAINER` and its path. You can
76
+also use `:` when specifying paths to a `SRC_PATH` or `DEST_PATH` on a local
77
+machine, for example  `file:name.txt`. If you use a `:` in a local machine path,
78
+you must be explicit with a relative or absolute path, for example:
77 79
 
78 80
     `/path/to/file:name.txt` or `./file:name.txt`
79 81
 
80 82
 It is not possible to copy certain system files such as resources under
81 83
 `/proc`, `/sys`, `/dev`, and mounts created by the user in the container.
82 84
 
83
-Using `-` as the first argument in place of a `LOCALPATH` will stream the
84
-contents of `STDIN` as a tar archive which will be extracted to the `PATH` in
85
-the filesystem of the destination container. In this case, `PATH` must specify
86
-a directory.
87
-
88
-Using `-` as the second argument in place of a `LOCALPATH` will stream the
89
-contents of the resource from the source container as a tar archive to
90
-`STDOUT`.
85
+Using `-` as the `SRC_PATH` streams the contents of `STDIN` as a tar archive.
86
+The command extracts the content of the tar to the `DEST_PATH` in container's
87
+filesystem. In this case, `DEST_PATH` must specify a directory. Using `-` as
88
+`DEST_PATH` streams the contents of the resource as a tar archive to `STDOUT`.
... ...
@@ -609,3 +609,57 @@ func (s *DockerSuite) TestCopyCreatedContainer(c *check.C) {
609 609
 	defer os.RemoveAll(tmpDir)
610 610
 	dockerCmd(c, "cp", "test_cp:/bin/sh", tmpDir)
611 611
 }
612
+
613
+// test copy with option `-L`: following symbol link
614
+// Check that symlinks to a file behave as expected when copying one from
615
+// a container to host following symbol link
616
+func (s *DockerSuite) TestCpSymlinkFromConToHostFollowSymlink(c *check.C) {
617
+	testRequires(c, DaemonIsLinux)
618
+	out, exitCode := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath+" && ln -s "+cpFullPath+" /dir_link")
619
+	if exitCode != 0 {
620
+		c.Fatal("failed to create a container", out)
621
+	}
622
+
623
+	cleanedContainerID := strings.TrimSpace(out)
624
+
625
+	out, _ = dockerCmd(c, "wait", cleanedContainerID)
626
+	if strings.TrimSpace(out) != "0" {
627
+		c.Fatal("failed to set up container", out)
628
+	}
629
+
630
+	testDir, err := ioutil.TempDir("", "test-cp-symlink-container-to-host-follow-symlink")
631
+	if err != nil {
632
+		c.Fatal(err)
633
+	}
634
+	defer os.RemoveAll(testDir)
635
+
636
+	// This copy command should copy the symlink, not the target, into the
637
+	// temporary directory.
638
+	dockerCmd(c, "cp", "-L", cleanedContainerID+":"+"/dir_link", testDir)
639
+
640
+	expectedPath := filepath.Join(testDir, "dir_link")
641
+
642
+	expected := []byte(cpContainerContents)
643
+	actual, err := ioutil.ReadFile(expectedPath)
644
+
645
+	if !bytes.Equal(actual, expected) {
646
+		c.Fatalf("Expected copied file to be duplicate of the container symbol link target")
647
+	}
648
+	os.Remove(expectedPath)
649
+
650
+	// now test copy symbol link to an non-existing file in host
651
+	expectedPath = filepath.Join(testDir, "somefile_host")
652
+	// expectedPath shouldn't exist, if exists, remove it
653
+	if _, err := os.Lstat(expectedPath); err == nil {
654
+		os.Remove(expectedPath)
655
+	}
656
+
657
+	dockerCmd(c, "cp", "-L", cleanedContainerID+":"+"/dir_link", expectedPath)
658
+
659
+	actual, err = ioutil.ReadFile(expectedPath)
660
+
661
+	if !bytes.Equal(actual, expected) {
662
+		c.Fatalf("Expected copied file to be duplicate of the container symbol link target")
663
+	}
664
+	defer os.Remove(expectedPath)
665
+}
... ...
@@ -7,87 +7,87 @@ docker-cp - Copy files/folders between a container and the local filesystem.
7 7
 # SYNOPSIS
8 8
 **docker cp**
9 9
 [**--help**]
10
-CONTAINER:PATH LOCALPATH|-
10
+CONTAINER:SRC_PATH DEST_PATH|-
11 11
 
12 12
 **docker cp**
13 13
 [**--help**]
14
-LOCALPATH|- CONTAINER:PATH
14
+SRC_PATH|- CONTAINER:DEST_PATH
15 15
 
16 16
 # DESCRIPTION
17 17
 
18
-In the first synopsis form, the `docker cp` utility copies the contents of
19
-`PATH` from the filesystem of `CONTAINER` to the `LOCALPATH` (or stream as
20
-a tar archive to `STDOUT` if `-` is specified).
21
-
22
-In the second synopsis form, the contents of `LOCALPATH` (or a tar archive
23
-streamed from `STDIN` if `-` is specified) are copied from the local machine to
24
-`PATH` in the filesystem of `CONTAINER`.
25
-
26
-You can copy to or from either a running or stopped container. The `PATH` can
27
-be a file or directory. The `docker cp` command assumes all `CONTAINER:PATH`
28
-values are relative to the `/` (root) directory of the container. This means
29
-supplying the initial forward slash is optional; The command sees
30
-`compassionate_darwin:/tmp/foo/myfile.txt` and
31
-`compassionate_darwin:tmp/foo/myfile.txt` as identical. If a `LOCALPATH` value
32
-is not absolute, is it considered relative to the current working directory.
33
-
34
-Behavior is similar to the common Unix utility `cp -a` in that directories are
18
+The `docker cp` utility copies the contents of `SRC_PATH` to the `DEST_PATH`.
19
+You can copy from the container's file system to the local machine or the
20
+reverse, from the local filesystem to the container. If `-` is specified for
21
+either the `SRC_PATH` or `DEST_PATH`, you can also stream a tar archive from
22
+`STDIN` or to `STDOUT`. The `CONTAINER` can be a running or stopped container.
23
+The `SRC_PATH` or `DEST_PATH` be a file or directory.
24
+
25
+The `docker cp` command assumes container paths are relative to the container's 
26
+`/` (root) directory. This means supplying the initial forward slash is optional; 
27
+The command sees `compassionate_darwin:/tmp/foo/myfile.txt` and
28
+`compassionate_darwin:tmp/foo/myfile.txt` as identical. Local machine paths can
29
+be an absolute or relative value. The command interprets a local machine's
30
+relative paths as relative to the current working directory where `docker cp` is
31
+run.
32
+
33
+The `cp` command behaves like the Unix `cp -a` command in that directories are
35 34
 copied recursively with permissions preserved if possible. Ownership is set to
36
-the user and primary group on the receiving end of the transfer. For example,
37
-files copied to a container will be created with `UID:GID` of the root user.
38
-Files copied to the local machine will be created with the `UID:GID` of the
39
-user which invoked the `docker cp` command.
35
+the user and primary group at the destination. For example, files copied to a
36
+container are created with `UID:GID` of the root user. Files copied to the local
37
+machine are created with the `UID:GID` of the user which invoked the `docker cp`
38
+command.  If you specify the `-L` option, `docker cp` follows any symbolic link
39
+in the `SRC_PATH`.
40 40
 
41 41
 Assuming a path separator of `/`, a first argument of `SRC_PATH` and second
42
-argument of `DST_PATH`, the behavior is as follows:
42
+argument of `DEST_PATH`, the behavior is as follows:
43 43
 
44 44
 - `SRC_PATH` specifies a file
45
-    - `DST_PATH` does not exist
46
-        - the file is saved to a file created at `DST_PATH`
47
-    - `DST_PATH` does not exist and ends with `/`
45
+    - `DEST_PATH` does not exist
46
+        - the file is saved to a file created at `DEST_PATH`
47
+    - `DEST_PATH` does not exist and ends with `/`
48 48
         - Error condition: the destination directory must exist.
49
-    - `DST_PATH` exists and is a file
50
-        - the destination is overwritten with the contents of the source file
51
-    - `DST_PATH` exists and is a directory
49
+    - `DEST_PATH` exists and is a file
50
+        - the destination is overwritten with the source file's contents
51
+    - `DEST_PATH` exists and is a directory
52 52
         - the file is copied into this directory using the basename from
53 53
           `SRC_PATH`
54 54
 - `SRC_PATH` specifies a directory
55
-    - `DST_PATH` does not exist
56
-        - `DST_PATH` is created as a directory and the *contents* of the source
55
+    - `DEST_PATH` does not exist
56
+        - `DEST_PATH` is created as a directory and the *contents* of the source
57 57
            directory are copied into this directory
58
-    - `DST_PATH` exists and is a file
58
+    - `DEST_PATH` exists and is a file
59 59
         - Error condition: cannot copy a directory to a file
60
-    - `DST_PATH` exists and is a directory
60
+    - `DEST_PATH` exists and is a directory
61 61
         - `SRC_PATH` does not end with `/.`
62 62
             - the source directory is copied into this directory
63 63
         - `SRC_PATH` does end with `/.`
64 64
             - the *content* of the source directory is copied into this
65 65
               directory
66 66
 
67
-The command requires `SRC_PATH` and `DST_PATH` to exist according to the above
67
+The command requires `SRC_PATH` and `DEST_PATH` to exist according to the above
68 68
 rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not
69
-the target, is copied.
69
+the target, is copied by default. To copy the link target and not the link, 
70
+specify the `-L` option.
70 71
 
71
-A colon (`:`) is used as a delimiter between `CONTAINER` and `PATH`, but `:`
72
-could also be in a valid `LOCALPATH`, like `file:name.txt`. This ambiguity is
73
-resolved by requiring a `LOCALPATH` with a `:` to be made explicit with a
74
-relative or absolute path, for example:
72
+A colon (`:`) is used as a delimiter between `CONTAINER` and its path. You can
73
+also use `:` when specifying paths to a `SRC_PATH` or `DEST_PATH` on a local
74
+machine, for example  `file:name.txt`. If you use a `:` in a local machine path,
75
+you must be explicit with a relative or absolute path, for example:
75 76
 
76 77
     `/path/to/file:name.txt` or `./file:name.txt`
77 78
 
78 79
 It is not possible to copy certain system files such as resources under
79 80
 `/proc`, `/sys`, `/dev`, and mounts created by the user in the container.
80 81
 
81
-Using `-` as the first argument in place of a `LOCALPATH` will stream the
82
-contents of `STDIN` as a tar archive which will be extracted to the `PATH` in
83
-the filesystem of the destination container. In this case, `PATH` must specify
84
-a directory.
85
-
86
-Using `-` as the second argument in place of a `LOCALPATH` will stream the
87
-contents of the resource from the source container as a tar archive to
88
-`STDOUT`.
82
+Using `-` as the `SRC_PATH` streams the contents of `STDIN` as a tar archive.
83
+The command extracts the content of the tar to the `DEST_PATH` in container's
84
+filesystem. In this case, `DEST_PATH` must specify a directory. Using `-` as
85
+`DEST_PATH` streams the contents of the resource as a tar archive to `STDOUT`.
89 86
 
90 87
 # OPTIONS
88
+**-L**, **--follow-link**=*true*|*false*
89
+  Follow symbol link in SRC_PATH
90
+
91 91
 **--help**
92 92
   Print usage statement
93 93
 
... ...
@@ -102,13 +102,13 @@ If you want to copy the `/tmp/foo` directory from a container to the
102 102
 existing `/tmp` directory on your host. If you run `docker cp` in your `~`
103 103
 (home) directory on the local host:
104 104
 
105
-		$ docker cp compassionate_darwin:tmp/foo /tmp
105
+    $ docker cp compassionate_darwin:tmp/foo /tmp
106 106
 
107 107
 Docker creates a `/tmp/foo` directory on your host. Alternatively, you can omit
108 108
 the leading slash in the command. If you execute this command from your home
109 109
 directory:
110 110
 
111
-		$ docker cp compassionate_darwin:tmp/foo tmp
111
+    $ docker cp compassionate_darwin:tmp/foo tmp
112 112
 
113 113
 If `~/tmp` does not exist, Docker will create it and copy the contents of
114 114
 `/tmp/foo` from the container into this new directory. If `~/tmp` already
... ...
@@ -120,7 +120,7 @@ will either overwrite the contents of `LOCALPATH` if it is a file or place it
120 120
 into `LOCALPATH` if it is a directory, overwriting an existing file of the same
121 121
 name if one exists. For example, this command:
122 122
 
123
-		$ docker cp sharp_ptolemy:/tmp/foo/myfile.txt /test
123
+    $ docker cp sharp_ptolemy:/tmp/foo/myfile.txt /test
124 124
 
125 125
 If `/test` does not exist on the local machine, it will be created as a file
126 126
 with the contents of `/tmp/foo/myfile.txt` from the container. If `/test`
... ...
@@ -137,16 +137,27 @@ If you have a file, `config.yml`, in the current directory on your local host
137 137
 and wish to copy it to an existing directory at `/etc/my-app.d` in a container,
138 138
 this command can be used:
139 139
 
140
-		$ docker cp config.yml myappcontainer:/etc/my-app.d
140
+    $ docker cp config.yml myappcontainer:/etc/my-app.d
141 141
 
142 142
 If you have several files in a local directory `/config` which you need to copy
143 143
 to a directory `/etc/my-app.d` in a container:
144 144
 
145
-		$ docker cp /config/. myappcontainer:/etc/my-app.d
145
+    $ docker cp /config/. myappcontainer:/etc/my-app.d
146 146
 
147 147
 The above command will copy the contents of the local `/config` directory into
148 148
 the directory `/etc/my-app.d` in the container.
149 149
 
150
+Finally, if you want to copy a symbolic link into a container, you typically
151
+want to  copy the linked target and not the link itself. To copy the target, use
152
+the `-L` option, for example:
153
+
154
+    $ ln -s /tmp/somefile /tmp/somefile.ln
155
+    $ docker cp -L /tmp/somefile.ln myappcontainer:/tmp/
156
+
157
+This command copies content of the local `/tmp/somefile` into the file
158
+`/tmp/somefile.ln` in the container. Without `-L` option, the `/tmp/somefile.ln`
159
+preserves its symbolic link but not its content.
160
+
150 161
 # HISTORY
151 162
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
152 163
 based on docker.com source material and internal work.
... ...
@@ -63,6 +63,9 @@ func createSampleDir(t *testing.T, root string) {
63 63
 		{Regular, "dir4/file3-2", "file4-2\n", 0666},
64 64
 		{Symlink, "symlink1", "target1", 0666},
65 65
 		{Symlink, "symlink2", "target2", 0666},
66
+		{Symlink, "symlink3", root + "/file1", 0666},
67
+		{Symlink, "symlink4", root + "/symlink3", 0666},
68
+		{Symlink, "dirSymlink", root + "/dir1", 0740},
66 69
 	}
67 70
 
68 71
 	now := time.Now()
... ...
@@ -135,30 +135,17 @@ type CopyInfo struct {
135 135
 // operation. The given path should be an absolute local path. A source path
136 136
 // has all symlinks evaluated that appear before the last path separator ("/"
137 137
 // on Unix). As it is to be a copy source, the path must exist.
138
-func CopyInfoSourcePath(path string) (CopyInfo, error) {
139
-	// Split the given path into its Directory and Base components. We will
140
-	// evaluate symlinks in the directory component then append the base.
138
+func CopyInfoSourcePath(path string, followLink bool) (CopyInfo, error) {
139
+	// normalize the file path and then evaluate the symbol link
140
+	// we will use the target file instead of the symbol link if
141
+	// followLink is set
141 142
 	path = normalizePath(path)
142
-	dirPath, basePath := filepath.Split(path)
143 143
 
144
-	resolvedDirPath, err := filepath.EvalSymlinks(dirPath)
144
+	resolvedPath, rebaseName, err := ResolveHostSourcePath(path, followLink)
145 145
 	if err != nil {
146 146
 		return CopyInfo{}, err
147 147
 	}
148 148
 
149
-	// resolvedDirPath will have been cleaned (no trailing path separators) so
150
-	// we can manually join it with the base path element.
151
-	resolvedPath := resolvedDirPath + string(filepath.Separator) + basePath
152
-
153
-	var rebaseName string
154
-	if hasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) {
155
-		// In the case where the path had a trailing separator and a symlink
156
-		// evaluation has changed the last path component, we will need to
157
-		// rebase the name in the archive that is being copied to match the
158
-		// originally requested name.
159
-		rebaseName = filepath.Base(path)
160
-	}
161
-
162 149
 	stat, err := os.Lstat(resolvedPath)
163 150
 	if err != nil {
164 151
 		return CopyInfo{}, err
... ...
@@ -279,7 +266,10 @@ func PrepareArchiveCopy(srcContent Reader, srcInfo, dstInfo CopyInfo) (dstDir st
279 279
 		// The destination exists as some type of file and the source content
280 280
 		// is also a file. The source content entry will have to be renamed to
281 281
 		// have a basename which matches the destination path's basename.
282
-		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
282
+		if len(srcInfo.RebaseName) != 0 {
283
+			srcBase = srcInfo.RebaseName
284
+		}
285
+		return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
283 286
 	case srcInfo.IsDir:
284 287
 		// The destination does not exist and the source content is an archive
285 288
 		// of a directory. The archive should be extracted to the parent of
... ...
@@ -287,7 +277,10 @@ func PrepareArchiveCopy(srcContent Reader, srcInfo, dstInfo CopyInfo) (dstDir st
287 287
 		// created as a result should take the name of the destination path.
288 288
 		// The source content entries will have to be renamed to have a
289 289
 		// basename which matches the destination path's basename.
290
-		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
290
+		if len(srcInfo.RebaseName) != 0 {
291
+			srcBase = srcInfo.RebaseName
292
+		}
293
+		return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
291 294
 	case assertsDirectory(dstInfo.Path):
292 295
 		// The destination does not exist and is asserted to be created as a
293 296
 		// directory, but the source content is not a directory. This is an
... ...
@@ -301,14 +294,17 @@ func PrepareArchiveCopy(srcContent Reader, srcInfo, dstInfo CopyInfo) (dstDir st
301 301
 		// to be created when the archive is extracted and the source content
302 302
 		// entry will have to be renamed to have a basename which matches the
303 303
 		// destination path's basename.
304
-		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
304
+		if len(srcInfo.RebaseName) != 0 {
305
+			srcBase = srcInfo.RebaseName
306
+		}
307
+		return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
305 308
 	}
306 309
 
307 310
 }
308 311
 
309
-// rebaseArchiveEntries rewrites the given srcContent archive replacing
312
+// RebaseArchiveEntries rewrites the given srcContent archive replacing
310 313
 // an occurrence of oldBase with newBase at the beginning of entry names.
311
-func rebaseArchiveEntries(srcContent Reader, oldBase, newBase string) Archive {
314
+func RebaseArchiveEntries(srcContent Reader, oldBase, newBase string) Archive {
312 315
 	if oldBase == string(os.PathSeparator) {
313 316
 		// If oldBase specifies the root directory, use an empty string as
314 317
 		// oldBase instead so that newBase doesn't replace the path separator
... ...
@@ -355,7 +351,7 @@ func rebaseArchiveEntries(srcContent Reader, oldBase, newBase string) Archive {
355 355
 // CopyResource performs an archive copy from the given source path to the
356 356
 // given destination path. The source path MUST exist and the destination
357 357
 // path's parent directory must exist.
358
-func CopyResource(srcPath, dstPath string) error {
358
+func CopyResource(srcPath, dstPath string, followLink bool) error {
359 359
 	var (
360 360
 		srcInfo CopyInfo
361 361
 		err     error
... ...
@@ -369,7 +365,7 @@ func CopyResource(srcPath, dstPath string) error {
369 369
 	srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
370 370
 	dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
371 371
 
372
-	if srcInfo, err = CopyInfoSourcePath(srcPath); err != nil {
372
+	if srcInfo, err = CopyInfoSourcePath(srcPath, followLink); err != nil {
373 373
 		return err
374 374
 	}
375 375
 
... ...
@@ -405,3 +401,58 @@ func CopyTo(content Reader, srcInfo CopyInfo, dstPath string) error {
405 405
 
406 406
 	return Untar(copyArchive, dstDir, options)
407 407
 }
408
+
409
+// ResolveHostSourcePath decides real path need to be copied with parameters such as
410
+// whether to follow symbol link or not, if followLink is true, resolvedPath will return
411
+// link target of any symbol link file, else it will only resolve symlink of directory
412
+// but return symbol link file itself without resolving.
413
+func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseName string, err error) {
414
+	if followLink {
415
+		resolvedPath, err = filepath.EvalSymlinks(path)
416
+		if err != nil {
417
+			return
418
+		}
419
+
420
+		resolvedPath, rebaseName = GetRebaseName(path, resolvedPath)
421
+	} else {
422
+		dirPath, basePath := filepath.Split(path)
423
+
424
+		// if not follow symbol link, then resolve symbol link of parent dir
425
+		var resolvedDirPath string
426
+		resolvedDirPath, err = filepath.EvalSymlinks(dirPath)
427
+		if err != nil {
428
+			return
429
+		}
430
+		// resolvedDirPath will have been cleaned (no trailing path separators) so
431
+		// we can manually join it with the base path element.
432
+		resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath
433
+		if hasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) {
434
+			rebaseName = filepath.Base(path)
435
+		}
436
+	}
437
+	return resolvedPath, rebaseName, nil
438
+}
439
+
440
+// GetRebaseName normalizes and compares path and resolvedPath,
441
+// return completed resolved path and rebased file name
442
+func GetRebaseName(path, resolvedPath string) (string, string) {
443
+	// linkTarget will have been cleaned (no trailing path separators and dot) so
444
+	// we can manually join it with them
445
+	var rebaseName string
446
+	if specifiesCurrentDir(path) && !specifiesCurrentDir(resolvedPath) {
447
+		resolvedPath += string(filepath.Separator) + "."
448
+	}
449
+
450
+	if hasTrailingPathSeparator(path) && !hasTrailingPathSeparator(resolvedPath) {
451
+		resolvedPath += string(filepath.Separator)
452
+	}
453
+
454
+	if filepath.Base(path) != filepath.Base(resolvedPath) {
455
+		// In the case where the path had a trailing separator and a symlink
456
+		// evaluation has changed the last path component, we will need to
457
+		// rebase the name in the archive that is being copied to match the
458
+		// originally requested name.
459
+		rebaseName = filepath.Base(path)
460
+	}
461
+	return resolvedPath, rebaseName
462
+}
... ...
@@ -120,9 +120,15 @@ func logDirContents(t *testing.T, dirPath string) {
120 120
 }
121 121
 
122 122
 func testCopyHelper(t *testing.T, srcPath, dstPath string) (err error) {
123
-	t.Logf("copying from %q to %q", srcPath, dstPath)
123
+	t.Logf("copying from %q to %q (not follow symbol link)", srcPath, dstPath)
124 124
 
125
-	return CopyResource(srcPath, dstPath)
125
+	return CopyResource(srcPath, dstPath, false)
126
+}
127
+
128
+func testCopyHelperFSym(t *testing.T, srcPath, dstPath string) (err error) {
129
+	t.Logf("copying from %q to %q (follow symbol link)", srcPath, dstPath)
130
+
131
+	return CopyResource(srcPath, dstPath, true)
126 132
 }
127 133
 
128 134
 // Basic assumptions about SRC and DST:
... ...
@@ -138,7 +144,7 @@ func TestCopyErrSrcNotExists(t *testing.T) {
138 138
 	tmpDirA, tmpDirB := getTestTempDirs(t)
139 139
 	defer removeAllPaths(tmpDirA, tmpDirB)
140 140
 
141
-	if _, err := CopyInfoSourcePath(filepath.Join(tmpDirA, "file1")); !os.IsNotExist(err) {
141
+	if _, err := CopyInfoSourcePath(filepath.Join(tmpDirA, "file1"), false); !os.IsNotExist(err) {
142 142
 		t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
143 143
 	}
144 144
 }
... ...
@@ -152,7 +158,7 @@ func TestCopyErrSrcNotDir(t *testing.T) {
152 152
 	// Load A with some sample files and directories.
153 153
 	createSampleDir(t, tmpDirA)
154 154
 
155
-	if _, err := CopyInfoSourcePath(joinTrailingSep(tmpDirA, "file1")); !isNotDir(err) {
155
+	if _, err := CopyInfoSourcePath(joinTrailingSep(tmpDirA, "file1"), false); !isNotDir(err) {
156 156
 		t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
157 157
 	}
158 158
 }
... ...
@@ -286,6 +292,27 @@ func TestCopyCaseA(t *testing.T) {
286 286
 	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
287 287
 		t.Fatal(err)
288 288
 	}
289
+	os.Remove(dstPath)
290
+
291
+	symlinkPath := filepath.Join(tmpDirA, "symlink3")
292
+	symlinkPath1 := filepath.Join(tmpDirA, "symlink4")
293
+	linkTarget := filepath.Join(tmpDirA, "file1")
294
+
295
+	if err = testCopyHelperFSym(t, symlinkPath, dstPath); err != nil {
296
+		t.Fatalf("unexpected error %T: %s", err, err)
297
+	}
298
+
299
+	if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
300
+		t.Fatal(err)
301
+	}
302
+	os.Remove(dstPath)
303
+	if err = testCopyHelperFSym(t, symlinkPath1, dstPath); err != nil {
304
+		t.Fatalf("unexpected error %T: %s", err, err)
305
+	}
306
+
307
+	if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
308
+		t.Fatal(err)
309
+	}
289 310
 }
290 311
 
291 312
 // B. SRC specifies a file and DST (with trailing path separator) doesn't
... ...
@@ -310,6 +337,16 @@ func TestCopyCaseB(t *testing.T) {
310 310
 	if err != ErrDirNotExists {
311 311
 		t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err)
312 312
 	}
313
+
314
+	symlinkPath := filepath.Join(tmpDirA, "symlink3")
315
+
316
+	if err = testCopyHelperFSym(t, symlinkPath, dstDir); err == nil {
317
+		t.Fatal("expected ErrDirNotExists error, but got nil instead")
318
+	}
319
+	if err != ErrDirNotExists {
320
+		t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err)
321
+	}
322
+
313 323
 }
314 324
 
315 325
 // C. SRC specifies a file and DST exists as a file. This should overwrite
... ...
@@ -341,6 +378,44 @@ func TestCopyCaseC(t *testing.T) {
341 341
 	}
342 342
 }
343 343
 
344
+// C. Symbol link following version:
345
+//    SRC specifies a file and DST exists as a file. This should overwrite
346
+//    the file at DST with the contents of the source file.
347
+func TestCopyCaseCFSym(t *testing.T) {
348
+	tmpDirA, tmpDirB := getTestTempDirs(t)
349
+	defer removeAllPaths(tmpDirA, tmpDirB)
350
+
351
+	// Load A and B with some sample files and directories.
352
+	createSampleDir(t, tmpDirA)
353
+	createSampleDir(t, tmpDirB)
354
+
355
+	symlinkPathBad := filepath.Join(tmpDirA, "symlink1")
356
+	symlinkPath := filepath.Join(tmpDirA, "symlink3")
357
+	linkTarget := filepath.Join(tmpDirA, "file1")
358
+	dstPath := filepath.Join(tmpDirB, "file2")
359
+
360
+	var err error
361
+
362
+	// first to test broken link
363
+	if err = testCopyHelperFSym(t, symlinkPathBad, dstPath); err == nil {
364
+		t.Fatalf("unexpected error %T: %s", err, err)
365
+	}
366
+
367
+	// test symbol link -> symbol link -> target
368
+	// Ensure they start out different.
369
+	if err = fileContentsEqual(t, linkTarget, dstPath); err == nil {
370
+		t.Fatal("expected different file contents")
371
+	}
372
+
373
+	if err = testCopyHelperFSym(t, symlinkPath, dstPath); err != nil {
374
+		t.Fatalf("unexpected error %T: %s", err, err)
375
+	}
376
+
377
+	if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
378
+		t.Fatal(err)
379
+	}
380
+}
381
+
344 382
 // D. SRC specifies a file and DST exists as a directory. This should place
345 383
 //    a copy of the source file inside it using the basename from SRC. Ensure
346 384
 //    this works whether DST has a trailing path separator or not.
... ...
@@ -392,6 +467,59 @@ func TestCopyCaseD(t *testing.T) {
392 392
 	}
393 393
 }
394 394
 
395
+// D. Symbol link following version:
396
+//    SRC specifies a file and DST exists as a directory. This should place
397
+//    a copy of the source file inside it using the basename from SRC. Ensure
398
+//    this works whether DST has a trailing path separator or not.
399
+func TestCopyCaseDFSym(t *testing.T) {
400
+	tmpDirA, tmpDirB := getTestTempDirs(t)
401
+	defer removeAllPaths(tmpDirA, tmpDirB)
402
+
403
+	// Load A and B with some sample files and directories.
404
+	createSampleDir(t, tmpDirA)
405
+	createSampleDir(t, tmpDirB)
406
+
407
+	srcPath := filepath.Join(tmpDirA, "symlink4")
408
+	linkTarget := filepath.Join(tmpDirA, "file1")
409
+	dstDir := filepath.Join(tmpDirB, "dir1")
410
+	dstPath := filepath.Join(dstDir, "symlink4")
411
+
412
+	var err error
413
+
414
+	// Ensure that dstPath doesn't exist.
415
+	if _, err = os.Stat(dstPath); !os.IsNotExist(err) {
416
+		t.Fatalf("did not expect dstPath %q to exist", dstPath)
417
+	}
418
+
419
+	if err = testCopyHelperFSym(t, srcPath, dstDir); err != nil {
420
+		t.Fatalf("unexpected error %T: %s", err, err)
421
+	}
422
+
423
+	if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
424
+		t.Fatal(err)
425
+	}
426
+
427
+	// Now try again but using a trailing path separator for dstDir.
428
+
429
+	if err = os.RemoveAll(dstDir); err != nil {
430
+		t.Fatalf("unable to remove dstDir: %s", err)
431
+	}
432
+
433
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
434
+		t.Fatalf("unable to make dstDir: %s", err)
435
+	}
436
+
437
+	dstDir = joinTrailingSep(tmpDirB, "dir1")
438
+
439
+	if err = testCopyHelperFSym(t, srcPath, dstDir); err != nil {
440
+		t.Fatalf("unexpected error %T: %s", err, err)
441
+	}
442
+
443
+	if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
444
+		t.Fatal(err)
445
+	}
446
+}
447
+
395 448
 // E. SRC specifies a directory and DST does not exist. This should create a
396 449
 //    directory at DST and copy the contents of the SRC directory into the DST
397 450
 //    directory. Ensure this works whether DST has a trailing path separator or
... ...
@@ -436,6 +564,52 @@ func TestCopyCaseE(t *testing.T) {
436 436
 	}
437 437
 }
438 438
 
439
+// E. Symbol link following version:
440
+//    SRC specifies a directory and DST does not exist. This should create a
441
+//    directory at DST and copy the contents of the SRC directory into the DST
442
+//    directory. Ensure this works whether DST has a trailing path separator or
443
+//    not.
444
+func TestCopyCaseEFSym(t *testing.T) {
445
+	tmpDirA, tmpDirB := getTestTempDirs(t)
446
+	defer removeAllPaths(tmpDirA, tmpDirB)
447
+
448
+	// Load A with some sample files and directories.
449
+	createSampleDir(t, tmpDirA)
450
+
451
+	srcDir := filepath.Join(tmpDirA, "dirSymlink")
452
+	linkTarget := filepath.Join(tmpDirA, "dir1")
453
+	dstDir := filepath.Join(tmpDirB, "testDir")
454
+
455
+	var err error
456
+
457
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
458
+		t.Fatalf("unexpected error %T: %s", err, err)
459
+	}
460
+
461
+	if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
462
+		t.Log("dir contents not equal")
463
+		logDirContents(t, tmpDirA)
464
+		logDirContents(t, tmpDirB)
465
+		t.Fatal(err)
466
+	}
467
+
468
+	// Now try again but using a trailing path separator for dstDir.
469
+
470
+	if err = os.RemoveAll(dstDir); err != nil {
471
+		t.Fatalf("unable to remove dstDir: %s", err)
472
+	}
473
+
474
+	dstDir = joinTrailingSep(tmpDirB, "testDir")
475
+
476
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
477
+		t.Fatalf("unexpected error %T: %s", err, err)
478
+	}
479
+
480
+	if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
481
+		t.Fatal(err)
482
+	}
483
+}
484
+
439 485
 // F. SRC specifies a directory and DST exists as a file. This should cause an
440 486
 //    error as it is not possible to overwrite a file with a directory.
441 487
 func TestCopyCaseF(t *testing.T) {
... ...
@@ -447,6 +621,7 @@ func TestCopyCaseF(t *testing.T) {
447 447
 	createSampleDir(t, tmpDirB)
448 448
 
449 449
 	srcDir := filepath.Join(tmpDirA, "dir1")
450
+	symSrcDir := filepath.Join(tmpDirA, "dirSymlink")
450 451
 	dstFile := filepath.Join(tmpDirB, "file1")
451 452
 
452 453
 	var err error
... ...
@@ -458,6 +633,15 @@ func TestCopyCaseF(t *testing.T) {
458 458
 	if err != ErrCannotCopyDir {
459 459
 		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
460 460
 	}
461
+
462
+	// now test with symbol link
463
+	if err = testCopyHelperFSym(t, symSrcDir, dstFile); err == nil {
464
+		t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
465
+	}
466
+
467
+	if err != ErrCannotCopyDir {
468
+		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
469
+	}
461 470
 }
462 471
 
463 472
 // G. SRC specifies a directory and DST exists as a directory. This should copy
... ...
@@ -506,6 +690,54 @@ func TestCopyCaseG(t *testing.T) {
506 506
 	}
507 507
 }
508 508
 
509
+// G. Symbol link version:
510
+//    SRC specifies a directory and DST exists as a directory. This should copy
511
+//    the SRC directory and all its contents to the DST directory. Ensure this
512
+//    works whether DST has a trailing path separator or not.
513
+func TestCopyCaseGFSym(t *testing.T) {
514
+	tmpDirA, tmpDirB := getTestTempDirs(t)
515
+	defer removeAllPaths(tmpDirA, tmpDirB)
516
+
517
+	// Load A and B with some sample files and directories.
518
+	createSampleDir(t, tmpDirA)
519
+	createSampleDir(t, tmpDirB)
520
+
521
+	srcDir := filepath.Join(tmpDirA, "dirSymlink")
522
+	linkTarget := filepath.Join(tmpDirA, "dir1")
523
+	dstDir := filepath.Join(tmpDirB, "dir2")
524
+	resultDir := filepath.Join(dstDir, "dirSymlink")
525
+
526
+	var err error
527
+
528
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
529
+		t.Fatalf("unexpected error %T: %s", err, err)
530
+	}
531
+
532
+	if err = dirContentsEqual(t, resultDir, linkTarget); err != nil {
533
+		t.Fatal(err)
534
+	}
535
+
536
+	// Now try again but using a trailing path separator for dstDir.
537
+
538
+	if err = os.RemoveAll(dstDir); err != nil {
539
+		t.Fatalf("unable to remove dstDir: %s", err)
540
+	}
541
+
542
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
543
+		t.Fatalf("unable to make dstDir: %s", err)
544
+	}
545
+
546
+	dstDir = joinTrailingSep(tmpDirB, "dir2")
547
+
548
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
549
+		t.Fatalf("unexpected error %T: %s", err, err)
550
+	}
551
+
552
+	if err = dirContentsEqual(t, resultDir, linkTarget); err != nil {
553
+		t.Fatal(err)
554
+	}
555
+}
556
+
509 557
 // H. SRC specifies a directory's contents only and DST does not exist. This
510 558
 //    should create a directory at DST and copy the contents of the SRC
511 559
 //    directory (but not the directory itself) into the DST directory. Ensure
... ...
@@ -553,6 +785,55 @@ func TestCopyCaseH(t *testing.T) {
553 553
 	}
554 554
 }
555 555
 
556
+// H. Symbol link following version:
557
+//    SRC specifies a directory's contents only and DST does not exist. This
558
+//    should create a directory at DST and copy the contents of the SRC
559
+//    directory (but not the directory itself) into the DST directory. Ensure
560
+//    this works whether DST has a trailing path separator or not.
561
+func TestCopyCaseHFSym(t *testing.T) {
562
+	tmpDirA, tmpDirB := getTestTempDirs(t)
563
+	defer removeAllPaths(tmpDirA, tmpDirB)
564
+
565
+	// Load A with some sample files and directories.
566
+	createSampleDir(t, tmpDirA)
567
+
568
+	srcDir := joinTrailingSep(tmpDirA, "dirSymlink") + "."
569
+	linkTarget := filepath.Join(tmpDirA, "dir1")
570
+	dstDir := filepath.Join(tmpDirB, "testDir")
571
+
572
+	var err error
573
+
574
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
575
+		t.Fatalf("unexpected error %T: %s", err, err)
576
+	}
577
+
578
+	if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
579
+		t.Log("dir contents not equal")
580
+		logDirContents(t, tmpDirA)
581
+		logDirContents(t, tmpDirB)
582
+		t.Fatal(err)
583
+	}
584
+
585
+	// Now try again but using a trailing path separator for dstDir.
586
+
587
+	if err = os.RemoveAll(dstDir); err != nil {
588
+		t.Fatalf("unable to remove dstDir: %s", err)
589
+	}
590
+
591
+	dstDir = joinTrailingSep(tmpDirB, "testDir")
592
+
593
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
594
+		t.Fatalf("unexpected error %T: %s", err, err)
595
+	}
596
+
597
+	if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
598
+		t.Log("dir contents not equal")
599
+		logDirContents(t, tmpDirA)
600
+		logDirContents(t, tmpDirB)
601
+		t.Fatal(err)
602
+	}
603
+}
604
+
556 605
 // I. SRC specifies a directory's contents only and DST exists as a file. This
557 606
 //    should cause an error as it is not possible to overwrite a file with a
558 607
 //    directory.
... ...
@@ -565,6 +846,7 @@ func TestCopyCaseI(t *testing.T) {
565 565
 	createSampleDir(t, tmpDirB)
566 566
 
567 567
 	srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
568
+	symSrcDir := filepath.Join(tmpDirB, "dirSymlink")
568 569
 	dstFile := filepath.Join(tmpDirB, "file1")
569 570
 
570 571
 	var err error
... ...
@@ -576,6 +858,15 @@ func TestCopyCaseI(t *testing.T) {
576 576
 	if err != ErrCannotCopyDir {
577 577
 		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
578 578
 	}
579
+
580
+	// now try with symbol link of dir
581
+	if err = testCopyHelperFSym(t, symSrcDir, dstFile); err == nil {
582
+		t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
583
+	}
584
+
585
+	if err != ErrCannotCopyDir {
586
+		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
587
+	}
579 588
 }
580 589
 
581 590
 // J. SRC specifies a directory's contents only and DST exists as a directory.
... ...
@@ -595,6 +886,11 @@ func TestCopyCaseJ(t *testing.T) {
595 595
 
596 596
 	var err error
597 597
 
598
+	// first to create an empty dir
599
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
600
+		t.Fatalf("unable to make dstDir: %s", err)
601
+	}
602
+
598 603
 	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
599 604
 		t.Fatalf("unexpected error %T: %s", err, err)
600 605
 	}
... ...
@@ -623,3 +919,56 @@ func TestCopyCaseJ(t *testing.T) {
623 623
 		t.Fatal(err)
624 624
 	}
625 625
 }
626
+
627
+// J. Symbol link following version:
628
+//    SRC specifies a directory's contents only and DST exists as a directory.
629
+//    This should copy the contents of the SRC directory (but not the directory
630
+//    itself) into the DST directory. Ensure this works whether DST has a
631
+//    trailing path separator or not.
632
+func TestCopyCaseJFSym(t *testing.T) {
633
+	tmpDirA, tmpDirB := getTestTempDirs(t)
634
+	defer removeAllPaths(tmpDirA, tmpDirB)
635
+
636
+	// Load A and B with some sample files and directories.
637
+	createSampleDir(t, tmpDirA)
638
+	createSampleDir(t, tmpDirB)
639
+
640
+	srcDir := joinTrailingSep(tmpDirA, "dirSymlink") + "."
641
+	linkTarget := filepath.Join(tmpDirA, "dir1")
642
+	dstDir := filepath.Join(tmpDirB, "dir5")
643
+
644
+	var err error
645
+
646
+	// first to create an empty dir
647
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
648
+		t.Fatalf("unable to make dstDir: %s", err)
649
+	}
650
+
651
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
652
+		t.Fatalf("unexpected error %T: %s", err, err)
653
+	}
654
+
655
+	if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
656
+		t.Fatal(err)
657
+	}
658
+
659
+	// Now try again but using a trailing path separator for dstDir.
660
+
661
+	if err = os.RemoveAll(dstDir); err != nil {
662
+		t.Fatalf("unable to remove dstDir: %s", err)
663
+	}
664
+
665
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
666
+		t.Fatalf("unable to make dstDir: %s", err)
667
+	}
668
+
669
+	dstDir = joinTrailingSep(tmpDirB, "dir5")
670
+
671
+	if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
672
+		t.Fatalf("unexpected error %T: %s", err, err)
673
+	}
674
+
675
+	if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
676
+		t.Fatal(err)
677
+	}
678
+}