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