diff --git a/api/client/cp.go b/api/client/cp.go index c838e12589..0463a994b7 100644 --- a/api/client/cp.go +++ b/api/client/cp.go @@ -1,8 +1,14 @@ package client import ( + "encoding/base64" + "encoding/json" "fmt" "io" + "net/http" + "net/url" + "os" + "path/filepath" "strings" "github.com/docker/docker/api/types" @@ -10,48 +16,289 @@ import ( flag "github.com/docker/docker/pkg/mflag" ) -// CmdCp copies files/folders from a path on the container to a directory on the host running the command. -// -// If HOSTDIR is '-', the data is written as a tar file to STDOUT. -// -// Usage: docker cp CONTAINER:PATH HOSTDIR -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) +type copyDirection int +const ( + fromContainer copyDirection = (1 << iota) + toContainer + acrossContainers = fromContainer | toContainer +) + +// CmdCp copies files/folders to or from a path in a container. +// +// When copying from a container, if LOCALPATH is '-' the data is written as a +// tar archive file to STDOUT. +// +// When copying to a container, if LOCALPATH is '-' the data is read as a tar +// archive file from STDIN, and the destination CONTAINER:PATH, must specify +// a directory. +// +// Usage: +// docker cp CONTAINER:PATH LOCALPATH|- +// docker cp LOCALPATH|- CONTAINER:PATH +func (cli *DockerCli) CmdCp(args ...string) error { + cmd := cli.Subcmd( + "cp", + []string{"CONTAINER:PATH LOCALPATH|-", "LOCALPATH|- CONTAINER:PATH"}, + strings.Join([]string{ + "Copy files/folders between a container and your host.\n", + "Use '-' as the source to read a tar archive from stdin\n", + "and extract it to a directory destination in a container.\n", + "Use '-' as the destination to stream a tar archive of a\n", + "container source to stdout.", + }, ""), + true, + ) + + cmd.Require(flag.Exact, 2) cmd.ParseFlags(args, true) - // deal with path name with `:` - info := strings.SplitN(cmd.Arg(0), ":", 2) - - if len(info) != 2 { - return fmt.Errorf("Error: Path not specified") + if cmd.Arg(0) == "" { + return fmt.Errorf("source can not be empty") + } + if cmd.Arg(1) == "" { + return fmt.Errorf("destination can not be empty") } - cfg := &types.CopyConfig{ - Resource: info[1], + srcContainer, srcPath := splitCpArg(cmd.Arg(0)) + dstContainer, dstPath := splitCpArg(cmd.Arg(1)) + + var direction copyDirection + if srcContainer != "" { + direction |= fromContainer } - serverResp, err := cli.call("POST", "/containers/"+info[0]+"/copy", cfg, nil) - if serverResp.body != nil { - defer serverResp.body.Close() + if dstContainer != "" { + direction |= toContainer } - if serverResp.statusCode == 404 { - return fmt.Errorf("No such container: %v", info[0]) + + 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") } +} + +// 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 + } + + 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 + } + + 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 err + return stat, err + } + defer response.body.Close() + + if response.statusCode != http.StatusOK { + return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) } - 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}) - } + return getContainerPathStatFromHeader(response.header) +} + +func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) { + var stat types.ContainerPathStat + + encodedStat := header.Get("X-Docker-Container-Path-Stat") + statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat)) + + err := json.NewDecoder(statDecoder).Decode(&stat) + if err != nil { + err = fmt.Errorf("unable to decode container path stat header: %s", err) + } + + return stat, err +} + +func resolveLocalPath(localPath string) (absPath string, err error) { + if absPath, err = filepath.Abs(localPath); err != nil { + return + } + + return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil +} + +func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (err error) { + if dstPath != "-" { + // Get an absolute destination path. + dstPath, err = resolveLocalPath(dstPath) if err != nil { return err } } + + query := make(url.Values, 1) + query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. + + urlStr := fmt.Sprintf("/containers/%s/archive?%s", srcContainer, query.Encode()) + + response, err := cli.call("GET", urlStr, nil, nil) + if err != nil { + return err + } + defer response.body.Close() + + if response.statusCode != http.StatusOK { + return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) + } + + if dstPath == "-" { + // Send the response to STDOUT. + _, err = io.Copy(os.Stdout, response.body) + + return err + } + + // In order to get the copy behavior right, we need to know information + // about both the source and the destination. The response headers include + // stat info about the source that we can use in deciding exactly how to + // copy it locally. Along with the stat info about the local destination, + // we have everything we need to handle the multiple possibilities there + // can be when copying a file/dir from one location to another file/dir. + stat, err := getContainerPathStatFromHeader(response.header) + if err != nil { + return fmt.Errorf("unable to get resource stat from response: %s", err) + } + + // Prepare source copy info. + srcInfo := archive.CopyInfo{ + Path: srcPath, + Exists: true, + IsDir: stat.Mode.IsDir(), + } + + // See comments in the implementation of `archive.CopyTo` for exactly what + // goes into deciding how and whether the source archive needs to be + // altered for the correct copy behavior. + return archive.CopyTo(response.body, srcInfo, dstPath) +} + +func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (err error) { + if srcPath != "-" { + // Get an absolute source path. + srcPath, err = resolveLocalPath(srcPath) + if err != nil { + return err + } + } + + // In order to get the copy behavior right, we need to know information + // about both the source and destination. The API is a simple tar + // archive/extract API but we can use the stat info header about the + // destination to be more informed about exactly what the destination is. + + // Prepare destination copy info by stat-ing the container path. + dstInfo := archive.CopyInfo{Path: dstPath} + dstStat, err := cli.statContainerPath(dstContainer, dstPath) + // Ignore any error and assume that the parent directory of the destination + // path exists, in which case the copy may still succeed. If there is any + // type of conflict (e.g., non-directory overwriting an existing directory + // or vice versia) the extraction will fail. If the destination simply did + // not exist, but the parent directory does, the extraction will still + // succeed. + if err == nil { + dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() + } + + var content io.Reader + if srcPath == "-" { + // Use STDIN. + content = os.Stdin + if !dstInfo.IsDir { + return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath)) + } + } else { + srcArchive, err := archive.TarResource(srcPath) + if err != nil { + return err + } + defer srcArchive.Close() + + // With the stat info about the local source as well as the + // destination, we have enough information to know whether we need to + // alter the archive that we upload so that when the server extracts + // it to the specified directory in the container we get the disired + // copy behavior. + + // Prepare source copy info. + srcInfo, err := archive.CopyInfoStatPath(srcPath, true) + if err != nil { + return err + } + + // See comments in the implementation of `archive.PrepareArchiveCopy` + // for exactly what goes into deciding how and whether the source + // archive needs to be altered for the correct copy behavior when it is + // extracted. This function also infers from the source and destination + // info which directory to extract to, which may be the parent of the + // destination that the user specified. + dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) + if err != nil { + return err + } + defer preparedArchive.Close() + + dstPath = dstDir + content = preparedArchive + } + + query := make(url.Values, 2) + query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API. + // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. + query.Set("noOverwriteDirNonDir", "true") + + urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode()) + + response, err := cli.stream("PUT", urlStr, &streamOpts{in: content}) + if err != nil { + return err + } + defer response.body.Close() + + if response.statusCode != http.StatusOK { + return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) + } + return nil } diff --git a/api/server/server.go b/api/server/server.go index 0c165d024e..fcd65b8fac 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1309,6 +1309,7 @@ func (s *Server) postBuild(version version.Version, w http.ResponseWriter, r *ht return nil } +// postContainersCopy is deprecated in favor of getContainersArchivePath. func (s *Server) postContainersCopy(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") @@ -1348,6 +1349,104 @@ func (s *Server) postContainersCopy(version version.Version, w http.ResponseWrit return nil } +// // Encode the stat to JSON, base64 encode, and place in a header. +func setContainerPathStatHeader(stat *types.ContainerPathStat, header http.Header) error { + statJSON, err := json.Marshal(stat) + if err != nil { + return err + } + + header.Set( + "X-Docker-Container-Path-Stat", + base64.StdEncoding.EncodeToString(statJSON), + ) + + return nil +} + +func (s *Server) headContainersArchive(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if vars == nil { + return fmt.Errorf("Missing parameter") + } + if err := parseForm(r); err != nil { + return err + } + + name := vars["name"] + path := r.Form.Get("path") + + switch { + case name == "": + return fmt.Errorf("bad parameter: 'name' cannot be empty") + case path == "": + return fmt.Errorf("bad parameter: 'path' cannot be empty") + } + + stat, err := s.daemon.ContainerStatPath(name, path) + if err != nil { + return err + } + + return setContainerPathStatHeader(stat, w.Header()) +} + +func (s *Server) getContainersArchive(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if vars == nil { + return fmt.Errorf("Missing parameter") + } + if err := parseForm(r); err != nil { + return err + } + + name := vars["name"] + path := r.Form.Get("path") + + switch { + case name == "": + return fmt.Errorf("bad parameter: 'name' cannot be empty") + case path == "": + return fmt.Errorf("bad parameter: 'path' cannot be empty") + } + + tarArchive, stat, err := s.daemon.ContainerArchivePath(name, path) + if err != nil { + return err + } + defer tarArchive.Close() + + if err := setContainerPathStatHeader(stat, w.Header()); err != nil { + return err + } + + w.Header().Set("Content-Type", "application/x-tar") + _, err = io.Copy(w, tarArchive) + + return err +} + +func (s *Server) putContainersArchive(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if vars == nil { + return fmt.Errorf("Missing parameter") + } + if err := parseForm(r); err != nil { + return err + } + + name := vars["name"] + path := r.Form.Get("path") + + noOverwriteDirNonDir := boolValue(r, "noOverwriteDirNonDir") + + switch { + case name == "": + return fmt.Errorf("bad parameter: 'name' cannot be empty") + case path == "": + return fmt.Errorf("bad parameter: 'path' cannot be empty") + } + + return s.daemon.ContainerExtractToDir(name, path, noOverwriteDirNonDir, r.Body) +} + func (s *Server) postContainerExecCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err @@ -1536,6 +1635,9 @@ func createRouter(s *Server) *mux.Router { ProfilerSetup(r, "/debug/") } m := map[string]map[string]HttpApiFunc{ + "HEAD": { + "/containers/{name:.*}/archive": s.headContainersArchive, + }, "GET": { "/_ping": s.ping, "/events": s.getEvents, @@ -1557,6 +1659,7 @@ func createRouter(s *Server) *mux.Router { "/containers/{name:.*}/stats": s.getContainersStats, "/containers/{name:.*}/attach/ws": s.wsContainersAttach, "/exec/{id:.*}/json": s.getExecByID, + "/containers/{name:.*}/archive": s.getContainersArchive, }, "POST": { "/auth": s.postAuth, @@ -1582,6 +1685,9 @@ func createRouter(s *Server) *mux.Router { "/exec/{name:.*}/resize": s.postContainerExecResize, "/containers/{name:.*}/rename": s.postContainerRename, }, + "PUT": { + "/containers/{name:.*}/archive": s.putContainersArchive, + }, "DELETE": { "/containers/{name:.*}": s.deleteContainers, "/images/{name:.*}": s.deleteImages, diff --git a/api/types/types.go b/api/types/types.go index a27755f69f..42fc6793ef 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -1,6 +1,7 @@ package types import ( + "os" "time" "github.com/docker/docker/daemon/network" @@ -127,6 +128,18 @@ type CopyConfig struct { Resource string } +// ContainerPathStat is used to encode the header from +// GET /containers/{name:.*}/archive +// "name" is the file or directory name. +// "path" is the absolute path to the resource in the container. +type ContainerPathStat struct { + Name string `json:"name"` + Path string `json:"path"` + Size int64 `json:"size"` + Mode os.FileMode `json:"mode"` + Mtime time.Time `json:"mtime"` +} + // GET "/containers/{name:.*}/top" type ContainerProcessList struct { Processes [][]string diff --git a/daemon/archive.go b/daemon/archive.go new file mode 100644 index 0000000000..f6b5698353 --- /dev/null +++ b/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 +} diff --git a/daemon/container.go b/daemon/container.go index bc9d5ab043..6060d0149f 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -35,10 +35,11 @@ import ( ) var ( - ErrNotATTY = errors.New("The PTY is not a file") - ErrNoTTY = errors.New("No PTY found") - ErrContainerStart = errors.New("The container failed to start. Unknown error") - ErrContainerStartTimeout = errors.New("The container failed to start due to timed out.") + ErrNotATTY = errors.New("The PTY is not a file") + ErrNoTTY = errors.New("No PTY found") + ErrContainerStart = errors.New("The container failed to start. Unknown error") + ErrContainerStartTimeout = errors.New("The container failed to start due to timed out.") + ErrContainerRootfsReadonly = errors.New("container rootfs is marked read-only") ) type StreamConfig struct { @@ -616,13 +617,22 @@ func validateID(id string) error { return nil } -func (container *Container) Copy(resource string) (io.ReadCloser, error) { +func (container *Container) Copy(resource string) (rc io.ReadCloser, err error) { container.Lock() - defer container.Unlock() - var err error + + defer func() { + if err != nil { + // Wait to unlock the container until the archive is fully read + // (see the ReadCloseWrapper func below) or if there is an error + // before that occurs. + container.Unlock() + } + }() + if err := container.Mount(); err != nil { return nil, err } + defer func() { if err != nil { // unmount any volumes @@ -631,28 +641,11 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { container.Unmount() } }() - mounts, err := container.setupMounts() - if err != nil { + + if err := container.mountVolumes(); err != nil { return nil, err } - for _, m := range mounts { - var dest string - dest, err = container.GetResourcePath(m.Destination) - if err != nil { - return nil, err - } - var stat os.FileInfo - stat, err = os.Stat(m.Source) - if err != nil { - return nil, err - } - if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil { - return nil, err - } - if err = mount.Mount(m.Source, dest, "bind", "rbind,ro"); err != nil { - return nil, err - } - } + basePath, err := container.GetResourcePath(resource) if err != nil { return nil, err @@ -688,6 +681,7 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { container.CleanupStorage() container.UnmountVolumes(true) container.Unmount() + container.Unlock() return err }) container.LogEvent("copy") @@ -1190,6 +1184,40 @@ func (container *Container) shouldRestart() bool { (container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0) } +func (container *Container) mountVolumes() error { + mounts, err := container.setupMounts() + if err != nil { + return err + } + + for _, m := range mounts { + dest, err := container.GetResourcePath(m.Destination) + if err != nil { + return err + } + + var stat os.FileInfo + stat, err = os.Stat(m.Source) + if err != nil { + return err + } + if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil { + return err + } + + opts := "rbind,ro" + if m.Writable { + opts = "rbind,rw" + } + + if err := mount.Mount(m.Source, dest, "bind", opts); err != nil { + return err + } + } + + return nil +} + func (container *Container) copyImagePathContent(v volume.Volume, destination string) error { rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs) if err != nil { diff --git a/daemon/copy.go b/daemon/copy.go deleted file mode 100644 index dec30d8f37..0000000000 --- a/daemon/copy.go +++ /dev/null @@ -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) -} diff --git a/daemon/volumes.go b/daemon/volumes.go index 71a3ed941f..556e304977 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -1,6 +1,7 @@ package daemon import ( + "errors" "fmt" "io/ioutil" "os" @@ -17,6 +18,10 @@ import ( "github.com/opencontainers/runc/libcontainer/label" ) +// ErrVolumeReadonly is used to signal an error when trying to copy data into +// a volume mount that is not writable. +var ErrVolumeReadonly = errors.New("mounted volume is marked read-only") + type mountPoint struct { Name string Destination string @@ -47,6 +52,16 @@ func (m *mountPoint) Setup() (string, error) { return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined") } +// hasResource checks whether the given absolute path for a container is in +// this mount point. If the relative path starts with `../` then the resource +// is outside of this mount point, but we can't simply check for this prefix +// because it misses `..` which is also outside of the mount, so check both. +func (m *mountPoint) hasResource(absolutePath string) bool { + relPath, err := filepath.Rel(m.Destination, absolutePath) + + return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator)) +} + func (m *mountPoint) Path() string { if m.Volume != nil { return m.Volume.Path() diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index e82283609a..6ed93afb24 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -68,6 +68,23 @@ Running `docker rmi` emits an **untag** event when removing an image name. The ### What's new +`GET /containers/(id)/archive` + +**New!** +Get an archive of filesystem content from a container. + +`PUT /containers/(id)/archive` + +**New!** +Upload an archive of content to be extracted to an +existing directory inside a container's filesystem. + +`POST /containers/(id)/copy` + +**Deprecated!** +This copy endpoint has been deprecated in favor of the above `archive` endpoint +which can be used to download files and directories from a container. + **New!** The `hostConfig` option now accepts the field `GroupAdd`, which specifies a list of additional groups that the container process will run as. diff --git a/docs/reference/api/docker_remote_api_v1.20.md b/docs/reference/api/docker_remote_api_v1.20.md index c3701f8973..f5eff4aa33 100644 --- a/docs/reference/api/docker_remote_api_v1.20.md +++ b/docs/reference/api/docker_remote_api_v1.20.md @@ -1039,6 +1039,8 @@ Status Codes: Copy files or folders of container `id` +**Deprecated** in favor of the `archive` endpoint below. + **Example request**: POST /containers/4fa6e0f0c678/copy HTTP/1.1 @@ -1061,6 +1063,120 @@ Status Codes: - **404** – no such container - **500** – server error +### Retrieving information about files and folders in a container + +`HEAD /containers/(id)/archive` + +See the description of the `X-Docker-Container-Path-Stat` header in the +folowing section. + +### Get an archive of a filesystem resource in a container + +`GET /containers/(id)/archive` + +Get an tar archive of a resource in the filesystem of container `id`. + +Query Parameters: + +- **path** - resource in the container's filesystem to archive. Required. + + If not an absolute path, it is relative to the container's root directory. + The resource specified by **path** must exist. To assert that the resource + is expected to be a directory, **path** should end in `/` or `/.` + (assuming a path separator of `/`). If **path** ends in `/.` then this + indicates that only the contents of the **path** directory should be + copied. A symlink is always resolved to its target. + + **Note**: It is not possible to copy certain system files such as resources + under `/proc`, `/sys`, `/dev`, and mounts created by the user in the + container. + +**Example request**: + + GET /containers/8cce319429b2/archive?path=/root HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + X-Docker-Container-Path-Stat: eyJuYW1lIjoicm9vdCIsInBhdGgiOiIvcm9vdCIsInNpemUiOjQwOTYsIm1vZGUiOjIxNDc0ODQwOTYsIm10aW1lIjoiMjAxNC0wMi0yN1QyMDo1MToyM1oifQ== + + {{ TAR STREAM }} + +On success, a response header `X-Docker-Container-Path-Stat` will be set to a +base64-encoded JSON object containing some filesystem header information about +the archived resource. The above example value would decode to the following +JSON object (whitespace added for readability): + + { + "name": "root", + "path": "/root", + "size": 4096, + "mode": 2147484096, + "mtime": "2014-02-27T20:51:23Z" + } + +A `HEAD` request can also be made to this endpoint if only this information is +desired. + +Status Codes: + +- **200** - success, returns archive of copied resource +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** was asserted to be a directory but exists as a + file) +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** does not exist) +- **500** - server error + +### Extract an archive of files or folders to a directory in a container + +`PUT /containers/(id)/archive` + +Upload a tar archive to be extracted to a path in the filesystem of container +`id`. + +Query Parameters: + +- **path** - path to a directory in the container + to extract the archive's contents into. Required. + + If not an absolute path, it is relative to the container's root directory. + The **path** resource must exist. +- **noOverwriteDirNonDir** - If "1", "true", or "True" then it will be an error + if unpacking the given content would cause an existing directory to be + replaced with a non-directory and vice versa. + +**Example request**: + + PUT /containers/8cce319429b2/archive?path=/vol1 HTTP/1.1 + Content-Type: application/x-tar + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – the content was extracted successfully +- **400** - client error, bad parameter, details in JSON response body, one of: + - must specify path parameter (**path** cannot be empty) + - not a directory (**path** should be a directory but exists as a file) + - unable to overwrite existing directory with non-directory + (if **noOverwriteDirNonDir**) + - unable to overwrite existing non-directory with directory + (if **noOverwriteDirNonDir**) +- **403** - client error, permission denied, the volume + or container rootfs is marked as read-only. +- **404** - client error, resource not found, one of: + – no such container (container `id` does not exist) + - no such file or directory (**path** resource does not exist) +- **500** – server error + ## 2.2 Images ### List Images diff --git a/docs/reference/commandline/cp.md b/docs/reference/commandline/cp.md index 359af40b0b..45c4b53955 100644 --- a/docs/reference/commandline/cp.md +++ b/docs/reference/commandline/cp.md @@ -11,12 +11,81 @@ weight=1 # cp - Usage: docker cp CONTAINER:PATH HOSTDIR|- +Copy files/folders between a container and the local filesystem. - Copy files/folders from the PATH to the HOSTDIR. + Usage: docker cp [options] CONTAINER:PATH LOCALPATH|- + docker cp [options] LOCALPATH|- CONTAINER:PATH -Copy files or folders from a container's filesystem to the directory on the -host. Use '-' to write the data as a tar file to `STDOUT`. `CONTAINER:PATH` is -relative to the root of the container's filesystem. + --help Print usage statement +In the first synopsis form, the `docker cp` utility copies the contents of +`PATH` from the filesystem of `CONTAINER` to the `LOCALPATH` (or stream as +a tar archive to `STDOUT` if `-` is specified). +In the second synopsis form, the contents of `LOCALPATH` (or a tar archive +streamed from `STDIN` if `-` is specified) are copied from the local machine to +`PATH` in the filesystem of `CONTAINER`. + +You can copy to or from either a running or stopped container. The `PATH` can +be a file or directory. The `docker cp` command assumes all `CONTAINER:PATH` +values are relative to the `/` (root) directory of the container. This means +supplying the initial forward slash is optional; The command sees +`compassionate_darwin:/tmp/foo/myfile.txt` and +`compassionate_darwin:tmp/foo/myfile.txt` as identical. If a `LOCALPATH` value +is not absolute, is it considered relative to the current working directory. + +Behavior is similar to the common Unix utility `cp -a` in that directories are +copied recursively with permissions preserved if possible. Ownership is set to +the user and primary group on the receiving end of the transfer. For example, +files copied to a container will be created with `UID:GID` of the root user. +Files copied to the local machine will be created with the `UID:GID` of the +user which invoked the `docker cp` command. + +Assuming a path separator of `/`, a first argument of `SRC_PATH` and second +argument of `DST_PATH`, the behavior is as follows: + +- `SRC_PATH` specifies a file + - `DST_PATH` does not exist + - the file is saved to a file created at `DST_PATH` + - `DST_PATH` does not exist and ends with `/` + - Error condition: the destination directory must exist. + - `DST_PATH` exists and is a file + - the destination is overwritten with the contents of the source file + - `DST_PATH` exists and is a directory + - the file is copied into this directory using the basename from + `SRC_PATH` +- `SRC_PATH` specifies a directory + - `DST_PATH` does not exist + - `DST_PATH` is created as a directory and the *contents* of the source + directory are copied into this directory + - `DST_PATH` exists and is a file + - Error condition: cannot copy a directory to a file + - `DST_PATH` exists and is a directory + - `SRC_PATH` does not end with `/.` + - the source directory is copied into this directory + - `SRC_PAPTH` does end with `/.` + - the *content* of the source directory is copied into this + directory + +The command requires `SRC_PATH` and `DST_PATH` to exist according to the above +rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not +the target, is copied. + +A colon (`:`) is used as a delimiter between `CONTAINER` and `PATH`, but `:` +could also be in a valid `LOCALPATH`, like `file:name.txt`. This ambiguity is +resolved by requiring a `LOCALPATH` with a `:` to be made explicit with a +relative or absolute path, for example: + + `/path/to/file:name.txt` or `./file:name.txt` + +It is not possible to copy certain system files such as resources under +`/proc`, `/sys`, `/dev`, and mounts created by the user in the container. + +Using `-` as the first argument in place of a `LOCALPATH` will stream the +contents of `STDIN` as a tar archive which will be extracted to the `PATH` in +the filesystem of the destination container. In this case, `PATH` must specify +a directory. + +Using `-` as the second argument in place of a `LOCALPATH` will stream the +contents of the resource from the source container as a tar archive to +`STDOUT`. diff --git a/integration-cli/docker_cli_cp_from_container_test.go b/integration-cli/docker_cli_cp_from_container_test.go new file mode 100644 index 0000000000..14536ce859 --- /dev/null +++ b/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) + } +} diff --git a/integration-cli/docker_cli_cp_test.go b/integration-cli/docker_cli_cp_test.go index 45340cf6c1..03c0a4a63e 100644 --- a/integration-cli/docker_cli_cp_test.go +++ b/integration-cli/docker_cli_cp_test.go @@ -23,6 +23,18 @@ const ( cpHostContents = "hello, i am the host" ) +// Ensure that an all-local path case returns an error. +func (s *DockerSuite) TestCpLocalOnly(c *check.C) { + err := runDockerCp(c, "foo", "bar") + if err == nil { + c.Fatal("expected failure, got success") + } + + if !strings.Contains(err.Error(), "must specify at least one container source") { + c.Fatalf("unexpected output: %s", err.Error()) + } +} + // Test for #5656 // Check that garbage paths don't escape the container's rootfs func (s *DockerSuite) TestCpGarbagePath(c *check.C) { diff --git a/integration-cli/docker_cli_cp_to_container_test.go b/integration-cli/docker_cli_cp_to_container_test.go new file mode 100644 index 0000000000..4179553d18 --- /dev/null +++ b/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) + } +} diff --git a/integration-cli/docker_cli_cp_utils.go b/integration-cli/docker_cli_cp_utils.go new file mode 100644 index 0000000000..96b7b466a4 --- /dev/null +++ b/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"} +} diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index 0873e64ab4..6742ea453e 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -3,7 +3,9 @@ package main import ( "bufio" "fmt" + "io/ioutil" "net/http" + "os" "os/exec" "regexp" "strconv" @@ -519,6 +521,7 @@ func (s *DockerSuite) TestEventsCommit(c *check.C) { func (s *DockerSuite) TestEventsCopy(c *check.C) { since := daemonTime(c).Unix() + // Build a test image. id, err := buildImage("cpimg", ` FROM busybox RUN echo HI > /tmp/file`, true) @@ -526,12 +529,31 @@ func (s *DockerSuite) TestEventsCopy(c *check.C) { c.Fatalf("Couldn't create image: %q", err) } - dockerCmd(c, "run", "--name=cptest", id, "true") - dockerCmd(c, "cp", "cptest:/tmp/file", "-") + // Create an empty test file. + tempFile, err := ioutil.TempFile("", "test-events-copy-") + if err != nil { + c.Fatal(err) + } + defer os.Remove(tempFile.Name()) + + if err := tempFile.Close(); err != nil { + c.Fatal(err) + } + + dockerCmd(c, "create", "--name=cptest", id) + + dockerCmd(c, "cp", "cptest:/tmp/file", tempFile.Name()) out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) - if !strings.Contains(out, " copy\n") { - c.Fatalf("Missing 'copy' log event\n%s", out) + if !strings.Contains(out, " archive-path\n") { + c.Fatalf("Missing 'archive-path' log event\n%s", out) + } + + dockerCmd(c, "cp", tempFile.Name(), "cptest:/tmp/filecopy") + + out, _ = dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since))) + if !strings.Contains(out, " extract-to-dir\n") { + c.Fatalf("Missing 'extract-to-dir' log event\n%s", out) } } diff --git a/man/docker-cp.1.md b/man/docker-cp.1.md index 3cd203a83d..fe1cd9c9cc 100644 --- a/man/docker-cp.1.md +++ b/man/docker-cp.1.md @@ -2,69 +2,150 @@ % Docker Community % JUNE 2014 # NAME -docker-cp - Copy files or folders from a container's PATH to a HOSTDIR -or to STDOUT. +docker-cp - Copy files/folders between a container and the local filesystem. # SYNOPSIS **docker cp** [**--help**] -CONTAINER:PATH HOSTDIR|- +CONTAINER:PATH LOCALPATH|- +LOCALPATH|- CONTAINER:PATH # DESCRIPTION -Copy files or folders from a `CONTAINER:PATH` to the `HOSTDIR` or to `STDOUT`. -The `CONTAINER:PATH` is relative to the root of the container's filesystem. You -can copy from either a running or stopped container. +In the first synopsis form, the `docker cp` utility copies the contents of +`PATH` from the filesystem of `CONTAINER` to the `LOCALPATH` (or stream as +a tar archive to `STDOUT` if `-` is specified). -The `PATH` can be a file or directory. The `docker cp` command assumes all -`PATH` values start at the `/` (root) directory. This means supplying the -initial forward slash is optional; The command sees +In the second synopsis form, the contents of `LOCALPATH` (or a tar archive +streamed from `STDIN` if `-` is specified) are copied from the local machine to +`PATH` in the filesystem of `CONTAINER`. + +You can copy to or from either a running or stopped container. The `PATH` can +be a file or directory. The `docker cp` command assumes all `CONTAINER:PATH` +values are relative to the `/` (root) directory of the container. This means +supplying the initial forward slash is optional; The command sees `compassionate_darwin:/tmp/foo/myfile.txt` and -`compassionate_darwin:tmp/foo/myfile.txt` as identical. +`compassionate_darwin:tmp/foo/myfile.txt` as identical. If a `LOCALPATH` value +is not absolute, is it considered relative to the current working directory. -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: +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. - $ docker cp compassionate_darwin:tmp/foo /tmp +Assuming a path separator of `/`, a first argument of `SRC_PATH` and second +argument of `DST_PATH`, the behavior is as follows: -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: +- `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 - $ docker cp compassionate_darwin:tmp/foo tmp +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. -Docker creates a `~/tmp/foo` subdirectory. +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: -When copying files to an existing `HOSTDIR`, the `cp` command adds the new files to -the directory. For example, this command: + `/path/to/file:name.txt` or `./file:name.txt` - $ docker cp sharp_ptolemy:/tmp/foo/myfile.txt /tmp +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. -Creates a `/tmp/foo` directory on the host containing the `myfile.txt` file. If -you repeat the command but change the filename: +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. - $ docker cp sharp_ptolemy:/tmp/foo/secondfile.txt /tmp - -Your host's `/tmp/foo` directory will contain both files: - - $ ls /tmp/foo - myfile.txt secondfile.txt - -Finally, use '-' to write the data as a `tar` file to STDOUT. +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 -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 c071f3c3ee81:setup.sh . +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. + +If you want to copy the `/tmp/foo` directory from a container to the +existing `/tmp` directory on your host. If you run `docker cp` in your `~` +(home) directory on the local host: + + $ docker cp compassionate_darwin:tmp/foo /tmp + +Docker creates a `/tmp/foo` directory on your host. Alternatively, you can omit +the leading slash in the command. If you execute this command from your home +directory: + + $ docker cp compassionate_darwin:tmp/foo tmp + +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 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 /test + +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`. + +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. + +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: + + $ docker cp config.yml myappcontainer:/etc/my-app.d + +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: + + $ docker cp /config/. myappcontainer:/etc/my-app.d + +The above command will copy the contents of the local `/config` directory into +the directory `/etc/my-app.d` in the container. # HISTORY April 2014, Originally compiled by William Henry (whenry at redhat dot com) based on docker.com source material and internal work. June 2014, updated by Sven Dowideit +May 2015, updated by Josh Hawn diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 9cd245cf91..04e40a94fd 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -25,15 +25,23 @@ import ( ) type ( - Archive io.ReadCloser - ArchiveReader io.Reader - Compression int - TarOptions struct { - IncludeFiles []string - ExcludePatterns []string - Compression Compression - NoLchown bool - Name string + Archive io.ReadCloser + ArchiveReader io.Reader + Compression int + TarChownOptions struct { + UID, GID int + } + TarOptions struct { + IncludeFiles []string + ExcludePatterns []string + Compression Compression + NoLchown bool + ChownOpts *TarChownOptions + Name string + IncludeSourceDir bool + // When unpacking, specifies whether overwriting a directory with a + // non-directory is allowed and vice versa. + NoOverwriteDirNonDir bool } // Archiver allows the reuse of most utility functions of this package @@ -262,7 +270,7 @@ func (ta *tarAppender) addTarFile(path, name string) error { return nil } -func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool) error { +func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool, chownOpts *TarChownOptions) error { // hdr.Mode is in linux format, which we can use for sycalls, // but for os.Foo() calls we need the mode converted to os.FileMode, // so use hdrInfo.Mode() (they differ for e.g. setuid bits) @@ -328,9 +336,12 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, L return fmt.Errorf("Unhandled tar header type %d\n", hdr.Typeflag) } - // Lchown is not supported on Windows - if runtime.GOOS != "windows" { - if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil && Lchown { + // Lchown is not supported on Windows. + if Lchown && runtime.GOOS != "windows" { + if chownOpts == nil { + chownOpts = &TarChownOptions{UID: hdr.Uid, GID: hdr.Gid} + } + if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil { return err } } @@ -396,6 +407,20 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) Buffer: pools.BufioWriter32KPool.Get(nil), SeenFiles: make(map[uint64]string), } + + defer func() { + // Make sure to check the error on Close. + if err := ta.TarWriter.Close(); err != nil { + logrus.Debugf("Can't close tar writer: %s", err) + } + if err := compressWriter.Close(); err != nil { + logrus.Debugf("Can't close compress writer: %s", err) + } + if err := pipeWriter.Close(); err != nil { + logrus.Debugf("Can't close pipe writer: %s", err) + } + }() + // this buffer is needed for the duration of this piped stream defer pools.BufioWriter32KPool.Put(ta.Buffer) @@ -404,7 +429,26 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) // mutating the filesystem and we can see transient errors // from this - if options.IncludeFiles == nil { + stat, err := os.Lstat(srcPath) + if err != nil { + return + } + + if !stat.IsDir() { + // We can't later join a non-dir with any includes because the + // 'walk' will error if "file/." is stat-ed and "file" is not a + // directory. So, we must split the source path and use the + // basename as the include. + if len(options.IncludeFiles) > 0 { + logrus.Warn("Tar: Can't archive a file with includes") + } + + dir, base := SplitPathDirEntry(srcPath) + srcPath = dir + options.IncludeFiles = []string{base} + } + + if len(options.IncludeFiles) == 0 { options.IncludeFiles = []string{"."} } @@ -412,19 +456,26 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) var renamedRelFilePath string // For when tar.Options.Name is set for _, include := range options.IncludeFiles { - filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { + // We can't use filepath.Join(srcPath, include) because this will + // clean away a trailing "." or "/" which may be important. + walkRoot := strings.Join([]string{srcPath, include}, string(filepath.Separator)) + filepath.Walk(walkRoot, func(filePath string, f os.FileInfo, err error) error { if err != nil { logrus.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err) return nil } relFilePath, err := filepath.Rel(srcPath, filePath) - if err != nil || (relFilePath == "." && f.IsDir()) { + if err != nil || (!options.IncludeSourceDir && relFilePath == "." && f.IsDir()) { // Error getting relative path OR we are looking - // at the root path. Skip in both situations. + // at the source directory path. Skip in both situations. return nil } + if options.IncludeSourceDir && include == "." && relFilePath != "." { + relFilePath = strings.Join([]string{".", relFilePath}, string(filepath.Separator)) + } + skip := false // If "include" is an exact match for the current file @@ -468,17 +519,6 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) return nil }) } - - // Make sure to check the error on Close. - if err := ta.TarWriter.Close(); err != nil { - logrus.Debugf("Can't close tar writer: %s", err) - } - if err := compressWriter.Close(); err != nil { - logrus.Debugf("Can't close compress writer: %s", err) - } - if err := pipeWriter.Close(); err != nil { - logrus.Debugf("Can't close pipe writer: %s", err) - } }() return pipeReader, nil @@ -543,9 +583,22 @@ loop: // the layer is also a directory. Then we want to merge them (i.e. // just apply the metadata from the layer). if fi, err := os.Lstat(path); err == nil { + if options.NoOverwriteDirNonDir && fi.IsDir() && hdr.Typeflag != tar.TypeDir { + // If NoOverwriteDirNonDir is true then we cannot replace + // an existing directory with a non-directory from the archive. + return fmt.Errorf("cannot overwrite directory %q with non-directory %q", path, dest) + } + + if options.NoOverwriteDirNonDir && !fi.IsDir() && hdr.Typeflag == tar.TypeDir { + // If NoOverwriteDirNonDir is true then we cannot replace + // an existing non-directory with a directory from the archive. + return fmt.Errorf("cannot overwrite non-directory %q with directory %q", path, dest) + } + if fi.IsDir() && hdr.Name == "." { continue } + if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { if err := os.RemoveAll(path); err != nil { return err @@ -553,7 +606,8 @@ loop: } } trBuf.Reset(tr) - if err := createTarFile(path, dest, hdr, trBuf, !options.NoLchown); err != nil { + + if err := createTarFile(path, dest, hdr, trBuf, !options.NoLchown, options.ChownOpts); err != nil { return err } diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go index 0bf878e0ae..b93c76cdad 100644 --- a/pkg/archive/archive_test.go +++ b/pkg/archive/archive_test.go @@ -719,7 +719,7 @@ func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) { t.Fatal(err) } defer os.RemoveAll(tmpDir) - err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true) + err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true, nil) if err != nil { t.Fatal(err) } diff --git a/pkg/archive/copy.go b/pkg/archive/copy.go new file mode 100644 index 0000000000..fee4a022b0 --- /dev/null +++ b/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) +} diff --git a/pkg/archive/copy_test.go b/pkg/archive/copy_test.go new file mode 100644 index 0000000000..d0cfa18bd6 --- /dev/null +++ b/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) + } +} diff --git a/pkg/archive/diff.go b/pkg/archive/diff.go index af8d3ee137..aed8542d76 100644 --- a/pkg/archive/diff.go +++ b/pkg/archive/diff.go @@ -93,7 +93,7 @@ func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) { } defer os.RemoveAll(aufsTempdir) } - if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true); err != nil { + if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true, nil); err != nil { return 0, err } } @@ -150,7 +150,7 @@ func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) { srcData = tmpFile } - if err := createTarFile(path, dest, srcHdr, srcData, true); err != nil { + if err := createTarFile(path, dest, srcHdr, srcData, true, nil); err != nil { return 0, err }