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