diff --git a/archive/archive.go b/archive/archive.go index 8fa6f55333..89a360c906 100644 --- a/archive/archive.go +++ b/archive/archive.go @@ -13,6 +13,7 @@ import ( "os/exec" "path" "path/filepath" + "strings" "syscall" ) @@ -285,45 +286,92 @@ func TarFilter(path string, options *TarOptions) (io.Reader, error) { // The archive may be compressed with one of the following algorithms: // identity (uncompressed), gzip, bzip2, xz. // FIXME: specify behavior when target path exists vs. doesn't exist. -func Untar(archive io.Reader, path string, options *TarOptions) error { +func Untar(archive io.Reader, dest string, options *TarOptions) error { if archive == nil { return fmt.Errorf("Empty archive") } - buf := make([]byte, 10) - totalN := 0 - for totalN < 10 { - n, err := archive.Read(buf[totalN:]) + archive, err := DecompressStream(archive) + if err != nil { + return err + } + + tr := tar.NewReader(archive) + + var dirs []*tar.Header + + // Iterate through the files in the archive. + for { + hdr, err := tr.Next() + if err == io.EOF { + // end of tar archive + break + } if err != nil { - if err == io.EOF { - return fmt.Errorf("Tarball too short") - } return err } - totalN += n - utils.Debugf("[tar autodetect] n: %d", n) - } - compression := DetectCompression(buf) + if options != nil { + excludeFile := false + for _, exclude := range options.Excludes { + if strings.HasPrefix(hdr.Name, exclude) { + excludeFile = true + break + } + } + if excludeFile { + continue + } + } - utils.Debugf("Archive compression detected: %s", compression.Extension()) - args := []string{"--numeric-owner", "-f", "-", "-C", path, "-x" + compression.Flag()} + // Normalize name, for safety and for a simple is-root check + hdr.Name = filepath.Clean(hdr.Name) - if options != nil { - for _, exclude := range options.Excludes { - args = append(args, fmt.Sprintf("--exclude=%s", exclude)) + if !strings.HasSuffix(hdr.Name, "/") { + // Not the root directory, ensure that the parent directory exists + parent := filepath.Dir(hdr.Name) + parentPath := filepath.Join(dest, parent) + if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { + err = os.MkdirAll(parentPath, 600) + if err != nil { + return err + } + } + } + + path := filepath.Join(dest, hdr.Name) + + // If path exits we almost always just want to remove and replace it + // The only exception is when it is a directory *and* the file from + // 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 !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { + if err := os.RemoveAll(path); err != nil { + return err + } + } + } + + if err := createTarFile(path, dest, hdr, tr); err != nil { + return err + } + + // Directory mtimes must be handled at the end to avoid further + // file creation in them to modify the directory mtime + if hdr.Typeflag == tar.TypeDir { + dirs = append(dirs, hdr) } } - cmd := exec.Command("tar", args...) - cmd.Stdin = io.MultiReader(bytes.NewReader(buf), archive) - // Hardcode locale environment for predictable outcome regardless of host configuration. - // (see https://github.com/dotcloud/docker/issues/355) - cmd.Env = []string{"LANG=en_US.utf-8", "LC_ALL=en_US.utf-8"} - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("%s: %s", err, output) + for _, hdr := range dirs { + path := filepath.Join(dest, hdr.Name) + ts := []syscall.Timespec{timeToTimespec(hdr.AccessTime), timeToTimespec(hdr.ModTime)} + if err := syscall.UtimesNano(path, ts); err != nil { + return err + } } + return nil }