Persist workspace configuration across restarts.

The manifests are stored under ~/.config/slothfs/manifests.

This is a boon to people that restart slothfs often, such as its
developers. It also guarantees that state survives machine reboots.

Change-Id: I5faa074a55920de2c4992aca66f7319832f233b4
diff --git a/README.md b/README.md
index e46734f..2aefb36 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,9 @@
 
 A more elaborate configuration file is included as `android.json`.
 
+By default, slothfs loads the configuration from =$HOME/.config/slothfs/clone.json=.
+
+
 DISCLAIMER
 ==========
 
diff --git a/cmd/slothfs-multifs/main.go b/cmd/slothfs-multifs/main.go
index 03e9214..5dbad99 100644
--- a/cmd/slothfs-multifs/main.go
+++ b/cmd/slothfs-multifs/main.go
@@ -32,7 +32,7 @@
 	gitilesURL := flag.String("gitiles", "", "gitiles URL. If unset, derive from manifest location.")
 	cacheDir := flag.String("cache", filepath.Join(os.Getenv("HOME"), ".cache", "slothfs"), "cache dir")
 	debug := flag.Bool("debug", false, "print debug info")
-	config := flag.String("config", "", "JSON file configuring what repositories should be cloned.")
+	config := flag.String("config", filepath.Join(os.Getenv("HOME"), ".config", "slothfs"), "directory with configuration files.")
 	cookieJarPath := flag.String("cookies", "", "path to cURL-style cookie jar file.")
 	agent := flag.String("agent", "slothfs-multifs", "gitiles User-Agent string to use.")
 	flag.Parse()
@@ -69,7 +69,8 @@
 
 	opts := fs.MultiFSOptions{}
 	if *config != "" {
-		configContents, err := ioutil.ReadFile(*config)
+		cloneJS := filepath.Join(*config, "clone.json")
+		configContents, err := ioutil.ReadFile(cloneJS)
 		if err != nil {
 			log.Fatal(err)
 		}
@@ -77,6 +78,11 @@
 		if err != nil {
 			log.Fatal(err)
 		}
+
+		opts.ManifestDir = filepath.Join(*config, "manifests")
+		if err := os.MkdirAll(opts.ManifestDir, 0755); err != nil {
+			log.Fatal(err)
+		}
 	}
 
 	root := fs.NewMultiFS(service, cache, opts)
diff --git a/fs/api.go b/fs/api.go
index 6bfb0d4..fd860ba 100644
--- a/fs/api.go
+++ b/fs/api.go
@@ -49,6 +49,8 @@
 
 // MultiFSOptions holds options for a file system with multiple manifests.
 type MultiFSOptions struct {
+	// ManifestDir stores configured manifest files.
+	ManifestDir string
 
 	// RepoCloneOption matches against the Path field of the
 	// repository within a manifest.
diff --git a/fs/gitilesfs_test.go b/fs/gitilesfs_test.go
index 9d17b30..a749287 100644
--- a/fs/gitilesfs_test.go
+++ b/fs/gitilesfs_test.go
@@ -758,3 +758,62 @@
 		t.Errorf("Lstat(%s): got %v, want error", wsDir, fi)
 	}
 }
+
+func TestMultiFSManifestDir(t *testing.T) {
+	fix, err := newTestFixture()
+	if err != nil {
+		t.Fatalf("newTestFixture: %v", err)
+	}
+	defer fix.cleanup()
+
+	mfDir := filepath.Join(fix.dir, "manifests")
+	if err := os.MkdirAll(mfDir, 0755); err != nil {
+		t.Fatalf("MkdirAll: %v", err)
+	}
+
+	xmlFile := filepath.Join(mfDir, "ws")
+	if err := ioutil.WriteFile(xmlFile, []byte(testManifestXML), 0644); err != nil {
+		t.Errorf("WriteFile(%s): %v", xmlFile, err)
+	}
+
+	opts := MultiFSOptions{
+		ManifestDir: mfDir,
+	}
+	fs := NewMultiFS(fix.service, fix.cache, opts)
+
+	if err := fix.mount(fs); err != nil {
+		t.Fatalf("mount: %v", err)
+	}
+
+	wsDir := filepath.Join(fix.mntDir, "ws")
+	if _, err := os.Lstat(wsDir); err != nil {
+		t.Fatalf("Lstat(%s): %v", wsDir)
+	}
+
+	if err := os.Remove(filepath.Join(fix.mntDir, "config", "ws")); err != nil {
+		t.Fatalf("Remove(config link): %v", err)
+	}
+
+	if fi, err := os.Lstat(filepath.Join(mfDir, "ws")); err == nil {
+		t.Errorf("'ws' still in manifest dir: %v", fi)
+	}
+
+	f, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatalf("TempFile: %v", err)
+	}
+	if err := ioutil.WriteFile(f.Name(), []byte(testManifestXML), 0644); err != nil {
+		t.Errorf("WriteFile(%s): %v", xmlFile, err)
+	}
+
+	configName := filepath.Join(fix.mntDir, "config", "ws2")
+	if err := os.Symlink(f.Name(), configName); err != nil {
+		t.Fatalf("Symlink(%s):  %v", xmlFile, err)
+	}
+
+	// XML file appears again.
+	xmlFile = filepath.Join(mfDir, "ws2")
+	if _, err := os.Stat(xmlFile); err != nil {
+		t.Errorf("Stat(%s): %v", xmlFile, err)
+	}
+}
diff --git a/fs/multifs.go b/fs/multifs.go
index e67a4fd..3c72c05 100644
--- a/fs/multifs.go
+++ b/fs/multifs.go
@@ -17,6 +17,7 @@
 import (
 	"io/ioutil"
 	"log"
+	"os"
 	"path/filepath"
 
 	"github.com/google/slothfs/cache"
@@ -35,12 +36,35 @@
 	gitiles   *gitiles.Service
 }
 
+func (c *configNode) configureWorkspaces() error {
+	if c.root.options.ManifestDir == "" {
+		return nil
+	}
+	fs, err := filepath.Glob(filepath.Join(c.root.options.ManifestDir, "*"))
+	if err != nil || len(fs) == 0 {
+		return err
+	}
+
+	log.Println("configuring workspaces...")
+	for _, f := range fs {
+		_, code := c.Symlink(filepath.Base(f), f, nil)
+		log.Printf("manifest %s: %v", f, code)
+	}
+	return nil
+}
+
 func (r *multiManifestFSRoot) OnMount(fsConn *nodefs.FileSystemConnector) {
 	r.fsConn = fsConn
-	r.Inode().NewChild("config", true, &configNode{
+
+	cfg := &configNode{
 		Node: nodefs.NewDefaultNode(),
 		root: r,
-	})
+	}
+	r.Inode().NewChild("config", true, cfg)
+
+	if err := cfg.configureWorkspaces(); err != nil {
+		log.Println("configureWorkspaces: %v", err)
+	}
 }
 
 func (r *configNode) Deletable() bool { return false }
@@ -98,6 +122,10 @@
 	// the Unlink method, will VFS already knows about the
 	// deletion once we return OK.
 
+	if dir := c.root.options.ManifestDir; dir != "" {
+		os.Remove(filepath.Join(dir, name))
+	}
+
 	return fuse.OK
 }
 
@@ -145,6 +173,27 @@
 		}
 
 		child.NewChild("ERROR", false, &dataNode{nodefs.NewDefaultNode(), []byte(err.Error())})
+	} else {
+		if dir := c.root.options.ManifestDir; dir != "" {
+			for {
+				f, err := ioutil.TempFile(dir, "")
+				if err != nil {
+					break
+				}
+
+				_, err = f.Write(mfBytes)
+				if err != nil {
+					break
+				}
+
+				if err := f.Close(); err != nil {
+					break
+				}
+
+				os.Rename(f.Name(), filepath.Join(dir, name))
+				break
+			}
+		}
 	}
 
 	c.root.fsConn.EntryNotify(c.root.Inode(), name)