| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 | /*   Copyright 2020 Docker Compose CLI authors   Licensed under the Apache License, Version 2.0 (the "License");   you may not use this file except in compliance with the License.   You may obtain a copy of the License at       http://www.apache.org/licenses/LICENSE-2.0   Unless required by applicable law or agreed to in writing, software   distributed under the License is distributed on an "AS IS" BASIS,   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   See the License for the specific language governing permissions and   limitations under the License.*/package composeimport (	"context"	"fmt"	"io"	"os"	"path/filepath"	"strings"	"golang.org/x/sync/errgroup"	"github.com/compose-spec/compose-go/types"	"github.com/docker/cli/cli/command"	"github.com/docker/compose/v2/pkg/api"	moby "github.com/docker/docker/api/types"	"github.com/docker/docker/api/types/filters"	"github.com/docker/docker/pkg/archive"	"github.com/docker/docker/pkg/system"	"github.com/pkg/errors")type copyDirection intconst (	fromService copyDirection = 1 << iota	toService	acrossServices = fromService | toService)func (s *composeService) Copy(ctx context.Context, project *types.Project, opts api.CopyOptions) error {	srcService, srcPath := splitCpArg(opts.Source)	destService, dstPath := splitCpArg(opts.Destination)	var direction copyDirection	var serviceName string	if srcService != "" {		direction |= fromService		serviceName = srcService		// copying from multiple containers of a services doesn't make sense.		if opts.All {			return errors.New("cannot use the --all flag when copying from a service")		}	}	if destService != "" {		direction |= toService		serviceName = destService	}	f := filters.NewArgs(		projectFilter(project.Name),		serviceFilter(serviceName),	)	if !opts.All {		f.Add("label", fmt.Sprintf("%s=%d", api.ContainerNumberLabel, opts.Index))	}	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{Filters: f})	if err != nil {		return err	}	if len(containers) < 1 {		return fmt.Errorf("service %s not running", serviceName)	}	g := errgroup.Group{}	for _, container := range containers {		containerID := container.ID		g.Go(func() error {			switch direction {			case fromService:				return s.copyFromContainer(ctx, containerID, srcPath, dstPath, opts)			case toService:				return s.copyToContainer(ctx, containerID, srcPath, dstPath, opts)			case acrossServices:				return errors.New("copying between services is not supported")			default:				return errors.New("unknown copy direction")			}		})	}	return g.Wait()}func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error {	var err error	if srcPath != "-" {		// Get an absolute source path.		srcPath, err = resolveLocalPath(srcPath)		if err != nil {			return err		}	}	// Prepare destination copy info by stat-ing the container path.	dstInfo := archive.CopyInfo{Path: dstPath}	dstStat, err := s.apiClient.ContainerStatPath(ctx, containerID, dstPath)	// If the destination is a symbolic link, we should evaluate it.	if err == nil && dstStat.Mode&os.ModeSymlink != 0 {		linkTarget := dstStat.LinkTarget		if !system.IsAbs(linkTarget) {			// Join with the parent directory.			dstParent, _ := archive.SplitPathDirEntry(dstPath)			linkTarget = filepath.Join(dstParent, linkTarget)		}		dstInfo.Path = linkTarget		dstStat, err = s.apiClient.ContainerStatPath(ctx, containerID, linkTarget)	}	// Validate the destination path	if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {		return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, containerID, dstPath)	}	// Ignore any error and assume that the parent directory of the destination	// path exists, in which case the copy may still succeed. If there is any	// type of conflict (e.g., non-directory overwriting an existing directory	// or vice versa) the extraction will fail. If the destination simply did	// not exist, but the parent directory does, the extraction will still	// succeed.	if err == nil {		dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()	}	var (		content         io.Reader		resolvedDstPath string	)	if srcPath == "-" {		content = os.Stdin		resolvedDstPath = dstInfo.Path		if !dstInfo.IsDir {			return errors.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)		}	} else {		// Prepare source copy info.		srcInfo, err := archive.CopyInfoSourcePath(srcPath, opts.FollowLink)		if err != nil {			return err		}		srcArchive, err := archive.TarResource(srcInfo)		if err != nil {			return err		}		defer srcArchive.Close() //nolint:errcheck		// With the stat info about the local source as well as the		// destination, we have enough information to know whether we need to		// alter the archive that we upload so that when the server extracts		// it to the specified directory in the container we get the desired		// copy behavior.		// See comments in the implementation of `archive.PrepareArchiveCopy`		// for exactly what goes into deciding how and whether the source		// archive needs to be altered for the correct copy behavior when it is		// extracted. This function also infers from the source and destination		// info which directory to extract to, which may be the parent of the		// destination that the user specified.		dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)		if err != nil {			return err		}		defer preparedArchive.Close() //nolint:errcheck		resolvedDstPath = dstDir		content = preparedArchive	}	options := moby.CopyToContainerOptions{		AllowOverwriteDirWithFile: false,		CopyUIDGID:                opts.CopyUIDGID,	}	return s.apiClient.CopyToContainer(ctx, containerID, resolvedDstPath, content, options)}func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error {	var err error	if dstPath != "-" {		// Get an absolute destination path.		dstPath, err = resolveLocalPath(dstPath)		if err != nil {			return err		}	}	if err := command.ValidateOutputPath(dstPath); err != nil {		return err	}	// if client requests to follow symbol link, then must decide target file to be copied	var rebaseName string	if opts.FollowLink {		srcStat, err := s.apiClient.ContainerStatPath(ctx, containerID, srcPath)		// If the destination is a symbolic link, we should follow it.		if err == nil && srcStat.Mode&os.ModeSymlink != 0 {			linkTarget := srcStat.LinkTarget			if !system.IsAbs(linkTarget) {				// Join with the parent directory.				srcParent, _ := archive.SplitPathDirEntry(srcPath)				linkTarget = filepath.Join(srcParent, linkTarget)			}			linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)			srcPath = linkTarget		}	}	content, stat, err := s.apiClient.CopyFromContainer(ctx, containerID, srcPath)	if err != nil {		return err	}	defer content.Close() //nolint:errcheck	if dstPath == "-" {		_, err = io.Copy(os.Stdout, content)		return err	}	srcInfo := archive.CopyInfo{		Path:       srcPath,		Exists:     true,		IsDir:      stat.Mode.IsDir(),		RebaseName: rebaseName,	}	preArchive := content	if len(srcInfo.RebaseName) != 0 {		_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)		preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)	}	return archive.CopyTo(preArchive, srcInfo, dstPath)}func splitCpArg(arg string) (container, path string) {	if system.IsAbs(arg) {		// Explicit local absolute path, e.g., `C:\foo` or `/foo`.		return "", arg	}	parts := strings.SplitN(arg, ":", 2)	if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {		// Either there's no `:` in the arg		// OR it's an explicit local relative path like `./file:name.txt`.		return "", arg	}	return parts[0], parts[1]}func resolveLocalPath(localPath string) (absPath string, err error) {	if absPath, err = filepath.Abs(localPath); err != nil {		return	}	return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil}
 |