mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-10-25 07:17:35 +00:00
Add build-tagged splits for cp command to support podman-remote
The original cp.go uses buildah/copier which depends on libimage,
making it unavailable in remote builds. This change splits cp
implementation into separate local and remote variants:
- cmd/podman/containers/cp.go: Shared command definitions, flags, and
init function (no build tag)
- cmd/podman/containers/cp_local.go: Local implementation using
buildah/copier (//go:build !remote)
- cmd/podman/containers/cp_remote.go: Remote implementation using
stdlib archive/tar (//go:build remote)
The remote implementation:
- Uses ContainerEngine interface methods that work over REST API:
ContainerStat, ContainerCopyToArchive, ContainerCopyFromArchive
- Properly handles file and directory copying in both directions
- Uses CopyOptions.Rename for file renaming when copying to specific
filenames
- Returns error for container-to-container copying (not supported with
podman-remote)
This follows the same pattern used for other libimage-dependent code
in commit 4231526e18.
Fixes system tests that require cp functionality with podman-remote.
Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
This commit is contained in:
parent
ca1a0db4e7
commit
8fd34d5bfd
3 changed files with 758 additions and 461 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
468
cmd/podman/containers/cp_local.go
Normal file
468
cmd/podman/containers/cp_local.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
290
cmd/podman/containers/cp_remote.go
Normal file
290
cmd/podman/containers/cp_remote.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue