github.com/ncdc/docker@v0.10.1-0.20160129113957-6c6729ef5b74/api/client/cp.go (about) 1 package client 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "strings" 9 10 Cli "github.com/docker/docker/cli" 11 "github.com/docker/docker/pkg/archive" 12 flag "github.com/docker/docker/pkg/mflag" 13 "github.com/docker/docker/pkg/system" 14 "github.com/docker/engine-api/types" 15 ) 16 17 type copyDirection int 18 19 const ( 20 fromContainer copyDirection = (1 << iota) 21 toContainer 22 acrossContainers = fromContainer | toContainer 23 ) 24 25 type cpConfig struct { 26 followLink bool 27 } 28 29 // CmdCp copies files/folders to or from a path in a container. 30 // 31 // When copying from a container, if DEST_PATH is '-' the data is written as a 32 // tar archive file to STDOUT. 33 // 34 // When copying to a container, if SRC_PATH is '-' the data is read as a tar 35 // archive file from STDIN, and the destination CONTAINER:DEST_PATH, must specify 36 // a directory. 37 // 38 // Usage: 39 // docker cp CONTAINER:SRC_PATH DEST_PATH|- 40 // docker cp SRC_PATH|- CONTAINER:DEST_PATH 41 func (cli *DockerCli) CmdCp(args ...string) error { 42 cmd := Cli.Subcmd( 43 "cp", 44 []string{"CONTAINER:SRC_PATH DEST_PATH|-", "SRC_PATH|- CONTAINER:DEST_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 followLink := cmd.Bool([]string{"L", "-follow-link"}, false, "Always follow symbol link in SRC_PATH") 56 57 cmd.Require(flag.Exact, 2) 58 cmd.ParseFlags(args, true) 59 60 if cmd.Arg(0) == "" { 61 return fmt.Errorf("source can not be empty") 62 } 63 if cmd.Arg(1) == "" { 64 return fmt.Errorf("destination can not be empty") 65 } 66 67 srcContainer, srcPath := splitCpArg(cmd.Arg(0)) 68 dstContainer, dstPath := splitCpArg(cmd.Arg(1)) 69 70 var direction copyDirection 71 if srcContainer != "" { 72 direction |= fromContainer 73 } 74 if dstContainer != "" { 75 direction |= toContainer 76 } 77 78 cpParam := &cpConfig{ 79 followLink: *followLink, 80 } 81 82 switch direction { 83 case fromContainer: 84 return cli.copyFromContainer(srcContainer, srcPath, dstPath, cpParam) 85 case toContainer: 86 return cli.copyToContainer(srcPath, dstContainer, dstPath, cpParam) 87 case acrossContainers: 88 // Copying between containers isn't supported. 89 return fmt.Errorf("copying between containers is not supported") 90 default: 91 // User didn't specify any container. 92 return fmt.Errorf("must specify at least one container source") 93 } 94 } 95 96 // We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be 97 // in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by 98 // requiring a LOCALPATH with a `:` to be made explicit with a relative or 99 // absolute path: 100 // `/path/to/file:name.txt` or `./file:name.txt` 101 // 102 // This is apparently how `scp` handles this as well: 103 // http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/ 104 // 105 // We can't simply check for a filepath separator because container names may 106 // have a separator, e.g., "host0/cname1" if container is in a Docker cluster, 107 // so we have to check for a `/` or `.` prefix. Also, in the case of a Windows 108 // client, a `:` could be part of an absolute Windows path, in which case it 109 // is immediately proceeded by a backslash. 110 func splitCpArg(arg string) (container, path string) { 111 if system.IsAbs(arg) { 112 // Explicit local absolute path, e.g., `C:\foo` or `/foo`. 113 return "", arg 114 } 115 116 parts := strings.SplitN(arg, ":", 2) 117 118 if len(parts) == 1 || strings.HasPrefix(parts[0], ".") { 119 // Either there's no `:` in the arg 120 // OR it's an explicit local relative path like `./file:name.txt`. 121 return "", arg 122 } 123 124 return parts[0], parts[1] 125 } 126 127 func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) { 128 return cli.client.ContainerStatPath(containerName, path) 129 } 130 131 func resolveLocalPath(localPath string) (absPath string, err error) { 132 if absPath, err = filepath.Abs(localPath); err != nil { 133 return 134 } 135 136 return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil 137 } 138 139 func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) { 140 if dstPath != "-" { 141 // Get an absolute destination path. 142 dstPath, err = resolveLocalPath(dstPath) 143 if err != nil { 144 return err 145 } 146 } 147 148 // if client requests to follow symbol link, then must decide target file to be copied 149 var rebaseName string 150 if cpParam.followLink { 151 srcStat, err := cli.statContainerPath(srcContainer, srcPath) 152 153 // If the destination is a symbolic link, we should follow it. 154 if err == nil && srcStat.Mode&os.ModeSymlink != 0 { 155 linkTarget := srcStat.LinkTarget 156 if !system.IsAbs(linkTarget) { 157 // Join with the parent directory. 158 srcParent, _ := archive.SplitPathDirEntry(srcPath) 159 linkTarget = filepath.Join(srcParent, linkTarget) 160 } 161 162 linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget) 163 srcPath = linkTarget 164 } 165 166 } 167 168 content, stat, err := cli.client.CopyFromContainer(srcContainer, srcPath) 169 if err != nil { 170 return err 171 } 172 defer content.Close() 173 174 if dstPath == "-" { 175 // Send the response to STDOUT. 176 _, err = io.Copy(os.Stdout, content) 177 178 return err 179 } 180 181 // Prepare source copy info. 182 srcInfo := archive.CopyInfo{ 183 Path: srcPath, 184 Exists: true, 185 IsDir: stat.Mode.IsDir(), 186 RebaseName: rebaseName, 187 } 188 189 preArchive := content 190 if len(srcInfo.RebaseName) != 0 { 191 _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) 192 preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) 193 } 194 // See comments in the implementation of `archive.CopyTo` for exactly what 195 // goes into deciding how and whether the source archive needs to be 196 // altered for the correct copy behavior. 197 return archive.CopyTo(preArchive, srcInfo, dstPath) 198 } 199 200 func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) { 201 if srcPath != "-" { 202 // Get an absolute source path. 203 srcPath, err = resolveLocalPath(srcPath) 204 if err != nil { 205 return err 206 } 207 } 208 209 // In order to get the copy behavior right, we need to know information 210 // about both the source and destination. The API is a simple tar 211 // archive/extract API but we can use the stat info header about the 212 // destination to be more informed about exactly what the destination is. 213 214 // Prepare destination copy info by stat-ing the container path. 215 dstInfo := archive.CopyInfo{Path: dstPath} 216 dstStat, err := cli.statContainerPath(dstContainer, dstPath) 217 218 // If the destination is a symbolic link, we should evaluate it. 219 if err == nil && dstStat.Mode&os.ModeSymlink != 0 { 220 linkTarget := dstStat.LinkTarget 221 if !system.IsAbs(linkTarget) { 222 // Join with the parent directory. 223 dstParent, _ := archive.SplitPathDirEntry(dstPath) 224 linkTarget = filepath.Join(dstParent, linkTarget) 225 } 226 227 dstInfo.Path = linkTarget 228 dstStat, err = cli.statContainerPath(dstContainer, linkTarget) 229 } 230 231 // Ignore any error and assume that the parent directory of the destination 232 // path exists, in which case the copy may still succeed. If there is any 233 // type of conflict (e.g., non-directory overwriting an existing directory 234 // or vice versa) the extraction will fail. If the destination simply did 235 // not exist, but the parent directory does, the extraction will still 236 // succeed. 237 if err == nil { 238 dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() 239 } 240 241 var ( 242 content io.Reader 243 resolvedDstPath string 244 ) 245 246 if srcPath == "-" { 247 // Use STDIN. 248 content = os.Stdin 249 resolvedDstPath = dstInfo.Path 250 if !dstInfo.IsDir { 251 return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath)) 252 } 253 } else { 254 // Prepare source copy info. 255 srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink) 256 if err != nil { 257 return err 258 } 259 260 srcArchive, err := archive.TarResource(srcInfo) 261 if err != nil { 262 return err 263 } 264 defer srcArchive.Close() 265 266 // With the stat info about the local source as well as the 267 // destination, we have enough information to know whether we need to 268 // alter the archive that we upload so that when the server extracts 269 // it to the specified directory in the container we get the desired 270 // copy behavior. 271 272 // See comments in the implementation of `archive.PrepareArchiveCopy` 273 // for exactly what goes into deciding how and whether the source 274 // archive needs to be altered for the correct copy behavior when it is 275 // extracted. This function also infers from the source and destination 276 // info which directory to extract to, which may be the parent of the 277 // destination that the user specified. 278 dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) 279 if err != nil { 280 return err 281 } 282 defer preparedArchive.Close() 283 284 resolvedDstPath = dstDir 285 content = preparedArchive 286 } 287 288 options := types.CopyToContainerOptions{ 289 ContainerID: dstContainer, 290 Path: resolvedDstPath, 291 Content: content, 292 AllowOverwriteDirWithFile: false, 293 } 294 295 return cli.client.CopyToContainer(options) 296 }