github.com/campoy/docker@v1.8.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 ) 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 // Ignore any error and assume that the parent directory of the destination 236 // path exists, in which case the copy may still succeed. If there is any 237 // type of conflict (e.g., non-directory overwriting an existing directory 238 // or vice versia) the extraction will fail. If the destination simply did 239 // not exist, but the parent directory does, the extraction will still 240 // succeed. 241 if err == nil { 242 dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() 243 } 244 245 var content io.Reader 246 if srcPath == "-" { 247 // Use STDIN. 248 content = os.Stdin 249 if !dstInfo.IsDir { 250 return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath)) 251 } 252 } else { 253 srcArchive, err := archive.TarResource(srcPath) 254 if err != nil { 255 return err 256 } 257 defer srcArchive.Close() 258 259 // With the stat info about the local source as well as the 260 // destination, we have enough information to know whether we need to 261 // alter the archive that we upload so that when the server extracts 262 // it to the specified directory in the container we get the disired 263 // copy behavior. 264 265 // Prepare source copy info. 266 srcInfo, err := archive.CopyInfoStatPath(srcPath, true) 267 if err != nil { 268 return err 269 } 270 271 // See comments in the implementation of `archive.PrepareArchiveCopy` 272 // for exactly what goes into deciding how and whether the source 273 // archive needs to be altered for the correct copy behavior when it is 274 // extracted. This function also infers from the source and destination 275 // info which directory to extract to, which may be the parent of the 276 // destination that the user specified. 277 dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) 278 if err != nil { 279 return err 280 } 281 defer preparedArchive.Close() 282 283 dstPath = dstDir 284 content = preparedArchive 285 } 286 287 query := make(url.Values, 2) 288 query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API. 289 // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. 290 query.Set("noOverwriteDirNonDir", "true") 291 292 urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode()) 293 294 response, err := cli.stream("PUT", urlStr, &streamOpts{in: content}) 295 if err != nil { 296 return err 297 } 298 defer response.body.Close() 299 300 if response.statusCode != http.StatusOK { 301 return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) 302 } 303 304 return nil 305 }