diff options
Diffstat (limited to 'pkg/copy')
-rw-r--r-- | pkg/copy/copy.go | 68 | ||||
-rw-r--r-- | pkg/copy/fileinfo.go | 56 | ||||
-rw-r--r-- | pkg/copy/item.go | 13 | ||||
-rw-r--r-- | pkg/copy/parse.go | 61 |
4 files changed, 167 insertions, 31 deletions
diff --git a/pkg/copy/copy.go b/pkg/copy/copy.go index 0e68eb450..13893deb2 100644 --- a/pkg/copy/copy.go +++ b/pkg/copy/copy.go @@ -25,31 +25,61 @@ import ( // // **************************************************************************** -// Copy the source item to destination. Use extract to untar the source if -// it's a tar archive. -func Copy(source *CopyItem, destination *CopyItem, extract bool) error { +// Copier copies data from a source to a destination CopyItem. +type Copier struct { + copyFunc func() error + cleanUpFuncs []deferFunc +} + +// cleanUp releases resources the Copier may hold open. +func (c *Copier) cleanUp() { + for _, f := range c.cleanUpFuncs { + f() + } +} + +// Copy data from a source to a destination CopyItem. +func (c *Copier) Copy() error { + defer c.cleanUp() + return c.copyFunc() +} + +// GetCopiers returns a Copier to copy the source item to destination. Use +// extract to untar the source if it's a tar archive. +func GetCopier(source *CopyItem, destination *CopyItem, extract bool) (*Copier, error) { + copier := &Copier{} + // First, do the man-page dance. See podman-cp(1) for details. if err := enforceCopyRules(source, destination); err != nil { - return err + return nil, err } // Destination is a stream (e.g., stdout or an http body). if destination.info.IsStream { // Source is a stream (e.g., stdin or an http body). if source.info.IsStream { - _, err := io.Copy(destination.writer, source.reader) - return err + copier.copyFunc = func() error { + _, err := io.Copy(destination.writer, source.reader) + return err + } + return copier, nil } root, glob, err := source.buildahGlobs() if err != nil { - return err + return nil, err } - return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer) + copier.copyFunc = func() error { + return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer) + } + return copier, nil } // Destination is either a file or a directory. if source.info.IsStream { - return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader) + copier.copyFunc = func() error { + return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader) + } + return copier, nil } tarOptions := &archive.TarOptions{ @@ -71,33 +101,36 @@ func Copy(source *CopyItem, destination *CopyItem, extract bool) error { var tarReader io.ReadCloser if extract && archive.IsArchivePath(source.resolved) { if !destination.info.IsDir { - return errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original) + return nil, errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original) } reader, err := os.Open(source.resolved) if err != nil { - return err + return nil, err } - defer reader.Close() + copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() }) // The stream from stdin may be compressed (e.g., via gzip). decompressedStream, err := archive.DecompressStream(reader) if err != nil { - return err + return nil, err } - defer decompressedStream.Close() + copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { decompressedStream.Close() }) tarReader = decompressedStream } else { reader, err := archive.TarWithOptions(source.resolved, tarOptions) if err != nil { - return err + return nil, err } - defer reader.Close() + copier.cleanUpFuncs = append(copier.cleanUpFuncs, func() { reader.Close() }) tarReader = reader } - return buildahCopiah.Put(root, dir, source.putOptions(), tarReader) + copier.copyFunc = func() error { + return buildahCopiah.Put(root, dir, source.putOptions(), tarReader) + } + return copier, nil } // enforceCopyRules enforces the rules for copying from a source to a @@ -114,7 +147,6 @@ func enforceCopyRules(source, destination *CopyItem) error { return nil } - // Source is a *stream*. if source.info.IsStream { if !(destination.info.IsDir || destination.info.IsStream) { return errors.New("destination must be a directory or stream when copying from a stream") diff --git a/pkg/copy/fileinfo.go b/pkg/copy/fileinfo.go new file mode 100644 index 000000000..08b4eb377 --- /dev/null +++ b/pkg/copy/fileinfo.go @@ -0,0 +1,56 @@ +package copy + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "os" + "strings" + "time" + + "github.com/pkg/errors" +) + +// XDockerContainerPathStatHeader is the *key* in http headers pointing to the +// base64 encoded JSON payload of stating a path in a container. +const XDockerContainerPathStatHeader = "X-Docker-Container-Path-Stat" + +// FileInfo describes a file or directory and is returned by +// (*CopyItem).Stat(). +type FileInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mode os.FileMode `json:"mode"` + ModTime time.Time `json:"mtime"` + IsDir bool `json:"isDir"` + IsStream bool `json:"isStream"` + LinkTarget string `json:"linkTarget"` +} + +// EncodeFileInfo serializes the specified FileInfo as a base64 encoded JSON +// payload. Intended for Docker compat. +func EncodeFileInfo(info *FileInfo) (string, error) { + buf, err := json.Marshal(&info) + if err != nil { + return "", errors.Wrap(err, "failed to serialize file stats") + } + return base64.URLEncoding.EncodeToString(buf), nil +} + +// ExtractFileInfoFromHeader extracts a base64 encoded JSON payload of a +// FileInfo in the http header. If no such header entry is found, nil is +// returned. Intended for Docker compat. +func ExtractFileInfoFromHeader(header *http.Header) (*FileInfo, error) { + rawData := header.Get(XDockerContainerPathStatHeader) + if len(rawData) == 0 { + return nil, nil + } + + info := FileInfo{} + base64Decoder := base64.NewDecoder(base64.URLEncoding, strings.NewReader(rawData)) + if err := json.NewDecoder(base64Decoder).Decode(&info); err != nil { + return nil, err + } + + return &info, nil +} diff --git a/pkg/copy/item.go b/pkg/copy/item.go index db6bca610..df8bf30b9 100644 --- a/pkg/copy/item.go +++ b/pkg/copy/item.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "strings" - "time" buildahCopiah "github.com/containers/buildah/copier" "github.com/containers/buildah/pkg/chrootuser" @@ -75,18 +74,6 @@ type CopyItem struct { // deferFunc allows for returning functions that must be deferred at call sites. type deferFunc func() -// FileInfo describes a file or directory and is returned by -// (*CopyItem).Stat(). -type FileInfo struct { - Name string `json:"name"` - Size int64 `json:"size"` - Mode os.FileMode `json:"mode"` - ModTime time.Time `json:"mtime"` - IsDir bool `json:"isDir"` - IsStream bool `json:"isStream"` - LinkTarget string `json:"linkTarget"` -} - // Stat returns the FileInfo. func (item *CopyItem) Stat() (*FileInfo, error) { return &item.info, item.statError diff --git a/pkg/copy/parse.go b/pkg/copy/parse.go new file mode 100644 index 000000000..39e0e1547 --- /dev/null +++ b/pkg/copy/parse.go @@ -0,0 +1,61 @@ +package copy + +import ( + "strings" + + "github.com/pkg/errors" +) + +// ParseSourceAndDestination parses the source and destination input into a +// possibly specified container and path. The input format is described in +// podman-cp(1) as "[nameOrID:]path". Colons in paths are supported as long +// they start with a dot or slash. +// +// It returns, in order, the source container and path, followed by the +// destination container and path, and an error. Note that exactly one +// container must be specified. +func ParseSourceAndDestination(source, destination string) (string, string, string, string, error) { + sourceContainer, sourcePath := parseUserInput(source) + destContainer, destPath := parseUserInput(destination) + + numContainers := 0 + if len(sourceContainer) > 0 { + numContainers++ + } + if len(destContainer) > 0 { + numContainers++ + } + + if numContainers != 1 { + return "", "", "", "", errors.Errorf("invalid arguments %q, %q: exactly 1 container expected but %d specified", source, destination, numContainers) + } + + if len(sourcePath) == 0 || len(destPath) == 0 { + return "", "", "", "", errors.Errorf("invalid arguments %q, %q: you must specify paths", source, destination) + } + + return sourceContainer, sourcePath, destContainer, destPath, nil +} + +// parseUserInput parses the input string and returns, if specified, the name +// or ID of the container and the path. The input format is described in +// podman-cp(1) as "[nameOrID:]path". Colons in paths are supported as long +// they start with a dot or slash. +func parseUserInput(input string) (container string, path string) { + if len(input) == 0 { + return + } + path = input + + // If the input starts with a dot or slash, it cannot refer to a + // container. + if input[0] == '.' || input[0] == '/' { + return + } + + if spl := strings.SplitN(path, ":", 2); len(spl) == 2 { + container = spl[0] + path = spl[1] + } + return +} |