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 = &copyProgressPrinter{
   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 = &copyProgressPrinter{
   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  }