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/binary/binaryd/test.sh b/services/mgmt/binary/binaryd/test.sh
index 51b7201..6cfc373 100755
--- a/services/mgmt/binary/binaryd/test.sh
+++ b/services/mgmt/binary/binaryd/test.sh
@@ -30,29 +30,49 @@
   # Create a binary file.
   local -r BINARY_SUFFIX="test-binary"
   local -r BINARY="${REPO}/${BINARY_SUFFIX}"
-  local -r BINARY_FILE=$(shell::tmp_file)
+  local -r BINARY_FILE="${WORKDIR}/bin1"
   dd if=/dev/urandom of="${BINARY_FILE}" bs=1000000 count=16 \
     || shell_test::fail "line ${LINENO}: faile to create a random binary file"
   "${BINARY_BIN}" upload "${BINARY}" "${BINARY_FILE}" || shell_test::fail "line ${LINENO}: 'upload' failed"
 
+  # Create TAR file.
+  local -r TAR="${REPO}/tarobj"
+  local -r TAR_FILE="${WORKDIR}/bin1.tar.gz"
+  tar zcvf "${TAR_FILE}" "${BINARY_FILE}"
+  "${BINARY_BIN}" upload "${TAR}" "${TAR_FILE}" || shell_test::fail "line ${LINENO}: 'upload' failed"
+
   # Download the binary file.
-  local -r BINARY_FILE2=$(shell::tmp_file)
+  local -r BINARY_FILE2="${WORKDIR}/bin2"
   "${BINARY_BIN}" download "${BINARY}" "${BINARY_FILE2}" || shell_test::fail "line ${LINENO}: 'RPC download' failed"
   if [[ $(cmp "${BINARY_FILE}" "${BINARY_FILE2}" &> /dev/null) ]]; then
     shell_test::fail "mismatching binary file downloaded via RPC"
   fi
+  local -r BINARY_FILE2_INFO=$(cat "${BINARY_FILE2}.info")
+  shell_test::assert_eq "${BINARY_FILE2_INFO}" '{"type":"application/octet-stream"}' "${LINENO}"
 
-  local -r BINARY_FILE3=$(shell::tmp_file)
+  # Download the tar file.
+  local -r TAR_FILE2="${WORKDIR}/downloadedtar"
+  "${BINARY_BIN}" download "${TAR}" "${TAR_FILE2}" || shell_test::fail "line ${LINENO}: 'RPC download' failed"
+  if [[ $(cmp "${TAR_FILE}" "${TAR_FILE2}" &> /dev/null) ]]; then
+    shell_test::fail "mismatching tar file downloaded via RPC"
+  fi
+  local -r TAR_FILE2_INFO=$(cat "${TAR_FILE2}.info")
+  shell_test::assert_eq "${TAR_FILE2_INFO}" '{"encoding":"gzip","type":"application/x-tar"}' "${LINENO}"
+
+  local -r BINARY_FILE3="${WORKDIR}/bin3"
   curl -f -o "${BINARY_FILE3}" "http://${HTTP_ADDR}/${BINARY_SUFFIX}" || shell_test::fail "line ${LINENO}: 'HTTP download' failed"
   if [[ $(cmp "${BINARY_FILE}" "${BINARY_FILE3}" &> /dev/null) ]]; then
     shell_test::fail "mismatching binary file downloaded via HTTP"
   fi
 
-  # Remove the binary file.
+  # Remove the files.
   "${BINARY_BIN}" delete "${BINARY}" || shell_test::fail "line ${LINENO}: 'delete' failed"
+  "${BINARY_BIN}" delete "${TAR}" || shell_test::fail "line ${LINENO}: 'delete' failed"
 
-  # Check the binary no longer exists.
-  local -r RESULT=$(shell::check_result "${BINARY_BIN}" download "${BINARY}" "${BINARY_FILE2}")
+  # Check the files no longer exist.
+  local RESULT=$(shell::check_result "${BINARY_BIN}" download "${BINARY}" "${BINARY_FILE2}")
+  shell_test::assert_ne "${RESULT}" "0" "${LINENO}"
+  RESULT=$(shell::check_result "${BINARY_BIN}" download "${TAR}" "${TAR_FILE2}")
   shell_test::assert_ne "${RESULT}" "0" "${LINENO}"
 
   shell_test::pass
diff --git a/services/mgmt/binary/impl/fs_utils.go b/services/mgmt/binary/impl/fs_utils.go
index 431f434..62eaab8 100644
--- a/services/mgmt/binary/impl/fs_utils.go
+++ b/services/mgmt/binary/impl/fs_utils.go
@@ -79,7 +79,7 @@
 			}
 			result[idx] = filepath.Join(path, partName)
 		} else {
-			if info.Name() == "name" {
+			if info.Name() == "name" || info.Name() == "mediainfo" {
 				continue
 			}
 			// The only entries should correspond to the part dirs.
diff --git a/services/mgmt/binary/impl/http_test.go b/services/mgmt/binary/impl/http_test.go
index 1ca59e8..bde93b3 100644
--- a/services/mgmt/binary/impl/http_test.go
+++ b/services/mgmt/binary/impl/http_test.go
@@ -9,6 +9,7 @@
 	"testing"
 
 	"veyron.io/veyron/veyron2/rt"
+	"veyron.io/veyron/veyron2/services/mgmt/repository"
 
 	"veyron.io/veyron/veyron/lib/testutil"
 )
@@ -27,7 +28,8 @@
 			size := testutil.Rand.Intn(1000*bufferLength) + 1
 			data[i] = testutil.RandomBytes(size)
 		}
-		if err := binary.Create(rt.R().NewContext(), int32(length), "application/octet-stream"); err != nil {
+		mediaInfo := repository.MediaInfo{Type: "application/octet-stream"}
+		if err := binary.Create(rt.R().NewContext(), int32(length), mediaInfo); err != nil {
 			t.Fatalf("Create() failed: %v", err)
 		}
 		for i := 0; i < length; i++ {
diff --git a/services/mgmt/binary/impl/impl_test.go b/services/mgmt/binary/impl/impl_test.go
index ec0e151..4eaa06e 100644
--- a/services/mgmt/binary/impl/impl_test.go
+++ b/services/mgmt/binary/impl/impl_test.go
@@ -162,7 +162,7 @@
 		size := testutil.Rand.Intn(1000 * bufferLength)
 		data := testutil.RandomBytes(size)
 		// Test the binary repository interface.
-		if err := binary.Create(rt.R().NewContext(), 1, "applicaton/octet-stream"); err != nil {
+		if err := binary.Create(rt.R().NewContext(), 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
 			t.Fatalf("Create() failed: %v", err)
 		}
 		if streamErr, err := invokeUpload(t, binary, data, 0); streamErr != nil || err != nil {
@@ -215,7 +215,7 @@
 			data[i] = testutil.RandomBytes(size)
 		}
 		// Test the binary repository interface.
-		if err := binary.Create(rt.R().NewContext(), int32(length), "applicaton/octet-stream"); err != nil {
+		if err := binary.Create(rt.R().NewContext(), int32(length), repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
 			t.Fatalf("Create() failed: %v", err)
 		}
 		for i := 0; i < length; i++ {
@@ -264,7 +264,7 @@
 			size := testutil.Rand.Intn(1000 * bufferLength)
 			data[i] = testutil.RandomBytes(size)
 		}
-		if err := binary.Create(rt.R().NewContext(), int32(length), "applicaton/octet-stream"); err != nil {
+		if err := binary.Create(rt.R().NewContext(), int32(length), repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
 			t.Fatalf("Create() failed: %v", err)
 		}
 		// Simulate a flaky upload client that keeps uploading parts until
@@ -309,10 +309,10 @@
 			data[i][j] = byte(testutil.Rand.Int())
 		}
 	}
-	if err := binary.Create(rt.R().NewContext(), int32(length), "applicaton/octet-stream"); err != nil {
+	if err := binary.Create(rt.R().NewContext(), int32(length), repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
 		t.Fatalf("Create() failed: %v", err)
 	}
-	if err := binary.Create(rt.R().NewContext(), int32(length), "applicaton/octet-stream"); err == nil {
+	if err := binary.Create(rt.R().NewContext(), int32(length), repository.MediaInfo{Type: "application/octet-stream"}); err == nil {
 		t.Fatalf("Create() did not fail when it should have")
 	} else if want := verror.Exists; !verror.Is(err, want) {
 		t.Fatalf("Unexpected error: %v, expected error id %v", err, want)
@@ -372,7 +372,7 @@
 		name := naming.JoinAddressName(ep, obj)
 		binary := repository.BinaryClient(name)
 
-		if err := binary.Create(rt.R().NewContext(), 1, "applicaton/octet-stream"); err != nil {
+		if err := binary.Create(rt.R().NewContext(), 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil {
 			t.Fatalf("Create() failed: %v", err)
 		}
 		if streamErr, err := invokeUpload(t, binary, data, 0); streamErr != nil || err != nil {
diff --git a/services/mgmt/binary/impl/service.go b/services/mgmt/binary/impl/service.go
index 44894a1..04cb34b 100644
--- a/services/mgmt/binary/impl/service.go
+++ b/services/mgmt/binary/impl/service.go
@@ -22,6 +22,7 @@
 import (
 	"crypto/md5"
 	"encoding/hex"
+	"encoding/json"
 	"io"
 	"io/ioutil"
 	"os"
@@ -75,8 +76,8 @@
 
 const bufferLength = 4096
 
-func (i *binaryService) Create(_ ipc.ServerContext, nparts int32, mediaType string) error {
-	vlog.Infof("%v.Create(%v, %v)", i.suffix, nparts, mediaType)
+func (i *binaryService) Create(_ ipc.ServerContext, nparts int32, mediaInfo repository.MediaInfo) error {
+	vlog.Infof("%v.Create(%v, %v)", i.suffix, nparts, mediaInfo)
 	if nparts < 1 {
 		return errInvalidParts
 	}
@@ -96,6 +97,16 @@
 		vlog.Errorf("WriteFile(%q) failed: %v", nameFile)
 		return errOperationFailed
 	}
+	infoFile := filepath.Join(tmpDir, "mediainfo")
+	jInfo, err := json.Marshal(mediaInfo)
+	if err != nil {
+		vlog.Errorf("json.Marshal(%v) failed: %v", mediaInfo, err)
+		return errOperationFailed
+	}
+	if err := ioutil.WriteFile(infoFile, jInfo, os.FileMode(0600)); err != nil {
+		vlog.Errorf("WriteFile(%q) failed: %v", infoFile, err)
+		return errOperationFailed
+	}
 	for j := 0; j < int(nparts); j++ {
 		partPath, partPerm := generatePartPath(tmpDir, j), os.FileMode(0700)
 		if err := os.MkdirAll(partPath, partPerm); err != nil {
@@ -199,12 +210,12 @@
 	return "", 0, nil
 }
 
-func (i *binaryService) Stat(ipc.ServerContext) ([]binary.PartInfo, string, error) {
+func (i *binaryService) Stat(ipc.ServerContext) ([]binary.PartInfo, repository.MediaInfo, error) {
 	vlog.Infof("%v.Stat()", i.suffix)
 	result := make([]binary.PartInfo, 0)
 	parts, err := getParts(i.path)
 	if err != nil {
-		return []binary.PartInfo{}, "", err
+		return []binary.PartInfo{}, repository.MediaInfo{}, err
 	}
 	for _, part := range parts {
 		checksumFile := filepath.Join(part, checksum)
@@ -215,7 +226,7 @@
 				continue
 			}
 			vlog.Errorf("ReadFile(%v) failed: %v", checksumFile, err)
-			return []binary.PartInfo{}, "", errOperationFailed
+			return []binary.PartInfo{}, repository.MediaInfo{}, errOperationFailed
 		}
 		dataFile := filepath.Join(part, data)
 		fi, err := os.Stat(dataFile)
@@ -225,12 +236,22 @@
 				continue
 			}
 			vlog.Errorf("Stat(%v) failed: %v", dataFile, err)
-			return []binary.PartInfo{}, "", errOperationFailed
+			return []binary.PartInfo{}, repository.MediaInfo{}, errOperationFailed
 		}
 		result = append(result, binary.PartInfo{Checksum: string(bytes), Size: fi.Size()})
 	}
-	// TODO(rthellend): Store the actual media type.
-	return result, "application/octet-stream", nil
+	infoFile := filepath.Join(i.path, "mediainfo")
+	jInfo, err := ioutil.ReadFile(infoFile)
+	if err != nil {
+		vlog.Errorf("ReadFile(%q) failed: %v", infoFile)
+		return []binary.PartInfo{}, repository.MediaInfo{}, errOperationFailed
+	}
+	var mediaInfo repository.MediaInfo
+	if err := json.Unmarshal(jInfo, &mediaInfo); err != nil {
+		vlog.Errorf("json.Unmarshal(%v) failed: %v", jInfo, err)
+		return []binary.PartInfo{}, repository.MediaInfo{}, errOperationFailed
+	}
+	return result, mediaInfo, nil
 }
 
 func (i *binaryService) Upload(context repository.BinaryUploadContext, part int32) error {
diff --git a/services/mgmt/lib/binary/impl.go b/services/mgmt/lib/binary/impl.go
index a1bedb8..07bedd9 100644
--- a/services/mgmt/lib/binary/impl.go
+++ b/services/mgmt/lib/binary/impl.go
@@ -18,6 +18,8 @@
 	"veyron.io/veyron/veyron2/services/mgmt/repository"
 	"veyron.io/veyron/veyron2/verror"
 	"veyron.io/veyron/veyron2/vlog"
+
+	"veyron.io/veyron/veyron/services/mgmt/lib/packages"
 )
 
 var (
@@ -41,17 +43,16 @@
 	return nil
 }
 
-func download(ctx context.T, w io.WriteSeeker, von string) error {
+func download(ctx context.T, w io.WriteSeeker, von string) (repository.MediaInfo, error) {
 	client := repository.BinaryClient(von)
-	// TODO(rthellend): Use the media type.
-	parts, _, err := client.Stat(ctx)
+	parts, mediaInfo, err := client.Stat(ctx)
 	if err != nil {
 		vlog.Errorf("Stat() failed: %v", err)
-		return err
+		return repository.MediaInfo{}, err
 	}
 	for _, part := range parts {
 		if part.Checksum == binary.MissingChecksum {
-			return errNotExist
+			return repository.MediaInfo{}, errNotExist
 		}
 	}
 	offset, whence := int64(0), 0
@@ -102,33 +103,34 @@
 			success = true
 		}
 		if !success {
-			return errOperationFailed
+			return repository.MediaInfo{}, errOperationFailed
 		}
 		offset += part.Size
 	}
-	return nil
+	return mediaInfo, nil
 }
 
-func Download(ctx context.T, von string) ([]byte, error) {
+func Download(ctx context.T, von string) ([]byte, repository.MediaInfo, error) {
 	dir, prefix := "", ""
 	file, err := ioutil.TempFile(dir, prefix)
 	if err != nil {
 		vlog.Errorf("TempFile(%v, %v) failed: %v", dir, prefix, err)
-		return nil, errOperationFailed
+		return nil, repository.MediaInfo{}, errOperationFailed
 	}
 	defer os.Remove(file.Name())
 	defer file.Close()
 	ctx, cancel := ctx.WithTimeout(time.Minute)
 	defer cancel()
-	if err := download(ctx, file, von); err != nil {
-		return nil, errOperationFailed
+	mediaInfo, err := download(ctx, file, von)
+	if err != nil {
+		return nil, repository.MediaInfo{}, errOperationFailed
 	}
 	bytes, err := ioutil.ReadFile(file.Name())
 	if err != nil {
 		vlog.Errorf("ReadFile(%v) failed: %v", file.Name(), err)
-		return nil, errOperationFailed
+		return nil, repository.MediaInfo{}, errOperationFailed
 	}
-	return bytes, nil
+	return bytes, mediaInfo, nil
 }
 
 func DownloadToFile(ctx context.T, von, path string) error {
@@ -141,13 +143,14 @@
 	defer file.Close()
 	ctx, cancel := ctx.WithTimeout(time.Minute)
 	defer cancel()
-	if err := download(ctx, file, von); err != nil {
+	mediaInfo, err := download(ctx, file, von)
+	if err != nil {
 		if err := os.Remove(file.Name()); err != nil {
 			vlog.Errorf("Remove(%v) failed: %v", file.Name(), err)
 		}
 		return errOperationFailed
 	}
-	perm := os.FileMode(0700)
+	perm := os.FileMode(0600)
 	if err := file.Chmod(perm); err != nil {
 		vlog.Errorf("Chmod(%v) failed: %v", perm, err)
 		if err := os.Remove(file.Name()); err != nil {
@@ -162,10 +165,17 @@
 		}
 		return errOperationFailed
 	}
+	if err := packages.SaveMediaInfo(path, mediaInfo); err != nil {
+		vlog.Errorf("packages.SaveMediaInfo(%v, %v) failed: %v", path, mediaInfo, err)
+		if err := os.Remove(path); err != nil {
+			vlog.Errorf("Remove(%v) failed: %v", path, err)
+		}
+		return errOperationFailed
+	}
 	return nil
 }
 
-func upload(ctx context.T, r io.ReadSeeker, von string) error {
+func upload(ctx context.T, r io.ReadSeeker, mediaInfo repository.MediaInfo, von string) error {
 	client := repository.BinaryClient(von)
 	offset, whence := int64(0), 2
 	size, err := r.Seek(offset, whence)
@@ -174,9 +184,7 @@
 		return errOperationFailed
 	}
 	nparts := (size-1)/partSize + 1
-	// TODO(rthellend): Determine the actual media type.
-	mediaType := "application/octet-stream"
-	if err := client.Create(ctx, int32(nparts), mediaType); err != nil {
+	if err := client.Create(ctx, int32(nparts), mediaInfo); err != nil {
 		vlog.Errorf("Create() failed: %v", err)
 		return err
 	}
@@ -258,11 +266,11 @@
 	return nil
 }
 
-func Upload(ctx context.T, von string, data []byte) error {
+func Upload(ctx context.T, von string, data []byte, mediaInfo repository.MediaInfo) error {
 	buffer := bytes.NewReader(data)
 	ctx, cancel := ctx.WithTimeout(time.Minute)
 	defer cancel()
-	return upload(ctx, buffer, von)
+	return upload(ctx, buffer, mediaInfo, von)
 }
 
 func UploadFromFile(ctx context.T, von, path string) error {
@@ -274,5 +282,6 @@
 	}
 	ctx, cancel := ctx.WithTimeout(time.Minute)
 	defer cancel()
-	return upload(ctx, file, von)
+	mediaInfo := packages.MediaInfoForFileName(path)
+	return upload(ctx, file, mediaInfo, von)
 }
diff --git a/services/mgmt/lib/binary/impl_test.go b/services/mgmt/lib/binary/impl_test.go
index ce66250..ca0a76f 100644
--- a/services/mgmt/lib/binary/impl_test.go
+++ b/services/mgmt/lib/binary/impl_test.go
@@ -5,11 +5,13 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"reflect"
 	"testing"
 
 	"veyron.io/veyron/veyron2"
 	"veyron.io/veyron/veyron2/naming"
 	"veyron.io/veyron/veyron2/rt"
+	"veyron.io/veyron/veyron2/services/mgmt/repository"
 	"veyron.io/veyron/veyron2/vlog"
 
 	"veyron.io/veyron/veyron/lib/testutil"
@@ -85,21 +87,25 @@
 	von, cleanup := setupRepository(t)
 	defer cleanup()
 	data := testutil.RandomBytes(testutil.Rand.Intn(10 << 20))
-	if err := Upload(runtime.NewContext(), von, data); err != nil {
+	mediaInfo := repository.MediaInfo{Type: "application/octet-stream"}
+	if err := Upload(runtime.NewContext(), von, data, mediaInfo); err != nil {
 		t.Fatalf("Upload(%v) failed: %v", von, err)
 	}
-	output, err := Download(runtime.NewContext(), von)
+	output, outInfo, err := Download(runtime.NewContext(), von)
 	if err != nil {
 		t.Fatalf("Download(%v) failed: %v", von, err)
 	}
 	if bytes.Compare(data, output) != 0 {
-		t.Fatalf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
+		t.Errorf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
 	}
 	if err := Delete(runtime.NewContext(), von); err != nil {
-		t.Fatalf("Delete(%v) failed: %v", von, err)
+		t.Errorf("Delete(%v) failed: %v", von, err)
 	}
-	if _, err := Download(runtime.NewContext(), von); err == nil {
-		t.Fatalf("Download(%v) did not fail", von)
+	if _, _, err := Download(runtime.NewContext(), von); err == nil {
+		t.Errorf("Download(%v) did not fail", von)
+	}
+	if !reflect.DeepEqual(mediaInfo, outInfo) {
+		t.Errorf("unexpected media info: expected %v, got %v", mediaInfo, outInfo)
 	}
 }
 
@@ -117,11 +123,15 @@
 	}
 	defer os.Remove(src.Name())
 	defer src.Close()
-	dst, err := ioutil.TempFile(dir, prefix)
+	dstdir, err := ioutil.TempDir(dir, prefix)
 	if err != nil {
-		t.Fatalf("TempFile(%v, %v) failed: %v", dir, prefix, err)
+		t.Fatalf("TempDir(%v, %v) failed: %v", dir, prefix, err)
 	}
-	defer os.Remove(dst.Name())
+	defer os.RemoveAll(dstdir)
+	dst, err := ioutil.TempFile(dstdir, prefix)
+	if err != nil {
+		t.Fatalf("TempFile(%v, %v) failed: %v", dstdir, prefix, err)
+	}
 	defer dst.Close()
 	if _, err := src.Write(data); err != nil {
 		t.Fatalf("Write() failed: %v", err)
@@ -134,12 +144,19 @@
 	}
 	output, err := ioutil.ReadFile(dst.Name())
 	if err != nil {
-		t.Fatalf("ReadFile(%v) failed: %v", dst.Name(), err)
+		t.Errorf("ReadFile(%v) failed: %v", dst.Name(), err)
 	}
 	if bytes.Compare(data, output) != 0 {
-		t.Fatalf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
+		t.Errorf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
+	}
+	jMediaInfo, err := ioutil.ReadFile(dst.Name() + ".__info")
+	if err != nil {
+		t.Errorf("ReadFile(%v) failed: %v", dst.Name()+".__info", err)
+	}
+	if expected := `{"Type":"application/octet-stream","Encoding":""}`; string(jMediaInfo) != expected {
+		t.Errorf("unexpected media info: expected %q, got %q", expected, string(jMediaInfo))
 	}
 	if err := Delete(runtime.NewContext(), von); err != nil {
-		t.Fatalf("Delete(%v) failed: %v", von, err)
+		t.Errorf("Delete(%v) failed: %v", von, err)
 	}
 }
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.
+	}
+}
diff --git a/services/mgmt/lib/packages/packages_test.go b/services/mgmt/lib/packages/packages_test.go
new file mode 100644
index 0000000..8182b44
--- /dev/null
+++ b/services/mgmt/lib/packages/packages_test.go
@@ -0,0 +1,242 @@
+package packages_test
+
+import (
+	"archive/tar"
+	"archive/zip"
+	"compress/gzip"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"sort"
+	"testing"
+
+	"veyron.io/veyron/veyron2/services/mgmt/repository"
+
+	"veyron.io/veyron/veyron/services/mgmt/lib/packages"
+)
+
+func TestInstall(t *testing.T) {
+	workdir, err := ioutil.TempDir("", "packages-test-")
+	if err != nil {
+		t.Fatalf("ioutil.TempDir failed: %v", err)
+	}
+	defer os.RemoveAll(workdir)
+	srcdir := filepath.Join(workdir, "src")
+	dstdir := filepath.Join(workdir, "dst")
+	createFiles(t, srcdir)
+
+	zipfile := filepath.Join(workdir, "archivezip")
+	tarfile := filepath.Join(workdir, "archivetar")
+	tgzfile := filepath.Join(workdir, "archivetgz")
+
+	makeZip(t, zipfile, srcdir)
+	makeTar(t, tarfile, srcdir)
+	doGzip(t, tarfile, tgzfile)
+
+	binfile := filepath.Join(workdir, "binfile")
+	ioutil.WriteFile(binfile, []byte("This is a binary file"), os.FileMode(0644))
+	ioutil.WriteFile(binfile+".__info", []byte(`{"type":"application/octet-stream"}`), os.FileMode(0644))
+
+	expected := []string{
+		"a perm:700",
+		"a/b perm:700",
+		"a/b/xyzzy.txt perm:600",
+		"a/bar.txt perm:600",
+		"a/foo.txt perm:600",
+	}
+	for _, file := range []string{zipfile, tarfile, tgzfile} {
+		setupDstDir(t, dstdir)
+		if err := packages.Install(file, dstdir); err != nil {
+			t.Errorf("packages.Install failed for %q: %v", file, err)
+		}
+		files := scanDir(dstdir)
+		if !reflect.DeepEqual(files, expected) {
+			t.Errorf("unexpected result for %q: Got %q, want %q", file, files, expected)
+		}
+	}
+
+	setupDstDir(t, dstdir)
+	if err := packages.Install(binfile, dstdir); err == nil {
+		t.Errorf("expected packages.Install to fail %q", binfile)
+	}
+}
+
+func TestMediaInfo(t *testing.T) {
+	testcases := []struct {
+		filename string
+		expected repository.MediaInfo
+	}{
+		{"foo.zip", repository.MediaInfo{Type: "application/zip"}},
+		{"foo.ZIP", repository.MediaInfo{Type: "application/zip"}},
+		{"foo.tar", repository.MediaInfo{Type: "application/x-tar"}},
+		{"foo.TAR", repository.MediaInfo{Type: "application/x-tar"}},
+		{"foo.tgz", repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}},
+		{"FOO.TAR.GZ", repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}},
+		{"foo.tbz2", repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}},
+		{"foo.tar.bz2", repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}},
+		{"foo", repository.MediaInfo{Type: "application/octet-stream"}},
+	}
+	for _, tc := range testcases {
+		if got := packages.MediaInfoForFileName(tc.filename); !reflect.DeepEqual(got, tc.expected) {
+			t.Errorf("unexpected result for %q: Got %v, want %v", tc.filename, got, tc.expected)
+		}
+	}
+}
+
+func createFiles(t *testing.T, dir string) {
+	if err := os.Mkdir(dir, os.FileMode(0755)); err != nil {
+		t.Fatalf("os.Mkdir(%q) failed: %v", dir, err)
+	}
+	dirs := []string{"a", "a/b"}
+	for _, d := range dirs {
+		fullname := filepath.Join(dir, d)
+		if err := os.Mkdir(fullname, os.FileMode(0755)); err != nil {
+			t.Fatalf("os.Mkdir(%q) failed: %v", fullname, err)
+		}
+	}
+	files := []string{"a/foo.txt", "a/bar.txt", "a/b/xyzzy.txt"}
+	for _, f := range files {
+		fullname := filepath.Join(dir, f)
+		if err := ioutil.WriteFile(fullname, []byte(f), os.FileMode(0644)); err != nil {
+			t.Fatalf("ioutil.WriteFile(%q) failed: %v", fullname, err)
+		}
+	}
+}
+
+func makeZip(t *testing.T, zipfile, dir string) {
+	z, err := os.OpenFile(zipfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
+	if err != nil {
+		t.Fatalf("os.OpenFile(%q) failed: %v", zipfile, err)
+	}
+	defer z.Close()
+	w := zip.NewWriter(z)
+	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			t.Fatalf("Walk(%q) error: %v", dir, err)
+		}
+		if dir == path {
+			return nil
+		}
+		fh, err := zip.FileInfoHeader(info)
+		if err != nil {
+			t.Fatalf("FileInfoHeader failed: %v", err)
+		}
+		fh.Name, _ = filepath.Rel(dir, path)
+		hdr, err := w.CreateHeader(fh)
+		if err != nil {
+			t.Fatalf("w.CreateHeader failed: %v", err)
+		}
+		if !info.IsDir() {
+			content, err := ioutil.ReadFile(path)
+			if err != nil {
+				t.Fatalf("ioutil.ReadFile(%q) failed: %v", path, err)
+			}
+			if _, err = hdr.Write(content); err != nil {
+				t.Fatalf("hdr.Write(%q) failed: %v", content, err)
+			}
+		}
+		return nil
+	})
+	if err := w.Close(); err != nil {
+		t.Fatalf("w.Close() failed: %v", err)
+	}
+	if err := ioutil.WriteFile(zipfile+".__info", []byte(`{"type":"application/zip"}`), os.FileMode(0644)); err != nil {
+		t.Fatalf("ioutil.WriteFile() failed: %v", err)
+	}
+}
+
+func makeTar(t *testing.T, tarfile, dir string) {
+	tf, err := os.OpenFile(tarfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
+	if err != nil {
+		t.Fatalf("os.OpenFile(%q) failed: %v", tarfile, err)
+	}
+	defer tf.Close()
+
+	tw := tar.NewWriter(tf)
+	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			t.Fatalf("Walk(%q) error: %v", dir, err)
+		}
+		if dir == path {
+			return nil
+		}
+		hdr, err := tar.FileInfoHeader(info, "")
+		if err != nil {
+			t.Fatalf("tar.FileInfoHeader failed: %v", err)
+		}
+		hdr.Name, _ = filepath.Rel(dir, path)
+		if err := tw.WriteHeader(hdr); err != nil {
+			t.Fatalf("tw.WriteHeader failed: %v", err)
+		}
+		if !info.IsDir() {
+			content, err := ioutil.ReadFile(path)
+			if err != nil {
+				t.Fatalf("ioutil.ReadFile(%q) failed: %v", path, err)
+			}
+			if _, err := tw.Write(content); err != nil {
+				t.Fatalf("tw.Write failed: %v", err)
+			}
+		}
+		return nil
+	})
+	if err := tw.Close(); err != nil {
+		t.Fatalf("tw.Close failed: %v", err)
+	}
+	if err := ioutil.WriteFile(tarfile+".__info", []byte(`{"type":"application/x-tar"}`), os.FileMode(0644)); err != nil {
+		t.Fatalf("ioutil.WriteFile() failed: %v", err)
+	}
+}
+
+func doGzip(t *testing.T, infile, outfile string) {
+	in, err := os.Open(infile)
+	if err != nil {
+		t.Fatalf("os.Open(%q) failed: %v", infile, err)
+	}
+	defer in.Close()
+	out, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
+	if err != nil {
+		t.Fatalf("os.OpenFile(%q) failed: %v", outfile, err)
+	}
+	defer out.Close()
+	writer := gzip.NewWriter(out)
+	defer writer.Close()
+	if _, err := io.Copy(writer, in); err != nil {
+		t.Fatalf("io.Copy() failed: %v", err)
+	}
+
+	info, err := packages.LoadMediaInfo(infile)
+	if err != nil {
+		t.Fatalf("LoadMediaInfo(%q) failed: %v", infile, err)
+	}
+	info.Encoding = "gzip"
+	if err := packages.SaveMediaInfo(outfile, info); err != nil {
+		t.Fatalf("SaveMediaInfo(%v) failed: %v", outfile, err)
+	}
+}
+
+func scanDir(root string) []string {
+	files := []string{}
+	filepath.Walk(root, func(path string, info os.FileInfo, _ error) error {
+		if root == path {
+			return nil
+		}
+		rel, _ := filepath.Rel(root, path)
+		perm := info.Mode() & 0700
+		files = append(files, fmt.Sprintf("%s perm:%o", rel, perm))
+		return nil
+	})
+	sort.Strings(files)
+	return files
+}
+
+func setupDstDir(t *testing.T, dst string) {
+	if err := os.RemoveAll(dst); err != nil {
+		t.Fatalf("os.RemoveAll(%q) failed: %v", dst, err)
+	}
+	if err := os.Mkdir(dst, os.FileMode(0755)); err != nil {
+		t.Fatalf("os.Mkdir(%q) failed: %v", dst, err)
+	}
+}
diff --git a/services/mgmt/node/impl/app_service.go b/services/mgmt/node/impl/app_service.go
index 794d9f0..0ad6e33 100644
--- a/services/mgmt/node/impl/app_service.go
+++ b/services/mgmt/node/impl/app_service.go
@@ -21,6 +21,10 @@
 //         bin                      - application binary
 //         previous                 - symbolic link to previous version directory
 //         envelope                 - application envelope (JSON-encoded)
+//         pkg/                     - the application packages
+//           <pkg name>
+//           <pkg name>.__info
+//           ...
 //       <version 2 timestamp>
 //       ...
 //       current                    - symbolic link to the current version
@@ -29,6 +33,9 @@
 //           credentials/           - holds veyron credentials (unless running
 //                                    through security agent)
 //           root/                  - workspace that the instance is run from
+//             packages/            - the installed packages
+//               <pkg name>/
+//               ...
 //           logs/                  - stderr/stdout and log files generated by instance
 //           info                   - metadata for the instance (such as app
 //                                    cycle manager name and process id)
@@ -136,6 +143,8 @@
 	vsecurity "veyron.io/veyron/veyron/security"
 	"veyron.io/veyron/veyron/security/agent"
 	"veyron.io/veyron/veyron/security/agent/keymgr"
+	libbinary "veyron.io/veyron/veyron/services/mgmt/lib/binary"
+	libpackages "veyron.io/veyron/veyron/services/mgmt/lib/packages"
 	iconfig "veyron.io/veyron/veyron/services/mgmt/node/config"
 )
 
@@ -319,10 +328,25 @@
 	if err := mkdir(versionDir); err != nil {
 		return "", errOperationFailed
 	}
+	pkgDir := filepath.Join(versionDir, "pkg")
+	if err := mkdir(pkgDir); err != nil {
+		return "", errOperationFailed
+	}
 	// TODO(caprita): Share binaries if already existing locally.
 	if err := downloadBinary(versionDir, "bin", envelope.Binary); err != nil {
 		return versionDir, err
 	}
+	for localPkg, pkgName := range envelope.Packages {
+		if localPkg == "" || localPkg[0] == '.' || strings.Contains(localPkg, string(filepath.Separator)) {
+			vlog.Infof("invalid local package name: %q", localPkg)
+			return versionDir, errOperationFailed
+		}
+		path := filepath.Join(pkgDir, localPkg)
+		if err := libbinary.DownloadToFile(rt.R().NewContext(), pkgName, path); err != nil {
+			vlog.Infof("DownloadToFile(%q, %q) failed: %v", pkgName, path, err)
+			return versionDir, errOperationFailed
+		}
+	}
 	if err := saveEnvelope(versionDir, envelope); err != nil {
 		return versionDir, err
 	}
@@ -554,6 +578,31 @@
 	return installationDirCore(i.suffix, i.config.Root)
 }
 
+// installPackages installs all the packages for a new instance.
+func installPackages(versionDir, instanceDir string) error {
+	envelope, err := loadEnvelope(versionDir)
+	if err != nil {
+		return err
+	}
+	packagesDir := filepath.Join(instanceDir, "root", "packages")
+	if err := os.MkdirAll(packagesDir, os.FileMode(0700)); err != nil {
+		return err
+	}
+	// TODO(rthellend): Consider making the packages read-only and sharing
+	// them between apps or instances.
+	for pkg, _ := range envelope.Packages {
+		pkgFile := filepath.Join(versionDir, "pkg", pkg)
+		dstDir := filepath.Join(packagesDir, pkg)
+		if err := os.MkdirAll(dstDir, os.FileMode(0700)); err != nil {
+			return err
+		}
+		if err := libpackages.Install(pkgFile, dstDir); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func initializeInstanceACLs(instanceDir string, blessings []string, acl security.ACL) error {
 	if acl.In == nil {
 		// The acl.In will be empty for an unclaimed node manager. In this case,
@@ -599,6 +648,10 @@
 		vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, versionLink, err)
 		return instanceDir, instanceID, errOperationFailed
 	}
+	if err := installPackages(versionDir, instanceDir); err != nil {
+		vlog.Errorf("installPackages(%v, %v) failed: %v", versionDir, instanceDir, err)
+		return instanceDir, instanceID, errOperationFailed
+	}
 	instanceInfo := new(instanceInfo)
 	if err := setupPrincipal(instanceDir, versionDir, call, i.securityAgent, instanceInfo); err != nil {
 		return instanceDir, instanceID, err
diff --git a/services/mgmt/node/impl/mock_repo_test.go b/services/mgmt/node/impl/mock_repo_test.go
index e669595..6de199e 100644
--- a/services/mgmt/node/impl/mock_repo_test.go
+++ b/services/mgmt/node/impl/mock_repo_test.go
@@ -80,7 +80,7 @@
 
 // BINARY REPOSITORY INTERFACE IMPLEMENTATION
 
-func (*brInvoker) Create(ipc.ServerContext, int32, string) error {
+func (*brInvoker) Create(ipc.ServerContext, int32, repository.MediaInfo) error {
 	vlog.VI(1).Infof("Create()")
 	return nil
 }
@@ -125,16 +125,16 @@
 	return "", 0, nil
 }
 
-func (*brInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, string, error) {
+func (*brInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, repository.MediaInfo, error) {
 	vlog.VI(1).Infof("Stat()")
 	h := md5.New()
 	bytes, err := ioutil.ReadFile(os.Args[0])
 	if err != nil {
-		return []binary.PartInfo{}, "", errOperationFailed
+		return []binary.PartInfo{}, repository.MediaInfo{}, errOperationFailed
 	}
 	h.Write(bytes)
 	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
-	return []binary.PartInfo{part}, "application/octet-stream", nil
+	return []binary.PartInfo{part}, repository.MediaInfo{Type: "application/octet-stream"}, nil
 }
 
 func (i *brInvoker) Upload(repository.BinaryUploadContext, int32) error {
diff --git a/services/mgmt/node/impl/util.go b/services/mgmt/node/impl/util.go
index d5ab40b..8446506 100644
--- a/services/mgmt/node/impl/util.go
+++ b/services/mgmt/node/impl/util.go
@@ -23,7 +23,7 @@
 )
 
 func downloadBinary(workspace, fileName, name string) error {
-	data, err := binary.Download(rt.R().NewContext(), name)
+	data, _, err := binary.Download(rt.R().NewContext(), name)
 	if err != nil {
 		vlog.Errorf("Download(%v) failed: %v", name, err)
 		return errOperationFailed
diff --git a/tools/binary/impl.go b/tools/binary/impl.go
index 3874c2c..df34d6c 100644
--- a/tools/binary/impl.go
+++ b/tools/binary/impl.go
@@ -71,6 +71,7 @@
 }
 
 func runUpload(cmd *cmdline.Command, args []string) error {
+	// TODO(rthellend): Add support for creating packages on the fly.
 	if expected, got := 2, len(args); expected != got {
 		return cmd.UsageErrorf("upload: incorrect number of arguments, expected %d, got %d", expected, got)
 	}
diff --git a/tools/binary/impl_test.go b/tools/binary/impl_test.go
index b1f18fd..865c35d 100644
--- a/tools/binary/impl_test.go
+++ b/tools/binary/impl_test.go
@@ -27,7 +27,7 @@
 	suffix string
 }
 
-func (s *server) Create(ipc.ServerContext, int32, string) error {
+func (s *server) Create(ipc.ServerContext, int32, repository.MediaInfo) error {
 	vlog.Infof("Create() was called. suffix=%v", s.suffix)
 	return nil
 }
@@ -53,13 +53,13 @@
 	return "", 0, nil
 }
 
-func (s *server) Stat(ipc.ServerContext) ([]binary.PartInfo, string, error) {
+func (s *server) Stat(ipc.ServerContext) ([]binary.PartInfo, repository.MediaInfo, error) {
 	vlog.Infof("Stat() was called. suffix=%v", s.suffix)
 	h := md5.New()
 	text := "HelloWorld"
 	h.Write([]byte(text))
 	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(text))}
-	return []binary.PartInfo{part}, "text/plain", nil
+	return []binary.PartInfo{part}, repository.MediaInfo{Type: "text/plain"}, nil
 }
 
 func (s *server) Upload(ctx repository.BinaryUploadContext, _ int32) error {