Selaa lähdekoodia

Merge pull request #934 from dotcloud/fix-add-behavior

* Build: Stabilize ADD behavior
Guillaume J. Charmes 12 vuotta sitten
vanhempi
commit
a660cc0d01
3 muutettua tiedostoa jossa 116 lisäystä ja 10 poistoa
  1. 79 3
      archive.go
  2. 4 4
      buildfile.go
  3. 33 3
      docs/sources/use/builder.rst

+ 79 - 3
archive.go

@@ -9,6 +9,7 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"path"
 )
 
 type Archive io.Reader
@@ -79,10 +80,29 @@ func (compression *Compression) Extension() string {
 	return ""
 }
 
+// Tar creates an archive from the directory at `path`, and returns it as a
+// stream of bytes.
 func Tar(path string, compression Compression) (io.Reader, error) {
-	return CmdStream(exec.Command("tar", "-f", "-", "-C", path, "-c"+compression.Flag(), "."))
+	return TarFilter(path, compression, nil)
 }
 
+// Tar creates an archive from the directory at `path`, only including files whose relative
+// paths are included in `filter`. If `filter` is nil, then all files are included.
+func TarFilter(path string, compression Compression, filter []string) (io.Reader, error) {
+	args := []string{"tar", "-f", "-", "-C", path}
+	if filter == nil {
+		filter = []string{"."}
+	}
+	for _, f := range filter {
+		args = append(args, "-c"+compression.Flag(), f)
+	}
+	return CmdStream(exec.Command(args[0], args[1:]...))
+}
+
+// Untar reads a stream of bytes from `archive`, parses it as a tar archive,
+// and unpacks it into the directory at `path`.
+// The archive may be compressed with one of the following algorithgms:
+//  identity (uncompressed), gzip, bzip2, xz.
 // FIXME: specify behavior when target path exists vs. doesn't exist.
 func Untar(archive io.Reader, path string) error {
 
@@ -107,6 +127,18 @@ func Untar(archive io.Reader, path string) error {
 	return nil
 }
 
+// TarUntar is a convenience function which calls Tar and Untar, with
+// the output of one piped into the other. If either Tar or Untar fails,
+// TarUntar aborts and returns the error.
+func TarUntar(src string, filter []string, dst string) error {
+	utils.Debugf("TarUntar(%s %s %s)", src, filter, dst)
+	archive, err := TarFilter(src, Uncompressed, filter)
+	if err != nil {
+		return err
+	}
+	return Untar(archive, dst)
+}
+
 // UntarPath is a convenience function which looks for an archive
 // at filesystem path `src`, and unpacks it at `dst`.
 func UntarPath(src, dst string) error {
@@ -124,11 +156,55 @@ func UntarPath(src, dst string) error {
 // intermediary disk IO.
 //
 func CopyWithTar(src, dst string) error {
-	archive, err := Tar(src, Uncompressed)
+	srcSt, err := os.Stat(src)
 	if err != nil {
 		return err
 	}
-	return Untar(archive, dst)
+	var dstExists bool
+	dstSt, err := os.Stat(dst)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return err
+		}
+	} else {
+		dstExists = true
+	}
+	// Things that can go wrong if the source is a directory
+	if srcSt.IsDir() {
+		// The destination exists and is a regular file
+		if dstExists && !dstSt.IsDir() {
+			return fmt.Errorf("Can't copy a directory over a regular file")
+		}
+		// Things that can go wrong if the source is a regular file
+	} else {
+		utils.Debugf("The destination exists, it's a directory, and doesn't end in /")
+		// The destination exists, it's a directory, and doesn't end in /
+		if dstExists && dstSt.IsDir() && dst[len(dst)-1] != '/' {
+			return fmt.Errorf("Can't copy a regular file over a directory %s |%s|", dst, dst[len(dst)-1])
+		}
+	}
+	// Create the destination
+	var dstDir string
+	if dst[len(dst)-1] == '/' {
+		// The destination ends in /
+		//   --> dst is the holding directory
+		dstDir = dst
+	} else {
+		// The destination doesn't end in /
+		//   --> dst is the file
+		dstDir = path.Dir(dst)
+	}
+	if !dstExists {
+		// Create the holding directory if necessary
+		utils.Debugf("Creating the holding directory %s", dstDir)
+		if err := os.MkdirAll(dstDir, 0700); err != nil && !os.IsExist(err) {
+			return err
+		}
+	}
+	if !srcSt.IsDir() {
+		return TarUntar(path.Dir(src), []string{path.Base(src)}, dstDir)
+	}
+	return TarUntar(src, nil, dstDir)
 }
 
 // CmdStream executes a command, and returns its stdout as a stream.

+ 4 - 4
buildfile.go

@@ -195,15 +195,15 @@ func (b *buildFile) CmdAdd(args string) error {
 
 	origPath := path.Join(b.context, orig)
 	destPath := path.Join(container.RootfsPath(), dest)
-
+	// Preserve the trailing '/'
+	if dest[len(dest)-1] == '/' {
+		destPath = destPath + "/"
+	}
 	fi, err := os.Stat(origPath)
 	if err != nil {
 		return err
 	}
 	if fi.IsDir() {
-		if err := os.MkdirAll(destPath, 0700); err != nil {
-			return err
-		}
 		if err := CopyWithTar(origPath, destPath); err != nil {
 			return err
 		}

+ 33 - 3
docs/sources/use/builder.rst

@@ -138,9 +138,39 @@ curl was installed within the image.
 
     ``ADD <src> <dest>``
 
-The `ADD` instruction will insert the files from the `<src>` path of the context into `<dest>` path 
-of the container.
-The context must be set in order to use this instruction. (see examples)
+The `ADD` instruction will copy new files from <src> and add them to the container's filesystem at path `<dest>`.
+
+`<src>` must be the path to a file or directory relative to the source directory being built (also called the
+context of the build).
+
+`<dest>` is the path at which the source will be copied in the destination container.
+
+The copy obeys the following rules:
+
+If `<src>` is a directory, the entire directory is copied, including filesystem metadata.
+
+If `<src>` is a tar archive in a recognized compression format (identity, gzip, bzip2 or xz), it
+is unpacked as a directory.
+
+When a directory is copied or unpacked, it has the same behavior as 'tar -x': the result is the union of
+a) whatever existed at the destination path and b) the contents of the source tree, with conflicts resolved
+in favor of b on a file-by-file basis.
+
+If `<src>` is any other kind of file, it is copied individually along with its metadata.
+
+If `<dest>` doesn't exist, it is created along with all missing directories in its path. All new
+files and directories are created with mode 0700, uid and gid 0.
+
+If `<dest>` ends with a trailing slash '/', the contents of `<src>` is copied `inside` it.
+For example "ADD foo /usr/src/" creates /usr/src/foo in the container. If `<dest>` already exists,
+it MUST be a directory.
+
+If `<dest>` does not end with a trailing slash '/', the contents of `<src>` is copied `over` it.
+For example "ADD foo /usr/src" creates /usr/src with the contents of the "foo". If `<dest>` already
+exists, it MUST be of the same type as the source.
+
+
+
 
 3. Dockerfile Examples
 ======================