github.com/olljanat/moby@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  }