Merge pull request #13171 from jlhawn/archive_copy
docker cp to and from containers
This commit is contained in:
commit
c986f85f73
21 changed files with 3589 additions and 148 deletions
303
api/client/cp.go
303
api/client/cp.go
|
@ -1,8 +1,14 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
|
@ -10,48 +16,289 @@ import (
|
|||
flag "github.com/docker/docker/pkg/mflag"
|
||||
)
|
||||
|
||||
// CmdCp copies files/folders from a path on the container to a directory on the host running the command.
|
||||
//
|
||||
// If HOSTDIR is '-', the data is written as a tar file to STDOUT.
|
||||
//
|
||||
// Usage: docker cp CONTAINER:PATH HOSTDIR
|
||||
func (cli *DockerCli) CmdCp(args ...string) error {
|
||||
cmd := cli.Subcmd("cp", []string{"CONTAINER:PATH HOSTDIR|-"}, "Copy files/folders from a container's PATH to a HOSTDIR on the host\nrunning the command. Use '-' to write the data as a tar file to STDOUT.", true)
|
||||
cmd.Require(flag.Exact, 2)
|
||||
type copyDirection int
|
||||
|
||||
const (
|
||||
fromContainer copyDirection = (1 << iota)
|
||||
toContainer
|
||||
acrossContainers = fromContainer | toContainer
|
||||
)
|
||||
|
||||
// CmdCp copies files/folders to or from a path in a container.
|
||||
//
|
||||
// When copying from a container, if LOCALPATH is '-' the data is written as a
|
||||
// tar archive file to STDOUT.
|
||||
//
|
||||
// When copying to a container, if LOCALPATH is '-' the data is read as a tar
|
||||
// archive file from STDIN, and the destination CONTAINER:PATH, must specify
|
||||
// a directory.
|
||||
//
|
||||
// Usage:
|
||||
// docker cp CONTAINER:PATH LOCALPATH|-
|
||||
// docker cp LOCALPATH|- CONTAINER:PATH
|
||||
func (cli *DockerCli) CmdCp(args ...string) error {
|
||||
cmd := cli.Subcmd(
|
||||
"cp",
|
||||
[]string{"CONTAINER:PATH LOCALPATH|-", "LOCALPATH|- CONTAINER:PATH"},
|
||||
strings.Join([]string{
|
||||
"Copy files/folders between a container and your host.\n",
|
||||
"Use '-' as the source to read a tar archive from stdin\n",
|
||||
"and extract it to a directory destination in a container.\n",
|
||||
"Use '-' as the destination to stream a tar archive of a\n",
|
||||
"container source to stdout.",
|
||||
}, ""),
|
||||
true,
|
||||
)
|
||||
|
||||
cmd.Require(flag.Exact, 2)
|
||||
cmd.ParseFlags(args, true)
|
||||
|
||||
// deal with path name with `:`
|
||||
info := strings.SplitN(cmd.Arg(0), ":", 2)
|
||||
|
||||
if len(info) != 2 {
|
||||
return fmt.Errorf("Error: Path not specified")
|
||||
if cmd.Arg(0) == "" {
|
||||
return fmt.Errorf("source can not be empty")
|
||||
}
|
||||
if cmd.Arg(1) == "" {
|
||||
return fmt.Errorf("destination can not be empty")
|
||||
}
|
||||
|
||||
cfg := &types.CopyConfig{
|
||||
Resource: info[1],
|
||||
srcContainer, srcPath := splitCpArg(cmd.Arg(0))
|
||||
dstContainer, dstPath := splitCpArg(cmd.Arg(1))
|
||||
|
||||
var direction copyDirection
|
||||
if srcContainer != "" {
|
||||
direction |= fromContainer
|
||||
}
|
||||
serverResp, err := cli.call("POST", "/containers/"+info[0]+"/copy", cfg, nil)
|
||||
if serverResp.body != nil {
|
||||
defer serverResp.body.Close()
|
||||
if dstContainer != "" {
|
||||
direction |= toContainer
|
||||
}
|
||||
if serverResp.statusCode == 404 {
|
||||
return fmt.Errorf("No such container: %v", info[0])
|
||||
|
||||
switch direction {
|
||||
case fromContainer:
|
||||
return cli.copyFromContainer(srcContainer, srcPath, dstPath)
|
||||
case toContainer:
|
||||
return cli.copyToContainer(srcPath, dstContainer, dstPath)
|
||||
case acrossContainers:
|
||||
// Copying between containers isn't supported.
|
||||
return fmt.Errorf("copying between containers is not supported")
|
||||
default:
|
||||
// User didn't specify any container.
|
||||
return fmt.Errorf("must specify at least one container source")
|
||||
}
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
|
||||
// requiring a LOCALPATH with a `:` to be made explicit with a relative or
|
||||
// absolute path:
|
||||
// `/path/to/file:name.txt` or `./file:name.txt`
|
||||
//
|
||||
// This is apparently how `scp` handles this as well:
|
||||
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
|
||||
//
|
||||
// We can't simply check for a filepath separator because container names may
|
||||
// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
|
||||
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
|
||||
// client, a `:` could be part of an absolute Windows path, in which case it
|
||||
// is immediately proceeded by a backslash.
|
||||
func splitCpArg(arg string) (container, path string) {
|
||||
if filepath.IsAbs(arg) {
|
||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
parts := strings.SplitN(arg, ":", 2)
|
||||
|
||||
if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
|
||||
// Either there's no `:` in the arg
|
||||
// OR it's an explicit local relative path like `./file:name.txt`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) {
|
||||
var stat types.ContainerPathStat
|
||||
|
||||
query := make(url.Values, 1)
|
||||
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
|
||||
|
||||
urlStr := fmt.Sprintf("/containers/%s/archive?%s", containerName, query.Encode())
|
||||
|
||||
response, err := cli.call("HEAD", urlStr, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return stat, err
|
||||
}
|
||||
defer response.body.Close()
|
||||
|
||||
if response.statusCode != http.StatusOK {
|
||||
return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
|
||||
}
|
||||
|
||||
hostPath := cmd.Arg(1)
|
||||
if serverResp.statusCode == 200 {
|
||||
if hostPath == "-" {
|
||||
_, err = io.Copy(cli.out, serverResp.body)
|
||||
} else {
|
||||
err = archive.Untar(serverResp.body, hostPath, &archive.TarOptions{NoLchown: true})
|
||||
}
|
||||
return getContainerPathStatFromHeader(response.header)
|
||||
}
|
||||
|
||||
func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
|
||||
var stat types.ContainerPathStat
|
||||
|
||||
encodedStat := header.Get("X-Docker-Container-Path-Stat")
|
||||
statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
|
||||
|
||||
err := json.NewDecoder(statDecoder).Decode(&stat)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to decode container path stat header: %s", err)
|
||||
}
|
||||
|
||||
return stat, err
|
||||
}
|
||||
|
||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (err error) {
|
||||
if dstPath != "-" {
|
||||
// Get an absolute destination path.
|
||||
dstPath, err = resolveLocalPath(dstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
query := make(url.Values, 1)
|
||||
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
|
||||
|
||||
urlStr := fmt.Sprintf("/containers/%s/archive?%s", srcContainer, query.Encode())
|
||||
|
||||
response, err := cli.call("GET", urlStr, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.body.Close()
|
||||
|
||||
if response.statusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
|
||||
}
|
||||
|
||||
if dstPath == "-" {
|
||||
// Send the response to STDOUT.
|
||||
_, err = io.Copy(os.Stdout, response.body)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and the destination. The response headers include
|
||||
// stat info about the source that we can use in deciding exactly how to
|
||||
// copy it locally. Along with the stat info about the local destination,
|
||||
// we have everything we need to handle the multiple possibilities there
|
||||
// can be when copying a file/dir from one location to another file/dir.
|
||||
stat, err := getContainerPathStatFromHeader(response.header)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get resource stat from response: %s", err)
|
||||
}
|
||||
|
||||
// Prepare source copy info.
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
}
|
||||
|
||||
// See comments in the implementation of `archive.CopyTo` for exactly what
|
||||
// goes into deciding how and whether the source archive needs to be
|
||||
// altered for the correct copy behavior.
|
||||
return archive.CopyTo(response.body, srcInfo, dstPath)
|
||||
}
|
||||
|
||||
func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (err error) {
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
srcPath, err = resolveLocalPath(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
|
||||
// Prepare destination copy info by stat-ing the container path.
|
||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||
dstStat, err := cli.statContainerPath(dstContainer, dstPath)
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
// path exists, in which case the copy may still succeed. If there is any
|
||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||
// or vice versia) the extraction will fail. If the destination simply did
|
||||
// not exist, but the parent directory does, the extraction will still
|
||||
// succeed.
|
||||
if err == nil {
|
||||
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
||||
}
|
||||
|
||||
var content io.Reader
|
||||
if srcPath == "-" {
|
||||
// Use STDIN.
|
||||
content = os.Stdin
|
||||
if !dstInfo.IsDir {
|
||||
return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
|
||||
}
|
||||
} else {
|
||||
srcArchive, err := archive.TarResource(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcArchive.Close()
|
||||
|
||||
// With the stat info about the local source as well as the
|
||||
// destination, we have enough information to know whether we need to
|
||||
// alter the archive that we upload so that when the server extracts
|
||||
// it to the specified directory in the container we get the disired
|
||||
// copy behavior.
|
||||
|
||||
// Prepare source copy info.
|
||||
srcInfo, err := archive.CopyInfoStatPath(srcPath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// See comments in the implementation of `archive.PrepareArchiveCopy`
|
||||
// for exactly what goes into deciding how and whether the source
|
||||
// archive needs to be altered for the correct copy behavior when it is
|
||||
// extracted. This function also infers from the source and destination
|
||||
// info which directory to extract to, which may be the parent of the
|
||||
// destination that the user specified.
|
||||
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer preparedArchive.Close()
|
||||
|
||||
dstPath = dstDir
|
||||
content = preparedArchive
|
||||
}
|
||||
|
||||
query := make(url.Values, 2)
|
||||
query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
|
||||
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
|
||||
query.Set("noOverwriteDirNonDir", "true")
|
||||
|
||||
urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode())
|
||||
|
||||
response, err := cli.stream("PUT", urlStr, &streamOpts{in: content})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.body.Close()
|
||||
|
||||
if response.statusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
297
daemon/archive.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,12 +11,81 @@ weight=1
|
|||
|
||||
# cp
|
||||
|
||||
Usage: docker cp CONTAINER:PATH HOSTDIR|-
|
||||
Copy files/folders between a container and the local filesystem.
|
||||
|
||||
Copy files/folders from the PATH to the HOSTDIR.
|
||||
Usage: docker cp [options] CONTAINER:PATH LOCALPATH|-
|
||||
docker cp [options] LOCALPATH|- CONTAINER:PATH
|
||||
|
||||
Copy files or folders from a container's filesystem to the directory on the
|
||||
host. Use '-' to write the data as a tar file to `STDOUT`. `CONTAINER:PATH` is
|
||||
relative to the root of the container's filesystem.
|
||||
--help Print usage statement
|
||||
|
||||
In the first synopsis form, the `docker cp` utility copies the contents of
|
||||
`PATH` from the filesystem of `CONTAINER` to the `LOCALPATH` (or stream as
|
||||
a tar archive to `STDOUT` if `-` is specified).
|
||||
|
||||
In the second synopsis form, the contents of `LOCALPATH` (or a tar archive
|
||||
streamed from `STDIN` if `-` is specified) are copied from the local machine to
|
||||
`PATH` in the filesystem of `CONTAINER`.
|
||||
|
||||
You can copy to or from either a running or stopped container. The `PATH` can
|
||||
be a file or directory. The `docker cp` command assumes all `CONTAINER:PATH`
|
||||
values are relative to the `/` (root) directory of the container. This means
|
||||
supplying the initial forward slash is optional; The command sees
|
||||
`compassionate_darwin:/tmp/foo/myfile.txt` and
|
||||
`compassionate_darwin:tmp/foo/myfile.txt` as identical. If a `LOCALPATH` value
|
||||
is not absolute, is it considered relative to the current working directory.
|
||||
|
||||
Behavior is similar to the common Unix utility `cp -a` in that directories are
|
||||
copied recursively with permissions preserved if possible. Ownership is set to
|
||||
the user and primary group on the receiving end of the transfer. For example,
|
||||
files copied to a container will be created with `UID:GID` of the root user.
|
||||
Files copied to the local machine will be created with the `UID:GID` of the
|
||||
user which invoked the `docker cp` command.
|
||||
|
||||
Assuming a path separator of `/`, a first argument of `SRC_PATH` and second
|
||||
argument of `DST_PATH`, the behavior is as follows:
|
||||
|
||||
- `SRC_PATH` specifies a file
|
||||
- `DST_PATH` does not exist
|
||||
- the file is saved to a file created at `DST_PATH`
|
||||
- `DST_PATH` does not exist and ends with `/`
|
||||
- Error condition: the destination directory must exist.
|
||||
- `DST_PATH` exists and is a file
|
||||
- the destination is overwritten with the contents of the source file
|
||||
- `DST_PATH` exists and is a directory
|
||||
- the file is copied into this directory using the basename from
|
||||
`SRC_PATH`
|
||||
- `SRC_PATH` specifies a directory
|
||||
- `DST_PATH` does not exist
|
||||
- `DST_PATH` is created as a directory and the *contents* of the source
|
||||
directory are copied into this directory
|
||||
- `DST_PATH` exists and is a file
|
||||
- Error condition: cannot copy a directory to a file
|
||||
- `DST_PATH` exists and is a directory
|
||||
- `SRC_PATH` does not end with `/.`
|
||||
- the source directory is copied into this directory
|
||||
- `SRC_PAPTH` does end with `/.`
|
||||
- the *content* of the source directory is copied into this
|
||||
directory
|
||||
|
||||
The command requires `SRC_PATH` and `DST_PATH` to exist according to the above
|
||||
rules. If `SRC_PATH` is local and is a symbolic link, the symbolic link, not
|
||||
the target, is copied.
|
||||
|
||||
A colon (`:`) is used as a delimiter between `CONTAINER` and `PATH`, but `:`
|
||||
could also be in a valid `LOCALPATH`, like `file:name.txt`. This ambiguity is
|
||||
resolved by requiring a `LOCALPATH` with a `:` to be made explicit with a
|
||||
relative or absolute path, for example:
|
||||
|
||||
`/path/to/file:name.txt` or `./file:name.txt`
|
||||
|
||||
It is not possible to copy certain system files such as resources under
|
||||
`/proc`, `/sys`, `/dev`, and mounts created by the user in the container.
|
||||
|
||||
Using `-` as the first argument in place of a `LOCALPATH` will stream the
|
||||
contents of `STDIN` as a tar archive which will be extracted to the `PATH` in
|
||||
the filesystem of the destination container. In this case, `PATH` must specify
|
||||
a directory.
|
||||
|
||||
Using `-` as the second argument in place of a `LOCALPATH` will stream the
|
||||
contents of the resource from the source container as a tar archive to
|
||||
`STDOUT`.
|
||||
|
|
503
integration-cli/docker_cli_cp_from_container_test.go
Normal file
503
integration-cli/docker_cli_cp_from_container_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -23,6 +23,18 @@ const (
|
|||
cpHostContents = "hello, i am the host"
|
||||
)
|
||||
|
||||
// Ensure that an all-local path case returns an error.
|
||||
func (s *DockerSuite) TestCpLocalOnly(c *check.C) {
|
||||
err := runDockerCp(c, "foo", "bar")
|
||||
if err == nil {
|
||||
c.Fatal("expected failure, got success")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "must specify at least one container source") {
|
||||
c.Fatalf("unexpected output: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Test for #5656
|
||||
// Check that garbage paths don't escape the container's rootfs
|
||||
func (s *DockerSuite) TestCpGarbagePath(c *check.C) {
|
||||
|
|
634
integration-cli/docker_cli_cp_to_container_test.go
Normal file
634
integration-cli/docker_cli_cp_to_container_test.go
Normal 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)
|
||||
}
|
||||
}
|
298
integration-cli/docker_cli_cp_utils.go
Normal file
298
integration-cli/docker_cli_cp_utils.go
Normal 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"}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
308
pkg/archive/copy.go
Normal 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
637
pkg/archive/copy_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue