github.com/goern/docker@v1.9.0-rc1/api/client/cp.go (about)

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