123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- package client
- import (
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "strings"
- "github.com/docker/docker/api/types"
- Cli "github.com/docker/docker/cli"
- "github.com/docker/docker/pkg/archive"
- flag "github.com/docker/docker/pkg/mflag"
- "github.com/docker/docker/pkg/system"
- )
- 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)
- if cmd.Arg(0) == "" {
- return fmt.Errorf("source can not be empty")
- }
- if cmd.Arg(1) == "" {
- return fmt.Errorf("destination can not be empty")
- }
- srcContainer, srcPath := splitCpArg(cmd.Arg(0))
- dstContainer, dstPath := splitCpArg(cmd.Arg(1))
- var direction copyDirection
- if srcContainer != "" {
- direction |= fromContainer
- }
- if dstContainer != "" {
- direction |= toContainer
- }
- 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 system.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 stat, err
- }
- defer response.body.Close()
- if response.statusCode != http.StatusOK {
- return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
- }
- 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)
- // If the destination is a symbolic link, we should evaluate it.
- if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
- linkTarget := dstStat.LinkTarget
- if !system.IsAbs(linkTarget) {
- // Join with the parent directory.
- dstParent, _ := archive.SplitPathDirEntry(dstPath)
- linkTarget = filepath.Join(dstParent, linkTarget)
- }
- dstInfo.Path = linkTarget
- dstStat, err = cli.statContainerPath(dstContainer, linkTarget)
- }
- // 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
- resolvedDstPath string
- )
- if srcPath == "-" {
- // Use STDIN.
- content = os.Stdin
- resolvedDstPath = dstInfo.Path
- if !dstInfo.IsDir {
- return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
- }
- } else {
- // Prepare source copy info.
- srcInfo, err := archive.CopyInfoSourcePath(srcPath)
- if err != nil {
- return err
- }
- srcArchive, err := archive.TarResource(srcInfo)
- 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.
- // 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()
- resolvedDstPath = dstDir
- content = preparedArchive
- }
- query := make(url.Values, 2)
- query.Set("path", filepath.ToSlash(resolvedDstPath)) // 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
- }
|