Merge pull request #13171 from jlhawn/archive_copy

docker cp to and from containers
This commit is contained in:
Arnaud Porterie 2015-07-21 16:59:44 -07:00
commit c986f85f73
21 changed files with 3589 additions and 148 deletions

View file

@ -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
}

View file

@ -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,

View file

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

297
daemon/archive.go Normal file
View file

@ -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
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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()

View file

@ -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.

View file

@ -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

View file

@ -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`.

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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"}
}

View file

@ -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)
}
}

View file

@ -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 <SvenDowideit@home.org.au>
May 2015, updated by Josh Hawn <josh.hawn@docker.com>

View file

@ -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
}

View file

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

308
pkg/archive/copy.go Normal file
View file

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

637
pkg/archive/copy_test.go Normal file
View file

@ -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)
}
}

View file

@ -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
}