github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/container/cp.go (about) 1 package container 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "os/signal" 10 "path/filepath" 11 "strings" 12 "sync/atomic" 13 "time" 14 15 "github.com/docker/cli/cli" 16 "github.com/docker/cli/cli/command" 17 "github.com/docker/cli/cli/streams" 18 "github.com/docker/docker/api/types" 19 "github.com/docker/docker/pkg/archive" 20 "github.com/docker/docker/pkg/system" 21 units "github.com/docker/go-units" 22 "github.com/morikuni/aec" 23 "github.com/pkg/errors" 24 "github.com/spf13/cobra" 25 ) 26 27 type copyOptions struct { 28 source string 29 destination string 30 followLink bool 31 copyUIDGID bool 32 quiet bool 33 } 34 35 type copyDirection int 36 37 const ( 38 fromContainer copyDirection = 1 << iota 39 toContainer 40 acrossContainers = fromContainer | toContainer 41 ) 42 43 type cpConfig struct { 44 followLink bool 45 copyUIDGID bool 46 quiet bool 47 sourcePath string 48 destPath string 49 container string 50 } 51 52 // copyProgressPrinter wraps io.ReadCloser to print progress information when 53 // copying files to/from a container. 54 type copyProgressPrinter struct { 55 io.ReadCloser 56 total *int64 57 } 58 59 const ( 60 copyToContainerHeader = "Copying to container - " 61 copyFromContainerHeader = "Copying from container - " 62 copyProgressUpdateThreshold = 75 * time.Millisecond 63 ) 64 65 func (pt *copyProgressPrinter) Read(p []byte) (int, error) { 66 n, err := pt.ReadCloser.Read(p) 67 atomic.AddInt64(pt.total, int64(n)) 68 return n, err 69 } 70 71 func copyProgress(ctx context.Context, dst io.Writer, header string, total *int64) (func(), <-chan struct{}) { 72 done := make(chan struct{}) 73 if !streams.NewOut(dst).IsTerminal() { 74 close(done) 75 return func() {}, done 76 } 77 78 fmt.Fprint(dst, aec.Save) 79 fmt.Fprint(dst, "Preparing to copy...") 80 81 restore := func() { 82 fmt.Fprint(dst, aec.Restore) 83 fmt.Fprint(dst, aec.EraseLine(aec.EraseModes.All)) 84 } 85 86 go func() { 87 defer close(done) 88 fmt.Fprint(dst, aec.Hide) 89 defer fmt.Fprint(dst, aec.Show) 90 91 fmt.Fprint(dst, aec.Restore) 92 fmt.Fprint(dst, aec.EraseLine(aec.EraseModes.All)) 93 fmt.Fprint(dst, header) 94 95 var last int64 96 fmt.Fprint(dst, progressHumanSize(last)) 97 98 buf := bytes.NewBuffer(nil) 99 ticker := time.NewTicker(copyProgressUpdateThreshold) 100 for { 101 select { 102 case <-ctx.Done(): 103 return 104 case <-ticker.C: 105 n := atomic.LoadInt64(total) 106 if n == last { 107 // Don't write to the terminal, if we don't need to. 108 continue 109 } 110 111 // Write to the buffer first to avoid flickering and context switching 112 fmt.Fprint(buf, aec.Column(uint(len(header)+1))) 113 fmt.Fprint(buf, aec.EraseLine(aec.EraseModes.Tail)) 114 fmt.Fprint(buf, progressHumanSize(n)) 115 116 buf.WriteTo(dst) 117 buf.Reset() 118 last += n 119 } 120 } 121 }() 122 return restore, done 123 } 124 125 // NewCopyCommand creates a new `docker cp` command 126 func NewCopyCommand(dockerCli command.Cli) *cobra.Command { 127 var opts copyOptions 128 129 cmd := &cobra.Command{ 130 Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- 131 docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`, 132 Short: "Copy files/folders between a container and the local filesystem", 133 Long: strings.Join([]string{ 134 "Copy files/folders between a container and the local filesystem\n", 135 "\nUse '-' as the source to read a tar archive from stdin\n", 136 "and extract it to a directory destination in a container.\n", 137 "Use '-' as the destination to stream a tar archive of a\n", 138 "container source to stdout.", 139 }, ""), 140 Args: cli.ExactArgs(2), 141 RunE: func(cmd *cobra.Command, args []string) error { 142 if args[0] == "" { 143 return errors.New("source can not be empty") 144 } 145 if args[1] == "" { 146 return errors.New("destination can not be empty") 147 } 148 opts.source = args[0] 149 opts.destination = args[1] 150 if !cmd.Flag("quiet").Changed { 151 // User did not specify "quiet" flag; suppress output if no terminal is attached 152 opts.quiet = !dockerCli.Out().IsTerminal() 153 } 154 return runCopy(cmd.Context(), dockerCli, opts) 155 }, 156 Annotations: map[string]string{ 157 "aliases": "docker container cp, docker cp", 158 }, 159 } 160 161 flags := cmd.Flags() 162 flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") 163 flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") 164 flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached") 165 return cmd 166 } 167 168 func progressHumanSize(n int64) string { 169 return units.HumanSizeWithPrecision(float64(n), 3) 170 } 171 172 func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error { 173 srcContainer, srcPath := splitCpArg(opts.source) 174 destContainer, destPath := splitCpArg(opts.destination) 175 176 copyConfig := cpConfig{ 177 followLink: opts.followLink, 178 copyUIDGID: opts.copyUIDGID, 179 quiet: opts.quiet, 180 sourcePath: srcPath, 181 destPath: destPath, 182 } 183 184 var direction copyDirection 185 if srcContainer != "" { 186 direction |= fromContainer 187 copyConfig.container = srcContainer 188 } 189 if destContainer != "" { 190 direction |= toContainer 191 copyConfig.container = destContainer 192 } 193 194 switch direction { 195 case fromContainer: 196 return copyFromContainer(ctx, dockerCli, copyConfig) 197 case toContainer: 198 return copyToContainer(ctx, dockerCli, copyConfig) 199 case acrossContainers: 200 return errors.New("copying between containers is not supported") 201 default: 202 return errors.New("must specify at least one container source") 203 } 204 } 205 206 func resolveLocalPath(localPath string) (absPath string, err error) { 207 if absPath, err = filepath.Abs(localPath); err != nil { 208 return 209 } 210 return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil 211 } 212 213 func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) { 214 dstPath := copyConfig.destPath 215 srcPath := copyConfig.sourcePath 216 217 if dstPath != "-" { 218 // Get an absolute destination path. 219 dstPath, err = resolveLocalPath(dstPath) 220 if err != nil { 221 return err 222 } 223 } 224 225 if err := command.ValidateOutputPath(dstPath); err != nil { 226 return err 227 } 228 229 client := dockerCli.Client() 230 // if client requests to follow symbol link, then must decide target file to be copied 231 var rebaseName string 232 if copyConfig.followLink { 233 srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath) 234 235 // If the destination is a symbolic link, we should follow it. 236 if err == nil && srcStat.Mode&os.ModeSymlink != 0 { 237 linkTarget := srcStat.LinkTarget 238 if !system.IsAbs(linkTarget) { 239 // Join with the parent directory. 240 srcParent, _ := archive.SplitPathDirEntry(srcPath) 241 linkTarget = filepath.Join(srcParent, linkTarget) 242 } 243 244 linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget) 245 srcPath = linkTarget 246 } 247 } 248 249 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) 250 defer cancel() 251 252 content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath) 253 if err != nil { 254 return err 255 } 256 defer content.Close() 257 258 if dstPath == "-" { 259 _, err = io.Copy(dockerCli.Out(), content) 260 return err 261 } 262 263 srcInfo := archive.CopyInfo{ 264 Path: srcPath, 265 Exists: true, 266 IsDir: stat.Mode.IsDir(), 267 RebaseName: rebaseName, 268 } 269 270 var copiedSize int64 271 if !copyConfig.quiet { 272 content = ©ProgressPrinter{ 273 ReadCloser: content, 274 total: &copiedSize, 275 } 276 } 277 278 preArchive := content 279 if len(srcInfo.RebaseName) != 0 { 280 _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) 281 preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) 282 } 283 284 if copyConfig.quiet { 285 return archive.CopyTo(preArchive, srcInfo, dstPath) 286 } 287 288 restore, done := copyProgress(ctx, dockerCli.Err(), copyFromContainerHeader, &copiedSize) 289 res := archive.CopyTo(preArchive, srcInfo, dstPath) 290 cancel() 291 <-done 292 restore() 293 fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath) 294 295 return res 296 } 297 298 // In order to get the copy behavior right, we need to know information 299 // about both the source and destination. The API is a simple tar 300 // archive/extract API but we can use the stat info header about the 301 // destination to be more informed about exactly what the destination is. 302 func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) { 303 srcPath := copyConfig.sourcePath 304 dstPath := copyConfig.destPath 305 306 if srcPath != "-" { 307 // Get an absolute source path. 308 srcPath, err = resolveLocalPath(srcPath) 309 if err != nil { 310 return err 311 } 312 } 313 314 client := dockerCli.Client() 315 // Prepare destination copy info by stat-ing the container path. 316 dstInfo := archive.CopyInfo{Path: dstPath} 317 dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath) 318 319 // If the destination is a symbolic link, we should evaluate it. 320 if err == nil && dstStat.Mode&os.ModeSymlink != 0 { 321 linkTarget := dstStat.LinkTarget 322 if !system.IsAbs(linkTarget) { 323 // Join with the parent directory. 324 dstParent, _ := archive.SplitPathDirEntry(dstPath) 325 linkTarget = filepath.Join(dstParent, linkTarget) 326 } 327 328 dstInfo.Path = linkTarget 329 dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget) 330 } 331 332 // Validate the destination path 333 if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil { 334 return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath) 335 } 336 337 // Ignore any error and assume that the parent directory of the destination 338 // path exists, in which case the copy may still succeed. If there is any 339 // type of conflict (e.g., non-directory overwriting an existing directory 340 // or vice versa) the extraction will fail. If the destination simply did 341 // not exist, but the parent directory does, the extraction will still 342 // succeed. 343 if err == nil { 344 dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() 345 } 346 347 var ( 348 content io.ReadCloser 349 resolvedDstPath string 350 copiedSize int64 351 ) 352 353 if srcPath == "-" { 354 content = os.Stdin 355 resolvedDstPath = dstInfo.Path 356 if !dstInfo.IsDir { 357 return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath) 358 } 359 } else { 360 // Prepare source copy info. 361 srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink) 362 if err != nil { 363 return err 364 } 365 366 srcArchive, err := archive.TarResource(srcInfo) 367 if err != nil { 368 return err 369 } 370 defer srcArchive.Close() 371 372 // With the stat info about the local source as well as the 373 // destination, we have enough information to know whether we need to 374 // alter the archive that we upload so that when the server extracts 375 // it to the specified directory in the container we get the desired 376 // copy behavior. 377 378 // See comments in the implementation of `archive.PrepareArchiveCopy` 379 // for exactly what goes into deciding how and whether the source 380 // archive needs to be altered for the correct copy behavior when it is 381 // extracted. This function also infers from the source and destination 382 // info which directory to extract to, which may be the parent of the 383 // destination that the user specified. 384 dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) 385 if err != nil { 386 return err 387 } 388 defer preparedArchive.Close() 389 390 resolvedDstPath = dstDir 391 content = preparedArchive 392 if !copyConfig.quiet { 393 content = ©ProgressPrinter{ 394 ReadCloser: content, 395 total: &copiedSize, 396 } 397 } 398 } 399 400 options := types.CopyToContainerOptions{ 401 AllowOverwriteDirWithFile: false, 402 CopyUIDGID: copyConfig.copyUIDGID, 403 } 404 405 if copyConfig.quiet { 406 return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options) 407 } 408 409 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) 410 restore, done := copyProgress(ctx, dockerCli.Err(), copyToContainerHeader, &copiedSize) 411 res := client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options) 412 cancel() 413 <-done 414 restore() 415 fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path) 416 417 return res 418 } 419 420 // We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be 421 // in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by 422 // requiring a LOCALPATH with a `:` to be made explicit with a relative or 423 // absolute path: 424 // 425 // `/path/to/file:name.txt` or `./file:name.txt` 426 // 427 // This is apparently how `scp` handles this as well: 428 // 429 // http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/ 430 // 431 // We can't simply check for a filepath separator because container names may 432 // have a separator, e.g., "host0/cname1" if container is in a Docker cluster, 433 // so we have to check for a `/` or `.` prefix. Also, in the case of a Windows 434 // client, a `:` could be part of an absolute Windows path, in which case it 435 // is immediately proceeded by a backslash. 436 func splitCpArg(arg string) (container, path string) { 437 if system.IsAbs(arg) { 438 // Explicit local absolute path, e.g., `C:\foo` or `/foo`. 439 return "", arg 440 } 441 442 container, path, ok := strings.Cut(arg, ":") 443 if !ok || strings.HasPrefix(container, ".") { 444 // Either there's no `:` in the arg 445 // OR it's an explicit local relative path like `./file:name.txt`. 446 return "", arg 447 } 448 449 return container, path 450 }