|
@@ -11,6 +11,7 @@ import (
|
|
|
"time"
|
|
|
|
|
|
"github.com/containerd/continuity/fs"
|
|
|
+ "github.com/docker/docker/pkg/fileutils"
|
|
|
"github.com/pkg/errors"
|
|
|
)
|
|
|
|
|
@@ -86,7 +87,10 @@ func Copy(ctx context.Context, srcRoot, src, dstRoot, dst string, opts ...Opt) e
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- c := newCopier(ci.Chown, ci.Utime, ci.Mode, ci.XAttrErrorHandler)
|
|
|
+ c, err := newCopier(ci.Chown, ci.Utime, ci.Mode, ci.XAttrErrorHandler, ci.IncludePatterns, ci.ExcludePatterns)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
srcs := []string{src}
|
|
|
|
|
|
if ci.AllowWildcards {
|
|
@@ -109,7 +113,8 @@ func Copy(ctx context.Context, srcRoot, src, dstRoot, dst string, opts ...Opt) e
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- if err := c.copy(ctx, srcFollowed, dst, false); err != nil {
|
|
|
+ skipIncludePatterns := c.includePatternMatcher == nil
|
|
|
+ if err := c.copy(ctx, srcFollowed, "", dst, false, skipIncludePatterns); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
@@ -162,6 +167,10 @@ type CopyInfo struct {
|
|
|
XAttrErrorHandler XAttrErrorHandler
|
|
|
CopyDirContents bool
|
|
|
FollowLinks bool
|
|
|
+ // Include only files/dirs matching at least one of these patterns
|
|
|
+ IncludePatterns []string
|
|
|
+ // Exclude files/dir matching any of these patterns (even if they match an include pattern)
|
|
|
+ ExcludePatterns []string
|
|
|
}
|
|
|
|
|
|
type Opt func(*CopyInfo)
|
|
@@ -197,36 +206,112 @@ func AllowXAttrErrors(ci *CopyInfo) {
|
|
|
WithXAttrErrorHandler(h)(ci)
|
|
|
}
|
|
|
|
|
|
+func WithIncludePattern(includePattern string) Opt {
|
|
|
+ return func(ci *CopyInfo) {
|
|
|
+ ci.IncludePatterns = append(ci.IncludePatterns, includePattern)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func WithExcludePattern(excludePattern string) Opt {
|
|
|
+ return func(ci *CopyInfo) {
|
|
|
+ ci.ExcludePatterns = append(ci.ExcludePatterns, excludePattern)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
type copier struct {
|
|
|
- chown Chowner
|
|
|
- utime *time.Time
|
|
|
- mode *int
|
|
|
- inodes map[uint64]string
|
|
|
- xattrErrorHandler XAttrErrorHandler
|
|
|
+ chown Chowner
|
|
|
+ utime *time.Time
|
|
|
+ mode *int
|
|
|
+ inodes map[uint64]string
|
|
|
+ xattrErrorHandler XAttrErrorHandler
|
|
|
+ includePatternMatcher *fileutils.PatternMatcher
|
|
|
+ excludePatternMatcher *fileutils.PatternMatcher
|
|
|
+ parentDirs []parentDir
|
|
|
+}
|
|
|
+
|
|
|
+type parentDir struct {
|
|
|
+ srcPath string
|
|
|
+ dstPath string
|
|
|
+ copied bool
|
|
|
}
|
|
|
|
|
|
-func newCopier(chown Chowner, tm *time.Time, mode *int, xeh XAttrErrorHandler) *copier {
|
|
|
+func newCopier(chown Chowner, tm *time.Time, mode *int, xeh XAttrErrorHandler, includePatterns, excludePatterns []string) (*copier, error) {
|
|
|
if xeh == nil {
|
|
|
xeh = func(dst, src, key string, err error) error {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
- return &copier{inodes: map[uint64]string{}, chown: chown, utime: tm, xattrErrorHandler: xeh, mode: mode}
|
|
|
+
|
|
|
+ var includePatternMatcher *fileutils.PatternMatcher
|
|
|
+ if len(includePatterns) != 0 {
|
|
|
+ var err error
|
|
|
+ includePatternMatcher, err = fileutils.NewPatternMatcher(includePatterns)
|
|
|
+ if err != nil {
|
|
|
+ return nil, errors.Wrapf(err, "invalid includepatterns: %s", includePatterns)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var excludePatternMatcher *fileutils.PatternMatcher
|
|
|
+ if len(excludePatterns) != 0 {
|
|
|
+ var err error
|
|
|
+ excludePatternMatcher, err = fileutils.NewPatternMatcher(excludePatterns)
|
|
|
+ if err != nil {
|
|
|
+ return nil, errors.Wrapf(err, "invalid excludepatterns: %s", excludePatterns)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return &copier{
|
|
|
+ inodes: map[uint64]string{},
|
|
|
+ chown: chown,
|
|
|
+ utime: tm,
|
|
|
+ xattrErrorHandler: xeh,
|
|
|
+ mode: mode,
|
|
|
+ includePatternMatcher: includePatternMatcher,
|
|
|
+ excludePatternMatcher: excludePatternMatcher,
|
|
|
+ }, nil
|
|
|
}
|
|
|
|
|
|
// dest is always clean
|
|
|
-func (c *copier) copy(ctx context.Context, src, target string, overwriteTargetMetadata bool) error {
|
|
|
+func (c *copier) copy(ctx context.Context, src, srcComponents, target string, overwriteTargetMetadata, skipIncludePatterns bool) error {
|
|
|
select {
|
|
|
case <-ctx.Done():
|
|
|
return ctx.Err()
|
|
|
default:
|
|
|
}
|
|
|
+
|
|
|
fi, err := os.Lstat(src)
|
|
|
if err != nil {
|
|
|
return errors.Wrapf(err, "failed to stat %s", src)
|
|
|
}
|
|
|
|
|
|
+ include := true
|
|
|
+ if srcComponents != "" {
|
|
|
+ if !skipIncludePatterns {
|
|
|
+ include, err = c.include(srcComponents, fi)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ exclude, err := c.exclude(srcComponents, fi)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if exclude {
|
|
|
+ include = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if include {
|
|
|
+ if err := c.createParentDirs(src, srcComponents, target, overwriteTargetMetadata); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if !fi.IsDir() {
|
|
|
+ if !include {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
if err := ensureEmptyFileTarget(target); err != nil {
|
|
|
return err
|
|
|
}
|
|
@@ -236,9 +321,9 @@ func (c *copier) copy(ctx context.Context, src, target string, overwriteTargetMe
|
|
|
|
|
|
switch {
|
|
|
case fi.IsDir():
|
|
|
- if created, err := c.copyDirectory(ctx, src, target, fi, overwriteTargetMetadata); err != nil {
|
|
|
+ if created, err := c.copyDirectory(ctx, src, srcComponents, target, fi, overwriteTargetMetadata, skipIncludePatterns, include); err != nil {
|
|
|
return err
|
|
|
- } else if !overwriteTargetMetadata {
|
|
|
+ } else if !overwriteTargetMetadata || !skipIncludePatterns {
|
|
|
copyFileInfo = created
|
|
|
}
|
|
|
case (fi.Mode() & os.ModeType) == 0:
|
|
@@ -282,36 +367,101 @@ func (c *copier) copy(ctx context.Context, src, target string, overwriteTargetMe
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-func (c *copier) copyDirectory(ctx context.Context, src, dst string, stat os.FileInfo, overwriteTargetMetadata bool) (bool, error) {
|
|
|
+func (c *copier) include(path string, fi os.FileInfo) (bool, error) {
|
|
|
+ if c.includePatternMatcher == nil {
|
|
|
+ return false, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ m, err := c.includePatternMatcher.Matches(path)
|
|
|
+ if err != nil {
|
|
|
+ return false, errors.Wrap(err, "failed to match includepatterns")
|
|
|
+ }
|
|
|
+ return m, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (c *copier) exclude(path string, fi os.FileInfo) (bool, error) {
|
|
|
+ if c.excludePatternMatcher == nil {
|
|
|
+ return false, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ m, err := c.excludePatternMatcher.Matches(path)
|
|
|
+ if err != nil {
|
|
|
+ return false, errors.Wrap(err, "failed to match excludepatterns")
|
|
|
+ }
|
|
|
+ return m, nil
|
|
|
+}
|
|
|
+
|
|
|
+// Delayed creation of parent directories when a file or dir matches an include
|
|
|
+// pattern.
|
|
|
+func (c *copier) createParentDirs(src, srcComponents, target string, overwriteTargetMetadata bool) error {
|
|
|
+ for i, parentDir := range c.parentDirs {
|
|
|
+ if parentDir.copied {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ fi, err := os.Stat(parentDir.srcPath)
|
|
|
+ if err != nil {
|
|
|
+ return errors.Wrapf(err, "failed to stat %s", src)
|
|
|
+ }
|
|
|
+ if !fi.IsDir() {
|
|
|
+ return errors.Errorf("%s is not a directory", parentDir.srcPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ created, err := copyDirectoryOnly(parentDir.srcPath, parentDir.dstPath, fi, overwriteTargetMetadata)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if created {
|
|
|
+ if err := c.copyFileInfo(fi, parentDir.dstPath); err != nil {
|
|
|
+ return errors.Wrap(err, "failed to copy file info")
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := copyXAttrs(parentDir.dstPath, parentDir.srcPath, c.xattrErrorHandler); err != nil {
|
|
|
+ return errors.Wrap(err, "failed to copy xattrs")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ c.parentDirs[i].copied = true
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (c *copier) copyDirectory(ctx context.Context, src, srcComponents, dst string, stat os.FileInfo, overwriteTargetMetadata, skipIncludePatterns, matchedExactly bool) (bool, error) {
|
|
|
if !stat.IsDir() {
|
|
|
return false, errors.Errorf("source is not directory")
|
|
|
}
|
|
|
|
|
|
created := false
|
|
|
|
|
|
- if st, err := os.Lstat(dst); err != nil {
|
|
|
- if !os.IsNotExist(err) {
|
|
|
- return false, err
|
|
|
- }
|
|
|
- created = true
|
|
|
- if err := os.Mkdir(dst, stat.Mode()); err != nil {
|
|
|
- return created, errors.Wrapf(err, "failed to mkdir %s", dst)
|
|
|
- }
|
|
|
- } else if !st.IsDir() {
|
|
|
- return false, errors.Errorf("cannot copy to non-directory: %s", dst)
|
|
|
- } else if overwriteTargetMetadata {
|
|
|
- if err := os.Chmod(dst, stat.Mode()); err != nil {
|
|
|
- return false, errors.Wrapf(err, "failed to chmod on %s", dst)
|
|
|
+ // If there are no include patterns or this directory matched an include
|
|
|
+ // pattern exactly, go ahead and create the directory. Otherwise, delay to
|
|
|
+ // handle include patterns like a/*/c where we do not want to create a/b
|
|
|
+ // until we encounter a/b/c.
|
|
|
+ if matchedExactly || skipIncludePatterns {
|
|
|
+ var err error
|
|
|
+ created, err = copyDirectoryOnly(src, dst, stat, overwriteTargetMetadata)
|
|
|
+ if err != nil {
|
|
|
+ return created, err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ c.parentDirs = append(c.parentDirs, parentDir{
|
|
|
+ srcPath: src,
|
|
|
+ dstPath: dst,
|
|
|
+ copied: skipIncludePatterns,
|
|
|
+ })
|
|
|
+
|
|
|
+ defer func() {
|
|
|
+ c.parentDirs = c.parentDirs[:len(c.parentDirs)-1]
|
|
|
+ }()
|
|
|
+
|
|
|
fis, err := ioutil.ReadDir(src)
|
|
|
if err != nil {
|
|
|
return false, errors.Wrapf(err, "failed to read %s", src)
|
|
|
}
|
|
|
|
|
|
for _, fi := range fis {
|
|
|
- if err := c.copy(ctx, filepath.Join(src, fi.Name()), filepath.Join(dst, fi.Name()), true); err != nil {
|
|
|
+ if err := c.copy(ctx, filepath.Join(src, fi.Name()), filepath.Join(srcComponents, fi.Name()), filepath.Join(dst, fi.Name()), true, skipIncludePatterns); err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
}
|
|
@@ -319,6 +469,25 @@ func (c *copier) copyDirectory(ctx context.Context, src, dst string, stat os.Fil
|
|
|
return created, nil
|
|
|
}
|
|
|
|
|
|
+func copyDirectoryOnly(src, dst string, stat os.FileInfo, overwriteTargetMetadata bool) (bool, error) {
|
|
|
+ if st, err := os.Lstat(dst); err != nil {
|
|
|
+ if !os.IsNotExist(err) {
|
|
|
+ return false, err
|
|
|
+ }
|
|
|
+ if err := os.Mkdir(dst, stat.Mode()); err != nil {
|
|
|
+ return false, errors.Wrapf(err, "failed to mkdir %s", dst)
|
|
|
+ }
|
|
|
+ return true, nil
|
|
|
+ } else if !st.IsDir() {
|
|
|
+ return false, errors.Errorf("cannot copy to non-directory: %s", dst)
|
|
|
+ } else if overwriteTargetMetadata {
|
|
|
+ if err := os.Chmod(dst, stat.Mode()); err != nil {
|
|
|
+ return false, errors.Wrapf(err, "failed to chmod on %s", dst)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false, nil
|
|
|
+}
|
|
|
+
|
|
|
func ensureEmptyFileTarget(dst string) error {
|
|
|
fi, err := os.Lstat(dst)
|
|
|
if err != nil {
|