Bladeren bron

Merge pull request #13171 from jlhawn/archive_copy

docker cp to and from containers
Arnaud Porterie 10 jaren geleden
bovenliggende
commit
c986f85f73

+ 269 - 22
api/client/cp.go

@@ -1,8 +1,14 @@
 package client
 package client
 
 
 import (
 import (
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
@@ -10,48 +16,289 @@ import (
 	flag "github.com/docker/docker/pkg/mflag"
 	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 {
 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)
 	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 {
 	if err != nil {
 		return err
 		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 {
 		if err != nil {
 			return err
 			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
 	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
 	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 {
 func (s *Server) postContainersCopy(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if vars == nil {
 	if vars == nil {
 		return fmt.Errorf("Missing parameter")
 		return fmt.Errorf("Missing parameter")
@@ -1348,6 +1349,104 @@ func (s *Server) postContainersCopy(version version.Version, w http.ResponseWrit
 	return nil
 	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 {
 func (s *Server) postContainerExecCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if err := parseForm(r); err != nil {
 	if err := parseForm(r); err != nil {
 		return err
 		return err
@@ -1536,6 +1635,9 @@ func createRouter(s *Server) *mux.Router {
 		ProfilerSetup(r, "/debug/")
 		ProfilerSetup(r, "/debug/")
 	}
 	}
 	m := map[string]map[string]HttpApiFunc{
 	m := map[string]map[string]HttpApiFunc{
+		"HEAD": {
+			"/containers/{name:.*}/archive": s.headContainersArchive,
+		},
 		"GET": {
 		"GET": {
 			"/_ping":                          s.ping,
 			"/_ping":                          s.ping,
 			"/events":                         s.getEvents,
 			"/events":                         s.getEvents,
@@ -1557,6 +1659,7 @@ func createRouter(s *Server) *mux.Router {
 			"/containers/{name:.*}/stats":     s.getContainersStats,
 			"/containers/{name:.*}/stats":     s.getContainersStats,
 			"/containers/{name:.*}/attach/ws": s.wsContainersAttach,
 			"/containers/{name:.*}/attach/ws": s.wsContainersAttach,
 			"/exec/{id:.*}/json":              s.getExecByID,
 			"/exec/{id:.*}/json":              s.getExecByID,
+			"/containers/{name:.*}/archive":   s.getContainersArchive,
 		},
 		},
 		"POST": {
 		"POST": {
 			"/auth":                         s.postAuth,
 			"/auth":                         s.postAuth,
@@ -1582,6 +1685,9 @@ func createRouter(s *Server) *mux.Router {
 			"/exec/{name:.*}/resize":        s.postContainerExecResize,
 			"/exec/{name:.*}/resize":        s.postContainerExecResize,
 			"/containers/{name:.*}/rename":  s.postContainerRename,
 			"/containers/{name:.*}/rename":  s.postContainerRename,
 		},
 		},
+		"PUT": {
+			"/containers/{name:.*}/archive": s.putContainersArchive,
+		},
 		"DELETE": {
 		"DELETE": {
 			"/containers/{name:.*}": s.deleteContainers,
 			"/containers/{name:.*}": s.deleteContainers,
 			"/images/{name:.*}":     s.deleteImages,
 			"/images/{name:.*}":     s.deleteImages,

+ 13 - 0
api/types/types.go

@@ -1,6 +1,7 @@
 package types
 package types
 
 
 import (
 import (
+	"os"
 	"time"
 	"time"
 
 
 	"github.com/docker/docker/daemon/network"
 	"github.com/docker/docker/daemon/network"
@@ -127,6 +128,18 @@ type CopyConfig struct {
 	Resource string
 	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"
 // GET "/containers/{name:.*}/top"
 type ContainerProcessList struct {
 type ContainerProcessList struct {
 	Processes [][]string
 	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 (
 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 {
 type StreamConfig struct {
@@ -616,13 +617,22 @@ func validateID(id string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (container *Container) Copy(resource string) (io.ReadCloser, error) {
+func (container *Container) Copy(resource string) (rc io.ReadCloser, err error) {
 	container.Lock()
 	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 {
 	if err := container.Mount(); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+
 	defer func() {
 	defer func() {
 		if err != nil {
 		if err != nil {
 			// unmount any volumes
 			// unmount any volumes
@@ -631,28 +641,11 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
 			container.Unmount()
 			container.Unmount()
 		}
 		}
 	}()
 	}()
-	mounts, err := container.setupMounts()
-	if err != nil {
+
+	if err := container.mountVolumes(); err != nil {
 		return nil, err
 		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)
 	basePath, err := container.GetResourcePath(resource)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -688,6 +681,7 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
 		container.CleanupStorage()
 		container.CleanupStorage()
 		container.UnmountVolumes(true)
 		container.UnmountVolumes(true)
 		container.Unmount()
 		container.Unmount()
+		container.Unlock()
 		return err
 		return err
 	})
 	})
 	container.LogEvent("copy")
 	container.LogEvent("copy")
@@ -1190,6 +1184,40 @@ func (container *Container) shouldRestart() bool {
 		(container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0)
 		(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 {
 func (container *Container) copyImagePathContent(v volume.Volume, destination string) error {
 	rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs)
 	rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs)
 	if err != nil {
 	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
 package daemon
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
@@ -17,6 +18,10 @@ import (
 	"github.com/opencontainers/runc/libcontainer/label"
 	"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 {
 type mountPoint struct {
 	Name        string
 	Name        string
 	Destination 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")
 	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 {
 func (m *mountPoint) Path() string {
 	if m.Volume != nil {
 	if m.Volume != nil {
 		return m.Volume.Path()
 		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
 ### 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!**
 **New!**
 The `hostConfig` option now accepts the field `GroupAdd`, which specifies a list of additional
 The `hostConfig` option now accepts the field `GroupAdd`, which specifies a list of additional
 groups that the container process will run as.
 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`
 Copy files or folders of container `id`
 
 
+**Deprecated** in favor of the `archive` endpoint below.
+
 **Example request**:
 **Example request**:
 
 
     POST /containers/4fa6e0f0c678/copy HTTP/1.1
     POST /containers/4fa6e0f0c678/copy HTTP/1.1
@@ -1061,6 +1063,120 @@ Status Codes:
 -   **404** – no such container
 -   **404** – no such container
 -   **500** – server error
 -   **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
 ## 2.2 Images
 
 
 ### List Images
 ### List Images

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

@@ -11,12 +11,81 @@ weight=1
 
 
 # cp
 # 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"
 	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
 // Test for #5656
 // Check that garbage paths don't escape the container's rootfs
 // Check that garbage paths don't escape the container's rootfs
 func (s *DockerSuite) TestCpGarbagePath(c *check.C) {
 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 (
 import (
 	"bufio"
 	"bufio"
 	"fmt"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http"
+	"os"
 	"os/exec"
 	"os/exec"
 	"regexp"
 	"regexp"
 	"strconv"
 	"strconv"
@@ -519,6 +521,7 @@ func (s *DockerSuite) TestEventsCommit(c *check.C) {
 func (s *DockerSuite) TestEventsCopy(c *check.C) {
 func (s *DockerSuite) TestEventsCopy(c *check.C) {
 	since := daemonTime(c).Unix()
 	since := daemonTime(c).Unix()
 
 
+	// Build a test image.
 	id, err := buildImage("cpimg", `
 	id, err := buildImage("cpimg", `
 		  FROM busybox
 		  FROM busybox
 		  RUN echo HI > /tmp/file`, true)
 		  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)
 		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)))
 	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
 % Docker Community
 % JUNE 2014
 % JUNE 2014
 # NAME
 # 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
 # SYNOPSIS
 **docker cp**
 **docker cp**
 [**--help**]
 [**--help**]
-CONTAINER:PATH HOSTDIR|-
+CONTAINER:PATH LOCALPATH|-
+LOCALPATH|- CONTAINER:PATH
 
 
 # DESCRIPTION
 # 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` 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 cp compassionate_darwin:tmp/foo /tmp
 
 
 Docker creates a `/tmp/foo` directory on your host. Alternatively, you can omit
 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 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
 # HISTORY
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
 based on docker.com source material and internal work.
 based on docker.com source material and internal work.
 June 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>
 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 (
 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
 	// 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
 	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,
 	// 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,
 	// but for os.Foo() calls we need the mode converted to os.FileMode,
 	// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
 	// 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)
 		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
 			return err
 		}
 		}
 	}
 	}
@@ -396,6 +407,20 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 			Buffer:    pools.BufioWriter32KPool.Get(nil),
 			Buffer:    pools.BufioWriter32KPool.Get(nil),
 			SeenFiles: make(map[uint64]string),
 			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
 		// this buffer is needed for the duration of this piped stream
 		defer pools.BufioWriter32KPool.Put(ta.Buffer)
 		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
 		// mutating the filesystem and we can see transient errors
 		// from this
 		// 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{"."}
 			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
 		var renamedRelFilePath string // For when tar.Options.Name is set
 		for _, include := range options.IncludeFiles {
 		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 {
 				if err != nil {
 					logrus.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err)
 					logrus.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err)
 					return nil
 					return nil
 				}
 				}
 
 
 				relFilePath, err := filepath.Rel(srcPath, filePath)
 				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
 					// 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
 					return nil
 				}
 				}
 
 
+				if options.IncludeSourceDir && include == "." && relFilePath != "." {
+					relFilePath = strings.Join([]string{".", relFilePath}, string(filepath.Separator))
+				}
+
 				skip := false
 				skip := false
 
 
 				// If "include" is an exact match for the current file
 				// 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
 				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
 	return pipeReader, nil
@@ -543,9 +583,22 @@ loop:
 		// the layer is also a directory. Then we want to merge them (i.e.
 		// the layer is also a directory. Then we want to merge them (i.e.
 		// just apply the metadata from the layer).
 		// just apply the metadata from the layer).
 		if fi, err := os.Lstat(path); err == nil {
 		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 == "." {
 			if fi.IsDir() && hdr.Name == "." {
 				continue
 				continue
 			}
 			}
+
 			if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) {
 			if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) {
 				if err := os.RemoveAll(path); err != nil {
 				if err := os.RemoveAll(path); err != nil {
 					return err
 					return err
@@ -553,7 +606,8 @@ loop:
 			}
 			}
 		}
 		}
 		trBuf.Reset(tr)
 		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
 			return err
 		}
 		}
 
 

+ 1 - 1
pkg/archive/archive_test.go

@@ -719,7 +719,7 @@ func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 	defer os.RemoveAll(tmpDir)
 	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 {
 	if err != nil {
 		t.Fatal(err)
 		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)
 					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
 					return 0, err
 				}
 				}
 			}
 			}
@@ -150,7 +150,7 @@ func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) {
 				srcData = tmpFile
 				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
 				return 0, err
 			}
 			}