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