浏览代码

Merge pull request #13171 from jlhawn/archive_copy

docker cp to and from containers
Arnaud Porterie 10 年之前
父节点
当前提交
c986f85f73

+ 269 - 22
api/client/cp.go

@@ -1,8 +1,14 @@
 package client
 
 import (
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
 	"strings"
 
 	"github.com/docker/docker/api/types"
@@ -10,48 +16,289 @@ import (
 	flag "github.com/docker/docker/pkg/mflag"
 )
 
-// CmdCp copies files/folders from a path on the container to a directory on the host running the command.
+type copyDirection int
+
+const (
+	fromContainer copyDirection = (1 << iota)
+	toContainer
+	acrossContainers = fromContainer | toContainer
+)
+
+// CmdCp copies files/folders to or from a path in a container.
 //
-// If HOSTDIR is '-', the data is written as a tar file to STDOUT.
+// When copying from a container, if LOCALPATH is '-' the data is written as a
+// tar archive file to STDOUT.
 //
-// Usage: docker cp CONTAINER:PATH HOSTDIR
+// When copying to a container, if LOCALPATH is '-' the data is read as a tar
+// archive file from STDIN, and the destination CONTAINER:PATH, must specify
+// a directory.
+//
+// Usage:
+// 	docker cp CONTAINER:PATH LOCALPATH|-
+// 	docker cp LOCALPATH|- CONTAINER:PATH
 func (cli *DockerCli) CmdCp(args ...string) error {
-	cmd := cli.Subcmd("cp", []string{"CONTAINER:PATH HOSTDIR|-"}, "Copy files/folders from a container's PATH to a HOSTDIR on the host\nrunning the command. Use '-' to write the data as a tar file to STDOUT.", true)
-	cmd.Require(flag.Exact, 2)
+	cmd := cli.Subcmd(
+		"cp",
+		[]string{"CONTAINER:PATH LOCALPATH|-", "LOCALPATH|- CONTAINER:PATH"},
+		strings.Join([]string{
+			"Copy files/folders between a container and your host.\n",
+			"Use '-' as the source to read a tar archive from stdin\n",
+			"and extract it to a directory destination in a container.\n",
+			"Use '-' as the destination to stream a tar archive of a\n",
+			"container source to stdout.",
+		}, ""),
+		true,
+	)
 
+	cmd.Require(flag.Exact, 2)
 	cmd.ParseFlags(args, true)
 
-	// deal with path name with `:`
-	info := strings.SplitN(cmd.Arg(0), ":", 2)
+	if cmd.Arg(0) == "" {
+		return fmt.Errorf("source can not be empty")
+	}
+	if cmd.Arg(1) == "" {
+		return fmt.Errorf("destination can not be empty")
+	}
+
+	srcContainer, srcPath := splitCpArg(cmd.Arg(0))
+	dstContainer, dstPath := splitCpArg(cmd.Arg(1))
+
+	var direction copyDirection
+	if srcContainer != "" {
+		direction |= fromContainer
+	}
+	if dstContainer != "" {
+		direction |= toContainer
+	}
+
+	switch direction {
+	case fromContainer:
+		return cli.copyFromContainer(srcContainer, srcPath, dstPath)
+	case toContainer:
+		return cli.copyToContainer(srcPath, dstContainer, dstPath)
+	case acrossContainers:
+		// Copying between containers isn't supported.
+		return fmt.Errorf("copying between containers is not supported")
+	default:
+		// User didn't specify any container.
+		return fmt.Errorf("must specify at least one container source")
+	}
+}
 
-	if len(info) != 2 {
-		return fmt.Errorf("Error: Path not specified")
+// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
+// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
+// requiring a LOCALPATH with a `:` to be made explicit with a relative or
+// absolute path:
+// 	`/path/to/file:name.txt` or `./file:name.txt`
+//
+// This is apparently how `scp` handles this as well:
+// 	http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
+//
+// We can't simply check for a filepath separator because container names may
+// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
+// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
+// client, a `:` could be part of an absolute Windows path, in which case it
+// is immediately proceeded by a backslash.
+func splitCpArg(arg string) (container, path string) {
+	if filepath.IsAbs(arg) {
+		// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
+		return "", arg
 	}
 
-	cfg := &types.CopyConfig{
-		Resource: info[1],
+	parts := strings.SplitN(arg, ":", 2)
+
+	if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
+		// Either there's no `:` in the arg
+		// OR it's an explicit local relative path like `./file:name.txt`.
+		return "", arg
 	}
-	serverResp, err := cli.call("POST", "/containers/"+info[0]+"/copy", cfg, nil)
-	if serverResp.body != nil {
-		defer serverResp.body.Close()
+
+	return parts[0], parts[1]
+}
+
+func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) {
+	var stat types.ContainerPathStat
+
+	query := make(url.Values, 1)
+	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
+
+	urlStr := fmt.Sprintf("/containers/%s/archive?%s", containerName, query.Encode())
+
+	response, err := cli.call("HEAD", urlStr, nil, nil)
+	if err != nil {
+		return stat, err
 	}
-	if serverResp.statusCode == 404 {
-		return fmt.Errorf("No such container: %v", info[0])
+	defer response.body.Close()
+
+	if response.statusCode != http.StatusOK {
+		return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
 	}
+
+	return getContainerPathStatFromHeader(response.header)
+}
+
+func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
+	var stat types.ContainerPathStat
+
+	encodedStat := header.Get("X-Docker-Container-Path-Stat")
+	statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
+
+	err := json.NewDecoder(statDecoder).Decode(&stat)
+	if err != nil {
+		err = fmt.Errorf("unable to decode container path stat header: %s", err)
+	}
+
+	return stat, err
+}
+
+func resolveLocalPath(localPath string) (absPath string, err error) {
+	if absPath, err = filepath.Abs(localPath); err != nil {
+		return
+	}
+
+	return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
+}
+
+func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (err error) {
+	if dstPath != "-" {
+		// Get an absolute destination path.
+		dstPath, err = resolveLocalPath(dstPath)
+		if err != nil {
+			return err
+		}
+	}
+
+	query := make(url.Values, 1)
+	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
+
+	urlStr := fmt.Sprintf("/containers/%s/archive?%s", srcContainer, query.Encode())
+
+	response, err := cli.call("GET", urlStr, nil, nil)
 	if err != nil {
 		return err
 	}
+	defer response.body.Close()
+
+	if response.statusCode != http.StatusOK {
+		return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
+	}
+
+	if dstPath == "-" {
+		// Send the response to STDOUT.
+		_, err = io.Copy(os.Stdout, response.body)
+
+		return err
+	}
+
+	// In order to get the copy behavior right, we need to know information
+	// about both the source and the destination. The response headers include
+	// stat info about the source that we can use in deciding exactly how to
+	// copy it locally. Along with the stat info about the local destination,
+	// we have everything we need to handle the multiple possibilities there
+	// can be when copying a file/dir from one location to another file/dir.
+	stat, err := getContainerPathStatFromHeader(response.header)
+	if err != nil {
+		return fmt.Errorf("unable to get resource stat from response: %s", err)
+	}
+
+	// Prepare source copy info.
+	srcInfo := archive.CopyInfo{
+		Path:   srcPath,
+		Exists: true,
+		IsDir:  stat.Mode.IsDir(),
+	}
 
-	hostPath := cmd.Arg(1)
-	if serverResp.statusCode == 200 {
-		if hostPath == "-" {
-			_, err = io.Copy(cli.out, serverResp.body)
-		} else {
-			err = archive.Untar(serverResp.body, hostPath, &archive.TarOptions{NoLchown: true})
+	// See comments in the implementation of `archive.CopyTo` for exactly what
+	// goes into deciding how and whether the source archive needs to be
+	// altered for the correct copy behavior.
+	return archive.CopyTo(response.body, srcInfo, dstPath)
+}
+
+func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (err error) {
+	if srcPath != "-" {
+		// Get an absolute source path.
+		srcPath, err = resolveLocalPath(srcPath)
+		if err != nil {
+			return err
 		}
+	}
+
+	// In order to get the copy behavior right, we need to know information
+	// about both the source and destination. The API is a simple tar
+	// archive/extract API but we can use the stat info header about the
+	// destination to be more informed about exactly what the destination is.
+
+	// Prepare destination copy info by stat-ing the container path.
+	dstInfo := archive.CopyInfo{Path: dstPath}
+	dstStat, err := cli.statContainerPath(dstContainer, dstPath)
+	// 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
+	// type of conflict (e.g., non-directory overwriting an existing directory
+	// or vice versia) the extraction will fail. If the destination simply did
+	// not exist, but the parent directory does, the extraction will still
+	// succeed.
+	if err == nil {
+		dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
+	}
+
+	var content io.Reader
+	if srcPath == "-" {
+		// Use STDIN.
+		content = os.Stdin
+		if !dstInfo.IsDir {
+			return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
+		}
+	} else {
+		srcArchive, err := archive.TarResource(srcPath)
+		if err != nil {
+			return err
+		}
+		defer srcArchive.Close()
+
+		// With the stat info about the local source as well as the
+		// destination, we have enough information to know whether we need to
+		// alter the archive that we upload so that when the server extracts
+		// it to the specified directory in the container we get the disired
+		// 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`
+		// for exactly what goes into deciding how and whether the source
+		// archive needs to be altered for the correct copy behavior when it is
+		// extracted. This function also infers from the source and destination
+		// info which directory to extract to, which may be the parent of the
+		// destination that the user specified.
+		dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
 		if err != nil {
 			return err
 		}
+		defer preparedArchive.Close()
+
+		dstPath = dstDir
+		content = preparedArchive
+	}
+
+	query := make(url.Values, 2)
+	query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
+	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
+	query.Set("noOverwriteDirNonDir", "true")
+
+	urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode())
+
+	response, err := cli.stream("PUT", urlStr, &streamOpts{in: content})
+	if err != nil {
+		return err
 	}
+	defer response.body.Close()
+
+	if response.statusCode != http.StatusOK {
+		return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
+	}
+
 	return nil
 }

+ 106 - 0
api/server/server.go

@@ -1309,6 +1309,7 @@ func (s *Server) postBuild(version version.Version, w http.ResponseWriter, r *ht
 	return nil
 }
 
+// postContainersCopy is deprecated in favor of getContainersArchivePath.
 func (s *Server) postContainersCopy(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if vars == nil {
 		return fmt.Errorf("Missing parameter")
@@ -1348,6 +1349,104 @@ func (s *Server) postContainersCopy(version version.Version, w http.ResponseWrit
 	return nil
 }
 
+// // Encode the stat to JSON, base64 encode, and place in a header.
+func setContainerPathStatHeader(stat *types.ContainerPathStat, header http.Header) error {
+	statJSON, err := json.Marshal(stat)
+	if err != nil {
+		return err
+	}
+
+	header.Set(
+		"X-Docker-Container-Path-Stat",
+		base64.StdEncoding.EncodeToString(statJSON),
+	)
+
+	return nil
+}
+
+func (s *Server) headContainersArchive(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if vars == nil {
+		return fmt.Errorf("Missing parameter")
+	}
+	if err := parseForm(r); err != nil {
+		return err
+	}
+
+	name := vars["name"]
+	path := r.Form.Get("path")
+
+	switch {
+	case name == "":
+		return fmt.Errorf("bad parameter: 'name' cannot be empty")
+	case path == "":
+		return fmt.Errorf("bad parameter: 'path' cannot be empty")
+	}
+
+	stat, err := s.daemon.ContainerStatPath(name, path)
+	if err != nil {
+		return err
+	}
+
+	return setContainerPathStatHeader(stat, w.Header())
+}
+
+func (s *Server) getContainersArchive(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if vars == nil {
+		return fmt.Errorf("Missing parameter")
+	}
+	if err := parseForm(r); err != nil {
+		return err
+	}
+
+	name := vars["name"]
+	path := r.Form.Get("path")
+
+	switch {
+	case name == "":
+		return fmt.Errorf("bad parameter: 'name' cannot be empty")
+	case path == "":
+		return fmt.Errorf("bad parameter: 'path' cannot be empty")
+	}
+
+	tarArchive, stat, err := s.daemon.ContainerArchivePath(name, path)
+	if err != nil {
+		return err
+	}
+	defer tarArchive.Close()
+
+	if err := setContainerPathStatHeader(stat, w.Header()); err != nil {
+		return err
+	}
+
+	w.Header().Set("Content-Type", "application/x-tar")
+	_, err = io.Copy(w, tarArchive)
+
+	return err
+}
+
+func (s *Server) putContainersArchive(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if vars == nil {
+		return fmt.Errorf("Missing parameter")
+	}
+	if err := parseForm(r); err != nil {
+		return err
+	}
+
+	name := vars["name"]
+	path := r.Form.Get("path")
+
+	noOverwriteDirNonDir := boolValue(r, "noOverwriteDirNonDir")
+
+	switch {
+	case name == "":
+		return fmt.Errorf("bad parameter: 'name' cannot be empty")
+	case path == "":
+		return fmt.Errorf("bad parameter: 'path' cannot be empty")
+	}
+
+	return s.daemon.ContainerExtractToDir(name, path, noOverwriteDirNonDir, r.Body)
+}
+
 func (s *Server) postContainerExecCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if err := parseForm(r); err != nil {
 		return err
@@ -1536,6 +1635,9 @@ func createRouter(s *Server) *mux.Router {
 		ProfilerSetup(r, "/debug/")
 	}
 	m := map[string]map[string]HttpApiFunc{
+		"HEAD": {
+			"/containers/{name:.*}/archive": s.headContainersArchive,
+		},
 		"GET": {
 			"/_ping":                          s.ping,
 			"/events":                         s.getEvents,
@@ -1557,6 +1659,7 @@ func createRouter(s *Server) *mux.Router {
 			"/containers/{name:.*}/stats":     s.getContainersStats,
 			"/containers/{name:.*}/attach/ws": s.wsContainersAttach,
 			"/exec/{id:.*}/json":              s.getExecByID,
+			"/containers/{name:.*}/archive":   s.getContainersArchive,
 		},
 		"POST": {
 			"/auth":                         s.postAuth,
@@ -1582,6 +1685,9 @@ func createRouter(s *Server) *mux.Router {
 			"/exec/{name:.*}/resize":        s.postContainerExecResize,
 			"/containers/{name:.*}/rename":  s.postContainerRename,
 		},
+		"PUT": {
+			"/containers/{name:.*}/archive": s.putContainersArchive,
+		},
 		"DELETE": {
 			"/containers/{name:.*}": s.deleteContainers,
 			"/images/{name:.*}":     s.deleteImages,

+ 13 - 0
api/types/types.go

@@ -1,6 +1,7 @@
 package types
 
 import (
+	"os"
 	"time"
 
 	"github.com/docker/docker/daemon/network"
@@ -127,6 +128,18 @@ type CopyConfig struct {
 	Resource string
 }
 
+// ContainerPathStat is used to encode the header from
+// 	GET /containers/{name:.*}/archive
+// "name" is the file or directory name.
+// "path" is the absolute path to the resource in the container.
+type ContainerPathStat struct {
+	Name  string      `json:"name"`
+	Path  string      `json:"path"`
+	Size  int64       `json:"size"`
+	Mode  os.FileMode `json:"mode"`
+	Mtime time.Time   `json:"mtime"`
+}
+
 // GET "/containers/{name:.*}/top"
 type ContainerProcessList struct {
 	Processes [][]string

+ 297 - 0
daemon/archive.go

@@ -0,0 +1,297 @@
+package daemon
+
+import (
+	"errors"
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/pkg/archive"
+	"github.com/docker/docker/pkg/chrootarchive"
+	"github.com/docker/docker/pkg/ioutils"
+)
+
+// ErrExtractPointNotDirectory is used to convey that the operation to extract
+// a tar archive to a directory in a container has failed because the specified
+// path does not refer to a directory.
+var ErrExtractPointNotDirectory = errors.New("extraction point is not a directory")
+
+// ContainerCopy performs a depracated operation of archiving the resource at
+// the specified path in the conatiner identified by the given name.
+func (daemon *Daemon) ContainerCopy(name string, res string) (io.ReadCloser, error) {
+	container, err := daemon.Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	if res[0] == '/' {
+		res = res[1:]
+	}
+
+	return container.Copy(res)
+}
+
+// ContainerStatPath stats the filesystem resource at the specified path in the
+// container identified by the given name.
+func (daemon *Daemon) ContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error) {
+	container, err := daemon.Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	return container.StatPath(path)
+}
+
+// ContainerArchivePath creates an archive of the filesystem resource at the
+// specified path in the container identified by the given name. Returns a
+// tar archive of the resource and whether it was a directory or a single file.
+func (daemon *Daemon) ContainerArchivePath(name string, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) {
+	container, err := daemon.Get(name)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return container.ArchivePath(path)
+}
+
+// ContainerExtractToDir extracts the given archive to the specified location
+// in the filesystem of the container identified by the given name. The given
+// path must be of a directory in the container. If it is not, the error will
+// be ErrExtractPointNotDirectory. If noOverwriteDirNonDir is true then it will
+// be an error if unpacking the given content would cause an existing directory
+// to be replaced with a non-directory and vice versa.
+func (daemon *Daemon) ContainerExtractToDir(name, path string, noOverwriteDirNonDir bool, content io.Reader) error {
+	container, err := daemon.Get(name)
+	if err != nil {
+		return err
+	}
+
+	return container.ExtractToDir(path, noOverwriteDirNonDir, content)
+}
+
+// StatPath stats the filesystem resource at the specified path in this
+// container. Returns stat info about the resource.
+func (container *Container) StatPath(path string) (stat *types.ContainerPathStat, err error) {
+	container.Lock()
+	defer container.Unlock()
+
+	if err = container.Mount(); err != nil {
+		return nil, err
+	}
+	defer container.Unmount()
+
+	err = container.mountVolumes()
+	defer container.UnmountVolumes(true)
+	if err != nil {
+		return nil, err
+	}
+
+	// Consider the given path as an absolute path in the container.
+	absPath := path
+	if !filepath.IsAbs(absPath) {
+		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)
+	}
+
+	resolvedPath, err := container.GetResourcePath(absPath)
+	if err != nil {
+		return nil, 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)
+
+	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
+// path in this container. Returns a tar archive of the resource and stat info
+// about the resource.
+func (container *Container) ArchivePath(path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) {
+	container.Lock()
+
+	defer func() {
+		if err != nil {
+			// Wait to unlock the container until the archive is fully read
+			// (see the ReadCloseWrapper func below) or if there is an error
+			// before that occurs.
+			container.Unlock()
+		}
+	}()
+
+	if err = container.Mount(); err != nil {
+		return nil, nil, err
+	}
+
+	defer func() {
+		if err != nil {
+			// unmount any volumes
+			container.UnmountVolumes(true)
+			// unmount the container's rootfs
+			container.Unmount()
+		}
+	}()
+
+	if err = container.mountVolumes(); err != nil {
+		return nil, nil, err
+	}
+
+	// Consider the given path as an absolute path in the container.
+	absPath := path
+	if !filepath.IsAbs(absPath) {
+		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)
+	}
+
+	resolvedPath, err := container.GetResourcePath(absPath)
+	if err != nil {
+		return nil, nil, 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)
+
+	lstat, err := os.Lstat(resolvedPath)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	stat = &types.ContainerPathStat{
+		Name:  lstat.Name(),
+		Path:  absPath,
+		Size:  lstat.Size(),
+		Mode:  lstat.Mode(),
+		Mtime: lstat.ModTime(),
+	}
+
+	data, err := archive.TarResource(resolvedPath)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	content = ioutils.NewReadCloserWrapper(data, func() error {
+		err := data.Close()
+		container.UnmountVolumes(true)
+		container.Unmount()
+		container.Unlock()
+		return err
+	})
+
+	container.LogEvent("archive-path")
+
+	return content, stat, nil
+}
+
+// ExtractToDir extracts the given tar archive to the specified location in the
+// filesystem of this container. The given path must be of a directory in the
+// container. If it is not, the error will be ErrExtractPointNotDirectory. If
+// noOverwriteDirNonDir is true then it will be an error if unpacking the
+// given content would cause an existing directory to be replaced with a non-
+// directory and vice versa.
+func (container *Container) ExtractToDir(path string, noOverwriteDirNonDir bool, content io.Reader) (err error) {
+	container.Lock()
+	defer container.Unlock()
+
+	if err = container.Mount(); err != nil {
+		return err
+	}
+	defer container.Unmount()
+
+	err = container.mountVolumes()
+	defer container.UnmountVolumes(true)
+	if err != nil {
+		return err
+	}
+
+	// Consider the given path as an absolute path in the container.
+	absPath := path
+	if !filepath.IsAbs(absPath) {
+		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)
+	}
+
+	resolvedPath, err := container.GetResourcePath(absPath)
+	if err != nil {
+		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)
+	if err != nil {
+		return err
+	}
+
+	if !stat.IsDir() {
+		return ErrExtractPointNotDirectory
+	}
+
+	baseRel, err := filepath.Rel(container.basefs, resolvedPath)
+	if err != nil {
+		return err
+	}
+	absPath = filepath.Join("/", 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.
+	var toVolume bool
+	for _, mnt := range container.MountPoints {
+		if toVolume = mnt.hasResource(absPath); toVolume {
+			if mnt.RW {
+				break
+			}
+			return ErrVolumeReadonly
+		}
+	}
+
+	if !toVolume && container.hostConfig.ReadonlyRootfs {
+		return ErrContainerRootfsReadonly
+	}
+
+	options := &archive.TarOptions{
+		ChownOpts: &archive.TarChownOptions{
+			UID: 0, GID: 0, // TODO: use config.User? Remap to userns root?
+		},
+		NoOverwriteDirNonDir: noOverwriteDirNonDir,
+	}
+
+	if err := chrootarchive.Untar(content, resolvedPath, options); err != nil {
+		return err
+	}
+
+	container.LogEvent("extract-to-dir")
+
+	return nil
+}

+ 55 - 27
daemon/container.go

@@ -35,10 +35,11 @@ import (
 )
 
 var (
-	ErrNotATTY               = errors.New("The PTY is not a file")
-	ErrNoTTY                 = errors.New("No PTY found")
-	ErrContainerStart        = errors.New("The container failed to start. Unknown error")
-	ErrContainerStartTimeout = errors.New("The container failed to start due to timed out.")
+	ErrNotATTY                 = errors.New("The PTY is not a file")
+	ErrNoTTY                   = errors.New("No PTY found")
+	ErrContainerStart          = errors.New("The container failed to start. Unknown error")
+	ErrContainerStartTimeout   = errors.New("The container failed to start due to timed out.")
+	ErrContainerRootfsReadonly = errors.New("container rootfs is marked read-only")
 )
 
 type StreamConfig struct {
@@ -616,13 +617,22 @@ func validateID(id string) error {
 	return nil
 }
 
-func (container *Container) Copy(resource string) (io.ReadCloser, error) {
+func (container *Container) Copy(resource string) (rc io.ReadCloser, err error) {
 	container.Lock()
-	defer container.Unlock()
-	var err error
+
+	defer func() {
+		if err != nil {
+			// Wait to unlock the container until the archive is fully read
+			// (see the ReadCloseWrapper func below) or if there is an error
+			// before that occurs.
+			container.Unlock()
+		}
+	}()
+
 	if err := container.Mount(); err != nil {
 		return nil, err
 	}
+
 	defer func() {
 		if err != nil {
 			// unmount any volumes
@@ -631,28 +641,11 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
 			container.Unmount()
 		}
 	}()
-	mounts, err := container.setupMounts()
-	if err != nil {
+
+	if err := container.mountVolumes(); err != nil {
 		return nil, err
 	}
-	for _, m := range mounts {
-		var dest string
-		dest, err = container.GetResourcePath(m.Destination)
-		if err != nil {
-			return nil, err
-		}
-		var stat os.FileInfo
-		stat, err = os.Stat(m.Source)
-		if err != nil {
-			return nil, err
-		}
-		if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
-			return nil, err
-		}
-		if err = mount.Mount(m.Source, dest, "bind", "rbind,ro"); err != nil {
-			return nil, err
-		}
-	}
+
 	basePath, err := container.GetResourcePath(resource)
 	if err != nil {
 		return nil, err
@@ -688,6 +681,7 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
 		container.CleanupStorage()
 		container.UnmountVolumes(true)
 		container.Unmount()
+		container.Unlock()
 		return err
 	})
 	container.LogEvent("copy")
@@ -1190,6 +1184,40 @@ func (container *Container) shouldRestart() bool {
 		(container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0)
 }
 
+func (container *Container) mountVolumes() error {
+	mounts, err := container.setupMounts()
+	if err != nil {
+		return err
+	}
+
+	for _, m := range mounts {
+		dest, err := container.GetResourcePath(m.Destination)
+		if err != nil {
+			return err
+		}
+
+		var stat os.FileInfo
+		stat, err = os.Stat(m.Source)
+		if err != nil {
+			return err
+		}
+		if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
+			return err
+		}
+
+		opts := "rbind,ro"
+		if m.Writable {
+			opts = "rbind,rw"
+		}
+
+		if err := mount.Mount(m.Source, dest, "bind", opts); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (container *Container) copyImagePathContent(v volume.Volume, destination string) error {
 	rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs)
 	if err != nil {

+ 0 - 16
daemon/copy.go

@@ -1,16 +0,0 @@
-package daemon
-
-import "io"
-
-func (daemon *Daemon) ContainerCopy(name string, res string) (io.ReadCloser, error) {
-	container, err := daemon.Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	if res[0] == '/' {
-		res = res[1:]
-	}
-
-	return container.Copy(res)
-}

+ 15 - 0
daemon/volumes.go

@@ -1,6 +1,7 @@
 package daemon
 
 import (
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -17,6 +18,10 @@ import (
 	"github.com/opencontainers/runc/libcontainer/label"
 )
 
+// ErrVolumeReadonly is used to signal an error when trying to copy data into
+// a volume mount that is not writable.
+var ErrVolumeReadonly = errors.New("mounted volume is marked read-only")
+
 type mountPoint struct {
 	Name        string
 	Destination string
@@ -47,6 +52,16 @@ func (m *mountPoint) Setup() (string, error) {
 	return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined")
 }
 
+// hasResource checks whether the given absolute path for a container is in
+// this mount point. If the relative path starts with `../` then the resource
+// is outside of this mount point, but we can't simply check for this prefix
+// because it misses `..` which is also outside of the mount, so check both.
+func (m *mountPoint) hasResource(absolutePath string) bool {
+	relPath, err := filepath.Rel(m.Destination, absolutePath)
+
+	return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
+}
+
 func (m *mountPoint) Path() string {
 	if m.Volume != nil {
 		return m.Volume.Path()

+ 17 - 0
docs/reference/api/docker_remote_api.md

@@ -68,6 +68,23 @@ Running `docker rmi` emits an **untag** event when removing an image name.  The
 
 ### What's new
 
+`GET /containers/(id)/archive`
+
+**New!**
+Get an archive of filesystem content from a container.
+
+`PUT /containers/(id)/archive`
+
+**New!**
+Upload an archive of content to be extracted to an
+existing directory inside a container's filesystem.
+
+`POST /containers/(id)/copy`
+
+**Deprecated!**
+This copy endpoint has been deprecated in favor of the above `archive` endpoint
+which can be used to download files and directories from a container.
+
 **New!**
 The `hostConfig` option now accepts the field `GroupAdd`, which specifies a list of additional
 groups that the container process will run as.

+ 116 - 0
docs/reference/api/docker_remote_api_v1.20.md

@@ -1039,6 +1039,8 @@ Status Codes:
 
 Copy files or folders of container `id`
 
+**Deprecated** in favor of the `archive` endpoint below.
+
 **Example request**:
 
     POST /containers/4fa6e0f0c678/copy HTTP/1.1
@@ -1061,6 +1063,120 @@ Status Codes:
 -   **404** – no such container
 -   **500** – server error
 
+### Retrieving information about files and folders in a container
+
+`HEAD /containers/(id)/archive`
+
+See the description of the `X-Docker-Container-Path-Stat` header in the
+folowing section.
+
+### Get an archive of a filesystem resource in a container
+
+`GET /containers/(id)/archive`
+
+Get an tar archive of a resource in the filesystem of container `id`.
+
+Query Parameters:
+
+- **path** - resource in the container's filesystem to archive. Required.
+
+    If not an absolute path, it is relative to the container's root directory.
+    The resource specified by **path** must exist. To assert that the resource
+    is expected to be a directory, **path** should end in `/` or  `/.`
+    (assuming a path separator of `/`). If **path** ends in `/.` then this
+    indicates that only the contents of the **path** directory should be
+    copied. A symlink is always resolved to its target.
+
+    **Note**: It is not possible to copy certain system files such as resources
+    under `/proc`, `/sys`, `/dev`, and mounts created by the user in the
+    container.
+
+**Example request**:
+
+        GET /containers/8cce319429b2/archive?path=/root HTTP/1.1
+
+**Example response**:
+
+        HTTP/1.1 200 OK
+        Content-Type: application/x-tar
+        X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInBhdGgiOiIvcm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oifQ==
+
+        {{ TAR STREAM }}
+
+On success, a response header `X-Docker-Container-Path-Stat` will be set to a
+base64-encoded JSON object containing some filesystem header information about
+the archived resource. The above example value would decode to the following
+JSON object (whitespace added for readability):
+
+        {
+            "name": "root",
+            "path": "/root",
+            "size": 4096,
+            "mode": 2147484096,
+            "mtime": "2014-02-27T20:51:23Z"
+        }
+
+A `HEAD` request can also be made to this endpoint if only this information is
+desired.
+
+Status Codes:
+
+- **200** - success, returns archive of copied resource
+- **400** - client error, bad parameter, details in JSON response body, one of:
+    - must specify path parameter (**path** cannot be empty)
+    - not a directory (**path** was asserted to be a directory but exists as a
+      file)
+- **404** - client error, resource not found, one of:
+    – no such container (container `id` does not exist)
+    - no such file or directory (**path** does not exist)
+- **500** - server error
+
+### Extract an archive of files or folders to a directory in a container
+
+`PUT /containers/(id)/archive`
+
+Upload a tar archive to be extracted to a path in the filesystem of container
+`id`.
+
+Query Parameters:
+
+- **path** - path to a directory in the container
+    to extract the archive's contents into. Required.
+
+    If not an absolute path, it is relative to the container's root directory.
+    The **path** resource must exist.
+- **noOverwriteDirNonDir** - If "1", "true", or "True" then it will be an error
+    if unpacking the given content would cause an existing directory to be
+    replaced with a non-directory and vice versa.
+
+**Example request**:
+
+        PUT /containers/8cce319429b2/archive?path=/vol1 HTTP/1.1
+        Content-Type: application/x-tar
+
+        {{ TAR STREAM }}
+
+**Example response**:
+
+        HTTP/1.1 200 OK
+
+Status Codes:
+
+- **200** – the content was extracted successfully
+- **400** - client error, bad parameter, details in JSON response body, one of:
+    - must specify path parameter (**path** cannot be empty)
+    - not a directory (**path** should be a directory but exists as a file)
+    - unable to overwrite existing directory with non-directory
+      (if **noOverwriteDirNonDir**)
+    - unable to overwrite existing non-directory with directory
+      (if **noOverwriteDirNonDir**)
+- **403** - client error, permission denied, the volume
+    or container rootfs is marked as read-only.
+- **404** - client error, resource not found, one of:
+    – no such container (container `id` does not exist)
+    - no such file or directory (**path** resource does not exist)
+- **500** – server error
+
 ## 2.2 Images
 
 ### List Images

+ 74 - 5
docs/reference/commandline/cp.md

@@ -11,12 +11,81 @@ weight=1
 
 # cp
 
-    Usage: docker cp CONTAINER:PATH HOSTDIR|-
+Copy files/folders between a container and the local filesystem.
 
-    Copy files/folders from the PATH to the HOSTDIR.
+    Usage:  docker cp [options] CONTAINER:PATH LOCALPATH|-
+            docker cp [options] LOCALPATH|- CONTAINER:PATH
 
-Copy files or folders from a container's filesystem to the directory on the
-host. Use '-' to write the data as a tar file to `STDOUT`. `CONTAINER:PATH` is
-relative to the root of the container's filesystem.
+    --help  Print usage statement
 
+In the first synopsis form, the `docker cp` utility copies the contents of
+`PATH` from the filesystem of `CONTAINER` to the `LOCALPATH` (or stream as
+a tar archive to `STDOUT` if `-` is specified).
 
+In the second synopsis form, the contents of `LOCALPATH` (or a tar archive
+streamed from `STDIN` if `-` is specified) are copied from the local machine to
+`PATH` in the filesystem of `CONTAINER`.
+
+You can copy to or from either a running or stopped container. The `PATH` can
+be a file or directory. The `docker cp` command assumes all `CONTAINER:PATH`
+values are relative to the `/` (root) directory of the container. This means
+supplying the initial forward slash is optional; The command sees
+`compassionate_darwin:/tmp/foo/myfile.txt` and
+`compassionate_darwin:tmp/foo/myfile.txt` as identical. If a `LOCALPATH` value
+is not absolute, is it considered relative to the current working directory.
+
+Behavior is similar to the common Unix utility `cp -a` in that directories are
+copied recursively with permissions preserved if possible. Ownership is set to
+the user and primary group on the receiving end of the transfer. For example,
+files copied to a container will be created with `UID:GID` of the root user.
+Files copied to the local machine will be created with the `UID:GID` of the
+user which invoked the `docker cp` command.
+
+Assuming a path separator of `/`, a first argument of `SRC_PATH` and second
+argument of `DST_PATH`, the behavior is as follows:
+
+- `SRC_PATH` specifies a file
+    - `DST_PATH` does not exist
+        - the file is saved to a file created at `DST_PATH`
+    - `DST_PATH` does not exist and ends with `/`
+        - Error condition: the destination directory must exist.
+    - `DST_PATH` exists and is a file
+        - the destination is overwritten with the contents of the source file
+    - `DST_PATH` exists and is a directory
+        - the file is copied into this directory using the basename from
+          `SRC_PATH`
+- `SRC_PATH` specifies a directory
+    - `DST_PATH` does not exist
+        - `DST_PATH` is created as a directory and the *contents* of the source
+           directory are copied into this directory
+    - `DST_PATH` exists and is a file
+        - Error condition: cannot copy a directory to a file
+    - `DST_PATH` exists and is a directory
+        - `SRC_PATH` does not end with `/.`
+            - the source directory is copied into this directory
+        - `SRC_PAPTH` does end with `/.`
+            - the *content* of the source directory is copied into this
+              directory
+
+The command requires `SRC_PATH` and `DST_PATH` to exist according to the above
+rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not
+the target, is copied.
+
+A colon (`:`) is used as a delimiter between `CONTAINER` and `PATH`, but `:`
+could also be in a valid `LOCALPATH`, like `file:name.txt`. This ambiguity is
+resolved by requiring a `LOCALPATH` with a `:` to be made explicit with a
+relative or absolute path, for example:
+
+    `/path/to/file:name.txt` or `./file:name.txt`
+
+It is not possible to copy certain system files such as resources under
+`/proc`, `/sys`, `/dev`, and mounts created by the user in the container.
+
+Using `-` as the first argument in place of a `LOCALPATH` will stream the
+contents of `STDIN` as a tar archive which will be extracted to the `PATH` in
+the filesystem of the destination container. In this case, `PATH` must specify
+a directory.
+
+Using `-` as the second argument in place of a `LOCALPATH` will stream the
+contents of the resource from the source container as a tar archive to
+`STDOUT`.

+ 503 - 0
integration-cli/docker_cli_cp_from_container_test.go

@@ -0,0 +1,503 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/go-check/check"
+)
+
+// docker cp CONTAINER:PATH LOCALPATH
+
+// Try all of the test cases from the archive package which implements the
+// internals of `docker cp` and ensure that the behavior matches when actually
+// copying to and from containers.
+
+// Basic assumptions about SRC and DST:
+// 1. SRC must exist.
+// 2. If SRC ends with a trailing separator, it must be a directory.
+// 3. DST parent directory must exist.
+// 4. If DST exists as a file, it must not end with a trailing separator.
+
+// First get these easy error cases out of the way.
+
+// Test for error when SRC does not exist.
+func (s *DockerSuite) TestCpFromErrSrcNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-err-src-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	err := runDockerCp(c, containerCpPath(cID, "file1"), tmpDir)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC ends in a trailing
+// path separator but it exists as a file.
+func (s *DockerSuite) TestCpFromErrSrcNotDir(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-err-src-not-dir")
+	defer os.RemoveAll(tmpDir)
+
+	err := runDockerCp(c, containerCpPathTrailingSep(cID, "file1"), tmpDir)
+	if err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC is a valid file or directory,
+// bu the DST parent directory does not exist.
+func (s *DockerSuite) TestCpFromErrDstParentNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-err-dst-parent-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	// Try with a file source.
+	srcPath := containerCpPath(cID, "/file1")
+	dstPath := cpPath(tmpDir, "notExists", "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = containerCpPath(cID, "/dir1")
+
+	if err := runDockerCp(c, srcPath, dstPath); err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when DST ends in a trailing
+// path separator but exists as a file.
+func (s *DockerSuite) TestCpFromErrDstNotDir(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)
+
+	// Try with a file source.
+	srcPath := containerCpPath(cID, "/file1")
+	dstPath := cpPathTrailingSep(tmpDir, "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = containerCpPath(cID, "/dir1")
+
+	if err := runDockerCp(c, srcPath, dstPath); err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Possibilities are reduced to the remaining 10 cases:
+//
+//  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
+// ===================================================================================================
+//   A   |  no      |  -              |  no       |  -       |  no      |  create file
+//   B   |  no      |  -              |  no       |  -       |  yes     |  error
+//   C   |  no      |  -              |  yes      |  no      |  -       |  overwrite file
+//   D   |  no      |  -              |  yes      |  yes     |  -       |  create file in dst dir
+//   E   |  yes     |  no             |  no       |  -       |  -       |  create dir, copy contents
+//   F   |  yes     |  no             |  yes      |  no      |  -       |  error
+//   G   |  yes     |  no             |  yes      |  yes     |  -       |  copy dir and contents
+//   H   |  yes     |  yes            |  no       |  -       |  -       |  create dir, copy contents
+//   I   |  yes     |  yes            |  yes      |  no      |  -       |  error
+//   J   |  yes     |  yes            |  yes      |  yes     |  -       |  copy dir contents
+//
+
+// A. SRC specifies a file and DST (no trailing path separator) doesn't
+//    exist. This should create a file with the name DST and copy the
+//    contents of the source file into it.
+func (s *DockerSuite) TestCpFromCaseA(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-a")
+	defer os.RemoveAll(tmpDir)
+
+	srcPath := containerCpPath(cID, "/root/file1")
+	dstPath := cpPath(tmpDir, "itWorks.txt")
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// B. SRC specifies a file and DST (with trailing path separator) doesn't
+//    exist. This should cause an error because the copy operation cannot
+//    create a directory when copying a single file.
+func (s *DockerSuite) TestCpFromCaseB(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-b")
+	defer os.RemoveAll(tmpDir)
+
+	srcPath := containerCpPath(cID, "/file1")
+	dstDir := cpPathTrailingSep(tmpDir, "testDir")
+
+	err := runDockerCp(c, srcPath, dstDir)
+	if err == nil {
+		c.Fatal("expected DirNotExists error, but got nil instead")
+	}
+
+	if !isCpDirNotExist(err) {
+		c.Fatalf("expected DirNotExists error, but got %T: %s", err, err)
+	}
+}
+
+// C. SRC specifies a file and DST exists as a file. This should overwrite
+//    the file at DST with the contents of the source file.
+func (s *DockerSuite) TestCpFromCaseC(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-c")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := containerCpPath(cID, "/root/file1")
+	dstPath := cpPath(tmpDir, "file2")
+
+	// Ensure the local file starts with different content.
+	if err := fileContentEquals(c, dstPath, "file2\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// D. SRC specifies a file and DST exists as a directory. This should place
+//    a copy of the source file inside it using the basename from SRC. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseD(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-d")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := containerCpPath(cID, "/file1")
+	dstDir := cpPath(tmpDir, "dir1")
+	dstPath := filepath.Join(dstDir, "file1")
+
+	// Ensure that dstPath doesn't exist.
+	if _, err := os.Stat(dstPath); !os.IsNotExist(err) {
+		c.Fatalf("did not expect dstPath %q to exist", dstPath)
+	}
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err := os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		c.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "dir1")
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// E. SRC specifies a directory and DST does not exist. This should create a
+//    directory at DST and copy the contents of the SRC directory into the DST
+//    directory. Ensure this works whether DST has a trailing path separator or
+//    not.
+func (s *DockerSuite) TestCpFromCaseE(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-e")
+	defer os.RemoveAll(tmpDir)
+
+	srcDir := containerCpPath(cID, "dir1")
+	dstDir := cpPath(tmpDir, "testDir")
+	dstPath := filepath.Join(dstDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// F. SRC specifies a directory and DST exists as a file. This should cause an
+//    error as it is not possible to overwrite a file with a directory.
+func (s *DockerSuite) TestCpFromCaseF(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-f")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPath(cID, "/root/dir1")
+	dstFile := cpPath(tmpDir, "file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// G. SRC specifies a directory and DST exists as a directory. This should copy
+//    the SRC directory and all its contents to the DST directory. Ensure this
+//    works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseG(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-g")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPath(cID, "/root/dir1")
+	dstDir := cpPath(tmpDir, "dir2")
+	resultDir := filepath.Join(dstDir, "dir1")
+	dstPath := filepath.Join(resultDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err := os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		c.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "dir2")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// H. SRC specifies a directory's contents only and DST does not exist. This
+//    should create a directory at DST and copy the contents of the SRC
+//    directory (but not the directory itself) into the DST directory. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseH(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-h")
+	defer os.RemoveAll(tmpDir)
+
+	srcDir := containerCpPathTrailingSep(cID, "dir1") + "."
+	dstDir := cpPath(tmpDir, "testDir")
+	dstPath := filepath.Join(dstDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove resultDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// I. SRC specifies a direcotry's contents only and DST exists as a file. This
+//    should cause an error as it is not possible to overwrite a file with a
+//    directory.
+func (s *DockerSuite) TestCpFromCaseI(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-i")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPathTrailingSep(cID, "/root/dir1") + "."
+	dstFile := cpPath(tmpDir, "file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// J. SRC specifies a directory's contents only and DST exists as a directory.
+//    This should copy the contents of the SRC directory (but not the directory
+//    itself) into the DST directory. Ensure this works whether DST has a
+//    trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseJ(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-j")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPathTrailingSep(cID, "/root/dir1") + "."
+	dstDir := cpPath(tmpDir, "dir2")
+	dstPath := filepath.Join(dstDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err := os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		c.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "dir2")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}

+ 12 - 0
integration-cli/docker_cli_cp_test.go

@@ -23,6 +23,18 @@ const (
 	cpHostContents      = "hello, i am the host"
 )
 
+// Ensure that an all-local path case returns an error.
+func (s *DockerSuite) TestCpLocalOnly(c *check.C) {
+	err := runDockerCp(c, "foo", "bar")
+	if err == nil {
+		c.Fatal("expected failure, got success")
+	}
+
+	if !strings.Contains(err.Error(), "must specify at least one container source") {
+		c.Fatalf("unexpected output: %s", err.Error())
+	}
+}
+
 // Test for #5656
 // Check that garbage paths don't escape the container's rootfs
 func (s *DockerSuite) TestCpGarbagePath(c *check.C) {

+ 634 - 0
integration-cli/docker_cli_cp_to_container_test.go

@@ -0,0 +1,634 @@
+package main
+
+import (
+	"os"
+
+	"github.com/go-check/check"
+)
+
+// docker cp LOCALPATH CONTAINER:PATH
+
+// Try all of the test cases from the archive package which implements the
+// internals of `docker cp` and ensure that the behavior matches when actually
+// copying to and from containers.
+
+// Basic assumptions about SRC and DST:
+// 1. SRC must exist.
+// 2. If SRC ends with a trailing separator, it must be a directory.
+// 3. DST parent directory must exist.
+// 4. If DST exists as a file, it must not end with a trailing separator.
+
+// First get these easy error cases out of the way.
+
+// Test for error when SRC does not exist.
+func (s *DockerSuite) TestCpToErrSrcNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-src-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC ends in a trailing
+// path separator but it exists as a file.
+func (s *DockerSuite) TestCpToErrSrcNotDir(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-src-not-dir")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPathTrailingSep(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "testDir")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC is a valid file or directory,
+// bu the DST parent directory does not exist.
+func (s *DockerSuite) TestCpToErrDstParentNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-dst-parent-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	// Try with a file source.
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/notExists", "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = cpPath(tmpDir, "dir1")
+
+	if err := runDockerCp(c, srcPath, dstPath); err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when DST ends in a trailing path separator but exists as a
+// file. Also test that we cannot overwirite an existing directory with a
+// non-directory and cannot overwrite an existing
+func (s *DockerSuite) TestCpToErrDstNotDir(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-dst-not-dir")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	// Try with a file source.
+	srcPath := cpPath(tmpDir, "dir1/file1-1")
+	dstPath := containerCpPathTrailingSep(cID, "file1")
+
+	// The client should encounter an error trying to stat the destination
+	// and then be unable to copy since the destination is asserted to be a
+	// directory but does not exist.
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected DirNotExist error, but got nil instead")
+	}
+
+	if !isCpDirNotExist(err) {
+		c.Fatalf("expected DirNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = cpPath(tmpDir, "dir1")
+
+	// The client should encounter an error trying to stat the destination and
+	// then decide to extract to the parent directory instead with a rebased
+	// name in the source archive, but this directory would overwrite the
+	// existing file with the same name.
+	err = runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected CannotOverwriteNonDirWithDir error, but got nil instead")
+	}
+
+	if !isCannotOverwriteNonDirWithDir(err) {
+		c.Fatalf("expected CannotOverwriteNonDirWithDir error, but got %T: %s", err, err)
+	}
+}
+
+// Possibilities are reduced to the remaining 10 cases:
+//
+//  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
+// ===================================================================================================
+//   A   |  no      |  -              |  no       |  -       |  no      |  create file
+//   B   |  no      |  -              |  no       |  -       |  yes     |  error
+//   C   |  no      |  -              |  yes      |  no      |  -       |  overwrite file
+//   D   |  no      |  -              |  yes      |  yes     |  -       |  create file in dst dir
+//   E   |  yes     |  no             |  no       |  -       |  -       |  create dir, copy contents
+//   F   |  yes     |  no             |  yes      |  no      |  -       |  error
+//   G   |  yes     |  no             |  yes      |  yes     |  -       |  copy dir and contents
+//   H   |  yes     |  yes            |  no       |  -       |  -       |  create dir, copy contents
+//   I   |  yes     |  yes            |  yes      |  no      |  -       |  error
+//   J   |  yes     |  yes            |  yes      |  yes     |  -       |  copy dir contents
+//
+
+// A. SRC specifies a file and DST (no trailing path separator) doesn't
+//    exist. This should create a file with the name DST and copy the
+//    contents of the source file into it.
+func (s *DockerSuite) TestCpToCaseA(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		workDir: "/root", command: makeCatFileCommand("itWorks.txt"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-a")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/root/itWorks.txt")
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// B. SRC specifies a file and DST (with trailing path separator) doesn't
+//    exist. This should cause an error because the copy operation cannot
+//    create a directory when copying a single file.
+func (s *DockerSuite) TestCpToCaseB(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("testDir/file1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-b")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstDir := containerCpPathTrailingSep(cID, "testDir")
+
+	err := runDockerCp(c, srcPath, dstDir)
+	if err == nil {
+		c.Fatal("expected DirNotExists error, but got nil instead")
+	}
+
+	if !isCpDirNotExist(err) {
+		c.Fatalf("expected DirNotExists error, but got %T: %s", err, err)
+	}
+}
+
+// C. SRC specifies a file and DST exists as a file. This should overwrite
+//    the file at DST with the contents of the source file.
+func (s *DockerSuite) TestCpToCaseC(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+		command: makeCatFileCommand("file2"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-c")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/root/file2")
+
+	// Ensure the container's file starts with the original content.
+	if err := containerStartOutputEquals(c, cID, "file2\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// D. SRC specifies a file and DST exists as a directory. This should place
+//    a copy of the source file inside it using the basename from SRC. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseD(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true,
+		command:    makeCatFileCommand("/dir1/file1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-d")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstDir := containerCpPath(cID, "dir1")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		addContent: true,
+		command:    makeCatFileCommand("/dir1/file1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "dir1")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// E. SRC specifies a directory and DST does not exist. This should create a
+//    directory at DST and copy the contents of the SRC directory into the DST
+//    directory. Ensure this works whether DST has a trailing path separator or
+//    not.
+func (s *DockerSuite) TestCpToCaseE(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-e")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPath(tmpDir, "dir1")
+	dstDir := containerCpPath(cID, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "testDir")
+
+	err := runDockerCp(c, srcDir, dstDir)
+	if err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// F. SRC specifies a directory and DST exists as a file. This should cause an
+//    error as it is not possible to overwrite a file with a directory.
+func (s *DockerSuite) TestCpToCaseF(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-f")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPath(tmpDir, "dir1")
+	dstFile := containerCpPath(cID, "/root/file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// G. SRC specifies a directory and DST exists as a directory. This should copy
+//    the SRC directory and all its contents to the DST directory. Ensure this
+//    works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseG(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+		command: makeCatFileCommand("dir2/dir1/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-g")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPath(tmpDir, "dir1")
+	dstDir := containerCpPath(cID, "/root/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		addContent: true,
+		command:    makeCatFileCommand("/dir2/dir1/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// H. SRC specifies a directory's contents only and DST does not exist. This
+//    should create a directory at DST and copy the contents of the SRC
+//    directory (but not the directory itself) into the DST directory. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseH(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-h")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
+	dstDir := containerCpPath(cID, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// I. SRC specifies a direcotry's contents only and DST exists as a file. This
+//    should cause an error as it is not possible to overwrite a file with a
+//    directory.
+func (s *DockerSuite) TestCpToCaseI(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-i")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
+	dstFile := containerCpPath(cID, "/root/file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// J. SRC specifies a directory's contents only and DST exists as a directory.
+//    This should copy the contents of the SRC directory (but not the directory
+//    itself) into the DST directory. Ensure this works whether DST has a
+//    trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseJ(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+		command: makeCatFileCommand("/dir2/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-j")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
+	dstDir := containerCpPath(cID, "/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/dir2/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// The `docker cp` command should also ensure that you cannot
+// write to a container rootfs that is marked as read-only.
+func (s *DockerSuite) TestCpToErrReadOnlyRootfs(c *check.C) {
+	tmpDir := getTestDir(c, "test-cp-to-err-read-only-rootfs")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	cID := makeTestContainer(c, testContainerOptions{
+		readOnly: true, workDir: "/root",
+		command: makeCatFileCommand("shouldNotExist"),
+	})
+	defer deleteContainer(cID)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/root/shouldNotExist")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected ErrContainerRootfsReadonly error, but got nil instead")
+	}
+
+	if !isCpCannotCopyReadOnly(err) {
+		c.Fatalf("expected ErrContainerRootfsReadonly error, but got %T: %s", err, err)
+	}
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// The `docker cp` command should also ensure that you
+// cannot write to a volume that is mounted as read-only.
+func (s *DockerSuite) TestCpToErrReadOnlyVolume(c *check.C) {
+	tmpDir := getTestDir(c, "test-cp-to-err-read-only-volume")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	cID := makeTestContainer(c, testContainerOptions{
+		volumes: defaultVolumes(tmpDir), workDir: "/root",
+		command: makeCatFileCommand("/vol_ro/shouldNotExist"),
+	})
+	defer deleteContainer(cID)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/vol_ro/shouldNotExist")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected ErrVolumeReadonly error, but got nil instead")
+	}
+
+	if !isCpCannotCopyReadOnly(err) {
+		c.Fatalf("expected ErrVolumeReadonly error, but got %T: %s", err, err)
+	}
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+}

+ 298 - 0
integration-cli/docker_cli_cp_utils.go

@@ -0,0 +1,298 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"github.com/docker/docker/pkg/archive"
+	"github.com/go-check/check"
+)
+
+type FileType uint32
+
+const (
+	Regular FileType = iota
+	Dir
+	Symlink
+)
+
+type FileData struct {
+	filetype FileType
+	path     string
+	contents string
+}
+
+func (fd FileData) creationCommand() string {
+	var command string
+
+	switch fd.filetype {
+	case Regular:
+		// Don't overwrite the file if it already exists!
+		command = fmt.Sprintf("if [ ! -f %s ]; then echo %q > %s; fi", fd.path, fd.contents, fd.path)
+	case Dir:
+		command = fmt.Sprintf("mkdir -p %s", fd.path)
+	case Symlink:
+		command = fmt.Sprintf("ln -fs %s %s", fd.contents, fd.path)
+	}
+
+	return command
+}
+
+func mkFilesCommand(fds []FileData) string {
+	commands := make([]string, len(fds))
+
+	for i, fd := range fds {
+		commands[i] = fd.creationCommand()
+	}
+
+	return strings.Join(commands, " && ")
+}
+
+var defaultFileData = []FileData{
+	{Regular, "file1", "file1"},
+	{Regular, "file2", "file2"},
+	{Regular, "file3", "file3"},
+	{Regular, "file4", "file4"},
+	{Regular, "file5", "file5"},
+	{Regular, "file6", "file6"},
+	{Regular, "file7", "file7"},
+	{Dir, "dir1", ""},
+	{Regular, "dir1/file1-1", "file1-1"},
+	{Regular, "dir1/file1-2", "file1-2"},
+	{Dir, "dir2", ""},
+	{Regular, "dir2/file2-1", "file2-1"},
+	{Regular, "dir2/file2-2", "file2-2"},
+	{Dir, "dir3", ""},
+	{Regular, "dir3/file3-1", "file3-1"},
+	{Regular, "dir3/file3-2", "file3-2"},
+	{Dir, "dir4", ""},
+	{Regular, "dir4/file3-1", "file4-1"},
+	{Regular, "dir4/file3-2", "file4-2"},
+	{Dir, "dir5", ""},
+	{Symlink, "symlink1", "target1"},
+	{Symlink, "symlink2", "target2"},
+}
+
+func defaultMkContentCommand() string {
+	return mkFilesCommand(defaultFileData)
+}
+
+func makeTestContentInDir(c *check.C, dir string) {
+	for _, fd := range defaultFileData {
+		path := filepath.Join(dir, filepath.FromSlash(fd.path))
+		switch fd.filetype {
+		case Regular:
+			if err := ioutil.WriteFile(path, []byte(fd.contents+"\n"), os.FileMode(0666)); err != nil {
+				c.Fatal(err)
+			}
+		case Dir:
+			if err := os.Mkdir(path, os.FileMode(0777)); err != nil {
+				c.Fatal(err)
+			}
+		case Symlink:
+			if err := os.Symlink(fd.contents, path); err != nil {
+				c.Fatal(err)
+			}
+		}
+	}
+}
+
+type testContainerOptions struct {
+	addContent bool
+	readOnly   bool
+	volumes    []string
+	workDir    string
+	command    string
+}
+
+func makeTestContainer(c *check.C, options testContainerOptions) (containerID string) {
+	if options.addContent {
+		mkContentCmd := defaultMkContentCommand()
+		if options.command == "" {
+			options.command = mkContentCmd
+		} else {
+			options.command = fmt.Sprintf("%s && %s", defaultMkContentCommand(), options.command)
+		}
+	}
+
+	if options.command == "" {
+		options.command = "#(nop)"
+	}
+
+	args := []string{"run", "-d"}
+
+	for _, volume := range options.volumes {
+		args = append(args, "-v", volume)
+	}
+
+	if options.workDir != "" {
+		args = append(args, "-w", options.workDir)
+	}
+
+	if options.readOnly {
+		args = append(args, "--read-only")
+	}
+
+	args = append(args, "busybox", "/bin/sh", "-c", options.command)
+
+	out, status := dockerCmd(c, args...)
+	if status != 0 {
+		c.Fatalf("failed to run container, status %d: %s", status, out)
+	}
+
+	containerID = strings.TrimSpace(out)
+
+	out, status = dockerCmd(c, "wait", containerID)
+	if status != 0 {
+		c.Fatalf("failed to wait for test container container, status %d: %s", status, out)
+	}
+
+	if exitCode := strings.TrimSpace(out); exitCode != "0" {
+		logs, status := dockerCmd(c, "logs", containerID)
+		if status != 0 {
+			logs = "UNABLE TO GET LOGS"
+		}
+		c.Fatalf("failed to make test container, exit code (%d): %s", exitCode, logs)
+	}
+
+	return
+}
+
+func makeCatFileCommand(path string) string {
+	return fmt.Sprintf("if [ -f %s ]; then cat %s; fi", path, path)
+}
+
+func cpPath(pathElements ...string) string {
+	localizedPathElements := make([]string, len(pathElements))
+	for i, path := range pathElements {
+		localizedPathElements[i] = filepath.FromSlash(path)
+	}
+	return strings.Join(localizedPathElements, string(filepath.Separator))
+}
+
+func cpPathTrailingSep(pathElements ...string) string {
+	return fmt.Sprintf("%s%c", cpPath(pathElements...), filepath.Separator)
+}
+
+func containerCpPath(containerID string, pathElements ...string) string {
+	joined := strings.Join(pathElements, "/")
+	return fmt.Sprintf("%s:%s", containerID, joined)
+}
+
+func containerCpPathTrailingSep(containerID string, pathElements ...string) string {
+	return fmt.Sprintf("%s/", containerCpPath(containerID, pathElements...))
+}
+
+func runDockerCp(c *check.C, src, dst string) (err error) {
+	c.Logf("running `docker cp %s %s`", src, dst)
+
+	args := []string{"cp", src, dst}
+
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, args...))
+	if err != nil {
+		err = fmt.Errorf("error executing `docker cp` command: %s: %s", err, out)
+	}
+
+	return
+}
+
+func startContainerGetOutput(c *check.C, cID string) (out string, err error) {
+	c.Logf("running `docker start -a %s`", cID)
+
+	args := []string{"start", "-a", cID}
+
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, args...))
+	if err != nil {
+		err = fmt.Errorf("error executing `docker start` command: %s: %s", err, out)
+	}
+
+	return
+}
+
+func getTestDir(c *check.C, label string) (tmpDir string) {
+	var err error
+
+	if tmpDir, err = ioutil.TempDir("", label); err != nil {
+		c.Fatalf("unable to make temporary directory: %s", err)
+	}
+
+	return
+}
+
+func isCpNotExist(err error) bool {
+	return strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "cannot find the file specified")
+}
+
+func isCpDirNotExist(err error) bool {
+	return strings.Contains(err.Error(), archive.ErrDirNotExists.Error())
+}
+
+func isCpNotDir(err error) bool {
+	return strings.Contains(err.Error(), archive.ErrNotDirectory.Error()) || strings.Contains(err.Error(), "filename, directory name, or volume label syntax is incorrect")
+}
+
+func isCpCannotCopyDir(err error) bool {
+	return strings.Contains(err.Error(), archive.ErrCannotCopyDir.Error())
+}
+
+func isCpCannotCopyReadOnly(err error) bool {
+	return strings.Contains(err.Error(), "marked read-only")
+}
+
+func isCannotOverwriteNonDirWithDir(err error) bool {
+	return strings.Contains(err.Error(), "cannot overwrite non-directory")
+}
+
+func fileContentEquals(c *check.C, filename, contents string) (err error) {
+	c.Logf("checking that file %q contains %q\n", filename, contents)
+
+	fileBytes, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return
+	}
+
+	expectedBytes, err := ioutil.ReadAll(strings.NewReader(contents))
+	if err != nil {
+		return
+	}
+
+	if !bytes.Equal(fileBytes, expectedBytes) {
+		err = fmt.Errorf("file content not equal - expected %q, got %q", string(expectedBytes), string(fileBytes))
+	}
+
+	return
+}
+
+func containerStartOutputEquals(c *check.C, cID, contents string) (err error) {
+	c.Logf("checking that container %q start output contains %q\n", cID, contents)
+
+	out, err := startContainerGetOutput(c, cID)
+	if err != nil {
+		return err
+	}
+
+	if out != contents {
+		err = fmt.Errorf("output contents not equal - expected %q, got %q", contents, out)
+	}
+
+	return
+}
+
+func defaultVolumes(tmpDir string) []string {
+	if SameHostDaemon.Condition() {
+		return []string{
+			"/vol1",
+			fmt.Sprintf("%s:/vol2", tmpDir),
+			fmt.Sprintf("%s:/vol3", filepath.Join(tmpDir, "vol3")),
+			fmt.Sprintf("%s:/vol_ro:ro", filepath.Join(tmpDir, "vol_ro")),
+		}
+	}
+
+	// Can't bind-mount volumes with separate host daemon.
+	return []string{"/vol1", "/vol2", "/vol3", "/vol_ro:/vol_ro:ro"}
+}

+ 26 - 4
integration-cli/docker_cli_events_test.go

@@ -3,7 +3,9 @@ package main
 import (
 	"bufio"
 	"fmt"
+	"io/ioutil"
 	"net/http"
+	"os"
 	"os/exec"
 	"regexp"
 	"strconv"
@@ -519,6 +521,7 @@ func (s *DockerSuite) TestEventsCommit(c *check.C) {
 func (s *DockerSuite) TestEventsCopy(c *check.C) {
 	since := daemonTime(c).Unix()
 
+	// Build a test image.
 	id, err := buildImage("cpimg", `
 		  FROM busybox
 		  RUN echo HI > /tmp/file`, true)
@@ -526,12 +529,31 @@ func (s *DockerSuite) TestEventsCopy(c *check.C) {
 		c.Fatalf("Couldn't create image: %q", err)
 	}
 
-	dockerCmd(c, "run", "--name=cptest", id, "true")
-	dockerCmd(c, "cp", "cptest:/tmp/file", "-")
+	// Create an empty test file.
+	tempFile, err := ioutil.TempFile("", "test-events-copy-")
+	if err != nil {
+		c.Fatal(err)
+	}
+	defer os.Remove(tempFile.Name())
+
+	if err := tempFile.Close(); err != nil {
+		c.Fatal(err)
+	}
+
+	dockerCmd(c, "create", "--name=cptest", id)
+
+	dockerCmd(c, "cp", "cptest:/tmp/file", tempFile.Name())
 
 	out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since)))
-	if !strings.Contains(out, " copy\n") {
-		c.Fatalf("Missing 'copy' log event\n%s", out)
+	if !strings.Contains(out, " archive-path\n") {
+		c.Fatalf("Missing 'archive-path' log event\n%s", out)
+	}
+
+	dockerCmd(c, "cp", tempFile.Name(), "cptest:/tmp/filecopy")
+
+	out, _ = dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since)))
+	if !strings.Contains(out, " extract-to-dir\n") {
+		c.Fatalf("Missing 'extract-to-dir' log event\n%s", out)
 	}
 }
 

+ 116 - 35
man/docker-cp.1.md

@@ -2,69 +2,150 @@
 % Docker Community
 % JUNE 2014
 # NAME
-docker-cp - Copy files or folders from a container's PATH to a HOSTDIR
-or to STDOUT.
+docker-cp - Copy files/folders between a container and the local filesystem.
 
 # SYNOPSIS
 **docker cp**
 [**--help**]
-CONTAINER:PATH HOSTDIR|-
+CONTAINER:PATH LOCALPATH|-
+LOCALPATH|- CONTAINER:PATH
 
 # DESCRIPTION
 
-Copy files or folders from a `CONTAINER:PATH` to the `HOSTDIR` or to `STDOUT`. 
-The `CONTAINER:PATH` is relative to the root of the container's filesystem. You
-can copy from either a running or stopped container. 
+In the first synopsis form, the `docker cp` utility copies the contents of
+`PATH` from the filesystem of `CONTAINER` to the `LOCALPATH` (or stream as
+a tar archive to `STDOUT` if `-` is specified).
 
-The `PATH` can be a file or directory. The `docker cp` command assumes all
-`PATH` values start at the `/` (root) directory. This means supplying the
-initial forward slash is optional; The command sees
+In the second synopsis form, the contents of `LOCALPATH` (or a tar archive
+streamed from `STDIN` if `-` is specified) are copied from the local machine to
+`PATH` in the filesystem of `CONTAINER`.
+
+You can copy to or from either a running or stopped container. The `PATH` can
+be a file or directory. The `docker cp` command assumes all `CONTAINER:PATH`
+values are relative to the `/` (root) directory of the container. This means
+supplying the initial forward slash is optional; The command sees
 `compassionate_darwin:/tmp/foo/myfile.txt` and
-`compassionate_darwin:tmp/foo/myfile.txt` as identical.
+`compassionate_darwin:tmp/foo/myfile.txt` as identical. If a `LOCALPATH` value
+is not absolute, is it considered relative to the current working directory.
+
+Behavior is similar to the common Unix utility `cp -a` in that directories are
+copied recursively with permissions preserved if possible. Ownership is set to
+the user and primary group on the receiving end of the transfer. For example,
+files copied to a container will be created with `UID:GID` of the root user.
+Files copied to the local machine will be created with the `UID:GID` of the
+user which invoked the `docker cp` command.
+
+Assuming a path separator of `/`, a first argument of `SRC_PATH` and second
+argument of `DST_PATH`, the behavior is as follows:
+
+- `SRC_PATH` specifies a file
+    - `DST_PATH` does not exist
+        - the file is saved to a file created at `DST_PATH`
+    - `DST_PATH` does not exist and ends with `/`
+        - Error condition: the destination directory must exist.
+    - `DST_PATH` exists and is a file
+        - the destination is overwritten with the contents of the source file
+    - `DST_PATH` exists and is a directory
+        - the file is copied into this directory using the basename from
+          `SRC_PATH`
+- `SRC_PATH` specifies a directory
+    - `DST_PATH` does not exist
+        - `DST_PATH` is created as a directory and the *contents* of the source
+           directory are copied into this directory
+    - `DST_PATH` exists and is a file
+        - Error condition: cannot copy a directory to a file
+    - `DST_PATH` exists and is a directory
+        - `SRC_PATH` does not end with `/.`
+            - the source directory is copied into this directory
+        - `SRC_PAPTH` does end with `/.`
+            - the *content* of the source directory is copied into this
+              directory
+
+The command requires `SRC_PATH` and `DST_PATH` to exist according to the above
+rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not
+the target, is copied.
+
+A colon (`:`) is used as a delimiter between `CONTAINER` and `PATH`, but `:`
+could also be in a valid `LOCALPATH`, like `file:name.txt`. This ambiguity is
+resolved by requiring a `LOCALPATH` with a `:` to be made explicit with a
+relative or absolute path, for example:
+
+    `/path/to/file:name.txt` or `./file:name.txt`
+
+It is not possible to copy certain system files such as resources under
+`/proc`, `/sys`, `/dev`, and mounts created by the user in the container.
+
+Using `-` as the first argument in place of a `LOCALPATH` will stream the
+contents of `STDIN` as a tar archive which will be extracted to the `PATH` in
+the filesystem of the destination container. In this case, `PATH` must specify
+a directory.
+
+Using `-` as the second argument in place of a `LOCALPATH` will stream the
+contents of the resource from the source container as a tar archive to
+`STDOUT`.
+
+# OPTIONS
+**--help**
+  Print usage statement
+
+# EXAMPLES
+
+Suppose a container has finished producing some output as a file it saves
+to somewhere in its filesystem. This could be the output of a build job or
+some other computation. You can copy these outputs from the container to a
+location on your local host.
 
-The `HOSTDIR` refers to a directory on the host. If you do not specify an
-absolute path for your `HOSTDIR` value, Docker creates the directory relative to
-where you run the `docker cp` command. For example, suppose you want to copy the
-`/tmp/foo` directory from a container to the `/tmp` directory on your host. If
-you run `docker cp` in your `~` (home) directory on the host:
+If you want to copy the `/tmp/foo` directory from a container to the
+existing `/tmp` directory on your host. If you run `docker cp` in your `~`
+(home) directory on the local host:
 
 		$ docker cp compassionate_darwin:tmp/foo /tmp
 
 Docker creates a `/tmp/foo` directory on your host. Alternatively, you can omit
-the leading slash in the command. If you execute this command from your home directory:
+the leading slash in the command. If you execute this command from your home
+directory:
 
 		$ docker cp compassionate_darwin:tmp/foo tmp
 
-Docker creates a `~/tmp/foo` subdirectory.  
+If `~/tmp` does not exist, Docker will create it and copy the contents of
+`/tmp/foo` from the container into this new directory. If `~/tmp` already
+exists as a directory, then Docker will copy the contents of `/tmp/foo` from
+the container into a directory at `~/tmp/foo`.
 
-When copying files to an existing `HOSTDIR`, the `cp` command adds the new files to
-the directory. For example, this command:
+When copying a single file to an existing `LOCALPATH`, the `docker cp` command
+will either overwrite the contents of `LOCALPATH` if it is a file or place it
+into `LOCALPATH` if it is a directory, overwriting an existing file of the same
+name if one exists. For example, this command:
 
-		$ docker cp sharp_ptolemy:/tmp/foo/myfile.txt /tmp
+		$ docker cp sharp_ptolemy:/tmp/foo/myfile.txt /test
 
-Creates a `/tmp/foo` directory on the host containing the `myfile.txt` file. If
-you repeat the command but change the filename:
+If `/test` does not exist on the local machine, it will be created as a file
+with the contents of `/tmp/foo/myfile.txt` from the container. If `/test`
+exists as a file, it will be overwritten. Lastly, if `/tmp` exists as a
+directory, the file will be copied to `/test/myfile.txt`.
 
-		$ docker cp sharp_ptolemy:/tmp/foo/secondfile.txt /tmp
+Next, suppose you want to copy a file or folder into a container. For example,
+this could be a configuration file or some other input to a long running
+computation that you would like to place into a created container before it
+starts. This is useful because it does not require the configuration file or
+other input to exist in the container image.
 
-Your host's `/tmp/foo` directory will contain both files:
+If you have a file, `config.yml`, in the current directory on your local host
+and wish to copy it to an existing directory at `/etc/my-app.d` in a container,
+this command can be used:
 
-		$ ls /tmp/foo
-		myfile.txt secondfile.txt
-		
-Finally, use '-' to write the data as a `tar` file to STDOUT.
+		$ docker cp config.yml myappcontainer:/etc/my-app.d
 
-# OPTIONS
-**--help**
-  Print usage statement
+If you have several files in a local directory `/config` which you need to copy
+to a directory `/etc/my-app.d` in a container:
 
-# EXAMPLES
-An important shell script file, created in a bash shell, is copied from
-the exited container to the current dir on the host:
+		$ docker cp /config/. myappcontainer:/etc/my-app.d
 
-    # docker cp c071f3c3ee81:setup.sh .
+The above command will copy the contents of the local `/config` directory into
+the directory `/etc/my-app.d` in the container.
 
 # HISTORY
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
 based on docker.com source material and internal work.
 June 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
+May 2015, updated by Josh Hawn <josh.hawn@docker.com>

+ 83 - 29
pkg/archive/archive.go

@@ -25,15 +25,23 @@ import (
 )
 
 type (
-	Archive       io.ReadCloser
-	ArchiveReader io.Reader
-	Compression   int
-	TarOptions    struct {
-		IncludeFiles    []string
-		ExcludePatterns []string
-		Compression     Compression
-		NoLchown        bool
-		Name            string
+	Archive         io.ReadCloser
+	ArchiveReader   io.Reader
+	Compression     int
+	TarChownOptions struct {
+		UID, GID int
+	}
+	TarOptions struct {
+		IncludeFiles     []string
+		ExcludePatterns  []string
+		Compression      Compression
+		NoLchown         bool
+		ChownOpts        *TarChownOptions
+		Name             string
+		IncludeSourceDir bool
+		// When unpacking, specifies whether overwriting a directory with a
+		// non-directory is allowed and vice versa.
+		NoOverwriteDirNonDir bool
 	}
 
 	// Archiver allows the reuse of most utility functions of this package
@@ -262,7 +270,7 @@ func (ta *tarAppender) addTarFile(path, name string) error {
 	return nil
 }
 
-func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool) error {
+func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool, chownOpts *TarChownOptions) error {
 	// hdr.Mode is in linux format, which we can use for sycalls,
 	// but for os.Foo() calls we need the mode converted to os.FileMode,
 	// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
@@ -328,9 +336,12 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, L
 		return fmt.Errorf("Unhandled tar header type %d\n", hdr.Typeflag)
 	}
 
-	// Lchown is not supported on Windows
-	if runtime.GOOS != "windows" {
-		if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil && Lchown {
+	// Lchown is not supported on Windows.
+	if Lchown && runtime.GOOS != "windows" {
+		if chownOpts == nil {
+			chownOpts = &TarChownOptions{UID: hdr.Uid, GID: hdr.Gid}
+		}
+		if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil {
 			return err
 		}
 	}
@@ -396,6 +407,20 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 			Buffer:    pools.BufioWriter32KPool.Get(nil),
 			SeenFiles: make(map[uint64]string),
 		}
+
+		defer func() {
+			// Make sure to check the error on Close.
+			if err := ta.TarWriter.Close(); err != nil {
+				logrus.Debugf("Can't close tar writer: %s", err)
+			}
+			if err := compressWriter.Close(); err != nil {
+				logrus.Debugf("Can't close compress writer: %s", err)
+			}
+			if err := pipeWriter.Close(); err != nil {
+				logrus.Debugf("Can't close pipe writer: %s", err)
+			}
+		}()
+
 		// this buffer is needed for the duration of this piped stream
 		defer pools.BufioWriter32KPool.Put(ta.Buffer)
 
@@ -404,7 +429,26 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 		// mutating the filesystem and we can see transient errors
 		// from this
 
-		if options.IncludeFiles == nil {
+		stat, err := os.Lstat(srcPath)
+		if err != nil {
+			return
+		}
+
+		if !stat.IsDir() {
+			// We can't later join a non-dir with any includes because the
+			// 'walk' will error if "file/." is stat-ed and "file" is not a
+			// directory. So, we must split the source path and use the
+			// basename as the include.
+			if len(options.IncludeFiles) > 0 {
+				logrus.Warn("Tar: Can't archive a file with includes")
+			}
+
+			dir, base := SplitPathDirEntry(srcPath)
+			srcPath = dir
+			options.IncludeFiles = []string{base}
+		}
+
+		if len(options.IncludeFiles) == 0 {
 			options.IncludeFiles = []string{"."}
 		}
 
@@ -412,19 +456,26 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 
 		var renamedRelFilePath string // For when tar.Options.Name is set
 		for _, include := range options.IncludeFiles {
-			filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
+			// We can't use filepath.Join(srcPath, include) because this will
+			// clean away a trailing "." or "/" which may be important.
+			walkRoot := strings.Join([]string{srcPath, include}, string(filepath.Separator))
+			filepath.Walk(walkRoot, func(filePath string, f os.FileInfo, err error) error {
 				if err != nil {
 					logrus.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err)
 					return nil
 				}
 
 				relFilePath, err := filepath.Rel(srcPath, filePath)
-				if err != nil || (relFilePath == "." && f.IsDir()) {
+				if err != nil || (!options.IncludeSourceDir && relFilePath == "." && f.IsDir()) {
 					// Error getting relative path OR we are looking
-					// at the root path. Skip in both situations.
+					// at the source directory path. Skip in both situations.
 					return nil
 				}
 
+				if options.IncludeSourceDir && include == "." && relFilePath != "." {
+					relFilePath = strings.Join([]string{".", relFilePath}, string(filepath.Separator))
+				}
+
 				skip := false
 
 				// If "include" is an exact match for the current file
@@ -468,17 +519,6 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 				return nil
 			})
 		}
-
-		// Make sure to check the error on Close.
-		if err := ta.TarWriter.Close(); err != nil {
-			logrus.Debugf("Can't close tar writer: %s", err)
-		}
-		if err := compressWriter.Close(); err != nil {
-			logrus.Debugf("Can't close compress writer: %s", err)
-		}
-		if err := pipeWriter.Close(); err != nil {
-			logrus.Debugf("Can't close pipe writer: %s", err)
-		}
 	}()
 
 	return pipeReader, nil
@@ -543,9 +583,22 @@ loop:
 		// the layer is also a directory. Then we want to merge them (i.e.
 		// just apply the metadata from the layer).
 		if fi, err := os.Lstat(path); err == nil {
+			if options.NoOverwriteDirNonDir && fi.IsDir() && hdr.Typeflag != tar.TypeDir {
+				// If NoOverwriteDirNonDir is true then we cannot replace
+				// an existing directory with a non-directory from the archive.
+				return fmt.Errorf("cannot overwrite directory %q with non-directory %q", path, dest)
+			}
+
+			if options.NoOverwriteDirNonDir && !fi.IsDir() && hdr.Typeflag == tar.TypeDir {
+				// If NoOverwriteDirNonDir is true then we cannot replace
+				// an existing non-directory with a directory from the archive.
+				return fmt.Errorf("cannot overwrite non-directory %q with directory %q", path, dest)
+			}
+
 			if fi.IsDir() && hdr.Name == "." {
 				continue
 			}
+
 			if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) {
 				if err := os.RemoveAll(path); err != nil {
 					return err
@@ -553,7 +606,8 @@ loop:
 			}
 		}
 		trBuf.Reset(tr)
-		if err := createTarFile(path, dest, hdr, trBuf, !options.NoLchown); err != nil {
+
+		if err := createTarFile(path, dest, hdr, trBuf, !options.NoLchown, options.ChownOpts); err != nil {
 			return err
 		}
 

+ 1 - 1
pkg/archive/archive_test.go

@@ -719,7 +719,7 @@ func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer os.RemoveAll(tmpDir)
-	err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true)
+	err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true, nil)
 	if err != nil {
 		t.Fatal(err)
 	}

+ 308 - 0
pkg/archive/copy.go

@@ -0,0 +1,308 @@
+package archive
+
+import (
+	"archive/tar"
+	"errors"
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	log "github.com/Sirupsen/logrus"
+)
+
+// Errors used or returned by this file.
+var (
+	ErrNotDirectory      = errors.New("not a directory")
+	ErrDirNotExists      = errors.New("no such directory")
+	ErrCannotCopyDir     = errors.New("cannot copy directory")
+	ErrInvalidCopySource = errors.New("invalid copy source content")
+)
+
+// PreserveTrailingDotOrSeparator returns the given cleaned path (after
+// processing using any utility functions from the path or filepath stdlib
+// packages) and appends a trailing `/.` or `/` if its corresponding  original
+// path (from before being processed by utility functions from the path or
+// filepath stdlib packages) ends with a trailing `/.` or `/`. If the cleaned
+// path already ends in a `.` path segment, then another is not added. If the
+// clean path already ends in a path separator, then another is not added.
+func PreserveTrailingDotOrSeparator(cleanedPath, originalPath string) string {
+	if !SpecifiesCurrentDir(cleanedPath) && SpecifiesCurrentDir(originalPath) {
+		if !HasTrailingPathSeparator(cleanedPath) {
+			// Add a separator if it doesn't already end with one (a cleaned
+			// path would only end in a separator if it is the root).
+			cleanedPath += string(filepath.Separator)
+		}
+		cleanedPath += "."
+	}
+
+	if !HasTrailingPathSeparator(cleanedPath) && HasTrailingPathSeparator(originalPath) {
+		cleanedPath += string(filepath.Separator)
+	}
+
+	return cleanedPath
+}
+
+// AssertsDirectory returns whether the given path is
+// asserted to be a directory, i.e., the path ends with
+// a trailing '/' or `/.`, assuming a path separator of `/`.
+func AssertsDirectory(path string) bool {
+	return HasTrailingPathSeparator(path) || SpecifiesCurrentDir(path)
+}
+
+// HasTrailingPathSeparator returns whether the given
+// path ends with the system's path separator character.
+func HasTrailingPathSeparator(path string) bool {
+	return len(path) > 0 && os.IsPathSeparator(path[len(path)-1])
+}
+
+// SpecifiesCurrentDir returns whether the given path specifies
+// a "current directory", i.e., the last path segment is `.`.
+func SpecifiesCurrentDir(path string) bool {
+	return filepath.Base(path) == "."
+}
+
+// SplitPathDirEntry splits the given path between its
+// parent directory and its basename in that directory.
+func SplitPathDirEntry(localizedPath string) (dir, base string) {
+	normalizedPath := filepath.ToSlash(localizedPath)
+	vol := filepath.VolumeName(normalizedPath)
+	normalizedPath = normalizedPath[len(vol):]
+
+	if normalizedPath == "/" {
+		// Specifies the root path.
+		return filepath.FromSlash(vol + normalizedPath), "."
+	}
+
+	trimmedPath := vol + strings.TrimRight(normalizedPath, "/")
+
+	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
+// 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.
+//
+// This function acts as a convenient wrapper around TarWithOptions, which
+// requires a directory as the source path. TarResource accepts either a
+// directory or a file path and correctly sets the Tar options.
+func TarResource(sourcePath string) (content Archive, err error) {
+	if _, err = os.Lstat(sourcePath); err != nil {
+		// 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
+		// error.
+		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
+	// the entry in that directory which we are archiving.
+	sourceDir, sourceBase := SplitPathDirEntry(sourcePath)
+
+	filter := []string{sourceBase}
+
+	log.Debugf("copying %q from %q", sourceBase, sourceDir)
+
+	return TarWithOptions(sourceDir, &TarOptions{
+		Compression:      Uncompressed,
+		IncludeFiles:     filter,
+		IncludeSourceDir: true,
+	})
+}
+
+// CopyInfo holds basic info about the source
+// or destination path of a copy operation.
+type CopyInfo struct {
+	Path   string
+	Exists bool
+	IsDir  bool
+}
+
+// CopyInfoStatPath stats the given path to create a CopyInfo
+// struct representing that resource. If mustExist is true, then
+// it is an error if there is no file or directory at the given path.
+func CopyInfoStatPath(path string, mustExist bool) (CopyInfo, error) {
+	pathInfo := CopyInfo{Path: path}
+
+	fileInfo, err := os.Lstat(path)
+
+	if err == nil {
+		pathInfo.Exists, pathInfo.IsDir = true, fileInfo.IsDir()
+	} else if os.IsNotExist(err) && !mustExist {
+		err = nil
+	}
+
+	return pathInfo, err
+}
+
+// PrepareArchiveCopy prepares the given srcContent archive, which should
+// contain the archived resource described by srcInfo, to the destination
+// described by dstInfo. Returns the possibly modified content archive along
+// with the path to the destination directory which it should be extracted to.
+func PrepareArchiveCopy(srcContent ArchiveReader, srcInfo, dstInfo CopyInfo) (dstDir string, content Archive, err error) {
+	// Separate the destination path between its directory and base
+	// components in case the source archive contents need to be rebased.
+	dstDir, dstBase := SplitPathDirEntry(dstInfo.Path)
+	_, srcBase := SplitPathDirEntry(srcInfo.Path)
+
+	switch {
+	case dstInfo.Exists && dstInfo.IsDir:
+		// The destination exists as a directory. No alteration
+		// to srcContent is needed as its contents can be
+		// simply extracted to the destination directory.
+		return dstInfo.Path, ioutil.NopCloser(srcContent), nil
+	case dstInfo.Exists && srcInfo.IsDir:
+		// The destination exists as some type of file and the source
+		// content is a directory. This is an error condition since
+		// you cannot copy a directory to an existing file location.
+		return "", nil, ErrCannotCopyDir
+	case dstInfo.Exists:
+		// The destination exists as some type of file and the source content
+		// is also a file. The source content entry will have to be renamed to
+		// have a basename which matches the destination path's basename.
+		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+	case srcInfo.IsDir:
+		// The destination does not exist and the source content is an archive
+		// of a directory. The archive should be extracted to the parent of
+		// the destination path instead, and when it is, the directory that is
+		// created as a result should take the name of the destination path.
+		// The source content entries will have to be renamed to have a
+		// basename which matches the destination path's basename.
+		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+	case AssertsDirectory(dstInfo.Path):
+		// The destination does not exist and is asserted to be created as a
+		// directory, but the source content is not a directory. This is an
+		// error condition since you cannot create a directory from a file
+		// source.
+		return "", nil, ErrDirNotExists
+	default:
+		// The last remaining case is when the destination does not exist, is
+		// not asserted to be a directory, and the source content is not an
+		// archive of a directory. It this case, the destination file will need
+		// to be created when the archive is extracted and the source content
+		// entry will have to be renamed to have a basename which matches the
+		// destination path's basename.
+		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+	}
+
+}
+
+// rebaseArchiveEntries rewrites the given srcContent archive replacing
+// an occurance of oldBase with newBase at the beginning of entry names.
+func rebaseArchiveEntries(srcContent ArchiveReader, oldBase, newBase string) Archive {
+	rebased, w := io.Pipe()
+
+	go func() {
+		srcTar := tar.NewReader(srcContent)
+		rebasedTar := tar.NewWriter(w)
+
+		for {
+			hdr, err := srcTar.Next()
+			if err == io.EOF {
+				// Signals end of archive.
+				rebasedTar.Close()
+				w.Close()
+				return
+			}
+			if err != nil {
+				w.CloseWithError(err)
+				return
+			}
+
+			hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
+
+			if err = rebasedTar.WriteHeader(hdr); err != nil {
+				w.CloseWithError(err)
+				return
+			}
+
+			if _, err = io.Copy(rebasedTar, srcTar); err != nil {
+				w.CloseWithError(err)
+				return
+			}
+		}
+	}()
+
+	return rebased
+}
+
+// CopyResource performs an archive copy from the given source path to the
+// given destination path. The source path MUST exist and the destination
+// path's parent directory must exist.
+func CopyResource(srcPath, dstPath string) error {
+	var (
+		srcInfo CopyInfo
+		err     error
+	)
+
+	// Clean the source and destination paths.
+	srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
+	dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
+
+	if srcInfo, err = CopyInfoStatPath(srcPath, true); err != nil {
+		return err
+	}
+
+	content, err := TarResource(srcPath)
+	if err != nil {
+		return err
+	}
+	defer content.Close()
+
+	return CopyTo(content, srcInfo, dstPath)
+}
+
+// CopyTo handles extracting the given content whose
+// entries should be sourced from srcInfo to dstPath.
+func CopyTo(content ArchiveReader, srcInfo CopyInfo, dstPath string) error {
+	dstInfo, err := CopyInfoStatPath(dstPath, false)
+	if err != nil {
+		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)
+	if err != nil {
+		return err
+	}
+	defer copyArchive.Close()
+
+	options := &TarOptions{
+		NoLchown:             true,
+		NoOverwriteDirNonDir: true,
+	}
+
+	return Untar(copyArchive, dstDir, options)
+}

+ 637 - 0
pkg/archive/copy_test.go

@@ -0,0 +1,637 @@
+package archive
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func removeAllPaths(paths ...string) {
+	for _, path := range paths {
+		os.RemoveAll(path)
+	}
+}
+
+func getTestTempDirs(t *testing.T) (tmpDirA, tmpDirB string) {
+	var err error
+
+	if tmpDirA, err = ioutil.TempDir("", "archive-copy-test"); err != nil {
+		t.Fatal(err)
+	}
+
+	if tmpDirB, err = ioutil.TempDir("", "archive-copy-test"); err != nil {
+		t.Fatal(err)
+	}
+
+	return
+}
+
+func isNotDir(err error) bool {
+	return strings.Contains(err.Error(), "not a directory")
+}
+
+func joinTrailingSep(pathElements ...string) string {
+	joined := filepath.Join(pathElements...)
+
+	return fmt.Sprintf("%s%c", joined, filepath.Separator)
+}
+
+func fileContentsEqual(t *testing.T, filenameA, filenameB string) (err error) {
+	t.Logf("checking for equal file contents: %q and %q\n", filenameA, filenameB)
+
+	fileA, err := os.Open(filenameA)
+	if err != nil {
+		return
+	}
+	defer fileA.Close()
+
+	fileB, err := os.Open(filenameB)
+	if err != nil {
+		return
+	}
+	defer fileB.Close()
+
+	hasher := sha256.New()
+
+	if _, err = io.Copy(hasher, fileA); err != nil {
+		return
+	}
+
+	hashA := hasher.Sum(nil)
+	hasher.Reset()
+
+	if _, err = io.Copy(hasher, fileB); err != nil {
+		return
+	}
+
+	hashB := hasher.Sum(nil)
+
+	if !bytes.Equal(hashA, hashB) {
+		err = fmt.Errorf("file content hashes not equal - expected %s, got %s", hex.EncodeToString(hashA), hex.EncodeToString(hashB))
+	}
+
+	return
+}
+
+func dirContentsEqual(t *testing.T, newDir, oldDir string) (err error) {
+	t.Logf("checking for equal directory contents: %q and %q\n", newDir, oldDir)
+
+	var changes []Change
+
+	if changes, err = ChangesDirs(newDir, oldDir); err != nil {
+		return
+	}
+
+	if len(changes) != 0 {
+		err = fmt.Errorf("expected no changes between directories, but got: %v", changes)
+	}
+
+	return
+}
+
+func logDirContents(t *testing.T, dirPath string) {
+	logWalkedPaths := filepath.WalkFunc(func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			t.Errorf("stat error for path %q: %s", path, err)
+			return nil
+		}
+
+		if info.IsDir() {
+			path = joinTrailingSep(path)
+		}
+
+		t.Logf("\t%s", path)
+
+		return nil
+	})
+
+	t.Logf("logging directory contents: %q", dirPath)
+
+	if err := filepath.Walk(dirPath, logWalkedPaths); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func testCopyHelper(t *testing.T, srcPath, dstPath string) (err error) {
+	t.Logf("copying from %q to %q", srcPath, dstPath)
+
+	return CopyResource(srcPath, dstPath)
+}
+
+// Basic assumptions about SRC and DST:
+// 1. SRC must exist.
+// 2. If SRC ends with a trailing separator, it must be a directory.
+// 3. DST parent directory must exist.
+// 4. If DST exists as a file, it must not end with a trailing separator.
+
+// First get these easy error cases out of the way.
+
+// Test for error when SRC does not exist.
+func TestCopyErrSrcNotExists(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	content, err := TarResource(filepath.Join(tmpDirA, "file1"))
+	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)
+	}
+}
+
+// Test for error when SRC ends in a trailing
+// path separator but it exists as a file.
+func TestCopyErrSrcNotDir(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	content, err := TarResource(joinTrailingSep(tmpDirA, "file1"))
+	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)
+	}
+}
+
+// Test for error when SRC is a valid file or directory,
+// but the DST parent directory does not exist.
+func TestCopyErrDstParentNotExists(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
+
+	// Try with a file source.
+	content, err := TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	// Copy to a file whose parent does not exist.
+	if err = CopyTo(content, srcInfo, filepath.Join(tmpDirB, "fakeParentDir", "file1")); err == nil {
+		t.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !os.IsNotExist(err) {
+		t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
+
+	content, err = TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	// Copy to a directory whose parent does not exist.
+	if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "fakeParentDir", "fakeDstDir")); err == nil {
+		t.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !os.IsNotExist(err) {
+		t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when DST ends in a trailing
+// path separator but exists as a file.
+func TestCopyErrDstNotDir(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	// Try with a file source.
+	srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
+
+	content, err := TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil {
+		t.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isNotDir(err) {
+		t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
+
+	content, err = TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil {
+		t.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isNotDir(err) {
+		t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Possibilities are reduced to the remaining 10 cases:
+//
+//  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
+// ===================================================================================================
+//   A   |  no      |  -              |  no       |  -       |  no      |  create file
+//   B   |  no      |  -              |  no       |  -       |  yes     |  error
+//   C   |  no      |  -              |  yes      |  no      |  -       |  overwrite file
+//   D   |  no      |  -              |  yes      |  yes     |  -       |  create file in dst dir
+//   E   |  yes     |  no             |  no       |  -       |  -       |  create dir, copy contents
+//   F   |  yes     |  no             |  yes      |  no      |  -       |  error
+//   G   |  yes     |  no             |  yes      |  yes     |  -       |  copy dir and contents
+//   H   |  yes     |  yes            |  no       |  -       |  -       |  create dir, copy contents
+//   I   |  yes     |  yes            |  yes      |  no      |  -       |  error
+//   J   |  yes     |  yes            |  yes      |  yes     |  -       |  copy dir contents
+//
+
+// A. SRC specifies a file and DST (no trailing path separator) doesn't
+//    exist. This should create a file with the name DST and copy the
+//    contents of the source file into it.
+func TestCopyCaseA(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstPath := filepath.Join(tmpDirB, "itWorks.txt")
+
+	var err error
+
+	if err = testCopyHelper(t, srcPath, dstPath); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// B. SRC specifies a file and DST (with trailing path separator) doesn't
+//    exist. This should cause an error because the copy operation cannot
+//    create a directory when copying a single file.
+func TestCopyCaseB(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstDir := joinTrailingSep(tmpDirB, "testDir")
+
+	var err error
+
+	if err = testCopyHelper(t, srcPath, dstDir); err == nil {
+		t.Fatal("expected ErrDirNotExists error, but got nil instead")
+	}
+
+	if err != ErrDirNotExists {
+		t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err)
+	}
+}
+
+// C. SRC specifies a file and DST exists as a file. This should overwrite
+//    the file at DST with the contents of the source file.
+func TestCopyCaseC(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstPath := filepath.Join(tmpDirB, "file2")
+
+	var err error
+
+	// Ensure they start out different.
+	if err = fileContentsEqual(t, srcPath, dstPath); err == nil {
+		t.Fatal("expected different file contents")
+	}
+
+	if err = testCopyHelper(t, srcPath, dstPath); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// D. SRC specifies a file and DST exists as a directory. This should place
+//    a copy of the source file inside it using the basename from SRC. Ensure
+//    this works whether DST has a trailing path separator or not.
+func TestCopyCaseD(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstDir := filepath.Join(tmpDirB, "dir1")
+	dstPath := filepath.Join(dstDir, "file1")
+
+	var err error
+
+	// Ensure that dstPath doesn't exist.
+	if _, err = os.Stat(dstPath); !os.IsNotExist(err) {
+		t.Fatalf("did not expect dstPath %q to exist", dstPath)
+	}
+
+	if err = testCopyHelper(t, srcPath, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		t.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "dir1")
+
+	if err = testCopyHelper(t, srcPath, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// E. SRC specifies a directory and DST does not exist. This should create a
+//    directory at DST and copy the contents of the SRC directory into the DST
+//    directory. Ensure this works whether DST has a trailing path separator or
+//    not.
+func TestCopyCaseE(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcDir := filepath.Join(tmpDirA, "dir1")
+	dstDir := filepath.Join(tmpDirB, "testDir")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Log("dir contents not equal")
+		logDirContents(t, tmpDirA)
+		logDirContents(t, tmpDirB)
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "testDir")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// F. SRC specifies a directory and DST exists as a file. This should cause an
+//    error as it is not possible to overwrite a file with a directory.
+func TestCopyCaseF(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := filepath.Join(tmpDirA, "dir1")
+	dstFile := filepath.Join(tmpDirB, "file1")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstFile); err == nil {
+		t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if err != ErrCannotCopyDir {
+		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// G. SRC specifies a directory and DST exists as a directory. This should copy
+//    the SRC directory and all its contents to the DST directory. Ensure this
+//    works whether DST has a trailing path separator or not.
+func TestCopyCaseG(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := filepath.Join(tmpDirA, "dir1")
+	dstDir := filepath.Join(tmpDirB, "dir2")
+	resultDir := filepath.Join(dstDir, "dir1")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, resultDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		t.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "dir2")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, resultDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// H. SRC specifies a directory's contents only and DST does not exist. This
+//    should create a directory at DST and copy the contents of the SRC
+//    directory (but not the directory itself) into the DST directory. Ensure
+//    this works whether DST has a trailing path separator or not.
+func TestCopyCaseH(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
+	dstDir := filepath.Join(tmpDirB, "testDir")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Log("dir contents not equal")
+		logDirContents(t, tmpDirA)
+		logDirContents(t, tmpDirB)
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "testDir")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Log("dir contents not equal")
+		logDirContents(t, tmpDirA)
+		logDirContents(t, tmpDirB)
+		t.Fatal(err)
+	}
+}
+
+// I. SRC specifies a direcotry's contents only and DST exists as a file. This
+//    should cause an error as it is not possible to overwrite a file with a
+//    directory.
+func TestCopyCaseI(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
+	dstFile := filepath.Join(tmpDirB, "file1")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstFile); err == nil {
+		t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if err != ErrCannotCopyDir {
+		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// J. SRC specifies a directory's contents only and DST exists as a directory.
+//    This should copy the contents of the SRC directory (but not the directory
+//    itself) into the DST directory. Ensure this works whether DST has a
+//    trailing path separator or not.
+func TestCopyCaseJ(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
+	dstDir := filepath.Join(tmpDirB, "dir5")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		t.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "dir5")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+}

+ 2 - 2
pkg/archive/diff.go

@@ -93,7 +93,7 @@ func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) {
 					}
 					defer os.RemoveAll(aufsTempdir)
 				}
-				if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true); err != nil {
+				if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true, nil); err != nil {
 					return 0, err
 				}
 			}
@@ -150,7 +150,7 @@ func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) {
 				srcData = tmpFile
 			}
 
-			if err := createTarFile(path, dest, srcHdr, srcData, true); err != nil {
+			if err := createTarFile(path, dest, srcHdr, srcData, true, nil); err != nil {
 				return 0, err
 			}