github.com/ncdc/docker@v0.10.1-0.20160129113957-6c6729ef5b74/api/client/cp.go (about)

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	Cli "github.com/docker/docker/cli"
    11  	"github.com/docker/docker/pkg/archive"
    12  	flag "github.com/docker/docker/pkg/mflag"
    13  	"github.com/docker/docker/pkg/system"
    14  	"github.com/docker/engine-api/types"
    15  )
    16  
    17  type copyDirection int
    18  
    19  const (
    20  	fromContainer copyDirection = (1 << iota)
    21  	toContainer
    22  	acrossContainers = fromContainer | toContainer
    23  )
    24  
    25  type cpConfig struct {
    26  	followLink bool
    27  }
    28  
    29  // CmdCp copies files/folders to or from a path in a container.
    30  //
    31  // When copying from a container, if DEST_PATH is '-' the data is written as a
    32  // tar archive file to STDOUT.
    33  //
    34  // When copying to a container, if SRC_PATH is '-' the data is read as a tar
    35  // archive file from STDIN, and the destination CONTAINER:DEST_PATH, must specify
    36  // a directory.
    37  //
    38  // Usage:
    39  // 	docker cp CONTAINER:SRC_PATH DEST_PATH|-
    40  // 	docker cp SRC_PATH|- CONTAINER:DEST_PATH
    41  func (cli *DockerCli) CmdCp(args ...string) error {
    42  	cmd := Cli.Subcmd(
    43  		"cp",
    44  		[]string{"CONTAINER:SRC_PATH DEST_PATH|-", "SRC_PATH|- CONTAINER:DEST_PATH"},
    45  		strings.Join([]string{
    46  			Cli.DockerCommands["cp"].Description,
    47  			"\nUse '-' as the source to read a tar archive from stdin\n",
    48  			"and extract it to a directory destination in a container.\n",
    49  			"Use '-' as the destination to stream a tar archive of a\n",
    50  			"container source to stdout.",
    51  		}, ""),
    52  		true,
    53  	)
    54  
    55  	followLink := cmd.Bool([]string{"L", "-follow-link"}, false, "Always follow symbol link in SRC_PATH")
    56  
    57  	cmd.Require(flag.Exact, 2)
    58  	cmd.ParseFlags(args, true)
    59  
    60  	if cmd.Arg(0) == "" {
    61  		return fmt.Errorf("source can not be empty")
    62  	}
    63  	if cmd.Arg(1) == "" {
    64  		return fmt.Errorf("destination can not be empty")
    65  	}
    66  
    67  	srcContainer, srcPath := splitCpArg(cmd.Arg(0))
    68  	dstContainer, dstPath := splitCpArg(cmd.Arg(1))
    69  
    70  	var direction copyDirection
    71  	if srcContainer != "" {
    72  		direction |= fromContainer
    73  	}
    74  	if dstContainer != "" {
    75  		direction |= toContainer
    76  	}
    77  
    78  	cpParam := &cpConfig{
    79  		followLink: *followLink,
    80  	}
    81  
    82  	switch direction {
    83  	case fromContainer:
    84  		return cli.copyFromContainer(srcContainer, srcPath, dstPath, cpParam)
    85  	case toContainer:
    86  		return cli.copyToContainer(srcPath, dstContainer, dstPath, cpParam)
    87  	case acrossContainers:
    88  		// Copying between containers isn't supported.
    89  		return fmt.Errorf("copying between containers is not supported")
    90  	default:
    91  		// User didn't specify any container.
    92  		return fmt.Errorf("must specify at least one container source")
    93  	}
    94  }
    95  
    96  // We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
    97  // in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
    98  // requiring a LOCALPATH with a `:` to be made explicit with a relative or
    99  // absolute path:
   100  // 	`/path/to/file:name.txt` or `./file:name.txt`
   101  //
   102  // This is apparently how `scp` handles this as well:
   103  // 	http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
   104  //
   105  // We can't simply check for a filepath separator because container names may
   106  // have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
   107  // so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
   108  // client, a `:` could be part of an absolute Windows path, in which case it
   109  // is immediately proceeded by a backslash.
   110  func splitCpArg(arg string) (container, path string) {
   111  	if system.IsAbs(arg) {
   112  		// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
   113  		return "", arg
   114  	}
   115  
   116  	parts := strings.SplitN(arg, ":", 2)
   117  
   118  	if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
   119  		// Either there's no `:` in the arg
   120  		// OR it's an explicit local relative path like `./file:name.txt`.
   121  		return "", arg
   122  	}
   123  
   124  	return parts[0], parts[1]
   125  }
   126  
   127  func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) {
   128  	return cli.client.ContainerStatPath(containerName, path)
   129  }
   130  
   131  func resolveLocalPath(localPath string) (absPath string, err error) {
   132  	if absPath, err = filepath.Abs(localPath); err != nil {
   133  		return
   134  	}
   135  
   136  	return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
   137  }
   138  
   139  func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
   140  	if dstPath != "-" {
   141  		// Get an absolute destination path.
   142  		dstPath, err = resolveLocalPath(dstPath)
   143  		if err != nil {
   144  			return err
   145  		}
   146  	}
   147  
   148  	// if client requests to follow symbol link, then must decide target file to be copied
   149  	var rebaseName string
   150  	if cpParam.followLink {
   151  		srcStat, err := cli.statContainerPath(srcContainer, srcPath)
   152  
   153  		// If the destination is a symbolic link, we should follow it.
   154  		if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
   155  			linkTarget := srcStat.LinkTarget
   156  			if !system.IsAbs(linkTarget) {
   157  				// Join with the parent directory.
   158  				srcParent, _ := archive.SplitPathDirEntry(srcPath)
   159  				linkTarget = filepath.Join(srcParent, linkTarget)
   160  			}
   161  
   162  			linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
   163  			srcPath = linkTarget
   164  		}
   165  
   166  	}
   167  
   168  	content, stat, err := cli.client.CopyFromContainer(srcContainer, srcPath)
   169  	if err != nil {
   170  		return err
   171  	}
   172  	defer content.Close()
   173  
   174  	if dstPath == "-" {
   175  		// Send the response to STDOUT.
   176  		_, err = io.Copy(os.Stdout, content)
   177  
   178  		return err
   179  	}
   180  
   181  	// Prepare source copy info.
   182  	srcInfo := archive.CopyInfo{
   183  		Path:       srcPath,
   184  		Exists:     true,
   185  		IsDir:      stat.Mode.IsDir(),
   186  		RebaseName: rebaseName,
   187  	}
   188  
   189  	preArchive := content
   190  	if len(srcInfo.RebaseName) != 0 {
   191  		_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
   192  		preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
   193  	}
   194  	// See comments in the implementation of `archive.CopyTo` for exactly what
   195  	// goes into deciding how and whether the source archive needs to be
   196  	// altered for the correct copy behavior.
   197  	return archive.CopyTo(preArchive, srcInfo, dstPath)
   198  }
   199  
   200  func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) {
   201  	if srcPath != "-" {
   202  		// Get an absolute source path.
   203  		srcPath, err = resolveLocalPath(srcPath)
   204  		if err != nil {
   205  			return err
   206  		}
   207  	}
   208  
   209  	// In order to get the copy behavior right, we need to know information
   210  	// about both the source and destination. The API is a simple tar
   211  	// archive/extract API but we can use the stat info header about the
   212  	// destination to be more informed about exactly what the destination is.
   213  
   214  	// Prepare destination copy info by stat-ing the container path.
   215  	dstInfo := archive.CopyInfo{Path: dstPath}
   216  	dstStat, err := cli.statContainerPath(dstContainer, dstPath)
   217  
   218  	// If the destination is a symbolic link, we should evaluate it.
   219  	if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
   220  		linkTarget := dstStat.LinkTarget
   221  		if !system.IsAbs(linkTarget) {
   222  			// Join with the parent directory.
   223  			dstParent, _ := archive.SplitPathDirEntry(dstPath)
   224  			linkTarget = filepath.Join(dstParent, linkTarget)
   225  		}
   226  
   227  		dstInfo.Path = linkTarget
   228  		dstStat, err = cli.statContainerPath(dstContainer, linkTarget)
   229  	}
   230  
   231  	// Ignore any error and assume that the parent directory of the destination
   232  	// path exists, in which case the copy may still succeed. If there is any
   233  	// type of conflict (e.g., non-directory overwriting an existing directory
   234  	// or vice versa) the extraction will fail. If the destination simply did
   235  	// not exist, but the parent directory does, the extraction will still
   236  	// succeed.
   237  	if err == nil {
   238  		dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
   239  	}
   240  
   241  	var (
   242  		content         io.Reader
   243  		resolvedDstPath string
   244  	)
   245  
   246  	if srcPath == "-" {
   247  		// Use STDIN.
   248  		content = os.Stdin
   249  		resolvedDstPath = dstInfo.Path
   250  		if !dstInfo.IsDir {
   251  			return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
   252  		}
   253  	} else {
   254  		// Prepare source copy info.
   255  		srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
   256  		if err != nil {
   257  			return err
   258  		}
   259  
   260  		srcArchive, err := archive.TarResource(srcInfo)
   261  		if err != nil {
   262  			return err
   263  		}
   264  		defer srcArchive.Close()
   265  
   266  		// With the stat info about the local source as well as the
   267  		// destination, we have enough information to know whether we need to
   268  		// alter the archive that we upload so that when the server extracts
   269  		// it to the specified directory in the container we get the desired
   270  		// copy behavior.
   271  
   272  		// See comments in the implementation of `archive.PrepareArchiveCopy`
   273  		// for exactly what goes into deciding how and whether the source
   274  		// archive needs to be altered for the correct copy behavior when it is
   275  		// extracted. This function also infers from the source and destination
   276  		// info which directory to extract to, which may be the parent of the
   277  		// destination that the user specified.
   278  		dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
   279  		if err != nil {
   280  			return err
   281  		}
   282  		defer preparedArchive.Close()
   283  
   284  		resolvedDstPath = dstDir
   285  		content = preparedArchive
   286  	}
   287  
   288  	options := types.CopyToContainerOptions{
   289  		ContainerID:               dstContainer,
   290  		Path:                      resolvedDstPath,
   291  		Content:                   content,
   292  		AllowOverwriteDirWithFile: false,
   293  	}
   294  
   295  	return cli.client.CopyToContainer(options)
   296  }