archive.go
ef711962
 package docker
97a82094
 
 import (
36d610a3
 	"archive/tar"
 	"bytes"
54db1862
 	"fmt"
5be7b9af
 	"github.com/dotcloud/docker/utils"
97a82094
 	"io"
 	"io/ioutil"
baacae83
 	"os"
97a82094
 	"os/exec"
5be7b9af
 	"path"
36d610a3
 	"path/filepath"
97a82094
 )
 
33f6a0aa
 type Archive io.Reader
 
97a82094
 type Compression uint32
 
 const (
 	Uncompressed Compression = iota
 	Bzip2
 	Gzip
3c5d2e46
 	Xz
97a82094
 )
 
0425f65e
 func DetectCompression(source []byte) Compression {
 	sourceLen := len(source)
 	for compression, m := range map[Compression][]byte{
 		Bzip2: {0x42, 0x5A, 0x68},
 		Gzip:  {0x1F, 0x8B, 0x08},
 		Xz:    {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00},
 	} {
 		fail := false
 		if len(m) > sourceLen {
 			utils.Debugf("Len too short")
 			continue
 		}
 		i := 0
 		for _, b := range m {
 			if b != source[i] {
 				fail = true
 				break
 			}
 			i++
 		}
 		if !fail {
 			return compression
 		}
 	}
 	return Uncompressed
 }
 
97a82094
 func (compression *Compression) Flag() string {
 	switch *compression {
 	case Bzip2:
 		return "j"
 	case Gzip:
 		return "z"
3c5d2e46
 	case Xz:
 		return "J"
97a82094
 	}
 	return ""
 }
 
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 ""
 }
 
5be7b9af
 // Tar creates an archive from the directory at `path`, and returns it as a
 // stream of bytes.
97a82094
 func Tar(path string, compression Compression) (io.Reader, error) {
5be7b9af
 	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) {
d7d42ff4
 	args := []string{"tar", "--numeric-owner", "-f", "-", "-C", path}
5be7b9af
 	if filter == nil {
 		filter = []string{"."}
 	}
 	for _, f := range filter {
 		args = append(args, "-c"+compression.Flag(), f)
 	}
b368d215
 	return CmdStream(exec.Command(args[0], args[1:]...))
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.
97a82094
 func Untar(archive io.Reader, path string) error {
290b1973
 	if archive == nil {
 		return fmt.Errorf("Empty archive")
 	}
08a87d4b
 
 	buf := make([]byte, 10)
 	totalN := 0
 	for totalN < 10 {
 		if n, err := archive.Read(buf[totalN:]); err != nil {
 			if err == io.EOF {
 				return fmt.Errorf("Tarball too short")
 			}
 			return err
 		} else {
 			totalN += n
 			utils.Debugf("[tar autodetect] n: %d", n)
 		}
0425f65e
 	}
 	compression := DetectCompression(buf)
 
 	utils.Debugf("Archive compression detected: %s", compression.Extension())
 
d7d42ff4
 	cmd := exec.Command("tar", "--numeric-owner", "-f", "-", "-C", path, "-x"+compression.Flag())
08a87d4b
 	cmd.Stdin = io.MultiReader(bytes.NewReader(buf), archive)
561ceac5
 	// 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"}
97a82094
 	output, err := cmd.CombinedOutput()
 	if err != nil {
54db1862
 		return fmt.Errorf("%s: %s", err, output)
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.
 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)
 }
 
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 {
 	if archive, err := os.Open(src); err != nil {
 		return err
 	} else if err := Untar(archive, dst); err != nil {
 		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)
 	return TarUntar(src, nil, dst)
 }
 
 // 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)`.
 func CopyFileWithTar(src, dst string) error {
 	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
 	}
 	buf := new(bytes.Buffer)
 	tw := tar.NewWriter(buf)
 	hdr, err := tar.FileInfoHeader(srcSt, "")
 	if err != nil {
 		return err
 	}
 	hdr.Name = filepath.Base(dst)
 	if err := tw.WriteHeader(hdr); err != nil {
 		return err
 	}
 	srcF, err := os.Open(src)
 	if err != nil {
 		return err
 	}
 	if _, err := io.Copy(tw, srcF); err != nil {
 		return err
5be7b9af
 	}
36d610a3
 	tw.Close()
 	return Untar(buf, filepath.Dir(dst))
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.
97a82094
 func CmdStream(cmd *exec.Cmd) (io.Reader, error) {
 	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
 	}
 	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
 }