[pkgfs] add /pkgfs/needs/packages/*

This adds a directory tree that can be used to identify packages that are
part way through being cached, and provides blob installation endpoints for
their blobs.

Change-Id: I54d0425401e0dcf4e96e2990da4a22b7b19173a8
diff --git a/garnet/go/src/pmd/index/dynamic_index.go b/garnet/go/src/pmd/index/dynamic_index.go
index defdc4b..4b977be 100644
--- a/garnet/go/src/pmd/index/dynamic_index.go
+++ b/garnet/go/src/pmd/index/dynamic_index.go
@@ -254,6 +254,22 @@
 	return found
 }
 
+func (idx *DynamicIndex) PkgHasNeed(pkg, root string) bool {
+	idx.mu.Lock()
+	defer idx.mu.Unlock()
+
+	needs, found := idx.needs[pkg]
+	if !found {
+		return found
+	}
+	for need := range needs {
+		if need == root {
+			return true
+		}
+	}
+	return false
+}
+
 func (idx *DynamicIndex) NeedsList() []string {
 	idx.mu.Lock()
 	defer idx.mu.Unlock()
@@ -266,6 +282,40 @@
 	return names
 }
 
+func (idx *DynamicIndex) PkgNeedsList(pkgRoot string) []string {
+	idx.mu.Lock()
+	defer idx.mu.Unlock()
+
+	pkgNeeds, found := idx.waiting[pkgRoot]
+	if !found {
+		return []string{}
+	}
+	blobs := make([]string, 0, len(pkgNeeds))
+	for blob := range pkgNeeds {
+		blobs = append(blobs, blob)
+	}
+	return blobs
+}
+
+func (idx *DynamicIndex) InstallingList() []string {
+	idx.mu.Lock()
+	defer idx.mu.Unlock()
+
+	names := make([]string, 0, len(idx.installing))
+	for name := range idx.installing {
+		names = append(names, name)
+	}
+	return names
+}
+
+func (idx *DynamicIndex) IsInstalling(merkle string) bool {
+	idx.mu.Lock()
+	defer idx.mu.Unlock()
+
+	_, found := idx.installing[merkle]
+	return found
+}
+
 func (idx *DynamicIndex) Notify(roots ...string) {
 	if len(roots) == 0 {
 		return
diff --git a/garnet/go/src/pmd/pkgfs/needs_directory.go b/garnet/go/src/pmd/pkgfs/needs_directory.go
index 57a8127..6ca8fc98 100644
--- a/garnet/go/src/pmd/pkgfs/needs_directory.go
+++ b/garnet/go/src/pmd/pkgfs/needs_directory.go
@@ -10,6 +10,10 @@
 	"time"
 )
 
+// needsRoot presents the following tree:
+//  /pkgfs/needs/blobs/$BLOB_HASH
+//  /pkgfs/needs/packages/$PACKAGE_HASH/$BLOB_HASH
+// the files are "needsFile" vnodes, so they're writable to blobfs.
 type needsRoot struct {
 	unsupportedDirectory
 
@@ -39,6 +43,13 @@
 			return nbd.Open(parts[1], flags)
 		}
 		return nil, nbd, nil, nil
+	case "packages":
+		npr := &needsPkgRoot{unsupportedDirectory: unsupportedDirectory("/needs/packages"), fs: d.fs}
+		if len(parts) > 1 {
+			return npr.Open(parts[1], flags)
+		}
+		return nil, npr, nil, nil
+
 	default:
 		if len(parts) != 1 || flags.Create() {
 			return nil, nil, nil, fs.ErrNotSupported
@@ -57,18 +68,108 @@
 	return 0, d.fs.mountTime, d.fs.mountTime, nil
 }
 
-type needsFile struct {
-	unsupportedFile
+// needsPkgRoot serves a directory that indexes the blobs needed to fulfill a
+// package that is presently part way through caching.
+type needsPkgRoot struct {
+	unsupportedDirectory
 
 	fs *Filesystem
 }
 
-func (f *needsFile) Close() error {
+func (d *needsPkgRoot) Dup() (fs.Directory, error) {
+	return d, nil
+}
+
+func (d *needsPkgRoot) Close() error {
 	return nil
 }
 
-func (f *needsFile) Stat() (int64, time.Time, time.Time, error) {
-	return 0, time.Time{}, time.Time{}, nil
+func (d *needsPkgRoot) Open(name string, flags fs.OpenFlags) (fs.File, fs.Directory, *fs.Remote, error) {
+	name = clean(name)
+	if name == "" {
+		return nil, d, nil, nil
+	}
+
+	parts := strings.SplitN(name, "/", 2)
+
+	root := parts[0]
+
+	if !d.fs.index.IsInstalling(root) {
+		return nil, nil, nil, fs.ErrNotFound
+	}
+
+	debugLog("pkgfs:needspkgroot:%q open", root)
+	pkgDir := &needsPkgDir{fs: d.fs, pkgRoot: root}
+
+	if len(parts) > 1 {
+		return pkgDir.Open(parts[1], flags)
+	}
+
+	return nil, pkgDir, nil, nil
+}
+
+func (d *needsPkgRoot) Read() ([]fs.Dirent, error) {
+	blobs := d.fs.index.InstallingList()
+	dirents := make([]fs.Dirent, len(blobs))
+	for i := range blobs {
+		dirents[i] = fileDirEnt(blobs[i])
+	}
+	return dirents, nil
+}
+
+func (d *needsPkgRoot) Stat() (int64, time.Time, time.Time, error) {
+	// TODO(raggi): provide more useful values
+	return 0, d.fs.mountTime, d.fs.mountTime, nil
+}
+
+// needsPkgDir serves a directory that indexes the blobs needed to fulfill a
+// package that is presently part way through caching.
+type needsPkgDir struct {
+	unsupportedDirectory
+
+	fs *Filesystem
+
+	pkgRoot string
+}
+
+func (d *needsPkgDir) Dup() (fs.Directory, error) {
+	return d, nil
+}
+
+func (d *needsPkgDir) Close() error {
+	return nil
+}
+
+func (d *needsPkgDir) Open(name string, flags fs.OpenFlags) (fs.File, fs.Directory, *fs.Remote, error) {
+	name = clean(name)
+	if name == "" {
+		return nil, d, nil, nil
+	}
+
+	if strings.Contains(name, "/") {
+		return nil, nil, nil, fs.ErrNotFound
+	}
+
+	if !d.fs.index.PkgHasNeed(d.pkgRoot, name) {
+		return nil, nil, nil, fs.ErrNotFound
+	}
+
+	debugLog("pkgfs:needspkgdir:%q open", name)
+	return &installFile{fs: d.fs, name: name, isPkg: false}, nil, nil, nil
+}
+
+func (d *needsPkgDir) Read() ([]fs.Dirent, error) {
+	names := d.fs.index.PkgNeedsList(d.pkgRoot)
+	dirents := make([]fs.Dirent, len(names))
+	for i := range names {
+		dirents[i] = fileDirEnt(names[i])
+	}
+	return dirents, nil
+}
+
+func (d *needsPkgDir) Stat() (int64, time.Time, time.Time, error) {
+	// TODO(raggi): provide more useful values
+	return 0, d.fs.mountTime, d.fs.mountTime, nil
 }
 
 type needsBlobsDir struct {
diff --git a/garnet/go/src/pmd/pkgfs/pkgfs_test.go b/garnet/go/src/pmd/pkgfs/pkgfs_test.go
index 07b9f25..3637e13 100644
--- a/garnet/go/src/pmd/pkgfs/pkgfs_test.go
+++ b/garnet/go/src/pmd/pkgfs/pkgfs_test.go
@@ -217,7 +217,6 @@
 		t.Fatalf("package blob missing after package write: %s", err)
 	}
 
-	// TODO(raggi): check that the pacakge content blobs appear in the needs tree
 	manifest, err := cfg.Manifest()
 	if err != nil {
 		t.Fatal(err)
@@ -249,6 +248,29 @@
 	}
 	sort.Strings(needs)
 
+	f, err = pkgfsOpen(filepath.Join("needs", "packages", merkleroot), zxio.OpenRightReadable, zxio.ModeTypeDirectory)
+
+	needs2, err := f.Readdirnames(256)
+	f.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for i := range needs2 {
+		needs2[i] = filepath.Base(needs2[i])
+	}
+	sort.Strings(needs2)
+
+	if len(needs) != len(needs2) {
+		t.Errorf("expected needs dirs to be the same: %d != %d", len(needs), len(needs2))
+	}
+
+	for i, need := range needs {
+		if needs2[i] != need {
+			t.Errorf("needs from needs/blobs didn't match package needs at %d", i)
+		}
+	}
+
 	contents, err := ioutil.ReadFile(manifest.Paths["meta/contents"])
 	if err != nil {
 		t.Fatal(err)