veyron/services/mgmt: Implement optional packages

This change implements optional packages in the mgmt services. The
binary repository now saves the media type and encoding of the objects,
e.g type=application/x-tar,encoding=gzip, or type=application/zip.

The node manager uses this information to download and install the
packages requested in the application envelopes.

TODO: Test the node manager implementation. This will require either using the
real binaryd, or significantly improving the mock binaryd.

Change-Id: Idc7d0ee7663ccd1816a271289de8f61403d8dcce
diff --git a/services/mgmt/lib/packages/packages.go b/services/mgmt/lib/packages/packages.go
new file mode 100644
index 0000000..57f9dde
--- /dev/null
+++ b/services/mgmt/lib/packages/packages.go
@@ -0,0 +1,192 @@
+// Package packages provides functionality to install ZIP and TAR packages.
+package packages
+
+import (
+	"archive/tar"
+	"archive/zip"
+	"compress/bzip2"
+	"compress/gzip"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"veyron.io/veyron/veyron2/services/mgmt/repository"
+)
+
+const defaultType = "application/octet-stream"
+
+var typemap = map[string]repository.MediaInfo{
+	".zip":     repository.MediaInfo{Type: "application/zip"},
+	".tar":     repository.MediaInfo{Type: "application/x-tar"},
+	".tgz":     repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
+	".tar.gz":  repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
+	".tbz2":    repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+	".tb2":     repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+	".tbz":     repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+	".tar.bz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+}
+
+// MediaInfoForFileName returns the MediaInfo based on the file's extension.
+func MediaInfoForFileName(fileName string) repository.MediaInfo {
+	fileName = strings.ToLower(fileName)
+	for k, v := range typemap {
+		if strings.HasSuffix(fileName, k) {
+			return v
+		}
+	}
+	return repository.MediaInfo{Type: defaultType}
+}
+
+// Install installs a package in the given directory. If the package is a TAR or
+// ZIP archive, its content is extracted in the destination directory.
+// Otherwise, the package file itself is copied to the destination directory.
+func Install(pkgFile, dir string) error {
+	mediaInfo, err := LoadMediaInfo(pkgFile)
+	if err != nil {
+		return err
+	}
+	switch mediaInfo.Type {
+	case "application/x-tar":
+		return extractTar(pkgFile, mediaInfo, dir)
+	case "application/zip":
+		return extractZip(pkgFile, dir)
+	default:
+		return fmt.Errorf("unsupported media type: %v", mediaInfo.Type)
+	}
+}
+
+// LoadMediaInfo returns the MediaInfo for the given package file.
+func LoadMediaInfo(pkgFile string) (repository.MediaInfo, error) {
+	jInfo, err := ioutil.ReadFile(pkgFile + ".__info")
+	if err != nil {
+		return repository.MediaInfo{}, err
+	}
+	var info repository.MediaInfo
+	if err := json.Unmarshal(jInfo, &info); err != nil {
+		return repository.MediaInfo{}, err
+	}
+	return info, nil
+}
+
+// SaveMediaInfo saves the media info for a package.
+func SaveMediaInfo(pkgFile string, mediaInfo repository.MediaInfo) error {
+	jInfo, err := json.Marshal(mediaInfo)
+	if err != nil {
+		return err
+	}
+	infoFile := pkgFile + ".__info"
+	if err := ioutil.WriteFile(infoFile, jInfo, os.FileMode(0600)); err != nil {
+		return err
+	}
+	return nil
+}
+
+func extractZip(zipFile, installDir string) error {
+	zr, err := zip.OpenReader(zipFile)
+	if err != nil {
+		return err
+	}
+	for _, file := range zr.File {
+		fi := file.FileInfo()
+		name := filepath.Join(installDir, file.Name)
+		if !strings.HasPrefix(name, installDir) {
+			return fmt.Errorf("failed to extract file %q outside of install directory", file.Name)
+		}
+		if fi.IsDir() {
+			if err := os.MkdirAll(name, os.FileMode(fi.Mode()&0700)); err != nil && !os.IsExist(err) {
+				return err
+			}
+			continue
+		}
+		in, err := file.Open()
+		if err != nil {
+			return err
+		}
+		parentName := filepath.Dir(name)
+		if err := os.MkdirAll(parentName, os.FileMode(0700)); err != nil {
+			return err
+		}
+		out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(fi.Mode()&0700))
+		if err != nil {
+			in.Close()
+			return err
+		}
+		nbytes, err := io.Copy(out, in)
+		in.Close()
+		out.Close()
+		if err != nil {
+			return err
+		}
+		if nbytes != fi.Size() {
+			return fmt.Errorf("file size doesn't match for %q: %d != %d", fi.Name(), nbytes, fi.Size())
+		}
+	}
+	return nil
+}
+
+func extractTar(pkgFile string, mediaInfo repository.MediaInfo, installDir string) error {
+	f, err := os.Open(pkgFile)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	var reader io.Reader
+	switch enc := mediaInfo.Encoding; enc {
+	case "":
+		reader = f
+	case "gzip":
+		var err error
+		if reader, err = gzip.NewReader(f); err != nil {
+			return err
+		}
+	case "bzip2":
+		reader = bzip2.NewReader(f)
+	default:
+		return fmt.Errorf("unsupported encoding: %q", enc)
+	}
+
+	tr := tar.NewReader(reader)
+	for {
+		hdr, err := tr.Next()
+		if err == io.EOF {
+			return nil
+		}
+		if err != nil {
+			return err
+		}
+		name := filepath.Join(installDir, hdr.Name)
+		if !strings.HasPrefix(name, installDir) {
+			return fmt.Errorf("failed to extract file %q outside of install directory", hdr.Name)
+		}
+		// Regular file
+		if hdr.Typeflag == tar.TypeReg {
+			out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(hdr.Mode&0700))
+			if err != nil {
+				return err
+			}
+			nbytes, err := io.Copy(out, tr)
+			out.Close()
+			if err != nil {
+				return err
+			}
+			if nbytes != hdr.Size {
+				return fmt.Errorf("file size doesn't match for %q: %d != %d", hdr.Name, nbytes, hdr.Size)
+			}
+			continue
+		}
+		// Directory
+		if hdr.Typeflag == tar.TypeDir {
+			if err := os.Mkdir(name, os.FileMode(hdr.Mode&0700)); err != nil && !os.IsExist(err) {
+				return err
+			}
+			continue
+		}
+		// Skip unsupported types
+		// TODO(rthellend): Consider adding support for Symlink.
+	}
+}