| 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" |
| ) |
| |
| 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 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 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 !filepath.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 |
| } |