github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+incompatible/cli/command/container/cp.go (about)

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