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>
| ... | ... |
@@ -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 |
+} |