Rename shards only after successfully completing all of them

Before, we could write the first shard, while we would error exit
writing the second shard. The webserver would then see a Frankenstein
version of the corpus.

Fixes #11.

Change-Id: I1bc045c74d0c0e5d46d8e97db5494d8a00748b6c
diff --git a/build/builder.go b/build/builder.go
index 2e0fb01..77ee611 100644
--- a/build/builder.go
+++ b/build/builder.go
@@ -88,6 +88,10 @@
 
 	errMu      sync.Mutex
 	buildError error
+
+	// temp name => final name for finished shards. We only rename
+	// them once all shards succeed to avoid Frankstein corpuses.
+	finishedShards map[string]string
 }
 
 // SetDefaults sets reasonable default options.
@@ -170,8 +174,9 @@
 	}
 
 	b := &Builder{
-		opts:     opt,
-		throttle: make(chan int, opt.Parallelism),
+		opts:           opt,
+		throttle:       make(chan int, opt.Parallelism),
+		finishedShards: map[string]string{},
 	}
 
 	if _, err := b.newShardBuilder(); err != nil {
@@ -206,6 +211,20 @@
 func (b *Builder) Finish() error {
 	b.flush()
 	b.building.Wait()
+
+	if b.buildError != nil {
+		for tmp := range b.finishedShards {
+			os.Remove(tmp)
+		}
+		return b.buildError
+	}
+
+	for tmp, final := range b.finishedShards {
+		if err := os.Rename(tmp, final); err != nil {
+			b.buildError = err
+		}
+	}
+
 	if b.nextShardNum > 0 {
 		b.deleteRemainingShards()
 	}
@@ -319,7 +338,11 @@
 	for _, t := range todo {
 		shardBuilder.Add(*t)
 	}
-	return writeShard(name, shardBuilder)
+
+	if err := b.writeShard(name, shardBuilder); err != nil {
+		return err
+	}
+	return nil
 }
 
 func (b *Builder) newShardBuilder() (*zoekt.IndexBuilder, error) {
@@ -333,7 +356,7 @@
 	return shardBuilder, nil
 }
 
-func writeShard(fn string, b *zoekt.IndexBuilder) error {
+func (b *Builder) writeShard(fn string, ib *zoekt.IndexBuilder) error {
 	dir := filepath.Dir(fn)
 	if err := os.MkdirAll(dir, 0700); err != nil {
 		return err
@@ -345,7 +368,7 @@
 	}
 
 	defer f.Close()
-	if err := b.Write(f); err != nil {
+	if err := ib.Write(f); err != nil {
 		return err
 	}
 	fi, err := f.Stat()
@@ -355,11 +378,10 @@
 	if err := f.Close(); err != nil {
 		return err
 	}
-	if err := os.Rename(f.Name(), fn); err != nil {
-		return err
-	}
 
-	log.Printf("wrote %s: %d index bytes (overhead %3.1f)", fn, fi.Size(),
-		float64(fi.Size())/float64(b.ContentSize()+1))
+	b.finishedShards[f.Name()] = fn
+	log.Printf("finished %s: %d index bytes (overhead %3.1f)", fn, fi.Size(),
+		float64(fi.Size())/float64(ib.ContentSize()+1))
+
 	return nil
 }
diff --git a/build/e2e_test.go b/build/e2e_test.go
index dc7ad0f..46087b7 100644
--- a/build/e2e_test.go
+++ b/build/e2e_test.go
@@ -244,3 +244,48 @@
 		t.Fatalf("Glob(%s): got %v, want 1 shard", glob, fs)
 	}
 }
+
+func TestPartialSuccess(t *testing.T) {
+	dir, err := ioutil.TempDir("", "")
+	if err != nil {
+		t.Fatalf("TempDir: %v", err)
+	}
+
+	opts := Options{
+		IndexDir: dir,
+		ShardMax: 1024,
+		RepoDir:  "/a",
+		SizeMax:  1 << 20,
+	}
+	opts.SetDefaults()
+
+	b, err := NewBuilder(opts)
+	if err != nil {
+		t.Fatalf("NewBuilder: %v", err)
+	}
+
+	for i := 0; i < 4; i++ {
+		nm := fmt.Sprintf("F%d", i)
+
+		// no error checking: the 2nd call will fail
+		b.AddFile(nm, []byte(strings.Repeat("01234567\n", 128)))
+		if i == 1 {
+			// force writes to fail.
+			if err := os.Chmod(dir, 0555); err != nil {
+				t.Fatalf("chmod(%s): %s", dir, err)
+			}
+		}
+	}
+
+	if err := os.Chmod(dir, 0755); err != nil {
+		t.Fatalf("chmod(%s, writable): %s", dir, err)
+	}
+
+	b.Finish()
+
+	if fs, err := filepath.Glob(dir + "/*"); err != nil {
+		t.Errorf("glob(%s): %v", dir, err)
+	} else if len(fs) != 0 {
+		t.Errorf("got shards %v, want []", fs)
+	}
+}