Browse code

Fix `docker cp` Behavior With Symlinks

[pkg/archive] Update archive/copy path handling

- Remove unused TarOptions.Name field.
- Add new TarOptions.RebaseNames field.
- Update some of the logic around path dir/base splitting.
- Update some of the logic behind archive entry name rebasing.

[api/types] Add LinkTarget field to PathStat

[daemon] Fix stat, archive, extract of symlinks

These operations *should* resolve symlinks that are in the path but if the
resource itself is a symlink then it *should not* be resolved. This patch
puts this logic into a common function `resolvePath` which resolves symlinks
of the path's dir in scope of the container rootfs but does not resolve the
final element of the path. Now archive, extract, and stat operations will
return symlinks if the path is indeed a symlink.

[api/client] Update cp path hanling

[docs/reference/api] Update description of stat

Add the linkTarget field to the header of the archive endpoint.
Remove path field.

[integration-cli] Fix/Add cp symlink test cases

Copying a symlink should do just that: copy the symlink NOT
copy the target of the symlink. Also, the resulting file from
the copy should have the name of the symlink NOT the name of
the target file.

Copying to a symlink should copy to the symlink target and not
modify the symlink itself.

Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)

Josh Hawn authored on 2015/07/25 06:12:55
Showing 14 changed files
... ...
@@ -232,6 +232,20 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
232 232
 	// Prepare destination copy info by stat-ing the container path.
233 233
 	dstInfo := archive.CopyInfo{Path: dstPath}
234 234
 	dstStat, err := cli.statContainerPath(dstContainer, dstPath)
235
+
236
+	// If the destination is a symbolic link, we should evaluate it.
237
+	if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
238
+		linkTarget := dstStat.LinkTarget
239
+		if !filepath.IsAbs(linkTarget) {
240
+			// Join with the parent directory.
241
+			dstParent, _ := archive.SplitPathDirEntry(dstPath)
242
+			linkTarget = filepath.Join(dstParent, linkTarget)
243
+		}
244
+
245
+		dstInfo.Path = linkTarget
246
+		dstStat, err = cli.statContainerPath(dstContainer, linkTarget)
247
+	}
248
+
235 249
 	// Ignore any error and assume that the parent directory of the destination
236 250
 	// path exists, in which case the copy may still succeed. If there is any
237 251
 	// type of conflict (e.g., non-directory overwriting an existing directory
... ...
@@ -242,15 +256,26 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
242 242
 		dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
243 243
 	}
244 244
 
245
-	var content io.Reader
245
+	var (
246
+		content         io.Reader
247
+		resolvedDstPath string
248
+	)
249
+
246 250
 	if srcPath == "-" {
247 251
 		// Use STDIN.
248 252
 		content = os.Stdin
253
+		resolvedDstPath = dstInfo.Path
249 254
 		if !dstInfo.IsDir {
250 255
 			return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
251 256
 		}
252 257
 	} else {
253
-		srcArchive, err := archive.TarResource(srcPath)
258
+		// Prepare source copy info.
259
+		srcInfo, err := archive.CopyInfoSourcePath(srcPath)
260
+		if err != nil {
261
+			return err
262
+		}
263
+
264
+		srcArchive, err := archive.TarResource(srcInfo)
254 265
 		if err != nil {
255 266
 			return err
256 267
 		}
... ...
@@ -262,12 +287,6 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
262 262
 		// it to the specified directory in the container we get the disired
263 263
 		// copy behavior.
264 264
 
265
-		// Prepare source copy info.
266
-		srcInfo, err := archive.CopyInfoStatPath(srcPath, true)
267
-		if err != nil {
268
-			return err
269
-		}
270
-
271 265
 		// See comments in the implementation of `archive.PrepareArchiveCopy`
272 266
 		// for exactly what goes into deciding how and whether the source
273 267
 		// archive needs to be altered for the correct copy behavior when it is
... ...
@@ -280,12 +299,12 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
280 280
 		}
281 281
 		defer preparedArchive.Close()
282 282
 
283
-		dstPath = dstDir
283
+		resolvedDstPath = dstDir
284 284
 		content = preparedArchive
285 285
 	}
286 286
 
287 287
 	query := make(url.Values, 2)
288
-	query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
288
+	query.Set("path", filepath.ToSlash(resolvedDstPath)) // Normalize the paths used in the API.
289 289
 	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
290 290
 	query.Set("noOverwriteDirNonDir", "true")
291 291
 
... ...
@@ -130,14 +130,13 @@ type CopyConfig struct {
130 130
 
131 131
 // ContainerPathStat is used to encode the header from
132 132
 // 	GET /containers/{name:.*}/archive
133
-// "name" is the file or directory name.
134
-// "path" is the absolute path to the resource in the container.
133
+// "name" is basename of the resource.
135 134
 type ContainerPathStat struct {
136
-	Name  string      `json:"name"`
137
-	Path  string      `json:"path"`
138
-	Size  int64       `json:"size"`
139
-	Mode  os.FileMode `json:"mode"`
140
-	Mtime time.Time   `json:"mtime"`
135
+	Name       string      `json:"name"`
136
+	Size       int64       `json:"size"`
137
+	Mode       os.FileMode `json:"mode"`
138
+	Mtime      time.Time   `json:"mtime"`
139
+	LinkTarget string      `json:"linkTarget"`
141 140
 }
142 141
 
143 142
 // GET "/containers/{name:.*}/top"
... ...
@@ -70,6 +70,66 @@ func (daemon *Daemon) ContainerExtractToDir(name, path string, noOverwriteDirNon
70 70
 	return container.ExtractToDir(path, noOverwriteDirNonDir, content)
71 71
 }
72 72
 
73
+// resolvePath resolves the given path in the container to a resource on the
74
+// host. Returns a resolved path (absolute path to the resource on the host),
75
+// the absolute path to the resource relative to the container's rootfs, and
76
+// a error if the path points to outside the container's rootfs.
77
+func (container *Container) resolvePath(path string) (resolvedPath, absPath string, err error) {
78
+	// Consider the given path as an absolute path in the container.
79
+	absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path)
80
+
81
+	// Split the absPath into its Directory and Base components. We will
82
+	// resolve the dir in the scope of the container then append the base.
83
+	dirPath, basePath := filepath.Split(absPath)
84
+
85
+	resolvedDirPath, err := container.GetResourcePath(dirPath)
86
+	if err != nil {
87
+		return "", "", err
88
+	}
89
+
90
+	// resolvedDirPath will have been cleaned (no trailing path separators) so
91
+	// we can manually join it with the base path element.
92
+	resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath
93
+
94
+	return resolvedPath, absPath, nil
95
+}
96
+
97
+// statPath is the unexported version of StatPath. Locks and mounts should
98
+// be aquired before calling this method and the given path should be fully
99
+// resolved to a path on the host corresponding to the given absolute path
100
+// inside the container.
101
+func (container *Container) statPath(resolvedPath, absPath string) (stat *types.ContainerPathStat, err error) {
102
+	lstat, err := os.Lstat(resolvedPath)
103
+	if err != nil {
104
+		return nil, err
105
+	}
106
+
107
+	var linkTarget string
108
+	if lstat.Mode()&os.ModeSymlink != 0 {
109
+		// Fully evaluate the symlink in the scope of the container rootfs.
110
+		hostPath, err := container.GetResourcePath(absPath)
111
+		if err != nil {
112
+			return nil, err
113
+		}
114
+
115
+		linkTarget, err = filepath.Rel(container.basefs, hostPath)
116
+		if err != nil {
117
+			return nil, err
118
+		}
119
+
120
+		// Make it an absolute path.
121
+		linkTarget = filepath.Join(string(filepath.Separator), linkTarget)
122
+	}
123
+
124
+	return &types.ContainerPathStat{
125
+		Name:       filepath.Base(absPath),
126
+		Size:       lstat.Size(),
127
+		Mode:       lstat.Mode(),
128
+		Mtime:      lstat.ModTime(),
129
+		LinkTarget: linkTarget,
130
+	}, nil
131
+}
132
+
73 133
 // StatPath stats the filesystem resource at the specified path in this
74 134
 // container. Returns stat info about the resource.
75 135
 func (container *Container) StatPath(path string) (stat *types.ContainerPathStat, err error) {
... ...
@@ -87,39 +147,12 @@ func (container *Container) StatPath(path string) (stat *types.ContainerPathStat
87 87
 		return nil, err
88 88
 	}
89 89
 
90
-	// Consider the given path as an absolute path in the container.
91
-	absPath := path
92
-	if !filepath.IsAbs(absPath) {
93
-		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(os.PathSeparator), path), path)
94
-	}
95
-
96
-	resolvedPath, err := container.GetResourcePath(absPath)
97
-	if err != nil {
98
-		return nil, err
99
-	}
100
-
101
-	// A trailing "." or separator has important meaning. For example, if
102
-	// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
103
-	// will stat the link itself, while `os.Lstat("foo/")` will stat the link
104
-	// target. If the basename of the path is ".", it means to archive the
105
-	// contents of the directory with "." as the first path component rather
106
-	// than the name of the directory. This would cause extraction of the
107
-	// archive to *not* make another directory, but instead use the current
108
-	// directory.
109
-	resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
110
-
111
-	lstat, err := os.Lstat(resolvedPath)
90
+	resolvedPath, absPath, err := container.resolvePath(path)
112 91
 	if err != nil {
113 92
 		return nil, err
114 93
 	}
115 94
 
116
-	return &types.ContainerPathStat{
117
-		Name:  lstat.Name(),
118
-		Path:  absPath,
119
-		Size:  lstat.Size(),
120
-		Mode:  lstat.Mode(),
121
-		Mtime: lstat.ModTime(),
122
-	}, nil
95
+	return container.statPath(resolvedPath, absPath)
123 96
 }
124 97
 
125 98
 // ArchivePath creates an archive of the filesystem resource at the specified
... ...
@@ -154,41 +187,25 @@ func (container *Container) ArchivePath(path string) (content io.ReadCloser, sta
154 154
 		return nil, nil, err
155 155
 	}
156 156
 
157
-	// Consider the given path as an absolute path in the container.
158
-	absPath := path
159
-	if !filepath.IsAbs(absPath) {
160
-		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(os.PathSeparator), path), path)
161
-	}
162
-
163
-	resolvedPath, err := container.GetResourcePath(absPath)
157
+	resolvedPath, absPath, err := container.resolvePath(path)
164 158
 	if err != nil {
165 159
 		return nil, nil, err
166 160
 	}
167 161
 
168
-	// A trailing "." or separator has important meaning. For example, if
169
-	// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
170
-	// will stat the link itself, while `os.Lstat("foo/")` will stat the link
171
-	// target. If the basename of the path is ".", it means to archive the
172
-	// contents of the directory with "." as the first path component rather
173
-	// than the name of the directory. This would cause extraction of the
174
-	// archive to *not* make another directory, but instead use the current
175
-	// directory.
176
-	resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
177
-
178
-	lstat, err := os.Lstat(resolvedPath)
162
+	stat, err = container.statPath(resolvedPath, absPath)
179 163
 	if err != nil {
180 164
 		return nil, nil, err
181 165
 	}
182 166
 
183
-	stat = &types.ContainerPathStat{
184
-		Name:  lstat.Name(),
185
-		Path:  absPath,
186
-		Size:  lstat.Size(),
187
-		Mode:  lstat.Mode(),
188
-		Mtime: lstat.ModTime(),
189
-	}
190
-
191
-	data, err := archive.TarResource(resolvedPath)
167
+	// We need to rebase the archive entries if the last element of the
168
+	// resolved path was a symlink that was evaluated and is now different
169
+	// than the requested path. For example, if the given path was "/foo/bar/",
170
+	// but it resolved to "/var/lib/docker/containers/{id}/foo/baz/", we want
171
+	// to ensure that the archive entries start with "bar" and not "baz". This
172
+	// also catches the case when the root directory of the container is
173
+	// requested: we want the archive entries to start with "/" and not the
174
+	// container ID.
175
+	data, err := archive.TarResourceRebase(resolvedPath, filepath.Base(absPath))
192 176
 	if err != nil {
193 177
 		return nil, nil, err
194 178
 	}
... ...
@@ -227,27 +244,21 @@ func (container *Container) ExtractToDir(path string, noOverwriteDirNonDir bool,
227 227
 		return err
228 228
 	}
229 229
 
230
+	// The destination path needs to be resolved to a host path, with all
231
+	// symbolic links followed in the scope of the container's rootfs. Note
232
+	// that we do not use `container.resolvePath(path)` here because we need
233
+	// to also evaluate the last path element if it is a symlink. This is so
234
+	// that you can extract an archive to a symlink that points to a directory.
235
+
230 236
 	// Consider the given path as an absolute path in the container.
231
-	absPath := path
232
-	if !filepath.IsAbs(absPath) {
233
-		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(os.PathSeparator), path), path)
234
-	}
237
+	absPath := archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path)
235 238
 
239
+	// This will evaluate the last path element if it is a symlink.
236 240
 	resolvedPath, err := container.GetResourcePath(absPath)
237 241
 	if err != nil {
238 242
 		return err
239 243
 	}
240 244
 
241
-	// A trailing "." or separator has important meaning. For example, if
242
-	// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
243
-	// will stat the link itself, while `os.Lstat("foo/")` will stat the link
244
-	// target. If the basename of the path is ".", it means to archive the
245
-	// contents of the directory with "." as the first path component rather
246
-	// than the name of the directory. This would cause extraction of the
247
-	// archive to *not* make another directory, but instead use the current
248
-	// directory.
249
-	resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
250
-
251 245
 	stat, err := os.Lstat(resolvedPath)
252 246
 	if err != nil {
253 247
 		return err
... ...
@@ -257,15 +268,20 @@ func (container *Container) ExtractToDir(path string, noOverwriteDirNonDir bool,
257 257
 		return ErrExtractPointNotDirectory
258 258
 	}
259 259
 
260
+	// Need to check if the path is in a volume. If it is, it cannot be in a
261
+	// read-only volume. If it is not in a volume, the container cannot be
262
+	// configured with a read-only rootfs.
263
+
264
+	// Use the resolved path relative to the container rootfs as the new
265
+	// absPath. This way we fully follow any symlinks in a volume that may
266
+	// lead back outside the volume.
260 267
 	baseRel, err := filepath.Rel(container.basefs, resolvedPath)
261 268
 	if err != nil {
262 269
 		return err
263 270
 	}
264
-	absPath = filepath.Join(string(os.PathSeparator), baseRel)
271
+	// Make it an absolute path.
272
+	absPath = filepath.Join(string(filepath.Separator), baseRel)
265 273
 
266
-	// Need to check if the path is in a volume. If it is, it cannot be in a
267
-	// read-only volume. If it is not in a volume, the container cannot be
268
-	// configured with a read-only rootfs.
269 274
 	toVolume, err := checkIfPathIsInAVolume(container, absPath)
270 275
 	if err != nil {
271 276
 		return err
... ...
@@ -1109,7 +1109,7 @@ Query Parameters:
1109 1109
 
1110 1110
         HTTP/1.1 200 OK
1111 1111
         Content-Type: application/x-tar
1112
-        X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInBhdGgiOiIvcm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oifQ==
1112
+        X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0=
1113 1113
 
1114 1114
         {{ TAR STREAM }}
1115 1115
 
... ...
@@ -1120,10 +1120,10 @@ JSON object (whitespace added for readability):
1120 1120
 
1121 1121
         {
1122 1122
             "name": "root",
1123
-            "path": "/root",
1124 1123
             "size": 4096,
1125 1124
             "mode": 2147484096,
1126
-            "mtime": "2014-02-27T20:51:23Z"
1125
+            "mtime": "2014-02-27T20:51:23Z",
1126
+            "linkTarget": ""
1127 1127
         }
1128 1128
 
1129 1129
 A `HEAD` request can also be made to this endpoint if only this information is
... ...
@@ -1109,7 +1109,7 @@ Query Parameters:
1109 1109
 
1110 1110
         HTTP/1.1 200 OK
1111 1111
         Content-Type: application/x-tar
1112
-        X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInBhdGgiOiIvcm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oifQ==
1112
+        X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0=
1113 1113
 
1114 1114
         {{ TAR STREAM }}
1115 1115
 
... ...
@@ -1120,10 +1120,10 @@ JSON object (whitespace added for readability):
1120 1120
 
1121 1121
         {
1122 1122
             "name": "root",
1123
-            "path": "/root",
1124 1123
             "size": 4096,
1125 1124
             "mode": 2147484096,
1126
-            "mtime": "2014-02-27T20:51:23Z"
1125
+            "mtime": "2014-02-27T20:51:23Z",
1126
+            "linkTarget": ""
1127 1127
         }
1128 1128
 
1129 1129
 A `HEAD` request can also be made to this endpoint if only this information is
... ...
@@ -4,9 +4,11 @@ import (
4 4
 	"archive/tar"
5 5
 	"bytes"
6 6
 	"encoding/json"
7
+	"fmt"
7 8
 	"io"
8 9
 	"net/http"
9 10
 	"net/http/httputil"
11
+	"net/url"
10 12
 	"os"
11 13
 	"strconv"
12 14
 	"strings"
... ...
@@ -1697,3 +1699,35 @@ func (s *DockerSuite) TestContainersApiCreateNoHostConfig118(c *check.C) {
1697 1697
 	c.Assert(err, check.IsNil)
1698 1698
 	c.Assert(status, check.Equals, http.StatusCreated)
1699 1699
 }
1700
+
1701
+// Ensure an error occurs when you have a container read-only rootfs but you
1702
+// extract an archive to a symlink in a writable volume which points to a
1703
+// directory outside of the volume.
1704
+func (s *DockerSuite) TestPutContainerArchiveErrSymlinkInVolumeToReadOnlyRootfs(c *check.C) {
1705
+	testRequires(c, SameHostDaemon) // Requires local volume mount bind.
1706
+
1707
+	testVol := getTestDir(c, "test-put-container-archive-err-symlink-in-volume-to-read-only-rootfs-")
1708
+	defer os.RemoveAll(testVol)
1709
+
1710
+	makeTestContentInDir(c, testVol)
1711
+
1712
+	cID := makeTestContainer(c, testContainerOptions{
1713
+		readOnly: true,
1714
+		volumes:  defaultVolumes(testVol), // Our bind mount is at /vol2
1715
+	})
1716
+	defer deleteContainer(cID)
1717
+
1718
+	// Attempt to extract to a symlink in the volume which points to a
1719
+	// directory outside the volume. This should cause an error because the
1720
+	// rootfs is read-only.
1721
+	query := make(url.Values, 1)
1722
+	query.Set("path", "/vol2/symlinkToAbsDir")
1723
+	urlPath := fmt.Sprintf("/v1.20/containers/%s/archive?%s", cID, query.Encode())
1724
+
1725
+	statusCode, body, err := sockRequest("PUT", urlPath, nil)
1726
+	c.Assert(err, check.IsNil)
1727
+
1728
+	if !isCpCannotCopyReadOnly(fmt.Errorf(string(body))) {
1729
+		c.Fatalf("expected ErrContainerRootfsReadonly error, but got %d: %s", statusCode, string(body))
1730
+	}
1731
+}
... ...
@@ -130,6 +130,114 @@ func (s *DockerSuite) TestCpFromErrDstNotDir(c *check.C) {
130 130
 	}
131 131
 }
132 132
 
133
+// Check that copying from a container to a local symlink copies to the symlink
134
+// target and does not overwrite the local symlink itself.
135
+func (s *DockerSuite) TestCpFromSymlinkDestination(c *check.C) {
136
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
137
+	defer deleteContainer(cID)
138
+
139
+	tmpDir := getTestDir(c, "test-cp-from-err-dst-not-dir")
140
+	defer os.RemoveAll(tmpDir)
141
+
142
+	makeTestContentInDir(c, tmpDir)
143
+
144
+	// First, copy a file from the container to a symlink to a file. This
145
+	// should overwrite the symlink target contents with the source contents.
146
+	srcPath := containerCpPath(cID, "/file2")
147
+	dstPath := cpPath(tmpDir, "symlinkToFile1")
148
+
149
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
150
+		c.Fatalf("unexpected error %T: %s", err, err)
151
+	}
152
+
153
+	// The symlink should not have been modified.
154
+	if err := symlinkTargetEquals(c, dstPath, "file1"); err != nil {
155
+		c.Fatal(err)
156
+	}
157
+
158
+	// The file should have the contents of "file2" now.
159
+	if err := fileContentEquals(c, cpPath(tmpDir, "file1"), "file2\n"); err != nil {
160
+		c.Fatal(err)
161
+	}
162
+
163
+	// Next, copy a file from the container to a symlink to a directory. This
164
+	// should copy the file into the symlink target directory.
165
+	dstPath = cpPath(tmpDir, "symlinkToDir1")
166
+
167
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
168
+		c.Fatalf("unexpected error %T: %s", err, err)
169
+	}
170
+
171
+	// The symlink should not have been modified.
172
+	if err := symlinkTargetEquals(c, dstPath, "dir1"); err != nil {
173
+		c.Fatal(err)
174
+	}
175
+
176
+	// The file should have the contents of "file2" now.
177
+	if err := fileContentEquals(c, cpPath(tmpDir, "file2"), "file2\n"); err != nil {
178
+		c.Fatal(err)
179
+	}
180
+
181
+	// Next, copy a file from the container to a symlink to a file that does
182
+	// not exist (a broken symlink). This should create the target file with
183
+	// the contents of the source file.
184
+	dstPath = cpPath(tmpDir, "brokenSymlinkToFileX")
185
+
186
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
187
+		c.Fatalf("unexpected error %T: %s", err, err)
188
+	}
189
+
190
+	// The symlink should not have been modified.
191
+	if err := symlinkTargetEquals(c, dstPath, "fileX"); err != nil {
192
+		c.Fatal(err)
193
+	}
194
+
195
+	// The file should have the contents of "file2" now.
196
+	if err := fileContentEquals(c, cpPath(tmpDir, "fileX"), "file2\n"); err != nil {
197
+		c.Fatal(err)
198
+	}
199
+
200
+	// Next, copy a directory from the container to a symlink to a local
201
+	// directory. This should copy the directory into the symlink target
202
+	// directory and not modify the symlink.
203
+	srcPath = containerCpPath(cID, "/dir2")
204
+	dstPath = cpPath(tmpDir, "symlinkToDir1")
205
+
206
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
207
+		c.Fatalf("unexpected error %T: %s", err, err)
208
+	}
209
+
210
+	// The symlink should not have been modified.
211
+	if err := symlinkTargetEquals(c, dstPath, "dir1"); err != nil {
212
+		c.Fatal(err)
213
+	}
214
+
215
+	// The directory should now contain a copy of "dir2".
216
+	if err := fileContentEquals(c, cpPath(tmpDir, "dir1/dir2/file2-1"), "file2-1\n"); err != nil {
217
+		c.Fatal(err)
218
+	}
219
+
220
+	// Next, copy a directory from the container to a symlink to a local
221
+	// directory that does not exist (a broken symlink). This should create
222
+	// the target as a directory with the contents of the source directory. It
223
+	// should not modify the symlink.
224
+	dstPath = cpPath(tmpDir, "brokenSymlinkToDirX")
225
+
226
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
227
+		c.Fatalf("unexpected error %T: %s", err, err)
228
+	}
229
+
230
+	// The symlink should not have been modified.
231
+	if err := symlinkTargetEquals(c, dstPath, "dirX"); err != nil {
232
+		c.Fatal(err)
233
+	}
234
+
235
+	// The "dirX" directory should now be a copy of "dir2".
236
+	if err := fileContentEquals(c, cpPath(tmpDir, "dirX/file2-1"), "file2-1\n"); err != nil {
237
+		c.Fatal(err)
238
+	}
239
+}
240
+
133 241
 // Possibilities are reduced to the remaining 10 cases:
134 242
 //
135 243
 //  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
... ...
@@ -250,29 +250,185 @@ func (s *DockerSuite) TestCpAbsoluteSymlink(c *check.C) {
250 250
 		c.Fatal(err)
251 251
 	}
252 252
 
253
-	tmpname := filepath.Join(tmpdir, cpTestName)
253
+	tmpname := filepath.Join(tmpdir, "container_path")
254 254
 	defer os.RemoveAll(tmpdir)
255 255
 
256 256
 	path := path.Join("/", "container_path")
257 257
 
258 258
 	dockerCmd(c, "cp", cleanedContainerID+":"+path, tmpdir)
259 259
 
260
-	file, _ := os.Open(tmpname)
261
-	defer file.Close()
260
+	// We should have copied a symlink *NOT* the file itself!
261
+	linkTarget, err := os.Readlink(tmpname)
262
+	if err != nil {
263
+		c.Fatal(err)
264
+	}
262 265
 
263
-	test, err := ioutil.ReadAll(file)
266
+	if linkTarget != filepath.FromSlash(cpFullPath) {
267
+		c.Errorf("symlink target was %q, but expected: %q", linkTarget, cpFullPath)
268
+	}
269
+}
270
+
271
+// Check that symlinks to a directory behave as expected when copying one from
272
+// a container.
273
+func (s *DockerSuite) TestCpFromSymlinkToDirectory(c *check.C) {
274
+	out, exitCode := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath+" && ln -s "+cpTestPathParent+" /dir_link")
275
+	if exitCode != 0 {
276
+		c.Fatal("failed to create a container", out)
277
+	}
278
+
279
+	cleanedContainerID := strings.TrimSpace(out)
280
+
281
+	out, _ = dockerCmd(c, "wait", cleanedContainerID)
282
+	if strings.TrimSpace(out) != "0" {
283
+		c.Fatal("failed to set up container", out)
284
+	}
285
+
286
+	testDir, err := ioutil.TempDir("", "test-cp-from-symlink-to-dir-")
264 287
 	if err != nil {
265 288
 		c.Fatal(err)
266 289
 	}
290
+	defer os.RemoveAll(testDir)
267 291
 
268
-	if string(test) == cpHostContents {
269
-		c.Errorf("output matched host file -- absolute symlink can escape container rootfs")
292
+	// This copy command should copy the symlink, not the target, into the
293
+	// temporary directory.
294
+	dockerCmd(c, "cp", cleanedContainerID+":"+"/dir_link", testDir)
295
+
296
+	expectedPath := filepath.Join(testDir, "dir_link")
297
+	linkTarget, err := os.Readlink(expectedPath)
298
+	if err != nil {
299
+		c.Fatalf("unable to read symlink at %q: %v", expectedPath, err)
270 300
 	}
271 301
 
272
-	if string(test) != cpContainerContents {
273
-		c.Errorf("output doesn't match the input for absolute symlink")
302
+	if linkTarget != filepath.FromSlash(cpTestPathParent) {
303
+		c.Errorf("symlink target was %q, but expected: %q", linkTarget, cpTestPathParent)
304
+	}
305
+
306
+	os.Remove(expectedPath)
307
+
308
+	// This copy command should resolve the symlink (note the trailing
309
+	// seperator), copying the target into the temporary directory.
310
+	dockerCmd(c, "cp", cleanedContainerID+":"+"/dir_link/", testDir)
311
+
312
+	// It *should not* have copied the directory using the target's name, but
313
+	// used the given name instead.
314
+	unexpectedPath := filepath.Join(testDir, cpTestPathParent)
315
+	if stat, err := os.Lstat(unexpectedPath); err == nil {
316
+		c.Fatalf("target name was copied: %q - %q", stat.Mode(), stat.Name())
317
+	}
318
+
319
+	// It *should* have copied the directory using the asked name "dir_link".
320
+	stat, err := os.Lstat(expectedPath)
321
+	if err != nil {
322
+		c.Fatalf("unable to stat resource at %q: %v", expectedPath, err)
323
+	}
324
+
325
+	if !stat.IsDir() {
326
+		c.Errorf("should have copied a directory but got %q instead", stat.Mode())
327
+	}
328
+}
329
+
330
+// Check that symlinks to a directory behave as expected when copying one to a
331
+// container.
332
+func (s *DockerSuite) TestCpToSymlinkToDirectory(c *check.C) {
333
+	testRequires(c, SameHostDaemon) // Requires local volume mount bind.
334
+
335
+	testVol, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-")
336
+	if err != nil {
337
+		c.Fatal(err)
338
+	}
339
+	defer os.RemoveAll(testVol)
340
+
341
+	// Create a test container with a local volume. We will test by copying
342
+	// to the volume path in the container which we can then verify locally.
343
+	out, exitCode := dockerCmd(c, "create", "-v", testVol+":/testVol", "busybox")
344
+	if exitCode != 0 {
345
+		c.Fatal("failed to create a container", out)
346
+	}
347
+
348
+	cleanedContainerID := strings.TrimSpace(out)
349
+
350
+	// Create a temp directory to hold a test file nested in a direcotry.
351
+	testDir, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-")
352
+	if err != nil {
353
+		c.Fatal(err)
354
+	}
355
+	defer os.RemoveAll(testDir)
356
+
357
+	// This file will be at "/testDir/some/path/test" and will be copied into
358
+	// the test volume later.
359
+	hostTestFilename := filepath.Join(testDir, cpFullPath)
360
+	if err := os.MkdirAll(filepath.Dir(hostTestFilename), os.FileMode(0700)); err != nil {
361
+		c.Fatal(err)
362
+	}
363
+	if err := ioutil.WriteFile(hostTestFilename, []byte(cpHostContents), os.FileMode(0600)); err != nil {
364
+		c.Fatal(err)
365
+	}
366
+
367
+	// Now create another temp directory to hold a symlink to the
368
+	// "/testDir/some" directory.
369
+	linkDir, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-")
370
+	if err != nil {
371
+		c.Fatal(err)
372
+	}
373
+	defer os.RemoveAll(linkDir)
374
+
375
+	// Then symlink "/linkDir/dir_link" to "/testdir/some".
376
+	linkTarget := filepath.Join(testDir, cpTestPathParent)
377
+	localLink := filepath.Join(linkDir, "dir_link")
378
+	if err := os.Symlink(linkTarget, localLink); err != nil {
379
+		c.Fatal(err)
274 380
 	}
275 381
 
382
+	// Now copy that symlink into the test volume in the container.
383
+	dockerCmd(c, "cp", localLink, cleanedContainerID+":/testVol")
384
+
385
+	// This copy command should have copied the symlink *not* the target.
386
+	expectedPath := filepath.Join(testVol, "dir_link")
387
+	actualLinkTarget, err := os.Readlink(expectedPath)
388
+	if err != nil {
389
+		c.Fatalf("unable to read symlink at %q: %v", expectedPath, err)
390
+	}
391
+
392
+	if actualLinkTarget != linkTarget {
393
+		c.Errorf("symlink target was %q, but expected: %q", actualLinkTarget, linkTarget)
394
+	}
395
+
396
+	// Good, now remove that copied link for the next test.
397
+	os.Remove(expectedPath)
398
+
399
+	// This copy command should resolve the symlink (note the trailing
400
+	// seperator), copying the target into the test volume directory in the
401
+	// container.
402
+	dockerCmd(c, "cp", localLink+"/", cleanedContainerID+":/testVol")
403
+
404
+	// It *should not* have copied the directory using the target's name, but
405
+	// used the given name instead.
406
+	unexpectedPath := filepath.Join(testVol, cpTestPathParent)
407
+	if stat, err := os.Lstat(unexpectedPath); err == nil {
408
+		c.Fatalf("target name was copied: %q - %q", stat.Mode(), stat.Name())
409
+	}
410
+
411
+	// It *should* have copied the directory using the asked name "dir_link".
412
+	stat, err := os.Lstat(expectedPath)
413
+	if err != nil {
414
+		c.Fatalf("unable to stat resource at %q: %v", expectedPath, err)
415
+	}
416
+
417
+	if !stat.IsDir() {
418
+		c.Errorf("should have copied a directory but got %q instead", stat.Mode())
419
+	}
420
+
421
+	// And this directory should contain the file copied from the host at the
422
+	// expected location: "/testVol/dir_link/path/test"
423
+	expectedFilepath := filepath.Join(testVol, "dir_link/path/test")
424
+	fileContents, err := ioutil.ReadFile(expectedFilepath)
425
+	if err != nil {
426
+		c.Fatal(err)
427
+	}
428
+
429
+	if string(fileContents) != cpHostContents {
430
+		c.Fatalf("file contains %q but expected %q", string(fileContents), cpHostContents)
431
+	}
276 432
 }
277 433
 
278 434
 // Test for #5619
... ...
@@ -146,6 +146,118 @@ func (s *DockerSuite) TestCpToErrDstNotDir(c *check.C) {
146 146
 	}
147 147
 }
148 148
 
149
+// Check that copying from a local path to a symlink in a container copies to
150
+// the symlink target and does not overwrite the container symlink itself.
151
+func (s *DockerSuite) TestCpToSymlinkDestination(c *check.C) {
152
+	testRequires(c, SameHostDaemon) // Requires local volume mount bind.
153
+
154
+	testVol := getTestDir(c, "test-cp-to-symlink-destination-")
155
+	defer os.RemoveAll(testVol)
156
+
157
+	makeTestContentInDir(c, testVol)
158
+
159
+	cID := makeTestContainer(c, testContainerOptions{
160
+		volumes: defaultVolumes(testVol), // Our bind mount is at /vol2
161
+	})
162
+	defer deleteContainer(cID)
163
+
164
+	// First, copy a local file to a symlink to a file in the container. This
165
+	// should overwrite the symlink target contents with the source contents.
166
+	srcPath := cpPath(testVol, "file2")
167
+	dstPath := containerCpPath(cID, "/vol2/symlinkToFile1")
168
+
169
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
170
+		c.Fatalf("unexpected error %T: %s", err, err)
171
+	}
172
+
173
+	// The symlink should not have been modified.
174
+	if err := symlinkTargetEquals(c, cpPath(testVol, "symlinkToFile1"), "file1"); err != nil {
175
+		c.Fatal(err)
176
+	}
177
+
178
+	// The file should have the contents of "file2" now.
179
+	if err := fileContentEquals(c, cpPath(testVol, "file1"), "file2\n"); err != nil {
180
+		c.Fatal(err)
181
+	}
182
+
183
+	// Next, copy a local file to a symlink to a directory in the container.
184
+	// This should copy the file into the symlink target directory.
185
+	dstPath = containerCpPath(cID, "/vol2/symlinkToDir1")
186
+
187
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
188
+		c.Fatalf("unexpected error %T: %s", err, err)
189
+	}
190
+
191
+	// The symlink should not have been modified.
192
+	if err := symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"); err != nil {
193
+		c.Fatal(err)
194
+	}
195
+
196
+	// The file should have the contents of "file2" now.
197
+	if err := fileContentEquals(c, cpPath(testVol, "file2"), "file2\n"); err != nil {
198
+		c.Fatal(err)
199
+	}
200
+
201
+	// Next, copy a file to a symlink to a file that does not exist (a broken
202
+	// symlink) in the container. This should create the target file with the
203
+	// contents of the source file.
204
+	dstPath = containerCpPath(cID, "/vol2/brokenSymlinkToFileX")
205
+
206
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
207
+		c.Fatalf("unexpected error %T: %s", err, err)
208
+	}
209
+
210
+	// The symlink should not have been modified.
211
+	if err := symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToFileX"), "fileX"); err != nil {
212
+		c.Fatal(err)
213
+	}
214
+
215
+	// The file should have the contents of "file2" now.
216
+	if err := fileContentEquals(c, cpPath(testVol, "fileX"), "file2\n"); err != nil {
217
+		c.Fatal(err)
218
+	}
219
+
220
+	// Next, copy a local directory to a symlink to a directory in the
221
+	// container. This should copy the directory into the symlink target
222
+	// directory and not modify the symlink.
223
+	srcPath = cpPath(testVol, "/dir2")
224
+	dstPath = containerCpPath(cID, "/vol2/symlinkToDir1")
225
+
226
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
227
+		c.Fatalf("unexpected error %T: %s", err, err)
228
+	}
229
+
230
+	// The symlink should not have been modified.
231
+	if err := symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"); err != nil {
232
+		c.Fatal(err)
233
+	}
234
+
235
+	// The directory should now contain a copy of "dir2".
236
+	if err := fileContentEquals(c, cpPath(testVol, "dir1/dir2/file2-1"), "file2-1\n"); err != nil {
237
+		c.Fatal(err)
238
+	}
239
+
240
+	// Next, copy a local directory to a symlink to a local directory that does
241
+	// not exist (a broken symlink) in the container. This should create the
242
+	// target as a directory with the contents of the source directory. It
243
+	// should not modify the symlink.
244
+	dstPath = containerCpPath(cID, "/vol2/brokenSymlinkToDirX")
245
+
246
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
247
+		c.Fatalf("unexpected error %T: %s", err, err)
248
+	}
249
+
250
+	// The symlink should not have been modified.
251
+	if err := symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToDirX"), "dirX"); err != nil {
252
+		c.Fatal(err)
253
+	}
254
+
255
+	// The "dirX" directory should now be a copy of "dir2".
256
+	if err := fileContentEquals(c, cpPath(testVol, "dirX/file2-1"), "file2-1\n"); err != nil {
257
+		c.Fatal(err)
258
+	}
259
+}
260
+
149 261
 // Possibilities are reduced to the remaining 10 cases:
150 262
 //
151 263
 //  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
... ...
@@ -74,8 +74,11 @@ var defaultFileData = []fileData{
74 74
 	{ftRegular, "dir4/file3-1", "file4-1"},
75 75
 	{ftRegular, "dir4/file3-2", "file4-2"},
76 76
 	{ftDir, "dir5", ""},
77
-	{ftSymlink, "symlink1", "target1"},
78
-	{ftSymlink, "symlink2", "target2"},
77
+	{ftSymlink, "symlinkToFile1", "file1"},
78
+	{ftSymlink, "symlinkToDir1", "dir1"},
79
+	{ftSymlink, "brokenSymlinkToFileX", "fileX"},
80
+	{ftSymlink, "brokenSymlinkToDirX", "dirX"},
81
+	{ftSymlink, "symlinkToAbsDir", "/root"},
79 82
 }
80 83
 
81 84
 func defaultMkContentCommand() string {
... ...
@@ -268,6 +271,21 @@ func fileContentEquals(c *check.C, filename, contents string) (err error) {
268 268
 	return
269 269
 }
270 270
 
271
+func symlinkTargetEquals(c *check.C, symlink, expectedTarget string) (err error) {
272
+	c.Logf("checking that the symlink %q points to %q\n", symlink, expectedTarget)
273
+
274
+	actualTarget, err := os.Readlink(symlink)
275
+	if err != nil {
276
+		return err
277
+	}
278
+
279
+	if actualTarget != expectedTarget {
280
+		return fmt.Errorf("symlink target points to %q not %q", actualTarget, expectedTarget)
281
+	}
282
+
283
+	return nil
284
+}
285
+
271 286
 func containerStartOutputEquals(c *check.C, cID, contents string) (err error) {
272 287
 	c.Logf("checking that container %q start output contains %q\n", cID, contents)
273 288
 
... ...
@@ -37,11 +37,13 @@ type (
37 37
 		Compression      Compression
38 38
 		NoLchown         bool
39 39
 		ChownOpts        *TarChownOptions
40
-		Name             string
41 40
 		IncludeSourceDir bool
42 41
 		// When unpacking, specifies whether overwriting a directory with a
43 42
 		// non-directory is allowed and vice versa.
44 43
 		NoOverwriteDirNonDir bool
44
+		// For each include when creating an archive, the included name will be
45
+		// replaced with the matching name from this map.
46
+		RebaseNames map[string]string
45 47
 	}
46 48
 
47 49
 	// Archiver allows the reuse of most utility functions of this package
... ...
@@ -454,8 +456,9 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
454 454
 
455 455
 		seen := make(map[string]bool)
456 456
 
457
-		var renamedRelFilePath string // For when tar.Options.Name is set
458 457
 		for _, include := range options.IncludeFiles {
458
+			rebaseName := options.RebaseNames[include]
459
+
459 460
 			// We can't use filepath.Join(srcPath, include) because this will
460 461
 			// clean away a trailing "." or "/" which may be important.
461 462
 			walkRoot := strings.Join([]string{srcPath, include}, string(filepath.Separator))
... ...
@@ -503,14 +506,17 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
503 503
 				}
504 504
 				seen[relFilePath] = true
505 505
 
506
-				// TODO Windows: Verify if this needs to be os.Pathseparator
507
-				// Rename the base resource
508
-				if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) {
509
-					renamedRelFilePath = relFilePath
510
-				}
511
-				// Set this to make sure the items underneath also get renamed
512
-				if options.Name != "" {
513
-					relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1)
506
+				// Rename the base resource.
507
+				if rebaseName != "" {
508
+					var replacement string
509
+					if rebaseName != string(filepath.Separator) {
510
+						// Special case the root directory to replace with an
511
+						// empty string instead so that we don't end up with
512
+						// double slashes in the paths.
513
+						replacement = rebaseName
514
+					}
515
+
516
+					relFilePath = strings.Replace(relFilePath, include, replacement, 1)
514 517
 				}
515 518
 
516 519
 				if err := ta.addTarFile(filePath, relFilePath); err != nil {
... ...
@@ -695,7 +695,7 @@ func TestTarWithOptions(t *testing.T) {
695 695
 		{&TarOptions{ExcludePatterns: []string{"2"}}, 1},
696 696
 		{&TarOptions{ExcludePatterns: []string{"1", "folder*"}}, 2},
697 697
 		{&TarOptions{IncludeFiles: []string{"1", "1"}}, 2},
698
-		{&TarOptions{Name: "test", IncludeFiles: []string{"1"}}, 4},
698
+		{&TarOptions{IncludeFiles: []string{"1"}, RebaseNames: map[string]string{"1": "test"}}, 4},
699 699
 	}
700 700
 	for _, testCase := range cases {
701 701
 		changes, err := tarUntar(t, origin, testCase.opts)
... ...
@@ -6,7 +6,6 @@ import (
6 6
 	"io"
7 7
 	"io/ioutil"
8 8
 	"os"
9
-	"path"
10 9
 	"path/filepath"
11 10
 	"strings"
12 11
 
... ...
@@ -64,34 +63,33 @@ func SpecifiesCurrentDir(path string) bool {
64 64
 	return filepath.Base(path) == "."
65 65
 }
66 66
 
67
-// SplitPathDirEntry splits the given path between its
68
-// parent directory and its basename in that directory.
69
-func SplitPathDirEntry(localizedPath string) (dir, base string) {
70
-	normalizedPath := filepath.ToSlash(localizedPath)
71
-	vol := filepath.VolumeName(normalizedPath)
72
-	normalizedPath = normalizedPath[len(vol):]
67
+// SplitPathDirEntry splits the given path between its directory name and its
68
+// basename by first cleaning the path but preserves a trailing "." if the
69
+// original path specified the current directory.
70
+func SplitPathDirEntry(path string) (dir, base string) {
71
+	cleanedPath := filepath.Clean(path)
73 72
 
74
-	if normalizedPath == "/" {
75
-		// Specifies the root path.
76
-		return filepath.FromSlash(vol + normalizedPath), "."
73
+	if SpecifiesCurrentDir(path) {
74
+		cleanedPath += string(filepath.Separator) + "."
77 75
 	}
78 76
 
79
-	trimmedPath := vol + strings.TrimRight(normalizedPath, "/")
80
-
81
-	dir = filepath.FromSlash(path.Dir(trimmedPath))
82
-	base = filepath.FromSlash(path.Base(trimmedPath))
83
-
84
-	return dir, base
77
+	return filepath.Dir(cleanedPath), filepath.Base(cleanedPath)
85 78
 }
86 79
 
87
-// TarResource archives the resource at the given sourcePath into a Tar
80
+// TarResource archives the resource described by the given CopyInfo to a Tar
88 81
 // archive. A non-nil error is returned if sourcePath does not exist or is
89 82
 // asserted to be a directory but exists as another type of file.
90 83
 //
91 84
 // This function acts as a convenient wrapper around TarWithOptions, which
92 85
 // requires a directory as the source path. TarResource accepts either a
93 86
 // directory or a file path and correctly sets the Tar options.
94
-func TarResource(sourcePath string) (content Archive, err error) {
87
+func TarResource(sourceInfo CopyInfo) (content Archive, err error) {
88
+	return TarResourceRebase(sourceInfo.Path, sourceInfo.RebaseName)
89
+}
90
+
91
+// TarResourceRebase is like TarResource but renames the first path element of
92
+// items in the resulting tar archive to match the given rebaseName if not "".
93
+func TarResourceRebase(sourcePath, rebaseName string) (content Archive, err error) {
95 94
 	if _, err = os.Lstat(sourcePath); err != nil {
96 95
 		// Catches the case where the source does not exist or is not a
97 96
 		// directory if asserted to be a directory, as this also causes an
... ...
@@ -99,22 +97,6 @@ func TarResource(sourcePath string) (content Archive, err error) {
99 99
 		return
100 100
 	}
101 101
 
102
-	if len(sourcePath) > 1 && HasTrailingPathSeparator(sourcePath) {
103
-		// In the case where the source path is a symbolic link AND it ends
104
-		// with a path separator, we will want to evaluate the symbolic link.
105
-		trimmedPath := sourcePath[:len(sourcePath)-1]
106
-		stat, err := os.Lstat(trimmedPath)
107
-		if err != nil {
108
-			return nil, err
109
-		}
110
-
111
-		if stat.Mode()&os.ModeSymlink != 0 {
112
-			if sourcePath, err = filepath.EvalSymlinks(trimmedPath); err != nil {
113
-				return nil, err
114
-			}
115
-		}
116
-	}
117
-
118 102
 	// Separate the source path between it's directory and
119 103
 	// the entry in that directory which we are archiving.
120 104
 	sourceDir, sourceBase := SplitPathDirEntry(sourcePath)
... ...
@@ -127,32 +109,137 @@ func TarResource(sourcePath string) (content Archive, err error) {
127 127
 		Compression:      Uncompressed,
128 128
 		IncludeFiles:     filter,
129 129
 		IncludeSourceDir: true,
130
+		RebaseNames: map[string]string{
131
+			sourceBase: rebaseName,
132
+		},
130 133
 	})
131 134
 }
132 135
 
133 136
 // CopyInfo holds basic info about the source
134 137
 // or destination path of a copy operation.
135 138
 type CopyInfo struct {
136
-	Path   string
137
-	Exists bool
138
-	IsDir  bool
139
+	Path       string
140
+	Exists     bool
141
+	IsDir      bool
142
+	RebaseName string
139 143
 }
140 144
 
141
-// CopyInfoStatPath stats the given path to create a CopyInfo
142
-// struct representing that resource. If mustExist is true, then
143
-// it is an error if there is no file or directory at the given path.
144
-func CopyInfoStatPath(path string, mustExist bool) (CopyInfo, error) {
145
-	pathInfo := CopyInfo{Path: path}
145
+// CopyInfoSourcePath stats the given path to create a CopyInfo
146
+// struct representing that resource for the source of an archive copy
147
+// operation. The given path should be an absolute local path. A source path
148
+// has all symlinks evaluated that appear before the last path separator ("/"
149
+// on Unix). As it is to be a copy source, the path must exist.
150
+func CopyInfoSourcePath(path string) (CopyInfo, error) {
151
+	// Split the given path into its Directory and Base components. We will
152
+	// evaluate symlinks in the directory component then append the base.
153
+	dirPath, basePath := filepath.Split(path)
154
+
155
+	resolvedDirPath, err := filepath.EvalSymlinks(dirPath)
156
+	if err != nil {
157
+		return CopyInfo{}, err
158
+	}
146 159
 
147
-	fileInfo, err := os.Lstat(path)
160
+	// resolvedDirPath will have been cleaned (no trailing path separators) so
161
+	// we can manually join it with the base path element.
162
+	resolvedPath := resolvedDirPath + string(filepath.Separator) + basePath
163
+
164
+	var rebaseName string
165
+	if HasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) {
166
+		// In the case where the path had a trailing separator and a symlink
167
+		// evaluation has changed the last path component, we will need to
168
+		// rebase the name in the archive that is being copied to match the
169
+		// originally requested name.
170
+		rebaseName = filepath.Base(path)
171
+	}
148 172
 
149
-	if err == nil {
150
-		pathInfo.Exists, pathInfo.IsDir = true, fileInfo.IsDir()
151
-	} else if os.IsNotExist(err) && !mustExist {
152
-		err = nil
173
+	stat, err := os.Lstat(resolvedPath)
174
+	if err != nil {
175
+		return CopyInfo{}, err
153 176
 	}
154 177
 
155
-	return pathInfo, err
178
+	return CopyInfo{
179
+		Path:       resolvedPath,
180
+		Exists:     true,
181
+		IsDir:      stat.IsDir(),
182
+		RebaseName: rebaseName,
183
+	}, nil
184
+}
185
+
186
+// CopyInfoDestinationPath stats the given path to create a CopyInfo
187
+// struct representing that resource for the destination of an archive copy
188
+// operation. The given path should be an absolute local path.
189
+func CopyInfoDestinationPath(path string) (info CopyInfo, err error) {
190
+	maxSymlinkIter := 10 // filepath.EvalSymlinks uses 255, but 10 already seems like a lot.
191
+	originalPath := path
192
+
193
+	stat, err := os.Lstat(path)
194
+
195
+	if err == nil && stat.Mode()&os.ModeSymlink == 0 {
196
+		// The path exists and is not a symlink.
197
+		return CopyInfo{
198
+			Path:   path,
199
+			Exists: true,
200
+			IsDir:  stat.IsDir(),
201
+		}, nil
202
+	}
203
+
204
+	// While the path is a symlink.
205
+	for n := 0; err == nil && stat.Mode()&os.ModeSymlink != 0; n++ {
206
+		if n > maxSymlinkIter {
207
+			// Don't follow symlinks more than this arbitrary number of times.
208
+			return CopyInfo{}, errors.New("too many symlinks in " + originalPath)
209
+		}
210
+
211
+		// The path is a symbolic link. We need to evaluate it so that the
212
+		// destination of the copy operation is the link target and not the
213
+		// link itself. This is notably different than CopyInfoSourcePath which
214
+		// only evaluates symlinks before the last appearing path separator.
215
+		// Also note that it is okay if the last path element is a broken
216
+		// symlink as the copy operation should create the target.
217
+		var linkTarget string
218
+
219
+		linkTarget, err = os.Readlink(path)
220
+		if err != nil {
221
+			return CopyInfo{}, err
222
+		}
223
+
224
+		if !filepath.IsAbs(linkTarget) {
225
+			// Join with the parent directory.
226
+			dstParent, _ := SplitPathDirEntry(path)
227
+			linkTarget = filepath.Join(dstParent, linkTarget)
228
+		}
229
+
230
+		path = linkTarget
231
+		stat, err = os.Lstat(path)
232
+	}
233
+
234
+	if err != nil {
235
+		// It's okay if the destination path doesn't exist. We can still
236
+		// continue the copy operation if the parent directory exists.
237
+		if !os.IsNotExist(err) {
238
+			return CopyInfo{}, err
239
+		}
240
+
241
+		// Ensure destination parent dir exists.
242
+		dstParent, _ := SplitPathDirEntry(path)
243
+
244
+		parentDirStat, err := os.Lstat(dstParent)
245
+		if err != nil {
246
+			return CopyInfo{}, err
247
+		}
248
+		if !parentDirStat.IsDir() {
249
+			return CopyInfo{}, ErrNotDirectory
250
+		}
251
+
252
+		return CopyInfo{Path: path}, nil
253
+	}
254
+
255
+	// The path exists after resolving symlinks.
256
+	return CopyInfo{
257
+		Path:   path,
258
+		Exists: true,
259
+		IsDir:  stat.IsDir(),
260
+	}, nil
156 261
 }
157 262
 
158 263
 // PrepareArchiveCopy prepares the given srcContent archive, which should
... ...
@@ -210,6 +297,13 @@ func PrepareArchiveCopy(srcContent ArchiveReader, srcInfo, dstInfo CopyInfo) (ds
210 210
 // rebaseArchiveEntries rewrites the given srcContent archive replacing
211 211
 // an occurance of oldBase with newBase at the beginning of entry names.
212 212
 func rebaseArchiveEntries(srcContent ArchiveReader, oldBase, newBase string) Archive {
213
+	if oldBase == "/" {
214
+		// If oldBase specifies the root directory, use an empty string as
215
+		// oldBase instead so that newBase doesn't replace the path separator
216
+		// that all paths will start with.
217
+		oldBase = ""
218
+	}
219
+
213 220
 	rebased, w := io.Pipe()
214 221
 
215 222
 	go func() {
... ...
@@ -259,11 +353,11 @@ func CopyResource(srcPath, dstPath string) error {
259 259
 	srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
260 260
 	dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
261 261
 
262
-	if srcInfo, err = CopyInfoStatPath(srcPath, true); err != nil {
262
+	if srcInfo, err = CopyInfoSourcePath(srcPath); err != nil {
263 263
 		return err
264 264
 	}
265 265
 
266
-	content, err := TarResource(srcPath)
266
+	content, err := TarResource(srcInfo)
267 267
 	if err != nil {
268 268
 		return err
269 269
 	}
... ...
@@ -275,24 +369,13 @@ func CopyResource(srcPath, dstPath string) error {
275 275
 // CopyTo handles extracting the given content whose
276 276
 // entries should be sourced from srcInfo to dstPath.
277 277
 func CopyTo(content ArchiveReader, srcInfo CopyInfo, dstPath string) error {
278
-	dstInfo, err := CopyInfoStatPath(dstPath, false)
278
+	// The destination path need not exist, but CopyInfoDestinationPath will
279
+	// ensure that at least the parent directory exists.
280
+	dstInfo, err := CopyInfoDestinationPath(dstPath)
279 281
 	if err != nil {
280 282
 		return err
281 283
 	}
282 284
 
283
-	if !dstInfo.Exists {
284
-		// Ensure destination parent dir exists.
285
-		dstParent, _ := SplitPathDirEntry(dstPath)
286
-
287
-		dstStat, err := os.Lstat(dstParent)
288
-		if err != nil {
289
-			return err
290
-		}
291
-		if !dstStat.IsDir() {
292
-			return ErrNotDirectory
293
-		}
294
-	}
295
-
296 285
 	dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo)
297 286
 	if err != nil {
298 287
 		return err
... ...
@@ -138,13 +138,7 @@ func TestCopyErrSrcNotExists(t *testing.T) {
138 138
 	tmpDirA, tmpDirB := getTestTempDirs(t)
139 139
 	defer removeAllPaths(tmpDirA, tmpDirB)
140 140
 
141
-	content, err := TarResource(filepath.Join(tmpDirA, "file1"))
142
-	if err == nil {
143
-		content.Close()
144
-		t.Fatal("expected IsNotExist error, but got nil instead")
145
-	}
146
-
147
-	if !os.IsNotExist(err) {
141
+	if _, err := CopyInfoSourcePath(filepath.Join(tmpDirA, "file1")); !os.IsNotExist(err) {
148 142
 		t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
149 143
 	}
150 144
 }
... ...
@@ -158,13 +152,7 @@ func TestCopyErrSrcNotDir(t *testing.T) {
158 158
 	// Load A with some sample files and directories.
159 159
 	createSampleDir(t, tmpDirA)
160 160
 
161
-	content, err := TarResource(joinTrailingSep(tmpDirA, "file1"))
162
-	if err == nil {
163
-		content.Close()
164
-		t.Fatal("expected IsNotDir error, but got nil instead")
165
-	}
166
-
167
-	if !isNotDir(err) {
161
+	if _, err := CopyInfoSourcePath(joinTrailingSep(tmpDirA, "file1")); !isNotDir(err) {
168 162
 		t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
169 163
 	}
170 164
 }
... ...
@@ -181,7 +169,7 @@ func TestCopyErrDstParentNotExists(t *testing.T) {
181 181
 	srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
182 182
 
183 183
 	// Try with a file source.
184
-	content, err := TarResource(srcInfo.Path)
184
+	content, err := TarResource(srcInfo)
185 185
 	if err != nil {
186 186
 		t.Fatalf("unexpected error %T: %s", err, err)
187 187
 	}
... ...
@@ -199,7 +187,7 @@ func TestCopyErrDstParentNotExists(t *testing.T) {
199 199
 	// Try with a directory source.
200 200
 	srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
201 201
 
202
-	content, err = TarResource(srcInfo.Path)
202
+	content, err = TarResource(srcInfo)
203 203
 	if err != nil {
204 204
 		t.Fatalf("unexpected error %T: %s", err, err)
205 205
 	}
... ...
@@ -228,7 +216,7 @@ func TestCopyErrDstNotDir(t *testing.T) {
228 228
 	// Try with a file source.
229 229
 	srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
230 230
 
231
-	content, err := TarResource(srcInfo.Path)
231
+	content, err := TarResource(srcInfo)
232 232
 	if err != nil {
233 233
 		t.Fatalf("unexpected error %T: %s", err, err)
234 234
 	}
... ...
@@ -245,7 +233,7 @@ func TestCopyErrDstNotDir(t *testing.T) {
245 245
 	// Try with a directory source.
246 246
 	srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
247 247
 
248
-	content, err = TarResource(srcInfo.Path)
248
+	content, err = TarResource(srcInfo)
249 249
 	if err != nil {
250 250
 		t.Fatalf("unexpected error %T: %s", err, err)
251 251
 	}