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