github.com/iaintshine/docker@v1.8.2/pkg/archive/copy.go (about)

     1  package archive
     2  
     3  import (
     4  	"archive/tar"
     5  	"errors"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	log "github.com/Sirupsen/logrus"
    13  )
    14  
    15  // Errors used or returned by this file.
    16  var (
    17  	ErrNotDirectory      = errors.New("not a directory")
    18  	ErrDirNotExists      = errors.New("no such directory")
    19  	ErrCannotCopyDir     = errors.New("cannot copy directory")
    20  	ErrInvalidCopySource = errors.New("invalid copy source content")
    21  )
    22  
    23  // PreserveTrailingDotOrSeparator returns the given cleaned path (after
    24  // processing using any utility functions from the path or filepath stdlib
    25  // packages) and appends a trailing `/.` or `/` if its corresponding  original
    26  // path (from before being processed by utility functions from the path or
    27  // filepath stdlib packages) ends with a trailing `/.` or `/`. If the cleaned
    28  // path already ends in a `.` path segment, then another is not added. If the
    29  // clean path already ends in a path separator, then another is not added.
    30  func PreserveTrailingDotOrSeparator(cleanedPath, originalPath string) string {
    31  	if !SpecifiesCurrentDir(cleanedPath) && SpecifiesCurrentDir(originalPath) {
    32  		if !HasTrailingPathSeparator(cleanedPath) {
    33  			// Add a separator if it doesn't already end with one (a cleaned
    34  			// path would only end in a separator if it is the root).
    35  			cleanedPath += string(filepath.Separator)
    36  		}
    37  		cleanedPath += "."
    38  	}
    39  
    40  	if !HasTrailingPathSeparator(cleanedPath) && HasTrailingPathSeparator(originalPath) {
    41  		cleanedPath += string(filepath.Separator)
    42  	}
    43  
    44  	return cleanedPath
    45  }
    46  
    47  // AssertsDirectory returns whether the given path is
    48  // asserted to be a directory, i.e., the path ends with
    49  // a trailing '/' or `/.`, assuming a path separator of `/`.
    50  func AssertsDirectory(path string) bool {
    51  	return HasTrailingPathSeparator(path) || SpecifiesCurrentDir(path)
    52  }
    53  
    54  // HasTrailingPathSeparator returns whether the given
    55  // path ends with the system's path separator character.
    56  func HasTrailingPathSeparator(path string) bool {
    57  	return len(path) > 0 && os.IsPathSeparator(path[len(path)-1])
    58  }
    59  
    60  // SpecifiesCurrentDir returns whether the given path specifies
    61  // a "current directory", i.e., the last path segment is `.`.
    62  func SpecifiesCurrentDir(path string) bool {
    63  	return filepath.Base(path) == "."
    64  }
    65  
    66  // SplitPathDirEntry splits the given path between its directory name and its
    67  // basename by first cleaning the path but preserves a trailing "." if the
    68  // original path specified the current directory.
    69  func SplitPathDirEntry(path string) (dir, base string) {
    70  	cleanedPath := filepath.Clean(path)
    71  
    72  	if SpecifiesCurrentDir(path) {
    73  		cleanedPath += string(filepath.Separator) + "."
    74  	}
    75  
    76  	return filepath.Dir(cleanedPath), filepath.Base(cleanedPath)
    77  }
    78  
    79  // TarResource archives the resource described by the given CopyInfo to a Tar
    80  // archive. A non-nil error is returned if sourcePath does not exist or is
    81  // asserted to be a directory but exists as another type of file.
    82  //
    83  // This function acts as a convenient wrapper around TarWithOptions, which
    84  // requires a directory as the source path. TarResource accepts either a
    85  // directory or a file path and correctly sets the Tar options.
    86  func TarResource(sourceInfo CopyInfo) (content Archive, err error) {
    87  	return TarResourceRebase(sourceInfo.Path, sourceInfo.RebaseName)
    88  }
    89  
    90  // TarResourceRebase is like TarResource but renames the first path element of
    91  // items in the resulting tar archive to match the given rebaseName if not "".
    92  func TarResourceRebase(sourcePath, rebaseName string) (content Archive, err error) {
    93  	if _, err = os.Lstat(sourcePath); err != nil {
    94  		// Catches the case where the source does not exist or is not a
    95  		// directory if asserted to be a directory, as this also causes an
    96  		// error.
    97  		return
    98  	}
    99  
   100  	// Separate the source path between it's directory and
   101  	// the entry in that directory which we are archiving.
   102  	sourceDir, sourceBase := SplitPathDirEntry(sourcePath)
   103  
   104  	filter := []string{sourceBase}
   105  
   106  	log.Debugf("copying %q from %q", sourceBase, sourceDir)
   107  
   108  	return TarWithOptions(sourceDir, &TarOptions{
   109  		Compression:      Uncompressed,
   110  		IncludeFiles:     filter,
   111  		IncludeSourceDir: true,
   112  		RebaseNames: map[string]string{
   113  			sourceBase: rebaseName,
   114  		},
   115  	})
   116  }
   117  
   118  // CopyInfo holds basic info about the source
   119  // or destination path of a copy operation.
   120  type CopyInfo struct {
   121  	Path       string
   122  	Exists     bool
   123  	IsDir      bool
   124  	RebaseName string
   125  }
   126  
   127  // CopyInfoSourcePath stats the given path to create a CopyInfo
   128  // struct representing that resource for the source of an archive copy
   129  // operation. The given path should be an absolute local path. A source path
   130  // has all symlinks evaluated that appear before the last path separator ("/"
   131  // on Unix). As it is to be a copy source, the path must exist.
   132  func CopyInfoSourcePath(path string) (CopyInfo, error) {
   133  	// Split the given path into its Directory and Base components. We will
   134  	// evaluate symlinks in the directory component then append the base.
   135  	dirPath, basePath := filepath.Split(path)
   136  
   137  	resolvedDirPath, err := filepath.EvalSymlinks(dirPath)
   138  	if err != nil {
   139  		return CopyInfo{}, err
   140  	}
   141  
   142  	// resolvedDirPath will have been cleaned (no trailing path separators) so
   143  	// we can manually join it with the base path element.
   144  	resolvedPath := resolvedDirPath + string(filepath.Separator) + basePath
   145  
   146  	var rebaseName string
   147  	if HasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) {
   148  		// In the case where the path had a trailing separator and a symlink
   149  		// evaluation has changed the last path component, we will need to
   150  		// rebase the name in the archive that is being copied to match the
   151  		// originally requested name.
   152  		rebaseName = filepath.Base(path)
   153  	}
   154  
   155  	stat, err := os.Lstat(resolvedPath)
   156  	if err != nil {
   157  		return CopyInfo{}, err
   158  	}
   159  
   160  	return CopyInfo{
   161  		Path:       resolvedPath,
   162  		Exists:     true,
   163  		IsDir:      stat.IsDir(),
   164  		RebaseName: rebaseName,
   165  	}, nil
   166  }
   167  
   168  // CopyInfoDestinationPath stats the given path to create a CopyInfo
   169  // struct representing that resource for the destination of an archive copy
   170  // operation. The given path should be an absolute local path.
   171  func CopyInfoDestinationPath(path string) (info CopyInfo, err error) {
   172  	maxSymlinkIter := 10 // filepath.EvalSymlinks uses 255, but 10 already seems like a lot.
   173  	originalPath := path
   174  
   175  	stat, err := os.Lstat(path)
   176  
   177  	if err == nil && stat.Mode()&os.ModeSymlink == 0 {
   178  		// The path exists and is not a symlink.
   179  		return CopyInfo{
   180  			Path:   path,
   181  			Exists: true,
   182  			IsDir:  stat.IsDir(),
   183  		}, nil
   184  	}
   185  
   186  	// While the path is a symlink.
   187  	for n := 0; err == nil && stat.Mode()&os.ModeSymlink != 0; n++ {
   188  		if n > maxSymlinkIter {
   189  			// Don't follow symlinks more than this arbitrary number of times.
   190  			return CopyInfo{}, errors.New("too many symlinks in " + originalPath)
   191  		}
   192  
   193  		// The path is a symbolic link. We need to evaluate it so that the
   194  		// destination of the copy operation is the link target and not the
   195  		// link itself. This is notably different than CopyInfoSourcePath which
   196  		// only evaluates symlinks before the last appearing path separator.
   197  		// Also note that it is okay if the last path element is a broken
   198  		// symlink as the copy operation should create the target.
   199  		var linkTarget string
   200  
   201  		linkTarget, err = os.Readlink(path)
   202  		if err != nil {
   203  			return CopyInfo{}, err
   204  		}
   205  
   206  		if !filepath.IsAbs(linkTarget) {
   207  			// Join with the parent directory.
   208  			dstParent, _ := SplitPathDirEntry(path)
   209  			linkTarget = filepath.Join(dstParent, linkTarget)
   210  		}
   211  
   212  		path = linkTarget
   213  		stat, err = os.Lstat(path)
   214  	}
   215  
   216  	if err != nil {
   217  		// It's okay if the destination path doesn't exist. We can still
   218  		// continue the copy operation if the parent directory exists.
   219  		if !os.IsNotExist(err) {
   220  			return CopyInfo{}, err
   221  		}
   222  
   223  		// Ensure destination parent dir exists.
   224  		dstParent, _ := SplitPathDirEntry(path)
   225  
   226  		parentDirStat, err := os.Lstat(dstParent)
   227  		if err != nil {
   228  			return CopyInfo{}, err
   229  		}
   230  		if !parentDirStat.IsDir() {
   231  			return CopyInfo{}, ErrNotDirectory
   232  		}
   233  
   234  		return CopyInfo{Path: path}, nil
   235  	}
   236  
   237  	// The path exists after resolving symlinks.
   238  	return CopyInfo{
   239  		Path:   path,
   240  		Exists: true,
   241  		IsDir:  stat.IsDir(),
   242  	}, nil
   243  }
   244  
   245  // PrepareArchiveCopy prepares the given srcContent archive, which should
   246  // contain the archived resource described by srcInfo, to the destination
   247  // described by dstInfo. Returns the possibly modified content archive along
   248  // with the path to the destination directory which it should be extracted to.
   249  func PrepareArchiveCopy(srcContent ArchiveReader, srcInfo, dstInfo CopyInfo) (dstDir string, content Archive, err error) {
   250  	// Separate the destination path between its directory and base
   251  	// components in case the source archive contents need to be rebased.
   252  	dstDir, dstBase := SplitPathDirEntry(dstInfo.Path)
   253  	_, srcBase := SplitPathDirEntry(srcInfo.Path)
   254  
   255  	switch {
   256  	case dstInfo.Exists && dstInfo.IsDir:
   257  		// The destination exists as a directory. No alteration
   258  		// to srcContent is needed as its contents can be
   259  		// simply extracted to the destination directory.
   260  		return dstInfo.Path, ioutil.NopCloser(srcContent), nil
   261  	case dstInfo.Exists && srcInfo.IsDir:
   262  		// The destination exists as some type of file and the source
   263  		// content is a directory. This is an error condition since
   264  		// you cannot copy a directory to an existing file location.
   265  		return "", nil, ErrCannotCopyDir
   266  	case dstInfo.Exists:
   267  		// The destination exists as some type of file and the source content
   268  		// is also a file. The source content entry will have to be renamed to
   269  		// have a basename which matches the destination path's basename.
   270  		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
   271  	case srcInfo.IsDir:
   272  		// The destination does not exist and the source content is an archive
   273  		// of a directory. The archive should be extracted to the parent of
   274  		// the destination path instead, and when it is, the directory that is
   275  		// created as a result should take the name of the destination path.
   276  		// The source content entries will have to be renamed to have a
   277  		// basename which matches the destination path's basename.
   278  		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
   279  	case AssertsDirectory(dstInfo.Path):
   280  		// The destination does not exist and is asserted to be created as a
   281  		// directory, but the source content is not a directory. This is an
   282  		// error condition since you cannot create a directory from a file
   283  		// source.
   284  		return "", nil, ErrDirNotExists
   285  	default:
   286  		// The last remaining case is when the destination does not exist, is
   287  		// not asserted to be a directory, and the source content is not an
   288  		// archive of a directory. It this case, the destination file will need
   289  		// to be created when the archive is extracted and the source content
   290  		// entry will have to be renamed to have a basename which matches the
   291  		// destination path's basename.
   292  		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
   293  	}
   294  
   295  }
   296  
   297  // rebaseArchiveEntries rewrites the given srcContent archive replacing
   298  // an occurance of oldBase with newBase at the beginning of entry names.
   299  func rebaseArchiveEntries(srcContent ArchiveReader, oldBase, newBase string) Archive {
   300  	if oldBase == "/" {
   301  		// If oldBase specifies the root directory, use an empty string as
   302  		// oldBase instead so that newBase doesn't replace the path separator
   303  		// that all paths will start with.
   304  		oldBase = ""
   305  	}
   306  
   307  	rebased, w := io.Pipe()
   308  
   309  	go func() {
   310  		srcTar := tar.NewReader(srcContent)
   311  		rebasedTar := tar.NewWriter(w)
   312  
   313  		for {
   314  			hdr, err := srcTar.Next()
   315  			if err == io.EOF {
   316  				// Signals end of archive.
   317  				rebasedTar.Close()
   318  				w.Close()
   319  				return
   320  			}
   321  			if err != nil {
   322  				w.CloseWithError(err)
   323  				return
   324  			}
   325  
   326  			hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
   327  
   328  			if err = rebasedTar.WriteHeader(hdr); err != nil {
   329  				w.CloseWithError(err)
   330  				return
   331  			}
   332  
   333  			if _, err = io.Copy(rebasedTar, srcTar); err != nil {
   334  				w.CloseWithError(err)
   335  				return
   336  			}
   337  		}
   338  	}()
   339  
   340  	return rebased
   341  }
   342  
   343  // CopyResource performs an archive copy from the given source path to the
   344  // given destination path. The source path MUST exist and the destination
   345  // path's parent directory must exist.
   346  func CopyResource(srcPath, dstPath string) error {
   347  	var (
   348  		srcInfo CopyInfo
   349  		err     error
   350  	)
   351  
   352  	// Clean the source and destination paths.
   353  	srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
   354  	dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
   355  
   356  	if srcInfo, err = CopyInfoSourcePath(srcPath); err != nil {
   357  		return err
   358  	}
   359  
   360  	content, err := TarResource(srcInfo)
   361  	if err != nil {
   362  		return err
   363  	}
   364  	defer content.Close()
   365  
   366  	return CopyTo(content, srcInfo, dstPath)
   367  }
   368  
   369  // CopyTo handles extracting the given content whose
   370  // entries should be sourced from srcInfo to dstPath.
   371  func CopyTo(content ArchiveReader, srcInfo CopyInfo, dstPath string) error {
   372  	// The destination path need not exist, but CopyInfoDestinationPath will
   373  	// ensure that at least the parent directory exists.
   374  	dstInfo, err := CopyInfoDestinationPath(dstPath)
   375  	if err != nil {
   376  		return err
   377  	}
   378  
   379  	dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo)
   380  	if err != nil {
   381  		return err
   382  	}
   383  	defer copyArchive.Close()
   384  
   385  	options := &TarOptions{
   386  		NoLchown:             true,
   387  		NoOverwriteDirNonDir: true,
   388  	}
   389  
   390  	return Untar(copyArchive, dstDir, options)
   391  }