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