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