Merge pull request #14885 from jlhawn/fix_cp_symlink

Fix copying of symlinks in containers
This commit is contained in:
David Calavera 2015-07-30 13:42:40 -07:00
commit 030f61df3d
14 changed files with 742 additions and 203 deletions

View File

@ -232,6 +232,20 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
// Prepare destination copy info by stat-ing the container path. // Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath} dstInfo := archive.CopyInfo{Path: dstPath}
dstStat, err := cli.statContainerPath(dstContainer, dstPath) dstStat, err := cli.statContainerPath(dstContainer, dstPath)
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
linkTarget := dstStat.LinkTarget
if !filepath.IsAbs(linkTarget) {
// Join with the parent directory.
dstParent, _ := archive.SplitPathDirEntry(dstPath)
linkTarget = filepath.Join(dstParent, linkTarget)
}
dstInfo.Path = linkTarget
dstStat, err = cli.statContainerPath(dstContainer, linkTarget)
}
// Ignore any error and assume that the parent directory of the destination // Ignore any error and assume that the parent directory of the destination
// path exists, in which case the copy may still succeed. If there is any // path exists, in which case the copy may still succeed. If there is any
// type of conflict (e.g., non-directory overwriting an existing directory // type of conflict (e.g., non-directory overwriting an existing directory
@ -242,15 +256,26 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
} }
var content io.Reader var (
content io.Reader
resolvedDstPath string
)
if srcPath == "-" { if srcPath == "-" {
// Use STDIN. // Use STDIN.
content = os.Stdin content = os.Stdin
resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir { if !dstInfo.IsDir {
return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath)) return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
} }
} else { } else {
srcArchive, err := archive.TarResource(srcPath) // Prepare source copy info.
srcInfo, err := archive.CopyInfoSourcePath(srcPath)
if err != nil {
return err
}
srcArchive, err := archive.TarResource(srcInfo)
if err != nil { if err != nil {
return err return err
} }
@ -262,12 +287,6 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (er
// it to the specified directory in the container we get the disired // it to the specified directory in the container we get the disired
// copy behavior. // copy behavior.
// Prepare source copy info.
srcInfo, err := archive.CopyInfoStatPath(srcPath, true)
if err != nil {
return err
}
// See comments in the implementation of `archive.PrepareArchiveCopy` // See comments in the implementation of `archive.PrepareArchiveCopy`
// for exactly what goes into deciding how and whether the source // for exactly what goes into deciding how and whether the source
// archive needs to be altered for the correct copy behavior when it is // 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
} }
defer preparedArchive.Close() defer preparedArchive.Close()
dstPath = dstDir resolvedDstPath = dstDir
content = preparedArchive content = preparedArchive
} }
query := make(url.Values, 2) query := make(url.Values, 2)
query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(resolvedDstPath)) // Normalize the paths used in the API.
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa. // Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
query.Set("noOverwriteDirNonDir", "true") query.Set("noOverwriteDirNonDir", "true")

View File

@ -130,14 +130,13 @@ type CopyConfig struct {
// ContainerPathStat is used to encode the header from // ContainerPathStat is used to encode the header from
// GET /containers/{name:.*}/archive // GET /containers/{name:.*}/archive
// "name" is the file or directory name. // "name" is basename of the resource.
// "path" is the absolute path to the resource in the container.
type ContainerPathStat struct { type ContainerPathStat struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Size int64 `json:"size"`
Size int64 `json:"size"` Mode os.FileMode `json:"mode"`
Mode os.FileMode `json:"mode"` Mtime time.Time `json:"mtime"`
Mtime time.Time `json:"mtime"` LinkTarget string `json:"linkTarget"`
} }
// GET "/containers/{name:.*}/top" // GET "/containers/{name:.*}/top"

View File

@ -70,6 +70,66 @@ func (daemon *Daemon) ContainerExtractToDir(name, path string, noOverwriteDirNon
return container.ExtractToDir(path, noOverwriteDirNonDir, content) return container.ExtractToDir(path, noOverwriteDirNonDir, content)
} }
// resolvePath resolves the given path in the container to a resource on the
// host. Returns a resolved path (absolute path to the resource on the host),
// the absolute path to the resource relative to the container's rootfs, and
// a error if the path points to outside the container's rootfs.
func (container *Container) resolvePath(path string) (resolvedPath, absPath string, err error) {
// Consider the given path as an absolute path in the container.
absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path)
// Split the absPath into its Directory and Base components. We will
// resolve the dir in the scope of the container then append the base.
dirPath, basePath := filepath.Split(absPath)
resolvedDirPath, err := container.GetResourcePath(dirPath)
if err != nil {
return "", "", err
}
// resolvedDirPath will have been cleaned (no trailing path separators) so
// we can manually join it with the base path element.
resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath
return resolvedPath, absPath, nil
}
// statPath is the unexported version of StatPath. Locks and mounts should
// be aquired before calling this method and the given path should be fully
// resolved to a path on the host corresponding to the given absolute path
// inside the container.
func (container *Container) statPath(resolvedPath, absPath string) (stat *types.ContainerPathStat, err error) {
lstat, err := os.Lstat(resolvedPath)
if err != nil {
return nil, err
}
var linkTarget string
if lstat.Mode()&os.ModeSymlink != 0 {
// Fully evaluate the symlink in the scope of the container rootfs.
hostPath, err := container.GetResourcePath(absPath)
if err != nil {
return nil, err
}
linkTarget, err = filepath.Rel(container.basefs, hostPath)
if err != nil {
return nil, err
}
// Make it an absolute path.
linkTarget = filepath.Join(string(filepath.Separator), linkTarget)
}
return &types.ContainerPathStat{
Name: filepath.Base(absPath),
Size: lstat.Size(),
Mode: lstat.Mode(),
Mtime: lstat.ModTime(),
LinkTarget: linkTarget,
}, nil
}
// StatPath stats the filesystem resource at the specified path in this // StatPath stats the filesystem resource at the specified path in this
// container. Returns stat info about the resource. // container. Returns stat info about the resource.
func (container *Container) StatPath(path string) (stat *types.ContainerPathStat, err error) { func (container *Container) StatPath(path string) (stat *types.ContainerPathStat, err error) {
@ -87,39 +147,12 @@ func (container *Container) StatPath(path string) (stat *types.ContainerPathStat
return nil, err return nil, err
} }
// Consider the given path as an absolute path in the container. resolvedPath, absPath, err := container.resolvePath(path)
absPath := path
if !filepath.IsAbs(absPath) {
absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(os.PathSeparator), path), path)
}
resolvedPath, err := container.GetResourcePath(absPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// A trailing "." or separator has important meaning. For example, if return container.statPath(resolvedPath, absPath)
// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
// will stat the link itself, while `os.Lstat("foo/")` will stat the link
// target. If the basename of the path is ".", it means to archive the
// contents of the directory with "." as the first path component rather
// than the name of the directory. This would cause extraction of the
// archive to *not* make another directory, but instead use the current
// directory.
resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
lstat, err := os.Lstat(resolvedPath)
if err != nil {
return nil, err
}
return &types.ContainerPathStat{
Name: lstat.Name(),
Path: absPath,
Size: lstat.Size(),
Mode: lstat.Mode(),
Mtime: lstat.ModTime(),
}, nil
} }
// ArchivePath creates an archive of the filesystem resource at the specified // 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
return nil, nil, err return nil, nil, err
} }
// Consider the given path as an absolute path in the container. resolvedPath, absPath, err := container.resolvePath(path)
absPath := path
if !filepath.IsAbs(absPath) {
absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(os.PathSeparator), path), path)
}
resolvedPath, err := container.GetResourcePath(absPath)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// A trailing "." or separator has important meaning. For example, if stat, err = container.statPath(resolvedPath, absPath)
// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
// will stat the link itself, while `os.Lstat("foo/")` will stat the link
// target. If the basename of the path is ".", it means to archive the
// contents of the directory with "." as the first path component rather
// than the name of the directory. This would cause extraction of the
// archive to *not* make another directory, but instead use the current
// directory.
resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
lstat, err := os.Lstat(resolvedPath)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
stat = &types.ContainerPathStat{ // We need to rebase the archive entries if the last element of the
Name: lstat.Name(), // resolved path was a symlink that was evaluated and is now different
Path: absPath, // than the requested path. For example, if the given path was "/foo/bar/",
Size: lstat.Size(), // but it resolved to "/var/lib/docker/containers/{id}/foo/baz/", we want
Mode: lstat.Mode(), // to ensure that the archive entries start with "bar" and not "baz". This
Mtime: lstat.ModTime(), // also catches the case when the root directory of the container is
} // requested: we want the archive entries to start with "/" and not the
// container ID.
data, err := archive.TarResource(resolvedPath) data, err := archive.TarResourceRebase(resolvedPath, filepath.Base(absPath))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -227,27 +244,21 @@ func (container *Container) ExtractToDir(path string, noOverwriteDirNonDir bool,
return err return err
} }
// Consider the given path as an absolute path in the container. // The destination path needs to be resolved to a host path, with all
absPath := path // symbolic links followed in the scope of the container's rootfs. Note
if !filepath.IsAbs(absPath) { // that we do not use `container.resolvePath(path)` here because we need
absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(os.PathSeparator), path), path) // to also evaluate the last path element if it is a symlink. This is so
} // that you can extract an archive to a symlink that points to a directory.
// Consider the given path as an absolute path in the container.
absPath := archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path)
// This will evaluate the last path element if it is a symlink.
resolvedPath, err := container.GetResourcePath(absPath) resolvedPath, err := container.GetResourcePath(absPath)
if err != nil { if err != nil {
return err return err
} }
// A trailing "." or separator has important meaning. For example, if
// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
// will stat the link itself, while `os.Lstat("foo/")` will stat the link
// target. If the basename of the path is ".", it means to archive the
// contents of the directory with "." as the first path component rather
// than the name of the directory. This would cause extraction of the
// archive to *not* make another directory, but instead use the current
// directory.
resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
stat, err := os.Lstat(resolvedPath) stat, err := os.Lstat(resolvedPath)
if err != nil { if err != nil {
return err return err
@ -257,15 +268,20 @@ func (container *Container) ExtractToDir(path string, noOverwriteDirNonDir bool,
return ErrExtractPointNotDirectory return ErrExtractPointNotDirectory
} }
// Need to check if the path is in a volume. If it is, it cannot be in a
// read-only volume. If it is not in a volume, the container cannot be
// configured with a read-only rootfs.
// Use the resolved path relative to the container rootfs as the new
// absPath. This way we fully follow any symlinks in a volume that may
// lead back outside the volume.
baseRel, err := filepath.Rel(container.basefs, resolvedPath) baseRel, err := filepath.Rel(container.basefs, resolvedPath)
if err != nil { if err != nil {
return err return err
} }
absPath = filepath.Join(string(os.PathSeparator), baseRel) // Make it an absolute path.
absPath = filepath.Join(string(filepath.Separator), baseRel)
// Need to check if the path is in a volume. If it is, it cannot be in a
// read-only volume. If it is not in a volume, the container cannot be
// configured with a read-only rootfs.
toVolume, err := checkIfPathIsInAVolume(container, absPath) toVolume, err := checkIfPathIsInAVolume(container, absPath)
if err != nil { if err != nil {
return err return err

View File

@ -1109,7 +1109,7 @@ Query Parameters:
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Type: application/x-tar Content-Type: application/x-tar
X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInBhdGgiOiIvcm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oifQ== X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0=
{{ TAR STREAM }} {{ TAR STREAM }}
@ -1120,10 +1120,10 @@ JSON object (whitespace added for readability):
{ {
"name": "root", "name": "root",
"path": "/root",
"size": 4096, "size": 4096,
"mode": 2147484096, "mode": 2147484096,
"mtime": "2014-02-27T20:51:23Z" "mtime": "2014-02-27T20:51:23Z",
"linkTarget": ""
} }
A `HEAD` request can also be made to this endpoint if only this information is A `HEAD` request can also be made to this endpoint if only this information is

View File

@ -1109,7 +1109,7 @@ Query Parameters:
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Type: application/x-tar Content-Type: application/x-tar
X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInBhdGgiOiIvcm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oifQ== X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oiLCJsaW5rVGFyZ2V0IjoiIn0=
{{ TAR STREAM }} {{ TAR STREAM }}
@ -1120,10 +1120,10 @@ JSON object (whitespace added for readability):
{ {
"name": "root", "name": "root",
"path": "/root",
"size": 4096, "size": 4096,
"mode": 2147484096, "mode": 2147484096,
"mtime": "2014-02-27T20:51:23Z" "mtime": "2014-02-27T20:51:23Z",
"linkTarget": ""
} }
A `HEAD` request can also be made to this endpoint if only this information is A `HEAD` request can also be made to this endpoint if only this information is

View File

@ -4,9 +4,11 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -1697,3 +1699,35 @@ func (s *DockerSuite) TestContainersApiCreateNoHostConfig118(c *check.C) {
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, http.StatusCreated) c.Assert(status, check.Equals, http.StatusCreated)
} }
// Ensure an error occurs when you have a container read-only rootfs but you
// extract an archive to a symlink in a writable volume which points to a
// directory outside of the volume.
func (s *DockerSuite) TestPutContainerArchiveErrSymlinkInVolumeToReadOnlyRootfs(c *check.C) {
testRequires(c, SameHostDaemon) // Requires local volume mount bind.
testVol := getTestDir(c, "test-put-container-archive-err-symlink-in-volume-to-read-only-rootfs-")
defer os.RemoveAll(testVol)
makeTestContentInDir(c, testVol)
cID := makeTestContainer(c, testContainerOptions{
readOnly: true,
volumes: defaultVolumes(testVol), // Our bind mount is at /vol2
})
defer deleteContainer(cID)
// Attempt to extract to a symlink in the volume which points to a
// directory outside the volume. This should cause an error because the
// rootfs is read-only.
query := make(url.Values, 1)
query.Set("path", "/vol2/symlinkToAbsDir")
urlPath := fmt.Sprintf("/v1.20/containers/%s/archive?%s", cID, query.Encode())
statusCode, body, err := sockRequest("PUT", urlPath, nil)
c.Assert(err, check.IsNil)
if !isCpCannotCopyReadOnly(fmt.Errorf(string(body))) {
c.Fatalf("expected ErrContainerRootfsReadonly error, but got %d: %s", statusCode, string(body))
}
}

View File

@ -130,6 +130,114 @@ func (s *DockerSuite) TestCpFromErrDstNotDir(c *check.C) {
} }
} }
// Check that copying from a container to a local symlink copies to the symlink
// target and does not overwrite the local symlink itself.
func (s *DockerSuite) TestCpFromSymlinkDestination(c *check.C) {
cID := makeTestContainer(c, testContainerOptions{addContent: true})
defer deleteContainer(cID)
tmpDir := getTestDir(c, "test-cp-from-err-dst-not-dir")
defer os.RemoveAll(tmpDir)
makeTestContentInDir(c, tmpDir)
// First, copy a file from the container to a symlink to a file. This
// should overwrite the symlink target contents with the source contents.
srcPath := containerCpPath(cID, "/file2")
dstPath := cpPath(tmpDir, "symlinkToFile1")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, dstPath, "file1"); err != nil {
c.Fatal(err)
}
// The file should have the contents of "file2" now.
if err := fileContentEquals(c, cpPath(tmpDir, "file1"), "file2\n"); err != nil {
c.Fatal(err)
}
// Next, copy a file from the container to a symlink to a directory. This
// should copy the file into the symlink target directory.
dstPath = cpPath(tmpDir, "symlinkToDir1")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, dstPath, "dir1"); err != nil {
c.Fatal(err)
}
// The file should have the contents of "file2" now.
if err := fileContentEquals(c, cpPath(tmpDir, "file2"), "file2\n"); err != nil {
c.Fatal(err)
}
// Next, copy a file from the container to a symlink to a file that does
// not exist (a broken symlink). This should create the target file with
// the contents of the source file.
dstPath = cpPath(tmpDir, "brokenSymlinkToFileX")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, dstPath, "fileX"); err != nil {
c.Fatal(err)
}
// The file should have the contents of "file2" now.
if err := fileContentEquals(c, cpPath(tmpDir, "fileX"), "file2\n"); err != nil {
c.Fatal(err)
}
// Next, copy a directory from the container to a symlink to a local
// directory. This should copy the directory into the symlink target
// directory and not modify the symlink.
srcPath = containerCpPath(cID, "/dir2")
dstPath = cpPath(tmpDir, "symlinkToDir1")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, dstPath, "dir1"); err != nil {
c.Fatal(err)
}
// The directory should now contain a copy of "dir2".
if err := fileContentEquals(c, cpPath(tmpDir, "dir1/dir2/file2-1"), "file2-1\n"); err != nil {
c.Fatal(err)
}
// Next, copy a directory from the container to a symlink to a local
// directory that does not exist (a broken symlink). This should create
// the target as a directory with the contents of the source directory. It
// should not modify the symlink.
dstPath = cpPath(tmpDir, "brokenSymlinkToDirX")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, dstPath, "dirX"); err != nil {
c.Fatal(err)
}
// The "dirX" directory should now be a copy of "dir2".
if err := fileContentEquals(c, cpPath(tmpDir, "dirX/file2-1"), "file2-1\n"); err != nil {
c.Fatal(err)
}
}
// Possibilities are reduced to the remaining 10 cases: // Possibilities are reduced to the remaining 10 cases:
// //
// case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action // case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action

View File

@ -250,29 +250,185 @@ func (s *DockerSuite) TestCpAbsoluteSymlink(c *check.C) {
c.Fatal(err) c.Fatal(err)
} }
tmpname := filepath.Join(tmpdir, cpTestName) tmpname := filepath.Join(tmpdir, "container_path")
defer os.RemoveAll(tmpdir) defer os.RemoveAll(tmpdir)
path := path.Join("/", "container_path") path := path.Join("/", "container_path")
dockerCmd(c, "cp", cleanedContainerID+":"+path, tmpdir) dockerCmd(c, "cp", cleanedContainerID+":"+path, tmpdir)
file, _ := os.Open(tmpname) // We should have copied a symlink *NOT* the file itself!
defer file.Close() linkTarget, err := os.Readlink(tmpname)
test, err := ioutil.ReadAll(file)
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
if string(test) == cpHostContents { if linkTarget != filepath.FromSlash(cpFullPath) {
c.Errorf("output matched host file -- absolute symlink can escape container rootfs") c.Errorf("symlink target was %q, but expected: %q", linkTarget, cpFullPath)
}
}
// Check that symlinks to a directory behave as expected when copying one from
// a container.
func (s *DockerSuite) TestCpFromSymlinkToDirectory(c *check.C) {
out, exitCode := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "mkdir -p '"+cpTestPath+"' && echo -n '"+cpContainerContents+"' > "+cpFullPath+" && ln -s "+cpTestPathParent+" /dir_link")
if exitCode != 0 {
c.Fatal("failed to create a container", out)
} }
if string(test) != cpContainerContents { cleanedContainerID := strings.TrimSpace(out)
c.Errorf("output doesn't match the input for absolute symlink")
out, _ = dockerCmd(c, "wait", cleanedContainerID)
if strings.TrimSpace(out) != "0" {
c.Fatal("failed to set up container", out)
} }
testDir, err := ioutil.TempDir("", "test-cp-from-symlink-to-dir-")
if err != nil {
c.Fatal(err)
}
defer os.RemoveAll(testDir)
// This copy command should copy the symlink, not the target, into the
// temporary directory.
dockerCmd(c, "cp", cleanedContainerID+":"+"/dir_link", testDir)
expectedPath := filepath.Join(testDir, "dir_link")
linkTarget, err := os.Readlink(expectedPath)
if err != nil {
c.Fatalf("unable to read symlink at %q: %v", expectedPath, err)
}
if linkTarget != filepath.FromSlash(cpTestPathParent) {
c.Errorf("symlink target was %q, but expected: %q", linkTarget, cpTestPathParent)
}
os.Remove(expectedPath)
// This copy command should resolve the symlink (note the trailing
// seperator), copying the target into the temporary directory.
dockerCmd(c, "cp", cleanedContainerID+":"+"/dir_link/", testDir)
// It *should not* have copied the directory using the target's name, but
// used the given name instead.
unexpectedPath := filepath.Join(testDir, cpTestPathParent)
if stat, err := os.Lstat(unexpectedPath); err == nil {
c.Fatalf("target name was copied: %q - %q", stat.Mode(), stat.Name())
}
// It *should* have copied the directory using the asked name "dir_link".
stat, err := os.Lstat(expectedPath)
if err != nil {
c.Fatalf("unable to stat resource at %q: %v", expectedPath, err)
}
if !stat.IsDir() {
c.Errorf("should have copied a directory but got %q instead", stat.Mode())
}
}
// Check that symlinks to a directory behave as expected when copying one to a
// container.
func (s *DockerSuite) TestCpToSymlinkToDirectory(c *check.C) {
testRequires(c, SameHostDaemon) // Requires local volume mount bind.
testVol, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-")
if err != nil {
c.Fatal(err)
}
defer os.RemoveAll(testVol)
// Create a test container with a local volume. We will test by copying
// to the volume path in the container which we can then verify locally.
out, exitCode := dockerCmd(c, "create", "-v", testVol+":/testVol", "busybox")
if exitCode != 0 {
c.Fatal("failed to create a container", out)
}
cleanedContainerID := strings.TrimSpace(out)
// Create a temp directory to hold a test file nested in a direcotry.
testDir, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-")
if err != nil {
c.Fatal(err)
}
defer os.RemoveAll(testDir)
// This file will be at "/testDir/some/path/test" and will be copied into
// the test volume later.
hostTestFilename := filepath.Join(testDir, cpFullPath)
if err := os.MkdirAll(filepath.Dir(hostTestFilename), os.FileMode(0700)); err != nil {
c.Fatal(err)
}
if err := ioutil.WriteFile(hostTestFilename, []byte(cpHostContents), os.FileMode(0600)); err != nil {
c.Fatal(err)
}
// Now create another temp directory to hold a symlink to the
// "/testDir/some" directory.
linkDir, err := ioutil.TempDir("", "test-cp-to-symlink-to-dir-")
if err != nil {
c.Fatal(err)
}
defer os.RemoveAll(linkDir)
// Then symlink "/linkDir/dir_link" to "/testdir/some".
linkTarget := filepath.Join(testDir, cpTestPathParent)
localLink := filepath.Join(linkDir, "dir_link")
if err := os.Symlink(linkTarget, localLink); err != nil {
c.Fatal(err)
}
// Now copy that symlink into the test volume in the container.
dockerCmd(c, "cp", localLink, cleanedContainerID+":/testVol")
// This copy command should have copied the symlink *not* the target.
expectedPath := filepath.Join(testVol, "dir_link")
actualLinkTarget, err := os.Readlink(expectedPath)
if err != nil {
c.Fatalf("unable to read symlink at %q: %v", expectedPath, err)
}
if actualLinkTarget != linkTarget {
c.Errorf("symlink target was %q, but expected: %q", actualLinkTarget, linkTarget)
}
// Good, now remove that copied link for the next test.
os.Remove(expectedPath)
// This copy command should resolve the symlink (note the trailing
// seperator), copying the target into the test volume directory in the
// container.
dockerCmd(c, "cp", localLink+"/", cleanedContainerID+":/testVol")
// It *should not* have copied the directory using the target's name, but
// used the given name instead.
unexpectedPath := filepath.Join(testVol, cpTestPathParent)
if stat, err := os.Lstat(unexpectedPath); err == nil {
c.Fatalf("target name was copied: %q - %q", stat.Mode(), stat.Name())
}
// It *should* have copied the directory using the asked name "dir_link".
stat, err := os.Lstat(expectedPath)
if err != nil {
c.Fatalf("unable to stat resource at %q: %v", expectedPath, err)
}
if !stat.IsDir() {
c.Errorf("should have copied a directory but got %q instead", stat.Mode())
}
// And this directory should contain the file copied from the host at the
// expected location: "/testVol/dir_link/path/test"
expectedFilepath := filepath.Join(testVol, "dir_link/path/test")
fileContents, err := ioutil.ReadFile(expectedFilepath)
if err != nil {
c.Fatal(err)
}
if string(fileContents) != cpHostContents {
c.Fatalf("file contains %q but expected %q", string(fileContents), cpHostContents)
}
} }
// Test for #5619 // Test for #5619

View File

@ -146,6 +146,118 @@ func (s *DockerSuite) TestCpToErrDstNotDir(c *check.C) {
} }
} }
// Check that copying from a local path to a symlink in a container copies to
// the symlink target and does not overwrite the container symlink itself.
func (s *DockerSuite) TestCpToSymlinkDestination(c *check.C) {
testRequires(c, SameHostDaemon) // Requires local volume mount bind.
testVol := getTestDir(c, "test-cp-to-symlink-destination-")
defer os.RemoveAll(testVol)
makeTestContentInDir(c, testVol)
cID := makeTestContainer(c, testContainerOptions{
volumes: defaultVolumes(testVol), // Our bind mount is at /vol2
})
defer deleteContainer(cID)
// First, copy a local file to a symlink to a file in the container. This
// should overwrite the symlink target contents with the source contents.
srcPath := cpPath(testVol, "file2")
dstPath := containerCpPath(cID, "/vol2/symlinkToFile1")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, cpPath(testVol, "symlinkToFile1"), "file1"); err != nil {
c.Fatal(err)
}
// The file should have the contents of "file2" now.
if err := fileContentEquals(c, cpPath(testVol, "file1"), "file2\n"); err != nil {
c.Fatal(err)
}
// Next, copy a local file to a symlink to a directory in the container.
// This should copy the file into the symlink target directory.
dstPath = containerCpPath(cID, "/vol2/symlinkToDir1")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"); err != nil {
c.Fatal(err)
}
// The file should have the contents of "file2" now.
if err := fileContentEquals(c, cpPath(testVol, "file2"), "file2\n"); err != nil {
c.Fatal(err)
}
// Next, copy a file to a symlink to a file that does not exist (a broken
// symlink) in the container. This should create the target file with the
// contents of the source file.
dstPath = containerCpPath(cID, "/vol2/brokenSymlinkToFileX")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToFileX"), "fileX"); err != nil {
c.Fatal(err)
}
// The file should have the contents of "file2" now.
if err := fileContentEquals(c, cpPath(testVol, "fileX"), "file2\n"); err != nil {
c.Fatal(err)
}
// Next, copy a local directory to a symlink to a directory in the
// container. This should copy the directory into the symlink target
// directory and not modify the symlink.
srcPath = cpPath(testVol, "/dir2")
dstPath = containerCpPath(cID, "/vol2/symlinkToDir1")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"); err != nil {
c.Fatal(err)
}
// The directory should now contain a copy of "dir2".
if err := fileContentEquals(c, cpPath(testVol, "dir1/dir2/file2-1"), "file2-1\n"); err != nil {
c.Fatal(err)
}
// Next, copy a local directory to a symlink to a local directory that does
// not exist (a broken symlink) in the container. This should create the
// target as a directory with the contents of the source directory. It
// should not modify the symlink.
dstPath = containerCpPath(cID, "/vol2/brokenSymlinkToDirX")
if err := runDockerCp(c, srcPath, dstPath); err != nil {
c.Fatalf("unexpected error %T: %s", err, err)
}
// The symlink should not have been modified.
if err := symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToDirX"), "dirX"); err != nil {
c.Fatal(err)
}
// The "dirX" directory should now be a copy of "dir2".
if err := fileContentEquals(c, cpPath(testVol, "dirX/file2-1"), "file2-1\n"); err != nil {
c.Fatal(err)
}
}
// Possibilities are reduced to the remaining 10 cases: // Possibilities are reduced to the remaining 10 cases:
// //
// case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action // case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action

View File

@ -74,8 +74,11 @@ var defaultFileData = []fileData{
{ftRegular, "dir4/file3-1", "file4-1"}, {ftRegular, "dir4/file3-1", "file4-1"},
{ftRegular, "dir4/file3-2", "file4-2"}, {ftRegular, "dir4/file3-2", "file4-2"},
{ftDir, "dir5", ""}, {ftDir, "dir5", ""},
{ftSymlink, "symlink1", "target1"}, {ftSymlink, "symlinkToFile1", "file1"},
{ftSymlink, "symlink2", "target2"}, {ftSymlink, "symlinkToDir1", "dir1"},
{ftSymlink, "brokenSymlinkToFileX", "fileX"},
{ftSymlink, "brokenSymlinkToDirX", "dirX"},
{ftSymlink, "symlinkToAbsDir", "/root"},
} }
func defaultMkContentCommand() string { func defaultMkContentCommand() string {
@ -268,6 +271,21 @@ func fileContentEquals(c *check.C, filename, contents string) (err error) {
return return
} }
func symlinkTargetEquals(c *check.C, symlink, expectedTarget string) (err error) {
c.Logf("checking that the symlink %q points to %q\n", symlink, expectedTarget)
actualTarget, err := os.Readlink(symlink)
if err != nil {
return err
}
if actualTarget != expectedTarget {
return fmt.Errorf("symlink target points to %q not %q", actualTarget, expectedTarget)
}
return nil
}
func containerStartOutputEquals(c *check.C, cID, contents string) (err error) { func containerStartOutputEquals(c *check.C, cID, contents string) (err error) {
c.Logf("checking that container %q start output contains %q\n", cID, contents) c.Logf("checking that container %q start output contains %q\n", cID, contents)

View File

@ -37,11 +37,13 @@ type (
Compression Compression Compression Compression
NoLchown bool NoLchown bool
ChownOpts *TarChownOptions ChownOpts *TarChownOptions
Name string
IncludeSourceDir bool IncludeSourceDir bool
// When unpacking, specifies whether overwriting a directory with a // When unpacking, specifies whether overwriting a directory with a
// non-directory is allowed and vice versa. // non-directory is allowed and vice versa.
NoOverwriteDirNonDir bool NoOverwriteDirNonDir bool
// For each include when creating an archive, the included name will be
// replaced with the matching name from this map.
RebaseNames map[string]string
} }
// Archiver allows the reuse of most utility functions of this package // Archiver allows the reuse of most utility functions of this package
@ -454,8 +456,9 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
seen := make(map[string]bool) seen := make(map[string]bool)
var renamedRelFilePath string // For when tar.Options.Name is set
for _, include := range options.IncludeFiles { for _, include := range options.IncludeFiles {
rebaseName := options.RebaseNames[include]
// We can't use filepath.Join(srcPath, include) because this will // We can't use filepath.Join(srcPath, include) because this will
// clean away a trailing "." or "/" which may be important. // clean away a trailing "." or "/" which may be important.
walkRoot := strings.Join([]string{srcPath, include}, string(filepath.Separator)) walkRoot := strings.Join([]string{srcPath, include}, string(filepath.Separator))
@ -503,14 +506,17 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
} }
seen[relFilePath] = true seen[relFilePath] = true
// TODO Windows: Verify if this needs to be os.Pathseparator // Rename the base resource.
// Rename the base resource if rebaseName != "" {
if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) { var replacement string
renamedRelFilePath = relFilePath if rebaseName != string(filepath.Separator) {
} // Special case the root directory to replace with an
// Set this to make sure the items underneath also get renamed // empty string instead so that we don't end up with
if options.Name != "" { // double slashes in the paths.
relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1) replacement = rebaseName
}
relFilePath = strings.Replace(relFilePath, include, replacement, 1)
} }
if err := ta.addTarFile(filePath, relFilePath); err != nil { if err := ta.addTarFile(filePath, relFilePath); err != nil {

View File

@ -695,7 +695,7 @@ func TestTarWithOptions(t *testing.T) {
{&TarOptions{ExcludePatterns: []string{"2"}}, 1}, {&TarOptions{ExcludePatterns: []string{"2"}}, 1},
{&TarOptions{ExcludePatterns: []string{"1", "folder*"}}, 2}, {&TarOptions{ExcludePatterns: []string{"1", "folder*"}}, 2},
{&TarOptions{IncludeFiles: []string{"1", "1"}}, 2}, {&TarOptions{IncludeFiles: []string{"1", "1"}}, 2},
{&TarOptions{Name: "test", IncludeFiles: []string{"1"}}, 4}, {&TarOptions{IncludeFiles: []string{"1"}, RebaseNames: map[string]string{"1": "test"}}, 4},
} }
for _, testCase := range cases { for _, testCase := range cases {
changes, err := tarUntar(t, origin, testCase.opts) changes, err := tarUntar(t, origin, testCase.opts)

View File

@ -6,7 +6,6 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -64,34 +63,33 @@ func SpecifiesCurrentDir(path string) bool {
return filepath.Base(path) == "." return filepath.Base(path) == "."
} }
// SplitPathDirEntry splits the given path between its // SplitPathDirEntry splits the given path between its directory name and its
// parent directory and its basename in that directory. // basename by first cleaning the path but preserves a trailing "." if the
func SplitPathDirEntry(localizedPath string) (dir, base string) { // original path specified the current directory.
normalizedPath := filepath.ToSlash(localizedPath) func SplitPathDirEntry(path string) (dir, base string) {
vol := filepath.VolumeName(normalizedPath) cleanedPath := filepath.Clean(path)
normalizedPath = normalizedPath[len(vol):]
if normalizedPath == "/" { if SpecifiesCurrentDir(path) {
// Specifies the root path. cleanedPath += string(filepath.Separator) + "."
return filepath.FromSlash(vol + normalizedPath), "."
} }
trimmedPath := vol + strings.TrimRight(normalizedPath, "/") return filepath.Dir(cleanedPath), filepath.Base(cleanedPath)
dir = filepath.FromSlash(path.Dir(trimmedPath))
base = filepath.FromSlash(path.Base(trimmedPath))
return dir, base
} }
// TarResource archives the resource at the given sourcePath into a Tar // TarResource archives the resource described by the given CopyInfo to a Tar
// archive. A non-nil error is returned if sourcePath does not exist or is // archive. A non-nil error is returned if sourcePath does not exist or is
// asserted to be a directory but exists as another type of file. // asserted to be a directory but exists as another type of file.
// //
// This function acts as a convenient wrapper around TarWithOptions, which // This function acts as a convenient wrapper around TarWithOptions, which
// requires a directory as the source path. TarResource accepts either a // requires a directory as the source path. TarResource accepts either a
// directory or a file path and correctly sets the Tar options. // directory or a file path and correctly sets the Tar options.
func TarResource(sourcePath string) (content Archive, err error) { func TarResource(sourceInfo CopyInfo) (content Archive, err error) {
return TarResourceRebase(sourceInfo.Path, sourceInfo.RebaseName)
}
// TarResourceRebase is like TarResource but renames the first path element of
// items in the resulting tar archive to match the given rebaseName if not "".
func TarResourceRebase(sourcePath, rebaseName string) (content Archive, err error) {
if _, err = os.Lstat(sourcePath); err != nil { if _, err = os.Lstat(sourcePath); err != nil {
// Catches the case where the source does not exist or is not a // Catches the case where the source does not exist or is not a
// directory if asserted to be a directory, as this also causes an // directory if asserted to be a directory, as this also causes an
@ -99,22 +97,6 @@ func TarResource(sourcePath string) (content Archive, err error) {
return return
} }
if len(sourcePath) > 1 && HasTrailingPathSeparator(sourcePath) {
// In the case where the source path is a symbolic link AND it ends
// with a path separator, we will want to evaluate the symbolic link.
trimmedPath := sourcePath[:len(sourcePath)-1]
stat, err := os.Lstat(trimmedPath)
if err != nil {
return nil, err
}
if stat.Mode()&os.ModeSymlink != 0 {
if sourcePath, err = filepath.EvalSymlinks(trimmedPath); err != nil {
return nil, err
}
}
}
// Separate the source path between it's directory and // Separate the source path between it's directory and
// the entry in that directory which we are archiving. // the entry in that directory which we are archiving.
sourceDir, sourceBase := SplitPathDirEntry(sourcePath) sourceDir, sourceBase := SplitPathDirEntry(sourcePath)
@ -127,32 +109,137 @@ func TarResource(sourcePath string) (content Archive, err error) {
Compression: Uncompressed, Compression: Uncompressed,
IncludeFiles: filter, IncludeFiles: filter,
IncludeSourceDir: true, IncludeSourceDir: true,
RebaseNames: map[string]string{
sourceBase: rebaseName,
},
}) })
} }
// CopyInfo holds basic info about the source // CopyInfo holds basic info about the source
// or destination path of a copy operation. // or destination path of a copy operation.
type CopyInfo struct { type CopyInfo struct {
Path string Path string
Exists bool Exists bool
IsDir bool IsDir bool
RebaseName string
} }
// CopyInfoStatPath stats the given path to create a CopyInfo // CopyInfoSourcePath stats the given path to create a CopyInfo
// struct representing that resource. If mustExist is true, then // struct representing that resource for the source of an archive copy
// it is an error if there is no file or directory at the given path. // operation. The given path should be an absolute local path. A source path
func CopyInfoStatPath(path string, mustExist bool) (CopyInfo, error) { // has all symlinks evaluated that appear before the last path separator ("/"
pathInfo := CopyInfo{Path: path} // on Unix). As it is to be a copy source, the path must exist.
func CopyInfoSourcePath(path string) (CopyInfo, error) {
// Split the given path into its Directory and Base components. We will
// evaluate symlinks in the directory component then append the base.
dirPath, basePath := filepath.Split(path)
fileInfo, err := os.Lstat(path) resolvedDirPath, err := filepath.EvalSymlinks(dirPath)
if err != nil {
if err == nil { return CopyInfo{}, err
pathInfo.Exists, pathInfo.IsDir = true, fileInfo.IsDir()
} else if os.IsNotExist(err) && !mustExist {
err = nil
} }
return pathInfo, err // resolvedDirPath will have been cleaned (no trailing path separators) so
// we can manually join it with the base path element.
resolvedPath := resolvedDirPath + string(filepath.Separator) + basePath
var rebaseName string
if HasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) {
// In the case where the path had a trailing separator and a symlink
// evaluation has changed the last path component, we will need to
// rebase the name in the archive that is being copied to match the
// originally requested name.
rebaseName = filepath.Base(path)
}
stat, err := os.Lstat(resolvedPath)
if err != nil {
return CopyInfo{}, err
}
return CopyInfo{
Path: resolvedPath,
Exists: true,
IsDir: stat.IsDir(),
RebaseName: rebaseName,
}, nil
}
// CopyInfoDestinationPath stats the given path to create a CopyInfo
// struct representing that resource for the destination of an archive copy
// operation. The given path should be an absolute local path.
func CopyInfoDestinationPath(path string) (info CopyInfo, err error) {
maxSymlinkIter := 10 // filepath.EvalSymlinks uses 255, but 10 already seems like a lot.
originalPath := path
stat, err := os.Lstat(path)
if err == nil && stat.Mode()&os.ModeSymlink == 0 {
// The path exists and is not a symlink.
return CopyInfo{
Path: path,
Exists: true,
IsDir: stat.IsDir(),
}, nil
}
// While the path is a symlink.
for n := 0; err == nil && stat.Mode()&os.ModeSymlink != 0; n++ {
if n > maxSymlinkIter {
// Don't follow symlinks more than this arbitrary number of times.
return CopyInfo{}, errors.New("too many symlinks in " + originalPath)
}
// The path is a symbolic link. We need to evaluate it so that the
// destination of the copy operation is the link target and not the
// link itself. This is notably different than CopyInfoSourcePath which
// only evaluates symlinks before the last appearing path separator.
// Also note that it is okay if the last path element is a broken
// symlink as the copy operation should create the target.
var linkTarget string
linkTarget, err = os.Readlink(path)
if err != nil {
return CopyInfo{}, err
}
if !filepath.IsAbs(linkTarget) {
// Join with the parent directory.
dstParent, _ := SplitPathDirEntry(path)
linkTarget = filepath.Join(dstParent, linkTarget)
}
path = linkTarget
stat, err = os.Lstat(path)
}
if err != nil {
// It's okay if the destination path doesn't exist. We can still
// continue the copy operation if the parent directory exists.
if !os.IsNotExist(err) {
return CopyInfo{}, err
}
// Ensure destination parent dir exists.
dstParent, _ := SplitPathDirEntry(path)
parentDirStat, err := os.Lstat(dstParent)
if err != nil {
return CopyInfo{}, err
}
if !parentDirStat.IsDir() {
return CopyInfo{}, ErrNotDirectory
}
return CopyInfo{Path: path}, nil
}
// The path exists after resolving symlinks.
return CopyInfo{
Path: path,
Exists: true,
IsDir: stat.IsDir(),
}, nil
} }
// PrepareArchiveCopy prepares the given srcContent archive, which should // PrepareArchiveCopy prepares the given srcContent archive, which should
@ -210,6 +297,13 @@ func PrepareArchiveCopy(srcContent ArchiveReader, srcInfo, dstInfo CopyInfo) (ds
// rebaseArchiveEntries rewrites the given srcContent archive replacing // rebaseArchiveEntries rewrites the given srcContent archive replacing
// an occurance of oldBase with newBase at the beginning of entry names. // an occurance of oldBase with newBase at the beginning of entry names.
func rebaseArchiveEntries(srcContent ArchiveReader, oldBase, newBase string) Archive { func rebaseArchiveEntries(srcContent ArchiveReader, oldBase, newBase string) Archive {
if oldBase == "/" {
// If oldBase specifies the root directory, use an empty string as
// oldBase instead so that newBase doesn't replace the path separator
// that all paths will start with.
oldBase = ""
}
rebased, w := io.Pipe() rebased, w := io.Pipe()
go func() { go func() {
@ -259,11 +353,11 @@ func CopyResource(srcPath, dstPath string) error {
srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath) srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath) dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
if srcInfo, err = CopyInfoStatPath(srcPath, true); err != nil { if srcInfo, err = CopyInfoSourcePath(srcPath); err != nil {
return err return err
} }
content, err := TarResource(srcPath) content, err := TarResource(srcInfo)
if err != nil { if err != nil {
return err return err
} }
@ -275,24 +369,13 @@ func CopyResource(srcPath, dstPath string) error {
// CopyTo handles extracting the given content whose // CopyTo handles extracting the given content whose
// entries should be sourced from srcInfo to dstPath. // entries should be sourced from srcInfo to dstPath.
func CopyTo(content ArchiveReader, srcInfo CopyInfo, dstPath string) error { func CopyTo(content ArchiveReader, srcInfo CopyInfo, dstPath string) error {
dstInfo, err := CopyInfoStatPath(dstPath, false) // The destination path need not exist, but CopyInfoDestinationPath will
// ensure that at least the parent directory exists.
dstInfo, err := CopyInfoDestinationPath(dstPath)
if err != nil { if err != nil {
return err return err
} }
if !dstInfo.Exists {
// Ensure destination parent dir exists.
dstParent, _ := SplitPathDirEntry(dstPath)
dstStat, err := os.Lstat(dstParent)
if err != nil {
return err
}
if !dstStat.IsDir() {
return ErrNotDirectory
}
}
dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo) dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo)
if err != nil { if err != nil {
return err return err

View File

@ -138,13 +138,7 @@ func TestCopyErrSrcNotExists(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t) tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB) defer removeAllPaths(tmpDirA, tmpDirB)
content, err := TarResource(filepath.Join(tmpDirA, "file1")) if _, err := CopyInfoSourcePath(filepath.Join(tmpDirA, "file1")); !os.IsNotExist(err) {
if err == nil {
content.Close()
t.Fatal("expected IsNotExist error, but got nil instead")
}
if !os.IsNotExist(err) {
t.Fatalf("expected IsNotExist error, but got %T: %s", err, err) t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
} }
} }
@ -158,13 +152,7 @@ func TestCopyErrSrcNotDir(t *testing.T) {
// Load A with some sample files and directories. // Load A with some sample files and directories.
createSampleDir(t, tmpDirA) createSampleDir(t, tmpDirA)
content, err := TarResource(joinTrailingSep(tmpDirA, "file1")) if _, err := CopyInfoSourcePath(joinTrailingSep(tmpDirA, "file1")); !isNotDir(err) {
if err == nil {
content.Close()
t.Fatal("expected IsNotDir error, but got nil instead")
}
if !isNotDir(err) {
t.Fatalf("expected IsNotDir error, but got %T: %s", err, err) t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
} }
} }
@ -181,7 +169,7 @@ func TestCopyErrDstParentNotExists(t *testing.T) {
srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false} srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
// Try with a file source. // Try with a file source.
content, err := TarResource(srcInfo.Path) content, err := TarResource(srcInfo)
if err != nil { if err != nil {
t.Fatalf("unexpected error %T: %s", err, err) t.Fatalf("unexpected error %T: %s", err, err)
} }
@ -199,7 +187,7 @@ func TestCopyErrDstParentNotExists(t *testing.T) {
// Try with a directory source. // Try with a directory source.
srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true} srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
content, err = TarResource(srcInfo.Path) content, err = TarResource(srcInfo)
if err != nil { if err != nil {
t.Fatalf("unexpected error %T: %s", err, err) t.Fatalf("unexpected error %T: %s", err, err)
} }
@ -228,7 +216,7 @@ func TestCopyErrDstNotDir(t *testing.T) {
// Try with a file source. // Try with a file source.
srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false} srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
content, err := TarResource(srcInfo.Path) content, err := TarResource(srcInfo)
if err != nil { if err != nil {
t.Fatalf("unexpected error %T: %s", err, err) t.Fatalf("unexpected error %T: %s", err, err)
} }
@ -245,7 +233,7 @@ func TestCopyErrDstNotDir(t *testing.T) {
// Try with a directory source. // Try with a directory source.
srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true} srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
content, err = TarResource(srcInfo.Path) content, err = TarResource(srcInfo)
if err != nil { if err != nil {
t.Fatalf("unexpected error %T: %s", err, err) t.Fatalf("unexpected error %T: %s", err, err)
} }