-
Notifications
You must be signed in to change notification settings - Fork 565
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
2 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |