cmd/benchsave: add header flag for specifying common keys

This allows easily adding additional information when uploading
existing files. Example usage:

benchsave -header <(echo goarch: arm; echo goos: linux) *.log

Change-Id: I46afafd39d8314d9c06c209f868b71cd5e58c63a
Reviewed-on: https://go-review.googlesource.com/35350
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/cmd/benchsave/benchsave.go b/cmd/benchsave/benchsave.go
index 3649c53..5913e41 100644
--- a/cmd/benchsave/benchsave.go
+++ b/cmd/benchsave/benchsave.go
@@ -6,21 +6,23 @@
 //
 // Usage:
 //
-//	benchsave [-server https://server.org] a.txt [b.txt ...]
+//	benchsave [-v] [-header file] [-server url] file...
 //
 // Each input file should contain the output from one or more runs of
 // ``go test -bench'', or another tool which uses the same format.
 //
-// benchsave will upload the input files to the specified server and
+// Benchsave will upload the input files to the specified server and
 // print a URL where they can be viewed.
 package main
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"flag"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"log"
 	"mime/multipart"
 	"os"
@@ -31,8 +33,9 @@
 )
 
 var (
-	server  = flag.String("server", "https://perfdata.golang.org", "perfdata server to upload benchmarks to")
-	verbose = flag.Bool("v", false, "verbose")
+	server  = flag.String("server", "https://perfdata.golang.org", "upload benchmarks to server at `url`")
+	verbose = flag.Bool("v", false, "print verbose log messages")
+	header  = flag.String("header", "", "insert `file` at the beginning of each uploaded file")
 )
 
 type uploadStatus struct {
@@ -45,25 +48,26 @@
 }
 
 // writeOneFile reads name and writes it to mpw.
-func writeOneFile(mpw *multipart.Writer, name string) {
+func writeOneFile(mpw *multipart.Writer, name string, header []byte) error {
 	w, err := mpw.CreateFormFile("file", filepath.Base(name))
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "Writing upload failed: %v\n", err)
-		os.Exit(1)
+		return err
+	}
+	if len(header) > 0 {
+		if _, err := w.Write(header); err != nil {
+			return err
+		}
 	}
 	f, err := os.Open(name)
 	if err != nil {
-		fmt.Fprint(os.Stderr, err)
-		mpw.WriteField("abort", "1")
-		// TODO(quentin): Wait until the abort field is written before exiting.
-		os.Exit(1)
+		return err
 	}
 	defer f.Close()
 
 	if _, err := io.Copy(w, f); err != nil {
-		fmt.Fprintf(os.Stderr, "Writing upload failed: %v", err)
-		os.Exit(1)
+		return err
 	}
+	return nil
 }
 
 func usage() {
@@ -85,6 +89,16 @@
 		log.Fatal("no files to upload")
 	}
 
+	var headerData []byte
+	if *header != "" {
+		var err error
+		headerData, err = ioutil.ReadFile(*header)
+		if err != nil {
+			log.Fatal(err)
+		}
+		headerData = append(bytes.TrimRight(headerData, "\n"), '\n', '\n')
+	}
+
 	// TODO(quentin): Some servers might not need authentication.
 	// We should somehow detect this and not force the user to get a token.
 	// Or they might need non-Google authentication.
@@ -98,8 +112,16 @@
 		defer mpw.Close()
 
 		for _, name := range files {
-			writeOneFile(mpw, name)
+			if err := writeOneFile(mpw, name, headerData); err != nil {
+				log.Print(err)
+				mpw.WriteField("abort", "1")
+				// Writing the 'abort' field will cause the server to send back an error response,
+				// which will cause the main goroutine to  below.
+				return
+			}
 		}
+
+		mpw.WriteField("commit", "1")
 	}()
 
 	start := time.Now()
diff --git a/storage/app/upload.go b/storage/app/upload.go
index d66de6c..475ffca 100644
--- a/storage/app/upload.go
+++ b/storage/app/upload.go
@@ -93,6 +93,9 @@
 		}
 
 		name := p.FormName()
+		if name == "commit" {
+			continue
+		}
 		if name != "file" {
 			return nil, fmt.Errorf("unexpected field %q", name)
 		}