diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index 18bd3b3943..44017a2915 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -1,27 +1,9 @@ -//go:build !remote - package containers import ( - "fmt" - "io" - "os" - "os/user" - "path/filepath" - "strconv" - "strings" - - "errors" - - buildahCopiah "github.com/containers/buildah/copier" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/registry" - "github.com/containers/podman/v4/pkg/copy" "github.com/containers/podman/v4/pkg/domain/entities" - "github.com/containers/podman/v4/pkg/errorhandling" - "github.com/containers/storage/pkg/archive" - "github.com/containers/storage/pkg/idtools" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -80,446 +62,3 @@ func init() { }) cpFlags(containerCpCommand) } - -func cp(cmd *cobra.Command, args []string) error { - // Parse user input. - sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(args[0], args[1]) - if err != nil { - return err - } - - if len(sourceContainerStr) > 0 && len(destContainerStr) > 0 { - return copyContainerToContainer(sourceContainerStr, sourcePath, destContainerStr, destPath) - } else if len(sourceContainerStr) > 0 { - return copyFromContainer(sourceContainerStr, sourcePath, destPath) - } - - return copyToContainer(destContainerStr, destPath, sourcePath) -} - -// containerMustExist returns an error if the specified container does not -// exist. -func containerMustExist(container string) error { - exists, err := registry.ContainerEngine().ContainerExists(registry.GetContext(), container, entities.ContainerExistsOptions{}) - if err != nil { - return err - } - if !exists.Value { - return fmt.Errorf("container %q does not exist", container) - } - return nil -} - -// doCopy executes the two functions in parallel to copy data from A to B and -// joins the errors if any. -func doCopy(funcA func() error, funcB func() error) error { - errChan := make(chan error) - go func() { - errChan <- funcA() - }() - var copyErrors []error - copyErrors = append(copyErrors, funcB()) - copyErrors = append(copyErrors, <-errChan) - return errorhandling.JoinErrors(copyErrors) -} - -func copyContainerToContainer(sourceContainer string, sourcePath string, destContainer string, destPath string) error { - if err := containerMustExist(sourceContainer); err != nil { - return err - } - - if err := containerMustExist(destContainer); err != nil { - return err - } - - sourceContainerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), sourceContainer, sourcePath) - if err != nil { - return fmt.Errorf("%q could not be found on container %s: %w", sourcePath, sourceContainer, err) - } - - destContainerBaseName, destContainerInfo, destResolvedToParentDir, err := resolvePathOnDestinationContainer(destContainer, destPath, false) - if err != nil { - return err - } - - if sourceContainerInfo.IsDir && !destContainerInfo.IsDir { - return errors.New("destination must be a directory when copying a directory") - } - - sourceContainerTarget := sourceContainerInfo.LinkTarget - destContainerTarget := destContainerInfo.LinkTarget - if !destContainerInfo.IsDir { - destContainerTarget = filepath.Dir(destPath) - } - - // If we copy a directory via the "." notation and the container path - // does not exist, we need to make sure that the destination on the - // container gets created; otherwise the contents of the source - // directory will be written to the destination's parent directory. - // - // Hence, whenever "." is the source and the destination does not - // exist, we copy the source's parent and let the copier package create - // the destination via the Rename option. - if destResolvedToParentDir && sourceContainerInfo.IsDir && filepath.Base(sourcePath) == "." { - sourceContainerTarget = filepath.Dir(sourceContainerTarget) - } - - reader, writer := io.Pipe() - - sourceContainerCopy := func() error { - defer writer.Close() - copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), sourceContainer, sourceContainerTarget, writer) - if err != nil { - return err - } - if err := copyFunc(); err != nil { - return fmt.Errorf("copying from container: %w", err) - } - return nil - } - - destContainerCopy := func() error { - defer reader.Close() - - copyOptions := entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir} - if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destResolvedToParentDir { - // If we're having a file-to-file copy, make sure to - // rename accordingly. - copyOptions.Rename = map[string]string{filepath.Base(sourceContainerTarget): destContainerBaseName} - } - - copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), destContainer, destContainerTarget, reader, copyOptions) - if err != nil { - return err - } - if err := copyFunc(); err != nil { - return fmt.Errorf("copying to container: %w", err) - } - return nil - } - - return doCopy(sourceContainerCopy, destContainerCopy) -} - -// copyFromContainer copies from the containerPath on the container to hostPath. -func copyFromContainer(container string, containerPath string, hostPath string) error { - if err := containerMustExist(container); err != nil { - return err - } - - isStdout := false - if hostPath == "-" { - isStdout = true - hostPath = os.Stdout.Name() - } - - containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) - if err != nil { - return fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) - } - - var hostBaseName string - var resolvedToHostParentDir bool - hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath) - if hostInfoErr != nil { - if strings.HasSuffix(hostPath, "/") { - return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr) - } - // If it doesn't exist, then let's have a look at the parent dir. - parentDir := filepath.Dir(hostPath) - hostInfo, err = copy.ResolveHostPath(parentDir) - if err != nil { - return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr) - } - // If the specified path does not exist, we need to assume that - // it'll be created while copying. Hence, we use it as the - // base path. - hostBaseName = filepath.Base(hostPath) - resolvedToHostParentDir = true - } else { - // If the specified path exists on the host, we must use its - // base path as it may have changed due to symlink evaluations. - hostBaseName = filepath.Base(hostInfo.LinkTarget) - } - - if !isStdout { - if err := validateFileInfo(hostInfo); err != nil { - return fmt.Errorf("invalid destination: %w", err) - } - } - - // If we copy a directory via the "." notation and the host path does - // not exist, we need to make sure that the destination on the host - // gets created; otherwise the contents of the source directory will be - // written to the destination's parent directory. - // - // While we could cut it short on the host and do create the directory - // ourselves, we would run into problems trying to that the other way - // around when copying into a container. Instead, to keep both - // implementations symmetrical, we need to massage the code a bit to - // let Buildah's copier package create the destination. - // - // Hence, whenever "." is the source and the destination does not exist, - // we copy the source's parent and let the copier package create the - // destination via the Rename option. - containerTarget := containerInfo.LinkTarget - if resolvedToHostParentDir && containerInfo.IsDir && filepath.Base(containerTarget) == "." { - containerTarget = filepath.Dir(containerTarget) - } - - if !isStdout && containerInfo.IsDir && !hostInfo.IsDir { - return errors.New("destination must be a directory when copying a directory") - } - - reader, writer := io.Pipe() - hostCopy := func() error { - defer reader.Close() - if isStdout { - _, err := io.Copy(os.Stdout, reader) - return err - } - - groot, err := user.Current() - if err != nil { - return err - } - - // Set the {G,U}ID. Let's be tolerant towards the different - // operating systems and only log the errors, so we can debug - // if necessary. - idPair := idtools.IDPair{} - if i, err := strconv.Atoi(groot.Uid); err == nil { - idPair.UID = i - } else { - logrus.Debugf("Error converting UID %q to int: %v", groot.Uid, err) - } - if i, err := strconv.Atoi(groot.Gid); err == nil { - idPair.GID = i - } else { - logrus.Debugf("Error converting GID %q to int: %v", groot.Gid, err) - } - - putOptions := buildahCopiah.PutOptions{ - ChownDirs: &idPair, - ChownFiles: &idPair, - IgnoreDevices: true, - NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir, - NoOverwriteNonDirDir: !cpOpts.OverwriteDirNonDir, - } - if (!containerInfo.IsDir && !hostInfo.IsDir) || resolvedToHostParentDir { - // If we're having a file-to-file copy, make sure to - // rename accordingly. - putOptions.Rename = map[string]string{filepath.Base(containerTarget): hostBaseName} - } - dir := hostInfo.LinkTarget - if !hostInfo.IsDir { - dir = filepath.Dir(dir) - } - if err := buildahCopiah.Put(dir, "", putOptions, reader); err != nil { - return fmt.Errorf("copying to host: %w", err) - } - return nil - } - - containerCopy := func() error { - defer writer.Close() - copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), container, containerTarget, writer) - if err != nil { - return err - } - if err := copyFunc(); err != nil { - return fmt.Errorf("copying from container: %w", err) - } - return nil - } - return doCopy(containerCopy, hostCopy) -} - -// copyToContainer copies the hostPath to containerPath on the container. -func copyToContainer(container string, containerPath string, hostPath string) error { - if err := containerMustExist(container); err != nil { - return err - } - - isStdin := false - if hostPath == "-" { - hostPath = os.Stdin.Name() - isStdin = true - } - - // Make sure that host path exists. - hostInfo, err := copy.ResolveHostPath(hostPath) - if err != nil { - return fmt.Errorf("%q could not be found on the host: %w", hostPath, err) - } - - containerBaseName, containerInfo, containerResolvedToParentDir, err := resolvePathOnDestinationContainer(container, containerPath, isStdin) - if err != nil { - return err - } - - // If we copy a directory via the "." notation and the container path - // does not exist, we need to make sure that the destination on the - // container gets created; otherwise the contents of the source - // directory will be written to the destination's parent directory. - // - // Hence, whenever "." is the source and the destination does not - // exist, we copy the source's parent and let the copier package create - // the destination via the Rename option. - hostTarget := hostInfo.LinkTarget - if containerResolvedToParentDir && hostInfo.IsDir && filepath.Base(hostTarget) == "." { - hostTarget = filepath.Dir(hostTarget) - } - - var stdinFile string - if isStdin { - if !containerInfo.IsDir { - return errors.New("destination must be a directory when copying from stdin") - } - - // Copy from stdin to a temporary file *before* throwing it - // over the wire. This allows for proper client-side error - // reporting. - tmpFile, err := os.CreateTemp("", "") - if err != nil { - return err - } - _, err = io.Copy(tmpFile, os.Stdin) - if err != nil { - return err - } - if err = tmpFile.Close(); err != nil { - return err - } - if !archive.IsArchivePath(tmpFile.Name()) { - return errors.New("source must be a (compressed) tar archive when copying from stdin") - } - stdinFile = tmpFile.Name() - } - - if hostInfo.IsDir && !containerInfo.IsDir { - return errors.New("destination must be a directory when copying a directory") - } - - reader, writer := io.Pipe() - hostCopy := func() error { - defer writer.Close() - if isStdin { - stream, err := os.Open(stdinFile) - if err != nil { - return err - } - defer stream.Close() - _, err = io.Copy(writer, stream) - return err - } - - getOptions := buildahCopiah.GetOptions{ - // Unless the specified path points to ".", we want to - // copy the base directory. - KeepDirectoryNames: hostInfo.IsDir && filepath.Base(hostTarget) != ".", - } - if (!hostInfo.IsDir && !containerInfo.IsDir) || containerResolvedToParentDir { - // If we're having a file-to-file copy, make sure to - // rename accordingly. - getOptions.Rename = map[string]string{filepath.Base(hostTarget): containerBaseName} - } - if err := buildahCopiah.Get("/", "", getOptions, []string{hostTarget}, writer); err != nil { - return fmt.Errorf("copying from host: %w", err) - } - return nil - } - - containerCopy := func() error { - defer reader.Close() - target := containerInfo.FileInfo.LinkTarget - if !containerInfo.IsDir { - target = filepath.Dir(target) - } - - copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader, entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir}) - if err != nil { - return err - } - if err := copyFunc(); err != nil { - return fmt.Errorf("copying to container: %w", err) - } - return nil - } - - return doCopy(hostCopy, containerCopy) -} - -// resolvePathOnDestinationContainer resolves the specified path on the -// container. If the path does not exist, it attempts to use the parent -// directory. -func resolvePathOnDestinationContainer(container string, containerPath string, isStdin bool) (baseName string, containerInfo *entities.ContainerStatReport, resolvedToParentDir bool, err error) { - containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) - if err == nil { - baseName = filepath.Base(containerInfo.LinkTarget) - return //nolint: nilerr - } - - if strings.HasSuffix(containerPath, "/") { - err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) - return - } - if isStdin { - err = errors.New("destination must be a directory when copying from stdin") - return - } - - // NOTE: containerInfo may actually be set. That happens when - // the container path is a symlink into nirvana. In that case, - // we must use the symlinked path instead. - path := containerPath - if containerInfo != nil { - baseName = filepath.Base(containerInfo.LinkTarget) - path = containerInfo.LinkTarget - } else { - baseName = filepath.Base(containerPath) - } - - parentDir, err := containerParentDir(container, path) - if err != nil { - err = fmt.Errorf("could not determine parent dir of %q on container %s: %w", path, container, err) - return - } - - containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir) - if err != nil { - err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) - return - } - - resolvedToParentDir = true - return baseName, containerInfo, resolvedToParentDir, nil -} - -// containerParentDir returns the parent directory of the specified path on the -// container. If the path is relative, it will be resolved relative to the -// container's working directory (or "/" if the work dir isn't set). -func containerParentDir(container string, containerPath string) (string, error) { - if filepath.IsAbs(containerPath) { - return filepath.Dir(containerPath), nil - } - inspectData, _, err := registry.ContainerEngine().ContainerInspect(registry.GetContext(), []string{container}, entities.InspectOptions{}) - if err != nil { - return "", err - } - if len(inspectData) != 1 { - return "", fmt.Errorf("inspecting container %q: expected 1 data item but got %d", container, len(inspectData)) - } - workDir := filepath.Join("/", inspectData[0].Config.WorkingDir) - workDir = filepath.Join(workDir, containerPath) - return filepath.Dir(workDir), nil -} - -// validateFileInfo returns an error if the specified FileInfo doesn't point to -// a directory or a regular file. -func validateFileInfo(info *copy.FileInfo) error { - if info.Mode.IsDir() || info.Mode.IsRegular() { - return nil - } - return fmt.Errorf("%q must be a directory or a regular file", info.LinkTarget) -} diff --git a/cmd/podman/containers/cp_local.go b/cmd/podman/containers/cp_local.go new file mode 100644 index 0000000000..804ad23c82 --- /dev/null +++ b/cmd/podman/containers/cp_local.go @@ -0,0 +1,468 @@ +//go:build !remote + +package containers + +import ( + "fmt" + "io" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + + "errors" + + buildahCopiah "github.com/containers/buildah/copier" + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/pkg/copy" + "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/errorhandling" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func cp(cmd *cobra.Command, args []string) error { + // Parse user input. + sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(args[0], args[1]) + if err != nil { + return err + } + + if len(sourceContainerStr) > 0 && len(destContainerStr) > 0 { + return copyContainerToContainer(sourceContainerStr, sourcePath, destContainerStr, destPath) + } else if len(sourceContainerStr) > 0 { + return copyFromContainer(sourceContainerStr, sourcePath, destPath) + } + + return copyToContainer(destContainerStr, destPath, sourcePath) +} + +// containerMustExist returns an error if the specified container does not +// exist. +func containerMustExist(container string) error { + exists, err := registry.ContainerEngine().ContainerExists(registry.GetContext(), container, entities.ContainerExistsOptions{}) + if err != nil { + return err + } + if !exists.Value { + return fmt.Errorf("container %q does not exist", container) + } + return nil +} + +// doCopy executes the two functions in parallel to copy data from A to B and +// joins the errors if any. +func doCopy(funcA func() error, funcB func() error) error { + errChan := make(chan error) + go func() { + errChan <- funcA() + }() + var copyErrors []error + copyErrors = append(copyErrors, funcB()) + copyErrors = append(copyErrors, <-errChan) + return errorhandling.JoinErrors(copyErrors) +} + +func copyContainerToContainer(sourceContainer string, sourcePath string, destContainer string, destPath string) error { + if err := containerMustExist(sourceContainer); err != nil { + return err + } + + if err := containerMustExist(destContainer); err != nil { + return err + } + + sourceContainerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), sourceContainer, sourcePath) + if err != nil { + return fmt.Errorf("%q could not be found on container %s: %w", sourcePath, sourceContainer, err) + } + + destContainerBaseName, destContainerInfo, destResolvedToParentDir, err := resolvePathOnDestinationContainer(destContainer, destPath, false) + if err != nil { + return err + } + + if sourceContainerInfo.IsDir && !destContainerInfo.IsDir { + return errors.New("destination must be a directory when copying a directory") + } + + sourceContainerTarget := sourceContainerInfo.LinkTarget + destContainerTarget := destContainerInfo.LinkTarget + if !destContainerInfo.IsDir { + destContainerTarget = filepath.Dir(destPath) + } + + // If we copy a directory via the "." notation and the container path + // does not exist, we need to make sure that the destination on the + // container gets created; otherwise the contents of the source + // directory will be written to the destination's parent directory. + // + // Hence, whenever "." is the source and the destination does not + // exist, we copy the source's parent and let the copier package create + // the destination via the Rename option. + if destResolvedToParentDir && sourceContainerInfo.IsDir && filepath.Base(sourcePath) == "." { + sourceContainerTarget = filepath.Dir(sourceContainerTarget) + } + + reader, writer := io.Pipe() + + sourceContainerCopy := func() error { + defer writer.Close() + copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), sourceContainer, sourceContainerTarget, writer) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return fmt.Errorf("copying from container: %w", err) + } + return nil + } + + destContainerCopy := func() error { + defer reader.Close() + + copyOptions := entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir} + if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destResolvedToParentDir { + // If we're having a file-to-file copy, make sure to + // rename accordingly. + copyOptions.Rename = map[string]string{filepath.Base(sourceContainerTarget): destContainerBaseName} + } + + copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), destContainer, destContainerTarget, reader, copyOptions) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return fmt.Errorf("copying to container: %w", err) + } + return nil + } + + return doCopy(sourceContainerCopy, destContainerCopy) +} + +// copyFromContainer copies from the containerPath on the container to hostPath. +func copyFromContainer(container string, containerPath string, hostPath string) error { + if err := containerMustExist(container); err != nil { + return err + } + + isStdout := false + if hostPath == "-" { + isStdout = true + hostPath = os.Stdout.Name() + } + + containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) + if err != nil { + return fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) + } + + var hostBaseName string + var resolvedToHostParentDir bool + hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath) + if hostInfoErr != nil { + if strings.HasSuffix(hostPath, "/") { + return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr) + } + // If it doesn't exist, then let's have a look at the parent dir. + parentDir := filepath.Dir(hostPath) + hostInfo, err = copy.ResolveHostPath(parentDir) + if err != nil { + return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr) + } + // If the specified path does not exist, we need to assume that + // it'll be created while copying. Hence, we use it as the + // base path. + hostBaseName = filepath.Base(hostPath) + resolvedToHostParentDir = true + } else { + // If the specified path exists on the host, we must use its + // base path as it may have changed due to symlink evaluations. + hostBaseName = filepath.Base(hostInfo.LinkTarget) + } + + if !isStdout { + if err := validateFileInfo(hostInfo); err != nil { + return fmt.Errorf("invalid destination: %w", err) + } + } + + // If we copy a directory via the "." notation and the host path does + // not exist, we need to make sure that the destination on the host + // gets created; otherwise the contents of the source directory will be + // written to the destination's parent directory. + // + // While we could cut it short on the host and do create the directory + // ourselves, we would run into problems trying to that the other way + // around when copying into a container. Instead, to keep both + // implementations symmetrical, we need to massage the code a bit to + // let Buildah's copier package create the destination. + // + // Hence, whenever "." is the source and the destination does not exist, + // we copy the source's parent and let the copier package create the + // destination via the Rename option. + containerTarget := containerInfo.LinkTarget + if resolvedToHostParentDir && containerInfo.IsDir && filepath.Base(containerTarget) == "." { + containerTarget = filepath.Dir(containerTarget) + } + + if !isStdout && containerInfo.IsDir && !hostInfo.IsDir { + return errors.New("destination must be a directory when copying a directory") + } + + reader, writer := io.Pipe() + hostCopy := func() error { + defer reader.Close() + if isStdout { + _, err := io.Copy(os.Stdout, reader) + return err + } + + groot, err := user.Current() + if err != nil { + return err + } + + // Set the {G,U}ID. Let's be tolerant towards the different + // operating systems and only log the errors, so we can debug + // if necessary. + idPair := idtools.IDPair{} + if i, err := strconv.Atoi(groot.Uid); err == nil { + idPair.UID = i + } else { + logrus.Debugf("Error converting UID %q to int: %v", groot.Uid, err) + } + if i, err := strconv.Atoi(groot.Gid); err == nil { + idPair.GID = i + } else { + logrus.Debugf("Error converting GID %q to int: %v", groot.Gid, err) + } + + putOptions := buildahCopiah.PutOptions{ + ChownDirs: &idPair, + ChownFiles: &idPair, + IgnoreDevices: true, + NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir, + NoOverwriteNonDirDir: !cpOpts.OverwriteDirNonDir, + } + if (!containerInfo.IsDir && !hostInfo.IsDir) || resolvedToHostParentDir { + // If we're having a file-to-file copy, make sure to + // rename accordingly. + putOptions.Rename = map[string]string{filepath.Base(containerTarget): hostBaseName} + } + dir := hostInfo.LinkTarget + if !hostInfo.IsDir { + dir = filepath.Dir(dir) + } + if err := buildahCopiah.Put(dir, "", putOptions, reader); err != nil { + return fmt.Errorf("copying to host: %w", err) + } + return nil + } + + containerCopy := func() error { + defer writer.Close() + copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), container, containerTarget, writer) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return fmt.Errorf("copying from container: %w", err) + } + return nil + } + return doCopy(containerCopy, hostCopy) +} + +// copyToContainer copies the hostPath to containerPath on the container. +func copyToContainer(container string, containerPath string, hostPath string) error { + if err := containerMustExist(container); err != nil { + return err + } + + isStdin := false + if hostPath == "-" { + hostPath = os.Stdin.Name() + isStdin = true + } + + // Make sure that host path exists. + hostInfo, err := copy.ResolveHostPath(hostPath) + if err != nil { + return fmt.Errorf("%q could not be found on the host: %w", hostPath, err) + } + + containerBaseName, containerInfo, containerResolvedToParentDir, err := resolvePathOnDestinationContainer(container, containerPath, isStdin) + if err != nil { + return err + } + + // If we copy a directory via the "." notation and the container path + // does not exist, we need to make sure that the destination on the + // container gets created; otherwise the contents of the source + // directory will be written to the destination's parent directory. + // + // Hence, whenever "." is the source and the destination does not + // exist, we copy the source's parent and let the copier package create + // the destination via the Rename option. + hostTarget := hostInfo.LinkTarget + if containerResolvedToParentDir && hostInfo.IsDir && filepath.Base(hostTarget) == "." { + hostTarget = filepath.Dir(hostTarget) + } + + var stdinFile string + if isStdin { + if !containerInfo.IsDir { + return errors.New("destination must be a directory when copying from stdin") + } + + // Copy from stdin to a temporary file *before* throwing it + // over the wire. This allows for proper client-side error + // reporting. + tmpFile, err := os.CreateTemp("", "") + if err != nil { + return err + } + _, err = io.Copy(tmpFile, os.Stdin) + if err != nil { + return err + } + if err = tmpFile.Close(); err != nil { + return err + } + if !archive.IsArchivePath(tmpFile.Name()) { + return errors.New("source must be a (compressed) tar archive when copying from stdin") + } + stdinFile = tmpFile.Name() + } + + if hostInfo.IsDir && !containerInfo.IsDir { + return errors.New("destination must be a directory when copying a directory") + } + + reader, writer := io.Pipe() + hostCopy := func() error { + defer writer.Close() + if isStdin { + stream, err := os.Open(stdinFile) + if err != nil { + return err + } + defer stream.Close() + _, err = io.Copy(writer, stream) + return err + } + + getOptions := buildahCopiah.GetOptions{ + // Unless the specified path points to ".", we want to + // copy the base directory. + KeepDirectoryNames: hostInfo.IsDir && filepath.Base(hostTarget) != ".", + } + if (!hostInfo.IsDir && !containerInfo.IsDir) || containerResolvedToParentDir { + // If we're having a file-to-file copy, make sure to + // rename accordingly. + getOptions.Rename = map[string]string{filepath.Base(hostTarget): containerBaseName} + } + if err := buildahCopiah.Get("/", "", getOptions, []string{hostTarget}, writer); err != nil { + return fmt.Errorf("copying from host: %w", err) + } + return nil + } + + containerCopy := func() error { + defer reader.Close() + target := containerInfo.FileInfo.LinkTarget + if !containerInfo.IsDir { + target = filepath.Dir(target) + } + + copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader, entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir}) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return fmt.Errorf("copying to container: %w", err) + } + return nil + } + + return doCopy(hostCopy, containerCopy) +} + +// resolvePathOnDestinationContainer resolves the specified path on the +// container. If the path does not exist, it attempts to use the parent +// directory. +func resolvePathOnDestinationContainer(container string, containerPath string, isStdin bool) (baseName string, containerInfo *entities.ContainerStatReport, resolvedToParentDir bool, err error) { + containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) + if err == nil { + baseName = filepath.Base(containerInfo.LinkTarget) + return //nolint: nilerr + } + + if strings.HasSuffix(containerPath, "/") { + err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) + return + } + if isStdin { + err = errors.New("destination must be a directory when copying from stdin") + return + } + + // NOTE: containerInfo may actually be set. That happens when + // the container path is a symlink into nirvana. In that case, + // we must use the symlinked path instead. + path := containerPath + if containerInfo != nil { + baseName = filepath.Base(containerInfo.LinkTarget) + path = containerInfo.LinkTarget + } else { + baseName = filepath.Base(containerPath) + } + + parentDir, err := containerParentDir(container, path) + if err != nil { + err = fmt.Errorf("could not determine parent dir of %q on container %s: %w", path, container, err) + return + } + + containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir) + if err != nil { + err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) + return + } + + resolvedToParentDir = true + return baseName, containerInfo, resolvedToParentDir, nil +} + +// containerParentDir returns the parent directory of the specified path on the +// container. If the path is relative, it will be resolved relative to the +// container's working directory (or "/" if the work dir isn't set). +func containerParentDir(container string, containerPath string) (string, error) { + if filepath.IsAbs(containerPath) { + return filepath.Dir(containerPath), nil + } + inspectData, _, err := registry.ContainerEngine().ContainerInspect(registry.GetContext(), []string{container}, entities.InspectOptions{}) + if err != nil { + return "", err + } + if len(inspectData) != 1 { + return "", fmt.Errorf("inspecting container %q: expected 1 data item but got %d", container, len(inspectData)) + } + workDir := filepath.Join("/", inspectData[0].Config.WorkingDir) + workDir = filepath.Join(workDir, containerPath) + return filepath.Dir(workDir), nil +} + +// validateFileInfo returns an error if the specified FileInfo doesn't point to +// a directory or a regular file. +func validateFileInfo(info *copy.FileInfo) error { + if info.Mode.IsDir() || info.Mode.IsRegular() { + return nil + } + return fmt.Errorf("%q must be a directory or a regular file", info.LinkTarget) +} diff --git a/cmd/podman/containers/cp_remote.go b/cmd/podman/containers/cp_remote.go new file mode 100644 index 0000000000..80b1e7cbfc --- /dev/null +++ b/cmd/podman/containers/cp_remote.go @@ -0,0 +1,290 @@ +//go:build remote + +package containers + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/pkg/copy" + "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/spf13/cobra" +) + +func cp(cmd *cobra.Command, args []string) error { + // Parse user input. + sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(args[0], args[1]) + if err != nil { + return err + } + + if len(sourceContainerStr) > 0 && len(destContainerStr) > 0 { + return errors.New("copying between containers is not supported with podman-remote") + } else if len(sourceContainerStr) > 0 { + return copyFromContainerRemote(sourceContainerStr, sourcePath, destPath) + } + + return copyToContainerRemote(destContainerStr, destPath, sourcePath) +} + +// copyFromContainerRemote copies from the containerPath on the container to hostPath. +func copyFromContainerRemote(container string, containerPath string, hostPath string) error { + isStdout := hostPath == "-" + if isStdout { + hostPath = os.Stdout.Name() + } + + // Get the file info from the container to know what we're copying + containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) + if err != nil { + return fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) + } + + reader, writer := io.Pipe() + + // Start copying from the container in a goroutine + var copyErr error + go func() { + defer writer.Close() + copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), container, containerInfo.LinkTarget, writer) + if err != nil { + copyErr = err + return + } + copyErr = copyFunc() + }() + + // Extract the tar archive to the host + defer reader.Close() + + if isStdout { + _, err := io.Copy(os.Stdout, reader) + return err + } + + // Extract tar to destination + if err := extractTar(reader, hostPath, containerInfo.IsDir); err != nil { + return err + } + + return copyErr +} + +// copyToContainerRemote copies from hostPath to the containerPath on the container. +func copyToContainerRemote(container string, containerPath string, hostPath string) error { + isStdin := hostPath == "-" + if isStdin { + hostPath = os.Stdin.Name() + } + + // Get info about the host path + hostInfo, err := os.Stat(hostPath) + if err != nil { + return fmt.Errorf("%q could not be found on the host: %w", hostPath, err) + } + + // Get info about the container destination path + containerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) + var containerExists bool + var containerIsDir bool + var containerResolvedToParentDir bool + var targetPath string + var containerBaseName string + + if err != nil { + // Container path doesn't exist + // If path has trailing /, it must be a directory (error if it doesn't exist) + if strings.HasSuffix(containerPath, "/") { + return fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err) + } + containerExists = false + + // If we're copying contents only (source ends with /.), use the dest path directly + // The server will create it as a directory + if strings.HasSuffix(hostPath, "/.") { + targetPath = containerPath + containerResolvedToParentDir = false + } else { + // Otherwise, use parent directory and rename + containerResolvedToParentDir = true + targetPath = filepath.Dir(containerPath) + containerBaseName = filepath.Base(containerPath) + } + } else { + containerExists = true + containerIsDir = containerInfo.IsDir + if containerIsDir { + // Destination is a directory - extract into it + targetPath = containerPath + } else { + // Destination is a file - use parent directory + targetPath = filepath.Dir(containerInfo.LinkTarget) + containerBaseName = filepath.Base(containerInfo.LinkTarget) + } + } + + // Validate: can't copy directory to a file + if hostInfo.IsDir() && containerExists && !containerIsDir { + return errors.New("destination must be a directory when copying a directory") + } + + reader, writer := io.Pipe() + + // Create tar archive from host path in a goroutine + var tarErr error + go func() { + defer writer.Close() + if isStdin { + _, tarErr = io.Copy(writer, os.Stdin) + } else { + tarErr = createTar(hostPath, writer) + } + }() + + // Copy the archive to the container + defer reader.Close() + + copyOptions := entities.CopyOptions{ + Chown: chown, + } + + // If we're copying to a non-existent path or file-to-file, use Rename + // But NOT when copying contents only (hostPath ends with /.) + if ((!hostInfo.IsDir() && !containerIsDir) || containerResolvedToParentDir) && !strings.HasSuffix(hostPath, "/.") { + copyOptions.Rename = map[string]string{filepath.Base(hostPath): containerBaseName} + } + + copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, targetPath, reader, copyOptions) + if err != nil { + return err + } + + if err := copyFunc(); err != nil { + return err + } + + return tarErr +} + +// createTar creates a tar archive from the specified path +func createTar(sourcePath string, writer io.Writer) error { + tw := tar.NewWriter(writer) + defer tw.Close() + + // Check if we should copy contents only (path ends with /.) + // vs copying the directory itself + // Must check BEFORE cleaning, as Clean will remove the trailing /. + keepDirName := !strings.HasSuffix(sourcePath, "/.") + cleanPath := filepath.Clean(sourcePath) + baseDir := filepath.Dir(cleanPath) + + return filepath.Walk(cleanPath, func(file string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(fi, file) + if err != nil { + return err + } + + // Update the name to be relative + var relPath string + if keepDirName { + // Include the directory name in the archive + relPath, err = filepath.Rel(baseDir, file) + } else { + // Copy contents only, exclude the directory name + relPath, err = filepath.Rel(cleanPath, file) + if relPath == "." { + // Skip the directory itself when copying contents + return nil + } + } + if err != nil { + return err + } + header.Name = relPath + + if err := tw.WriteHeader(header); err != nil { + return err + } + + if !fi.IsDir() { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(tw, f); err != nil { + return err + } + } + + return nil + }) +} + +// extractTar extracts a tar archive to the specified destination +func extractTar(reader io.Reader, destPath string, isDir bool) error { + tr := tar.NewReader(reader) + + // Check if destination exists + destInfo, destErr := os.Stat(destPath) + destExists := destErr == nil + destIsDir := destExists && destInfo.IsDir() + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + var target string + // If dest doesn't exist and we're extracting a single file, use dest as the filename + if !destExists && !isDir && header.Typeflag == tar.TypeReg { + target = destPath + } else if destIsDir { + // Dest is a directory, extract into it + target = filepath.Join(destPath, header.Name) + } else { + // Dest exists but isn't a directory, or we're extracting a directory + target = filepath.Join(destPath, header.Name) + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + // Ensure parent directory exists + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return err + } + + f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) + if err != nil { + return err + } + + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return err + } + f.Close() + } + } + + return nil +}