Skip to content

Commit

Permalink
cp cmd
Browse files Browse the repository at this point in the history
Signed-off-by: Fahed DORGAA <fahed_dorgaa@carrefour.com>

fixes

Signed-off-by: fahed dorgaa <fahed.dorgaa@gmail.com>

fixes

Signed-off-by: fahed dorgaa <fahed.dorgaa@gmail.com>

fixes

Signed-off-by: fahed dorgaa <fahed.dorgaa@gmail.com>

fixes

Signed-off-by: fahed dorgaa <fahed.dorgaa@gmail.com>
  • Loading branch information
Fahed DORGAA authored and fahedouch committed Dec 20, 2021
1 parent fa694a8 commit ada4497
Show file tree
Hide file tree
Showing 2 changed files with 304 additions and 0 deletions.
229 changes: 229 additions & 0 deletions cmd/nerdctl/cp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"

"github.com/docker/docker/pkg/system"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

type cpConfig struct {
container string
followLink bool
sourcePath string
destPath string
}

var errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [container:]file/path")

func newCpCommand() *cobra.Command {

shortHelp := "Copy files/folders between a container and the local filesystem"
longHelp := shortHelp + "\nNOTE: Use '-' as the source to read a tar archive from stdin\n and extract it to a directory destination in a container. \nUse '-' as the destination to stream a tar archive of a\ncontainer source to stdout."

var cpCommand = &cobra.Command{
Use: "cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-",
Args: cobra.MinimumNArgs(2),
Short: shortHelp,
Long: longHelp,
RunE: cpAction,
//ValidArgsFunction: cpShellComplete,
SilenceUsage: true,
SilenceErrors: true,
}

cpCommand.Flags().BoolP("follow-link", "L", false, "Always follow symbol link in SRC_PATH")
return cpCommand
}

func cpAction(cmd *cobra.Command, args []string) error {
srcSpec, err := splitCpArg(args[0])
if err != nil {
return err
}

destSpec, err := splitCpArg(args[0])
if err != nil {
return err
}

flagL, err := cmd.Flags().GetBool("follow-link")
if err != nil {
return err
}

copyConfig := cpConfig{
followLink: flagL,
sourcePath: srcSpec.Path,
destPath: destSpec.Path,
}

if len(srcSpec.Container) != 0 && len(destSpec.Container) != 0 {
return fmt.Errorf("one of src or dest must be a local file specification")
}
if len(srcSpec.Path) == 0 || len(destSpec.Path) == 0 {
return errors.New("filepath can not be empty")
}

_, ctx, cancel, err := newClient(cmd)
if err != nil {
return err
}
defer cancel()

if len(srcSpec.Container) != 0 {
copyConfig.container = srcSpec.Container
return copyFromContainer(ctx, cmd, copyConfig)
}
if len(destSpec.Container) != 0 {
copyConfig.container = destSpec.Container
return nil
}
return fmt.Errorf("one of src or dest must be a remote file specification")

}

func splitCpArg(arg string) (fileSpec, error) {
i := strings.Index(arg, ":")

// filespec starting with a semicolon is invalid
if i == 0 {
return fileSpec{}, errFileSpecDoesntMatchFormat
}

if system.IsAbs(arg) {
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
return fileSpec{
Path: arg,
}, nil
}

parts := strings.SplitN(arg, ":", 2)

if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
// Either there's no `:` in the arg
// OR it's an explicit local relative path like `./file:name.txt`.
return fileSpec{
Path: arg,
}, nil
}

return fileSpec{
Container: parts[0],
Path: parts[1],
}, nil
}

type fileSpec struct {
Container string
Path string
}

func copyFromContainer(ctx context.Context, cmd *cobra.Command, copyConfig cpConfig) (err error) {
dstPath := copyConfig.destPath
srcPath := copyConfig.sourcePath

if dstPath != "-" {
if dstPath, err = filepath.Abs(dstPath); err != nil {
return err
}
}

dir := filepath.Dir(filepath.Clean(dstPath))
if dir != "" && dir != "." {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return fmt.Errorf("invalid output path: directory %q does not exist", dir)
}
}

reader := newTarPipe(src, o)

tarReader := tar.NewReader(reader)
for {
header, err := tarReader.Next()
if err != nil {
if err != io.EOF {
return err
}
break
}

if copyConfig.followLink {
if mode&os.ModeSymlink != 0 {
linkTarget := header.FileInfo().Linkname()
if !filepath.IsAbs(linkTarget) {
// Join with the parent directory.
srcDir, _ := SplitPathDirEntry(srcPath)
linkTarget = filepath.Join(srcDir, linkTarget)
}

if isCurrentDir(srcDir) &&
!isCurrentDir(linkTarget) {
linkTarget += string(filepath.Separator) + "."
}

if hasTrailingPathSeparator(srcDir, os.PathSeparator) &&
!hasTrailingPathSeparator(linkTarget, os.PathSeparator) {
linkTarget += string(filepath.Separator)
}

srcPath = linkTarget
}
}

// srcPath Cleaning
trimmedPath := strings.TrimLeft(srcPath, `/\`) // tar strips the leading '/' and '\' if it's there, so we will too
cleanedPath := filepath.Clean(trimmed)
prefix := stripPathShortcuts(cleaned.String())

// basic file information
mode := header.FileInfo().Mode()

// header.Name is a name of the REMOTE file, so we need to create
// a remotePath that will goes through appropriate cleaning process
destFileName := path.Join(dstPath, filepath.Clean(header.Name[len(prefix):]))

if !isRelative(dstPath, destFileName) {
logrus.Warnf("skipping %q: file is outside target destination", destFileName)
continue
}

// docker does not manage the creation of non-existent path
if err := os.MkdirAll(destFileName.Dir().String(), 0755); err != nil {
return err
}
if header.FileInfo().IsDir() {
if err := os.MkdirAll(destFileName.String(), 0755); err != nil {
return err
}
continue
}

}

}
75 changes: 75 additions & 0 deletions pkg/copyutil/copyutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package copyutil

import (
"os"
"path/filepath"
"strings"
)

// strips trailing slash (if any) both unix and windows style
func stripTrailingSlash(file string) string {
if len(file) == 0 {
return file
}
if file != "/" && strings.HasSuffix(string(file[len(file)-1]), "/") {
return file[:len(file)-1]
}
return file
}

// SplitPathDirEntry splits the given path between its directory name and its
// basename. We assume that remote files are both linux and windows containers.
// So we first replace each slash ('/') character in path with a separator character
// then we clean the path.
// A trailing "." is preserved if the original path specified the current directory.
func SplitPathDirEntry(path string) (dir, base string) {
cleanedPath := filepath.Clean(filepath.FromSlash(path))

if filepath.Base(path) == "." {
cleanedPath += string(os.PathSeparator) + "."
}

return filepath.Dir(cleanedPath), filepath.Base(cleanedPath)
}

// isCurrentDir check whether the given path specifies
// a "current directory", i.e., the last path segment is `.`.
func isCurrentDir(path string) bool {
return filepath.Base(path) == "."
}

// hasTrailingPathSeparator returns whether the given
// path ends with the system's path separator character.
func hasTrailingPathSeparator(path string, sep byte) bool {
return len(path) > 0 && strings.HasSuffix(string(path[len(path)-1]), sep)
}

func isRelative(base, target string) bool {
relative, err := filepath.Rel(base, target)
if err != nil {
return false
}
return relative == "." || relative == stripPathShortcuts(relative)
}

// stripPathShortcuts removes any leading or trailing "../" from a given path
func stripPathShortcuts(p string) string {
newPath := p
trimmed := strings.TrimPrefix(newPath, "../")

for trimmed != newPath {
newPath = trimmed
trimmed = strings.TrimPrefix(newPath, "../")
}

// trim leftover {".", ".."}
if newPath == "." || newPath == ".." {
newPath = ""
}

if len(newPath) > 0 && string(newPath[0]) == "/" {
return newPath[1:]
}

return newPath
}

0 comments on commit ada4497

Please sign in to comment.