archive/archive.go
96d1e9bb
 package archive
97a82094
 
 import (
77c2b76e
 	"bufio"
4a3aefbb
 	"bytes"
a96a26c6
 	"compress/bzip2"
444a087a
 	"compress/gzip"
3dfc910d
 	"errors"
54db1862
 	"fmt"
97a82094
 	"io"
 	"io/ioutil"
baacae83
 	"os"
97a82094
 	"os/exec"
5be7b9af
 	"path"
36d610a3
 	"path/filepath"
a4868e23
 	"strings"
710d5a48
 	"syscall"
77c2b76e
 
 	"github.com/dotcloud/docker/pkg/system"
 	"github.com/dotcloud/docker/utils"
 	"github.com/dotcloud/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar"
97a82094
 )
 
3dfc910d
 type (
f198ee52
 	Archive       io.ReadCloser
 	ArchiveReader io.Reader
 	Compression   int
 	TarOptions    struct {
3dfc910d
 		Includes    []string
 		Compression Compression
9e51b7ab
 		NoLchown    bool
3dfc910d
 	}
 )
2c7f50a7
 
3dfc910d
 var (
 	ErrNotImplemented = errors.New("Function not implemented")
 )
97a82094
 
 const (
 	Uncompressed Compression = iota
 	Bzip2
 	Gzip
3c5d2e46
 	Xz
97a82094
 )
 
0425f65e
 func DetectCompression(source []byte) Compression {
 	for compression, m := range map[Compression][]byte{
 		Bzip2: {0x42, 0x5A, 0x68},
 		Gzip:  {0x1F, 0x8B, 0x08},
 		Xz:    {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00},
 	} {
67ce59a8
 		if len(source) < len(m) {
 			utils.Debugf("Len too short")
 			continue
 		}
3118952e
 		if bytes.Compare(m, source[:len(m)]) == 0 {
0425f65e
 			return compression
 		}
 	}
 	return Uncompressed
 }
 
0d9213f8
 func xzDecompress(archive io.Reader) (io.ReadCloser, error) {
b8a4f570
 	args := []string{"xz", "-d", "-c", "-q"}
 
4fb1db7f
 	return CmdStream(exec.Command(args[0], args[1:]...), archive)
b8a4f570
 }
 
0d9213f8
 func DecompressStream(archive io.Reader) (io.ReadCloser, error) {
77c2b76e
 	buf := bufio.NewReader(archive)
 	bs, err := buf.Peek(10)
 	if err != nil {
 		return nil, err
a96a26c6
 	}
77c2b76e
 	utils.Debugf("[tar autodetect] n: %v", bs)
 
 	compression := DetectCompression(bs)
a96a26c6
 
 	switch compression {
 	case Uncompressed:
77c2b76e
 		return ioutil.NopCloser(buf), nil
a96a26c6
 	case Gzip:
77c2b76e
 		return gzip.NewReader(buf)
a96a26c6
 	case Bzip2:
77c2b76e
 		return ioutil.NopCloser(bzip2.NewReader(buf)), nil
b8a4f570
 	case Xz:
77c2b76e
 		return xzDecompress(buf)
a96a26c6
 	default:
 		return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension())
 	}
 }
 
5ea48aa7
 func CompressStream(dest io.WriteCloser, compression Compression) (io.WriteCloser, error) {
 
 	switch compression {
 	case Uncompressed:
804690bd
 		return utils.NopWriteCloser(dest), nil
97a82094
 	case Gzip:
5ea48aa7
 		return gzip.NewWriter(dest), nil
 	case Bzip2, Xz:
 		// archive/bzip2 does not support writing, and there is no xz support at all
 		// However, this is not a problem as docker only currently generates gzipped tars
 		return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension())
 	default:
 		return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension())
97a82094
 	}
 }
 
54db1862
 func (compression *Compression) Extension() string {
 	switch *compression {
 	case Uncompressed:
 		return "tar"
 	case Bzip2:
 		return "tar.bz2"
 	case Gzip:
 		return "tar.gz"
 	case Xz:
 		return "tar.xz"
 	}
 	return ""
 }
 
5b77e51e
 func addTarFile(path, name string, tw *tar.Writer) error {
bab8efbf
 	fi, err := os.Lstat(path)
 	if err != nil {
5b77e51e
 		return err
 	}
 
bab8efbf
 	link := ""
5ea48aa7
 	if fi.Mode()&os.ModeSymlink != 0 {
bab8efbf
 		if link, err = os.Readlink(path); err != nil {
5b77e51e
 			return err
 		}
bab8efbf
 	}
 
 	hdr, err := tar.FileInfoHeader(fi, link)
 	if err != nil {
 		return err
 	}
 
f918fca3
 	if fi.IsDir() && !strings.HasSuffix(name, "/") {
 		name = name + "/"
 	}
 
bab8efbf
 	hdr.Name = name
 
 	stat, ok := fi.Sys().(*syscall.Stat_t)
 	if ok {
 		// Currently go does not fill in the major/minors
 		if stat.Mode&syscall.S_IFBLK == syscall.S_IFBLK ||
 			stat.Mode&syscall.S_IFCHR == syscall.S_IFCHR {
 			hdr.Devmajor = int64(major(uint64(stat.Rdev)))
 			hdr.Devminor = int64(minor(uint64(stat.Rdev)))
5b77e51e
 		}
3b995390
 
 	}
 
d6114c0d
 	capability, _ := system.Lgetxattr(path, "security.capability")
3b995390
 	if capability != nil {
 		hdr.Xattrs = make(map[string]string)
 		hdr.Xattrs["security.capability"] = string(capability)
5b77e51e
 	}
 
 	if err := tw.WriteHeader(hdr); err != nil {
 		return err
 	}
 
 	if hdr.Typeflag == tar.TypeReg {
 		if file, err := os.Open(path); err != nil {
 			return err
 		} else {
 			_, err := io.Copy(tw, file)
 			if err != nil {
 				return err
 			}
 			file.Close()
 		}
 	}
 
 	return nil
 }
 
9e51b7ab
 func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool) error {
5ba24629
 	// 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)
 	hdrInfo := hdr.FileInfo()
 
710d5a48
 	switch hdr.Typeflag {
 	case tar.TypeDir:
 		// Create directory unless it exists as a directory already.
 		// In that case we just want to merge the two
 		if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) {
5ba24629
 			if err := os.Mkdir(path, hdrInfo.Mode()); err != nil {
710d5a48
 				return err
 			}
 		}
 
 	case tar.TypeReg, tar.TypeRegA:
 		// Source is regular file
5ba24629
 		file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, hdrInfo.Mode())
710d5a48
 		if err != nil {
 			return err
 		}
 		if _, err := io.Copy(file, reader); err != nil {
 			file.Close()
 			return err
 		}
 		file.Close()
 
 	case tar.TypeBlock, tar.TypeChar, tar.TypeFifo:
 		mode := uint32(hdr.Mode & 07777)
 		switch hdr.Typeflag {
 		case tar.TypeBlock:
 			mode |= syscall.S_IFBLK
 		case tar.TypeChar:
 			mode |= syscall.S_IFCHR
 		case tar.TypeFifo:
 			mode |= syscall.S_IFIFO
 		}
 
 		if err := syscall.Mknod(path, mode, int(mkdev(hdr.Devmajor, hdr.Devminor))); err != nil {
 			return err
 		}
 
 	case tar.TypeLink:
 		if err := os.Link(filepath.Join(extractDir, hdr.Linkname), path); err != nil {
 			return err
 		}
 
 	case tar.TypeSymlink:
 		if err := os.Symlink(hdr.Linkname, path); err != nil {
 			return err
 		}
 
ce74c8b4
 	case tar.TypeXGlobalHeader:
 		utils.Debugf("PAX Global Extended Headers found and ignored")
 		return nil
 
710d5a48
 	default:
 		return fmt.Errorf("Unhandled tar header type %d\n", hdr.Typeflag)
 	}
 
9e51b7ab
 	if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil && Lchown {
710d5a48
 		return err
 	}
 
c8428d77
 	for key, value := range hdr.Xattrs {
d6114c0d
 		if err := system.Lsetxattr(path, key, []byte(value), 0); err != nil {
c8428d77
 			return err
 		}
 	}
 
710d5a48
 	// There is no LChmod, so ignore mode for symlink. Also, this
 	// must happen after chown, as that can modify the file mode
 	if hdr.Typeflag != tar.TypeSymlink {
5ba24629
 		if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
710d5a48
 			return err
 		}
 	}
 
 	ts := []syscall.Timespec{timeToTimespec(hdr.AccessTime), timeToTimespec(hdr.ModTime)}
 	// syscall.UtimesNano doesn't support a NOFOLLOW flag atm, and
 	if hdr.Typeflag != tar.TypeSymlink {
b132e7b3
 		if err := system.UtimesNano(path, ts); err != nil && err != system.ErrNotSupportedPlatform {
710d5a48
 			return err
 		}
 	} else {
b132e7b3
 		if err := system.LUtimesNano(path, ts); err != nil && err != system.ErrNotSupportedPlatform {
710d5a48
 			return err
 		}
 	}
 	return nil
 }
 
5be7b9af
 // Tar creates an archive from the directory at `path`, and returns it as a
 // stream of bytes.
f198ee52
 func Tar(path string, compression Compression) (io.ReadCloser, error) {
4fb1db7f
 	return TarFilter(path, &TarOptions{Compression: compression})
223280f3
 }
 
 func escapeName(name string) string {
ebfa24ac
 	escaped := make([]byte, 0)
223280f3
 	for i, c := range []byte(name) {
 		if i == 0 && c == '/' {
 			continue
 		}
 		// all printable chars except "-" which is 0x2d
ebfa24ac
 		if (0x20 <= c && c <= 0x7E) && c != 0x2d {
223280f3
 			escaped = append(escaped, c)
 		} else {
 			escaped = append(escaped, fmt.Sprintf("\\%03o", c)...)
 		}
 	}
 	return string(escaped)
5be7b9af
 }
 
31161495
 // TarFilter creates an archive from the directory at `srcPath` with `options`, and returns it as a
 // stream of bytes.
 //
 // Files are included according to `options.Includes`, default to including all files.
 // Stream is compressed according to `options.Compression', default to Uncompressed.
f198ee52
 func TarFilter(srcPath string, options *TarOptions) (io.ReadCloser, error) {
5ea48aa7
 	pipeReader, pipeWriter := io.Pipe()
2c7f50a7
 
5ea48aa7
 	compressWriter, err := CompressStream(pipeWriter, options.Compression)
 	if err != nil {
 		return nil, err
5be7b9af
 	}
223280f3
 
5ea48aa7
 	tw := tar.NewWriter(compressWriter)
 
 	go func() {
 		// In general we log errors here but ignore them because
 		// during e.g. a diff operation the container can continue
 		// mutating the filesystem and we can see transient errors
 		// from this
 
 		if options.Includes == nil {
 			options.Includes = []string{"."}
 		}
 
 		for _, include := range options.Includes {
 			filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
 				if err != nil {
 					utils.Debugf("Tar: Can't stat file %s to tar: %s\n", srcPath, err)
 					return nil
 				}
 
 				relFilePath, err := filepath.Rel(srcPath, filePath)
 				if err != nil {
 					return nil
 				}
 
 				if err := addTarFile(filePath, relFilePath, tw); err != nil {
 					utils.Debugf("Can't add file %s to tar: %s\n", srcPath, err)
 				}
 				return nil
 			})
 		}
 
 		// Make sure to check the error on Close.
 		if err := tw.Close(); err != nil {
 			utils.Debugf("Can't close tar writer: %s\n", err)
 		}
 		if err := compressWriter.Close(); err != nil {
 			utils.Debugf("Can't close compress writer: %s\n", err)
 		}
804690bd
 		if err := pipeWriter.Close(); err != nil {
 			utils.Debugf("Can't close pipe writer: %s\n", err)
 		}
5ea48aa7
 	}()
 
 	return pipeReader, nil
97a82094
 }
 
5be7b9af
 // Untar reads a stream of bytes from `archive`, parses it as a tar archive,
 // and unpacks it into the directory at `path`.
9b2a5964
 // The archive may be compressed with one of the following algorithms:
5be7b9af
 //  identity (uncompressed), gzip, bzip2, xz.
5b828761
 // FIXME: specify behavior when target path exists vs. doesn't exist.
a4868e23
 func Untar(archive io.Reader, dest string, options *TarOptions) error {
290b1973
 	if archive == nil {
 		return fmt.Errorf("Empty archive")
 	}
08a87d4b
 
0d9213f8
 	decompressedArchive, err := DecompressStream(archive)
a4868e23
 	if err != nil {
 		return err
 	}
0d9213f8
 	defer decompressedArchive.Close()
a4868e23
 
0d9213f8
 	tr := tar.NewReader(decompressedArchive)
a4868e23
 
 	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
 		}
5e941f1c
 		if err != nil {
08a87d4b
 			return err
 		}
5e941f1c
 
a4868e23
 		// Normalize name, for safety and for a simple is-root check
 		hdr.Name = filepath.Clean(hdr.Name)
 
 		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) {
2aeccdd3
 				err = os.MkdirAll(parentPath, 0777)
a4868e23
 				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 {
91b7d8eb
 			if fi.IsDir() && hdr.Name == "." {
 				continue
 			}
a4868e23
 			if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) {
 				if err := os.RemoveAll(path); err != nil {
 					return err
 				}
 			}
 		}
9e51b7ab
 		if err := createTarFile(path, dest, hdr, tr, options == nil || !options.NoLchown); err != nil {
a4868e23
 			return err
 		}
0425f65e
 
a4868e23
 		// 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)
5d972300
 		}
 	}
 
a4868e23
 	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
 		}
97a82094
 	}
a4868e23
 
97a82094
 	return nil
 }
 
5be7b9af
 // 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.
4fb1db7f
 func TarUntar(src string, dst string) error {
 	utils.Debugf("TarUntar(%s %s)", src, dst)
 	archive, err := TarFilter(src, &TarOptions{Compression: Uncompressed})
5be7b9af
 	if err != nil {
 		return err
 	}
f198ee52
 	defer archive.Close()
5d972300
 	return Untar(archive, dst, nil)
5be7b9af
 }
 
5b828761
 // 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 {
f198ee52
 	archive, err := os.Open(src)
 	if err != nil {
5b828761
 		return err
f198ee52
 	}
 	defer archive.Close()
 	if err := Untar(archive, dst, nil); err != nil {
5b828761
 		return err
 	}
 	return nil
 }
 
 // CopyWithTar creates a tar archive of filesystem path `src`, and
 // unpacks it at filesystem path `dst`.
 // The archive is streamed directly with fixed buffering and no
 // intermediary disk IO.
 //
 func CopyWithTar(src, dst string) error {
5be7b9af
 	srcSt, err := os.Stat(src)
5b828761
 	if err != nil {
 		return err
 	}
36d610a3
 	if !srcSt.IsDir() {
 		return CopyFileWithTar(src, dst)
 	}
 	// Create dst, copy src's content into it
 	utils.Debugf("Creating dest directory: %s", dst)
f7542664
 	if err := os.MkdirAll(dst, 0755); err != nil && !os.IsExist(err) {
36d610a3
 		return err
 	}
 	utils.Debugf("Calling TarUntar(%s, %s)", src, dst)
4fb1db7f
 	return TarUntar(src, dst)
36d610a3
 }
 
 // CopyFileWithTar emulates the behavior of the 'cp' command-line
 // for a single file. It copies a regular file from path `src` to
 // path `dst`, and preserves all its metadata.
 //
 // If `dst` ends with a trailing slash '/', the final destination path
 // will be `dst/base(src)`.
444a087a
 func CopyFileWithTar(src, dst string) (err error) {
36d610a3
 	utils.Debugf("CopyFileWithTar(%s, %s)", src, dst)
 	srcSt, err := os.Stat(src)
5be7b9af
 	if err != nil {
36d610a3
 		return err
5be7b9af
 	}
 	if srcSt.IsDir() {
36d610a3
 		return fmt.Errorf("Can't copy a directory")
5be7b9af
 	}
36d610a3
 	// Clean up the trailing /
 	if dst[len(dst)-1] == '/' {
 		dst = path.Join(dst, filepath.Base(src))
5be7b9af
 	}
36d610a3
 	// Create the holding directory if necessary
 	if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil && !os.IsExist(err) {
 		return err
 	}
444a087a
 
 	r, w := io.Pipe()
 	errC := utils.Go(func() error {
 		defer w.Close()
 
 		srcF, err := os.Open(src)
 		if err != nil {
 			return err
 		}
 		defer srcF.Close()
 
 		tw := tar.NewWriter(w)
 		hdr, err := tar.FileInfoHeader(srcSt, "")
 		if err != nil {
 			return err
 		}
 		hdr.Name = filepath.Base(dst)
 		if err := tw.WriteHeader(hdr); err != nil {
 			return err
 		}
 		if _, err := io.Copy(tw, srcF); err != nil {
 			return err
 		}
 		tw.Close()
 		return nil
 	})
 	defer func() {
 		if er := <-errC; err != nil {
 			err = er
 		}
 	}()
 	return Untar(r, filepath.Dir(dst), nil)
5b828761
 }
 
f85e6548
 // CmdStream executes a command, and returns its stdout as a stream.
 // If the command fails to run or doesn't complete successfully, an error
 // will be returned, including anything written on stderr.
0d9213f8
 func CmdStream(cmd *exec.Cmd, input io.Reader) (io.ReadCloser, error) {
223280f3
 	if input != nil {
 		stdin, err := cmd.StdinPipe()
 		if err != nil {
 			return nil, err
 		}
 		// Write stdin if any
 		go func() {
b8a4f570
 			io.Copy(stdin, input)
223280f3
 			stdin.Close()
 		}()
 	}
97a82094
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
 		return nil, err
 	}
 	stderr, err := cmd.StderrPipe()
 	if err != nil {
 		return nil, err
 	}
 	pipeR, pipeW := io.Pipe()
58befe30
 	errChan := make(chan []byte)
6ede6bc8
 	// Collect stderr, we will use it in case of an error
97a82094
 	go func() {
 		errText, e := ioutil.ReadAll(stderr)
 		if e != nil {
 			errText = []byte("(...couldn't fetch stderr: " + e.Error() + ")")
 		}
58befe30
 		errChan <- errText
 	}()
6ede6bc8
 	// Copy stdout to the returned pipe
58befe30
 	go func() {
 		_, err := io.Copy(pipeW, stdout)
 		if err != nil {
 			pipeW.CloseWithError(err)
 		}
 		errText := <-errChan
97a82094
 		if err := cmd.Wait(); err != nil {
e93afcdd
 			pipeW.CloseWithError(fmt.Errorf("%s: %s", err, errText))
97a82094
 		} else {
 			pipeW.Close()
 		}
 	}()
6ede6bc8
 	// Run the command and return the pipe
97a82094
 	if err := cmd.Start(); err != nil {
 		return nil, err
 	}
 	return pipeR, nil
 }
baacae83
 
 // NewTempArchive reads the content of src into a temporary file, and returns the contents
 // of that file as an archive. The archive can only be read once - as soon as reading completes,
 // the file will be deleted.
 func NewTempArchive(src Archive, dir string) (*TempArchive, error) {
 	f, err := ioutil.TempFile(dir, "")
 	if err != nil {
 		return nil, err
 	}
 	if _, err := io.Copy(f, src); err != nil {
 		return nil, err
 	}
367a679b
 	if err = f.Sync(); err != nil {
 		return nil, err
 	}
baacae83
 	if _, err := f.Seek(0, 0); err != nil {
 		return nil, err
 	}
 	st, err := f.Stat()
 	if err != nil {
 		return nil, err
 	}
 	size := st.Size()
 	return &TempArchive{f, size}, nil
 }
 
 type TempArchive struct {
 	*os.File
 	Size int64 // Pre-computed from Stat().Size() as a convenience
 }
 
 func (archive *TempArchive) Read(data []byte) (int, error) {
 	n, err := archive.File.Read(data)
 	if err != nil {
 		os.Remove(archive.File.Name())
 	}
 	return n, err
 }