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 }