浏览代码

pkg/archive: new utilities for copying resources

Adds TarResource and CopyTo functions to be used for creating
archives for use with the new `docker cp` behavior.

Adds multiple test cases for the CopyFrom and CopyTo
functions in the pkg/archive package.

Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)
Josh Hawn 10 年之前
父节点
当前提交
a74799b701
共有 5 个文件被更改,包括 1031 次插入32 次删除
  1. 83 29
      pkg/archive/archive.go
  2. 1 1
      pkg/archive/archive_test.go
  3. 308 0
      pkg/archive/copy.go
  4. 637 0
      pkg/archive/copy_test.go
  5. 2 2
      pkg/archive/diff.go

+ 83 - 29
pkg/archive/archive.go

@@ -25,15 +25,23 @@ import (
 )
 
 type (
-	Archive       io.ReadCloser
-	ArchiveReader io.Reader
-	Compression   int
-	TarOptions    struct {
-		IncludeFiles    []string
-		ExcludePatterns []string
-		Compression     Compression
-		NoLchown        bool
-		Name            string
+	Archive         io.ReadCloser
+	ArchiveReader   io.Reader
+	Compression     int
+	TarChownOptions struct {
+		UID, GID int
+	}
+	TarOptions struct {
+		IncludeFiles     []string
+		ExcludePatterns  []string
+		Compression      Compression
+		NoLchown         bool
+		ChownOpts        *TarChownOptions
+		Name             string
+		IncludeSourceDir bool
+		// When unpacking, specifies whether overwriting a directory with a
+		// non-directory is allowed and vice versa.
+		NoOverwriteDirNonDir bool
 	}
 
 	// Archiver allows the reuse of most utility functions of this package
@@ -262,7 +270,7 @@ func (ta *tarAppender) addTarFile(path, name string) error {
 	return nil
 }
 
-func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool) error {
+func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool, chownOpts *TarChownOptions) error {
 	// hdr.Mode is in linux format, which we can use for sycalls,
 	// but for os.Foo() calls we need the mode converted to os.FileMode,
 	// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
@@ -328,9 +336,12 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, L
 		return fmt.Errorf("Unhandled tar header type %d\n", hdr.Typeflag)
 	}
 
-	// Lchown is not supported on Windows
-	if runtime.GOOS != "windows" {
-		if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil && Lchown {
+	// Lchown is not supported on Windows.
+	if Lchown && runtime.GOOS != "windows" {
+		if chownOpts == nil {
+			chownOpts = &TarChownOptions{UID: hdr.Uid, GID: hdr.Gid}
+		}
+		if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil {
 			return err
 		}
 	}
@@ -396,6 +407,20 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 			Buffer:    pools.BufioWriter32KPool.Get(nil),
 			SeenFiles: make(map[uint64]string),
 		}
+
+		defer func() {
+			// Make sure to check the error on Close.
+			if err := ta.TarWriter.Close(); err != nil {
+				logrus.Debugf("Can't close tar writer: %s", err)
+			}
+			if err := compressWriter.Close(); err != nil {
+				logrus.Debugf("Can't close compress writer: %s", err)
+			}
+			if err := pipeWriter.Close(); err != nil {
+				logrus.Debugf("Can't close pipe writer: %s", err)
+			}
+		}()
+
 		// this buffer is needed for the duration of this piped stream
 		defer pools.BufioWriter32KPool.Put(ta.Buffer)
 
@@ -404,7 +429,26 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 		// mutating the filesystem and we can see transient errors
 		// from this
 
-		if options.IncludeFiles == nil {
+		stat, err := os.Lstat(srcPath)
+		if err != nil {
+			return
+		}
+
+		if !stat.IsDir() {
+			// We can't later join a non-dir with any includes because the
+			// 'walk' will error if "file/." is stat-ed and "file" is not a
+			// directory. So, we must split the source path and use the
+			// basename as the include.
+			if len(options.IncludeFiles) > 0 {
+				logrus.Warn("Tar: Can't archive a file with includes")
+			}
+
+			dir, base := SplitPathDirEntry(srcPath)
+			srcPath = dir
+			options.IncludeFiles = []string{base}
+		}
+
+		if len(options.IncludeFiles) == 0 {
 			options.IncludeFiles = []string{"."}
 		}
 
@@ -412,19 +456,26 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 
 		var renamedRelFilePath string // For when tar.Options.Name is set
 		for _, include := range options.IncludeFiles {
-			filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
+			// We can't use filepath.Join(srcPath, include) because this will
+			// clean away a trailing "." or "/" which may be important.
+			walkRoot := strings.Join([]string{srcPath, include}, string(filepath.Separator))
+			filepath.Walk(walkRoot, func(filePath string, f os.FileInfo, err error) error {
 				if err != nil {
 					logrus.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err)
 					return nil
 				}
 
 				relFilePath, err := filepath.Rel(srcPath, filePath)
-				if err != nil || (relFilePath == "." && f.IsDir()) {
+				if err != nil || (!options.IncludeSourceDir && relFilePath == "." && f.IsDir()) {
 					// Error getting relative path OR we are looking
-					// at the root path. Skip in both situations.
+					// at the source directory path. Skip in both situations.
 					return nil
 				}
 
+				if options.IncludeSourceDir && include == "." && relFilePath != "." {
+					relFilePath = strings.Join([]string{".", relFilePath}, string(filepath.Separator))
+				}
+
 				skip := false
 
 				// If "include" is an exact match for the current file
@@ -468,17 +519,6 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 				return nil
 			})
 		}
-
-		// Make sure to check the error on Close.
-		if err := ta.TarWriter.Close(); err != nil {
-			logrus.Debugf("Can't close tar writer: %s", err)
-		}
-		if err := compressWriter.Close(); err != nil {
-			logrus.Debugf("Can't close compress writer: %s", err)
-		}
-		if err := pipeWriter.Close(); err != nil {
-			logrus.Debugf("Can't close pipe writer: %s", err)
-		}
 	}()
 
 	return pipeReader, nil
@@ -543,9 +583,22 @@ loop:
 		// the layer is also a directory. Then we want to merge them (i.e.
 		// just apply the metadata from the layer).
 		if fi, err := os.Lstat(path); err == nil {
+			if options.NoOverwriteDirNonDir && fi.IsDir() && hdr.Typeflag != tar.TypeDir {
+				// If NoOverwriteDirNonDir is true then we cannot replace
+				// an existing directory with a non-directory from the archive.
+				return fmt.Errorf("cannot overwrite directory %q with non-directory %q", path, dest)
+			}
+
+			if options.NoOverwriteDirNonDir && !fi.IsDir() && hdr.Typeflag == tar.TypeDir {
+				// If NoOverwriteDirNonDir is true then we cannot replace
+				// an existing non-directory with a directory from the archive.
+				return fmt.Errorf("cannot overwrite non-directory %q with directory %q", path, dest)
+			}
+
 			if fi.IsDir() && hdr.Name == "." {
 				continue
 			}
+
 			if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) {
 				if err := os.RemoveAll(path); err != nil {
 					return err
@@ -553,7 +606,8 @@ loop:
 			}
 		}
 		trBuf.Reset(tr)
-		if err := createTarFile(path, dest, hdr, trBuf, !options.NoLchown); err != nil {
+
+		if err := createTarFile(path, dest, hdr, trBuf, !options.NoLchown, options.ChownOpts); err != nil {
 			return err
 		}
 

+ 1 - 1
pkg/archive/archive_test.go

@@ -719,7 +719,7 @@ func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer os.RemoveAll(tmpDir)
-	err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true)
+	err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true, nil)
 	if err != nil {
 		t.Fatal(err)
 	}

+ 308 - 0
pkg/archive/copy.go

@@ -0,0 +1,308 @@
+package archive
+
+import (
+	"archive/tar"
+	"errors"
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	log "github.com/Sirupsen/logrus"
+)
+
+// Errors used or returned by this file.
+var (
+	ErrNotDirectory      = errors.New("not a directory")
+	ErrDirNotExists      = errors.New("no such directory")
+	ErrCannotCopyDir     = errors.New("cannot copy directory")
+	ErrInvalidCopySource = errors.New("invalid copy source content")
+)
+
+// PreserveTrailingDotOrSeparator returns the given cleaned path (after
+// processing using any utility functions from the path or filepath stdlib
+// packages) and appends a trailing `/.` or `/` if its corresponding  original
+// path (from before being processed by utility functions from the path or
+// filepath stdlib packages) ends with a trailing `/.` or `/`. If the cleaned
+// path already ends in a `.` path segment, then another is not added. If the
+// clean path already ends in a path separator, then another is not added.
+func PreserveTrailingDotOrSeparator(cleanedPath, originalPath string) string {
+	if !SpecifiesCurrentDir(cleanedPath) && SpecifiesCurrentDir(originalPath) {
+		if !HasTrailingPathSeparator(cleanedPath) {
+			// Add a separator if it doesn't already end with one (a cleaned
+			// path would only end in a separator if it is the root).
+			cleanedPath += string(filepath.Separator)
+		}
+		cleanedPath += "."
+	}
+
+	if !HasTrailingPathSeparator(cleanedPath) && HasTrailingPathSeparator(originalPath) {
+		cleanedPath += string(filepath.Separator)
+	}
+
+	return cleanedPath
+}
+
+// AssertsDirectory returns whether the given path is
+// asserted to be a directory, i.e., the path ends with
+// a trailing '/' or `/.`, assuming a path separator of `/`.
+func AssertsDirectory(path string) bool {
+	return HasTrailingPathSeparator(path) || SpecifiesCurrentDir(path)
+}
+
+// HasTrailingPathSeparator returns whether the given
+// path ends with the system's path separator character.
+func HasTrailingPathSeparator(path string) bool {
+	return len(path) > 0 && os.IsPathSeparator(path[len(path)-1])
+}
+
+// SpecifiesCurrentDir returns whether the given path specifies
+// a "current directory", i.e., the last path segment is `.`.
+func SpecifiesCurrentDir(path string) bool {
+	return filepath.Base(path) == "."
+}
+
+// SplitPathDirEntry splits the given path between its
+// parent directory and its basename in that directory.
+func SplitPathDirEntry(localizedPath string) (dir, base string) {
+	normalizedPath := filepath.ToSlash(localizedPath)
+	vol := filepath.VolumeName(normalizedPath)
+	normalizedPath = normalizedPath[len(vol):]
+
+	if normalizedPath == "/" {
+		// Specifies the root path.
+		return filepath.FromSlash(vol + normalizedPath), "."
+	}
+
+	trimmedPath := vol + strings.TrimRight(normalizedPath, "/")
+
+	dir = filepath.FromSlash(path.Dir(trimmedPath))
+	base = filepath.FromSlash(path.Base(trimmedPath))
+
+	return dir, base
+}
+
+// TarResource archives the resource at the given sourcePath into a Tar
+// archive. A non-nil error is returned if sourcePath does not exist or is
+// asserted to be a directory but exists as another type of file.
+//
+// This function acts as a convenient wrapper around TarWithOptions, which
+// requires a directory as the source path. TarResource accepts either a
+// directory or a file path and correctly sets the Tar options.
+func TarResource(sourcePath string) (content Archive, err error) {
+	if _, err = os.Lstat(sourcePath); err != nil {
+		// Catches the case where the source does not exist or is not a
+		// directory if asserted to be a directory, as this also causes an
+		// error.
+		return
+	}
+
+	if len(sourcePath) > 1 && HasTrailingPathSeparator(sourcePath) {
+		// In the case where the source path is a symbolic link AND it ends
+		// with a path separator, we will want to evaluate the symbolic link.
+		trimmedPath := sourcePath[:len(sourcePath)-1]
+		stat, err := os.Lstat(trimmedPath)
+		if err != nil {
+			return nil, err
+		}
+
+		if stat.Mode()&os.ModeSymlink != 0 {
+			if sourcePath, err = filepath.EvalSymlinks(trimmedPath); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	// Separate the source path between it's directory and
+	// the entry in that directory which we are archiving.
+	sourceDir, sourceBase := SplitPathDirEntry(sourcePath)
+
+	filter := []string{sourceBase}
+
+	log.Debugf("copying %q from %q", sourceBase, sourceDir)
+
+	return TarWithOptions(sourceDir, &TarOptions{
+		Compression:      Uncompressed,
+		IncludeFiles:     filter,
+		IncludeSourceDir: true,
+	})
+}
+
+// CopyInfo holds basic info about the source
+// or destination path of a copy operation.
+type CopyInfo struct {
+	Path   string
+	Exists bool
+	IsDir  bool
+}
+
+// CopyInfoStatPath stats the given path to create a CopyInfo
+// struct representing that resource. If mustExist is true, then
+// it is an error if there is no file or directory at the given path.
+func CopyInfoStatPath(path string, mustExist bool) (CopyInfo, error) {
+	pathInfo := CopyInfo{Path: path}
+
+	fileInfo, err := os.Lstat(path)
+
+	if err == nil {
+		pathInfo.Exists, pathInfo.IsDir = true, fileInfo.IsDir()
+	} else if os.IsNotExist(err) && !mustExist {
+		err = nil
+	}
+
+	return pathInfo, err
+}
+
+// PrepareArchiveCopy prepares the given srcContent archive, which should
+// contain the archived resource described by srcInfo, to the destination
+// described by dstInfo. Returns the possibly modified content archive along
+// with the path to the destination directory which it should be extracted to.
+func PrepareArchiveCopy(srcContent ArchiveReader, srcInfo, dstInfo CopyInfo) (dstDir string, content Archive, err error) {
+	// Separate the destination path between its directory and base
+	// components in case the source archive contents need to be rebased.
+	dstDir, dstBase := SplitPathDirEntry(dstInfo.Path)
+	_, srcBase := SplitPathDirEntry(srcInfo.Path)
+
+	switch {
+	case dstInfo.Exists && dstInfo.IsDir:
+		// The destination exists as a directory. No alteration
+		// to srcContent is needed as its contents can be
+		// simply extracted to the destination directory.
+		return dstInfo.Path, ioutil.NopCloser(srcContent), nil
+	case dstInfo.Exists && srcInfo.IsDir:
+		// The destination exists as some type of file and the source
+		// content is a directory. This is an error condition since
+		// you cannot copy a directory to an existing file location.
+		return "", nil, ErrCannotCopyDir
+	case dstInfo.Exists:
+		// The destination exists as some type of file and the source content
+		// is also a file. The source content entry will have to be renamed to
+		// have a basename which matches the destination path's basename.
+		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+	case srcInfo.IsDir:
+		// The destination does not exist and the source content is an archive
+		// of a directory. The archive should be extracted to the parent of
+		// the destination path instead, and when it is, the directory that is
+		// created as a result should take the name of the destination path.
+		// The source content entries will have to be renamed to have a
+		// basename which matches the destination path's basename.
+		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+	case AssertsDirectory(dstInfo.Path):
+		// The destination does not exist and is asserted to be created as a
+		// directory, but the source content is not a directory. This is an
+		// error condition since you cannot create a directory from a file
+		// source.
+		return "", nil, ErrDirNotExists
+	default:
+		// The last remaining case is when the destination does not exist, is
+		// not asserted to be a directory, and the source content is not an
+		// archive of a directory. It this case, the destination file will need
+		// to be created when the archive is extracted and the source content
+		// entry will have to be renamed to have a basename which matches the
+		// destination path's basename.
+		return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil
+	}
+
+}
+
+// rebaseArchiveEntries rewrites the given srcContent archive replacing
+// an occurance of oldBase with newBase at the beginning of entry names.
+func rebaseArchiveEntries(srcContent ArchiveReader, oldBase, newBase string) Archive {
+	rebased, w := io.Pipe()
+
+	go func() {
+		srcTar := tar.NewReader(srcContent)
+		rebasedTar := tar.NewWriter(w)
+
+		for {
+			hdr, err := srcTar.Next()
+			if err == io.EOF {
+				// Signals end of archive.
+				rebasedTar.Close()
+				w.Close()
+				return
+			}
+			if err != nil {
+				w.CloseWithError(err)
+				return
+			}
+
+			hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
+
+			if err = rebasedTar.WriteHeader(hdr); err != nil {
+				w.CloseWithError(err)
+				return
+			}
+
+			if _, err = io.Copy(rebasedTar, srcTar); err != nil {
+				w.CloseWithError(err)
+				return
+			}
+		}
+	}()
+
+	return rebased
+}
+
+// CopyResource performs an archive copy from the given source path to the
+// given destination path. The source path MUST exist and the destination
+// path's parent directory must exist.
+func CopyResource(srcPath, dstPath string) error {
+	var (
+		srcInfo CopyInfo
+		err     error
+	)
+
+	// Clean the source and destination paths.
+	srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
+	dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
+
+	if srcInfo, err = CopyInfoStatPath(srcPath, true); err != nil {
+		return err
+	}
+
+	content, err := TarResource(srcPath)
+	if err != nil {
+		return err
+	}
+	defer content.Close()
+
+	return CopyTo(content, srcInfo, dstPath)
+}
+
+// CopyTo handles extracting the given content whose
+// entries should be sourced from srcInfo to dstPath.
+func CopyTo(content ArchiveReader, srcInfo CopyInfo, dstPath string) error {
+	dstInfo, err := CopyInfoStatPath(dstPath, false)
+	if err != nil {
+		return err
+	}
+
+	if !dstInfo.Exists {
+		// Ensure destination parent dir exists.
+		dstParent, _ := SplitPathDirEntry(dstPath)
+
+		dstStat, err := os.Lstat(dstParent)
+		if err != nil {
+			return err
+		}
+		if !dstStat.IsDir() {
+			return ErrNotDirectory
+		}
+	}
+
+	dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo)
+	if err != nil {
+		return err
+	}
+	defer copyArchive.Close()
+
+	options := &TarOptions{
+		NoLchown:             true,
+		NoOverwriteDirNonDir: true,
+	}
+
+	return Untar(copyArchive, dstDir, options)
+}

+ 637 - 0
pkg/archive/copy_test.go

@@ -0,0 +1,637 @@
+package archive
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func removeAllPaths(paths ...string) {
+	for _, path := range paths {
+		os.RemoveAll(path)
+	}
+}
+
+func getTestTempDirs(t *testing.T) (tmpDirA, tmpDirB string) {
+	var err error
+
+	if tmpDirA, err = ioutil.TempDir("", "archive-copy-test"); err != nil {
+		t.Fatal(err)
+	}
+
+	if tmpDirB, err = ioutil.TempDir("", "archive-copy-test"); err != nil {
+		t.Fatal(err)
+	}
+
+	return
+}
+
+func isNotDir(err error) bool {
+	return strings.Contains(err.Error(), "not a directory")
+}
+
+func joinTrailingSep(pathElements ...string) string {
+	joined := filepath.Join(pathElements...)
+
+	return fmt.Sprintf("%s%c", joined, filepath.Separator)
+}
+
+func fileContentsEqual(t *testing.T, filenameA, filenameB string) (err error) {
+	t.Logf("checking for equal file contents: %q and %q\n", filenameA, filenameB)
+
+	fileA, err := os.Open(filenameA)
+	if err != nil {
+		return
+	}
+	defer fileA.Close()
+
+	fileB, err := os.Open(filenameB)
+	if err != nil {
+		return
+	}
+	defer fileB.Close()
+
+	hasher := sha256.New()
+
+	if _, err = io.Copy(hasher, fileA); err != nil {
+		return
+	}
+
+	hashA := hasher.Sum(nil)
+	hasher.Reset()
+
+	if _, err = io.Copy(hasher, fileB); err != nil {
+		return
+	}
+
+	hashB := hasher.Sum(nil)
+
+	if !bytes.Equal(hashA, hashB) {
+		err = fmt.Errorf("file content hashes not equal - expected %s, got %s", hex.EncodeToString(hashA), hex.EncodeToString(hashB))
+	}
+
+	return
+}
+
+func dirContentsEqual(t *testing.T, newDir, oldDir string) (err error) {
+	t.Logf("checking for equal directory contents: %q and %q\n", newDir, oldDir)
+
+	var changes []Change
+
+	if changes, err = ChangesDirs(newDir, oldDir); err != nil {
+		return
+	}
+
+	if len(changes) != 0 {
+		err = fmt.Errorf("expected no changes between directories, but got: %v", changes)
+	}
+
+	return
+}
+
+func logDirContents(t *testing.T, dirPath string) {
+	logWalkedPaths := filepath.WalkFunc(func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			t.Errorf("stat error for path %q: %s", path, err)
+			return nil
+		}
+
+		if info.IsDir() {
+			path = joinTrailingSep(path)
+		}
+
+		t.Logf("\t%s", path)
+
+		return nil
+	})
+
+	t.Logf("logging directory contents: %q", dirPath)
+
+	if err := filepath.Walk(dirPath, logWalkedPaths); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func testCopyHelper(t *testing.T, srcPath, dstPath string) (err error) {
+	t.Logf("copying from %q to %q", srcPath, dstPath)
+
+	return CopyResource(srcPath, dstPath)
+}
+
+// Basic assumptions about SRC and DST:
+// 1. SRC must exist.
+// 2. If SRC ends with a trailing separator, it must be a directory.
+// 3. DST parent directory must exist.
+// 4. If DST exists as a file, it must not end with a trailing separator.
+
+// First get these easy error cases out of the way.
+
+// Test for error when SRC does not exist.
+func TestCopyErrSrcNotExists(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	content, err := TarResource(filepath.Join(tmpDirA, "file1"))
+	if err == nil {
+		content.Close()
+		t.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !os.IsNotExist(err) {
+		t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC ends in a trailing
+// path separator but it exists as a file.
+func TestCopyErrSrcNotDir(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	content, err := TarResource(joinTrailingSep(tmpDirA, "file1"))
+	if err == nil {
+		content.Close()
+		t.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isNotDir(err) {
+		t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC is a valid file or directory,
+// but the DST parent directory does not exist.
+func TestCopyErrDstParentNotExists(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
+
+	// Try with a file source.
+	content, err := TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	// Copy to a file whose parent does not exist.
+	if err = CopyTo(content, srcInfo, filepath.Join(tmpDirB, "fakeParentDir", "file1")); err == nil {
+		t.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !os.IsNotExist(err) {
+		t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
+
+	content, err = TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	// Copy to a directory whose parent does not exist.
+	if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "fakeParentDir", "fakeDstDir")); err == nil {
+		t.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !os.IsNotExist(err) {
+		t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when DST ends in a trailing
+// path separator but exists as a file.
+func TestCopyErrDstNotDir(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	// Try with a file source.
+	srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
+
+	content, err := TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil {
+		t.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isNotDir(err) {
+		t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
+
+	content, err = TarResource(srcInfo.Path)
+	if err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+	defer content.Close()
+
+	if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil {
+		t.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isNotDir(err) {
+		t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Possibilities are reduced to the remaining 10 cases:
+//
+//  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
+// ===================================================================================================
+//   A   |  no      |  -              |  no       |  -       |  no      |  create file
+//   B   |  no      |  -              |  no       |  -       |  yes     |  error
+//   C   |  no      |  -              |  yes      |  no      |  -       |  overwrite file
+//   D   |  no      |  -              |  yes      |  yes     |  -       |  create file in dst dir
+//   E   |  yes     |  no             |  no       |  -       |  -       |  create dir, copy contents
+//   F   |  yes     |  no             |  yes      |  no      |  -       |  error
+//   G   |  yes     |  no             |  yes      |  yes     |  -       |  copy dir and contents
+//   H   |  yes     |  yes            |  no       |  -       |  -       |  create dir, copy contents
+//   I   |  yes     |  yes            |  yes      |  no      |  -       |  error
+//   J   |  yes     |  yes            |  yes      |  yes     |  -       |  copy dir contents
+//
+
+// A. SRC specifies a file and DST (no trailing path separator) doesn't
+//    exist. This should create a file with the name DST and copy the
+//    contents of the source file into it.
+func TestCopyCaseA(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstPath := filepath.Join(tmpDirB, "itWorks.txt")
+
+	var err error
+
+	if err = testCopyHelper(t, srcPath, dstPath); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// B. SRC specifies a file and DST (with trailing path separator) doesn't
+//    exist. This should cause an error because the copy operation cannot
+//    create a directory when copying a single file.
+func TestCopyCaseB(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstDir := joinTrailingSep(tmpDirB, "testDir")
+
+	var err error
+
+	if err = testCopyHelper(t, srcPath, dstDir); err == nil {
+		t.Fatal("expected ErrDirNotExists error, but got nil instead")
+	}
+
+	if err != ErrDirNotExists {
+		t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err)
+	}
+}
+
+// C. SRC specifies a file and DST exists as a file. This should overwrite
+//    the file at DST with the contents of the source file.
+func TestCopyCaseC(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstPath := filepath.Join(tmpDirB, "file2")
+
+	var err error
+
+	// Ensure they start out different.
+	if err = fileContentsEqual(t, srcPath, dstPath); err == nil {
+		t.Fatal("expected different file contents")
+	}
+
+	if err = testCopyHelper(t, srcPath, dstPath); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// D. SRC specifies a file and DST exists as a directory. This should place
+//    a copy of the source file inside it using the basename from SRC. Ensure
+//    this works whether DST has a trailing path separator or not.
+func TestCopyCaseD(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcPath := filepath.Join(tmpDirA, "file1")
+	dstDir := filepath.Join(tmpDirB, "dir1")
+	dstPath := filepath.Join(dstDir, "file1")
+
+	var err error
+
+	// Ensure that dstPath doesn't exist.
+	if _, err = os.Stat(dstPath); !os.IsNotExist(err) {
+		t.Fatalf("did not expect dstPath %q to exist", dstPath)
+	}
+
+	if err = testCopyHelper(t, srcPath, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		t.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "dir1")
+
+	if err = testCopyHelper(t, srcPath, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// E. SRC specifies a directory and DST does not exist. This should create a
+//    directory at DST and copy the contents of the SRC directory into the DST
+//    directory. Ensure this works whether DST has a trailing path separator or
+//    not.
+func TestCopyCaseE(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcDir := filepath.Join(tmpDirA, "dir1")
+	dstDir := filepath.Join(tmpDirB, "testDir")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Log("dir contents not equal")
+		logDirContents(t, tmpDirA)
+		logDirContents(t, tmpDirB)
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "testDir")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// F. SRC specifies a directory and DST exists as a file. This should cause an
+//    error as it is not possible to overwrite a file with a directory.
+func TestCopyCaseF(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := filepath.Join(tmpDirA, "dir1")
+	dstFile := filepath.Join(tmpDirB, "file1")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstFile); err == nil {
+		t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if err != ErrCannotCopyDir {
+		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// G. SRC specifies a directory and DST exists as a directory. This should copy
+//    the SRC directory and all its contents to the DST directory. Ensure this
+//    works whether DST has a trailing path separator or not.
+func TestCopyCaseG(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := filepath.Join(tmpDirA, "dir1")
+	dstDir := filepath.Join(tmpDirB, "dir2")
+	resultDir := filepath.Join(dstDir, "dir1")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, resultDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		t.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "dir2")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, resultDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// H. SRC specifies a directory's contents only and DST does not exist. This
+//    should create a directory at DST and copy the contents of the SRC
+//    directory (but not the directory itself) into the DST directory. Ensure
+//    this works whether DST has a trailing path separator or not.
+func TestCopyCaseH(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+
+	srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
+	dstDir := filepath.Join(tmpDirB, "testDir")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Log("dir contents not equal")
+		logDirContents(t, tmpDirA)
+		logDirContents(t, tmpDirB)
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "testDir")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Log("dir contents not equal")
+		logDirContents(t, tmpDirA)
+		logDirContents(t, tmpDirB)
+		t.Fatal(err)
+	}
+}
+
+// I. SRC specifies a direcotry's contents only and DST exists as a file. This
+//    should cause an error as it is not possible to overwrite a file with a
+//    directory.
+func TestCopyCaseI(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
+	dstFile := filepath.Join(tmpDirB, "file1")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstFile); err == nil {
+		t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if err != ErrCannotCopyDir {
+		t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// J. SRC specifies a directory's contents only and DST exists as a directory.
+//    This should copy the contents of the SRC directory (but not the directory
+//    itself) into the DST directory. Ensure this works whether DST has a
+//    trailing path separator or not.
+func TestCopyCaseJ(t *testing.T) {
+	tmpDirA, tmpDirB := getTestTempDirs(t)
+	defer removeAllPaths(tmpDirA, tmpDirB)
+
+	// Load A and B with some sample files and directories.
+	createSampleDir(t, tmpDirA)
+	createSampleDir(t, tmpDirB)
+
+	srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
+	dstDir := filepath.Join(tmpDirB, "dir5")
+
+	var err error
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err = os.RemoveAll(dstDir); err != nil {
+		t.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		t.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = joinTrailingSep(tmpDirB, "dir5")
+
+	if err = testCopyHelper(t, srcDir, dstDir); err != nil {
+		t.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
+		t.Fatal(err)
+	}
+}

+ 2 - 2
pkg/archive/diff.go

@@ -93,7 +93,7 @@ func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) {
 					}
 					defer os.RemoveAll(aufsTempdir)
 				}
-				if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true); err != nil {
+				if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true, nil); err != nil {
 					return 0, err
 				}
 			}
@@ -150,7 +150,7 @@ func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) {
 				srcData = tmpFile
 			}
 
-			if err := createTarFile(path, dest, srcHdr, srcData, true); err != nil {
+			if err := createTarFile(path, dest, srcHdr, srcData, true, nil); err != nil {
 				return 0, err
 			}