[upload_debug_symbols] create a tool to upload symbol files

Bug: IN-796

Change-Id: I49bc03a235f701ef34302a61a0d6e006ff191d11
diff --git a/cmd/upload_debug_symbols/main.go b/cmd/upload_debug_symbols/main.go
new file mode 100644
index 0000000..74ccfac
--- /dev/null
+++ b/cmd/upload_debug_symbols/main.go
@@ -0,0 +1,132 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This tool upload debug symbols in ids.txt to Cloud storage
+//
+// Example Usage:
+// $ upload_debug_symbols -bucket=/bucket_name -idsFilePath=/path/to/ids.txt
+
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"strings"
+
+	"cloud.google.com/go/storage"
+	"fuchsia.googlesource.com/tools/elflib"
+)
+
+const usage = `upload_debug_symbols [flags] bucket idsFilePath
+
+Upload debug symbol files listed in ids.txt to Cloud storage
+`
+
+// Command line flag values
+var (
+	gcsBucket   string
+	idsFilePath string
+)
+
+func init() {
+	flag.Usage = func() {
+		fmt.Fprint(os.Stderr, usage)
+		flag.PrintDefaults()
+		os.Exit(0)
+	}
+
+	flag.StringVar(&gcsBucket, "bucket", "", "The bucket to upload")
+	flag.StringVar(&idsFilePath, "idsFilePath", "", "The path to file ids.txt")
+}
+
+func main() {
+	flag.Parse()
+
+	if gcsBucket == "" {
+		log.Fatal("Error: gcsBucket is not specified.")
+	}
+	if idsFilePath == "" {
+		log.Fatal("Error: idsFilePath is not specified.")
+	}
+
+	if err := execute(context.Background()); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func execute(ctx context.Context) error {
+	client, err := storage.NewClient(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to create client: %v", err)
+	}
+
+	bkt := client.Bucket(gcsBucket)
+
+	// Check if the bucket exists
+	if _, err = bkt.Attrs(ctx); err != nil {
+		return err
+	}
+
+	myClient := gcsClient{bkt}
+	if err = uploadSymbolFiles(ctx, &myClient, idsFilePath); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func uploadSymbolFiles(ctx context.Context, client GCSClient, idsFilePath string) error {
+	file, err := os.Open(idsFilePath)
+	if err != nil {
+		return fmt.Errorf("failed to open %s: %v", idsFilePath, err)
+	}
+	defer file.Close()
+	binaries, err := elflib.ReadIDsFile(file)
+	if err != nil {
+		return fmt.Errorf("failed to read %s with elflib: %v", idsFilePath, err)
+	}
+
+	for _, binaryFileRef := range binaries {
+		err := client.uploadSingleFile(ctx, binaryFileRef.BuildID+".debug", binaryFileRef.Filepath)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (client *gcsClient) uploadSingleFile(ctx context.Context, url string, filePath string) error {
+	content, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return fmt.Errorf("unable to read file in uploadSingleFile: %v", err)
+	}
+	// This writer only perform write when the precondition of DoesNotExist is true.
+	wc := client.bkt.Object(url).If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)
+	if _, err := wc.Write(content); err != nil {
+		return fmt.Errorf("failed to write to buffer of GCS onject writer: %v", err)
+	}
+	// Close completes the write operation and flushes any buffered data.
+	if err := wc.Close(); err != nil {
+		// Error 412 means the precondition of DoesNotExist doesn't match.
+		// It is the expected behavior since we don't want to upload duplicated files.
+		if !strings.Contains(err.Error(), "Error 412") {
+			return fmt.Errorf("failed in close: %v", err)
+		}
+	}
+	return nil
+}
+
+// GCSClient provide method to upload single file to gcs
+type GCSClient interface {
+	uploadSingleFile(ctx context.Context, url string, filePath string) error
+}
+
+// gcsClient is the object that implement GCSClient
+type gcsClient struct {
+	bkt *storage.BucketHandle
+}
diff --git a/cmd/upload_debug_symbols/main_test.go b/cmd/upload_debug_symbols/main_test.go
new file mode 100644
index 0000000..05a17ca
--- /dev/null
+++ b/cmd/upload_debug_symbols/main_test.go
@@ -0,0 +1,74 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package main
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"reflect"
+	"testing"
+)
+
+type FakeGCSClient struct {
+	contensts map[string]string
+}
+
+func (client *FakeGCSClient) uploadSingleFile(ctx context.Context, url string, filePath string) error {
+	client.contensts[url] = filePath
+	return nil
+}
+
+func TestRunCommand(t *testing.T) {
+	tests := []struct {
+		// The name of this test case.
+		name string
+		// The lines of the input ids.txt file
+		input string
+		// The lines of the input ids.txt file
+		hashes []string
+		// The set of files referenced in input.
+		files []string
+		// The expected objects that should be written
+		output map[string]string
+	}{
+		{
+			name: "should upload the files in idx.txt",
+			input: "01634b09 /path/to/binaryA.elf\n" +
+				"02298167 /path/to/binaryB\n" +
+				"025abbbc /path/to/binaryC.so",
+			output: map[string]string{
+				"01634b09.debug": "/path/to/binaryA.elf",
+				"02298167.debug": "/path/to/binaryB",
+				"025abbbc.debug": "/path/to/binaryC.so",
+			},
+		},
+		// {
+		// 	name:   "should upload nothing if nothing in ids.txt",
+		// 	input:  "",
+		// 	output: map[string]string{},
+		// },
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Create the ids.txt file
+			idsFile, err := ioutil.TempFile("", "ids.txt")
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer os.Remove(idsFile.Name())
+			idsFile.Write([]byte(tt.input))
+
+			ctx := context.Background()
+			client := FakeGCSClient{make(map[string]string)}
+			uploadSymbolFiles(ctx, &client, idsFile.Name())
+
+			eq := reflect.DeepEqual(client.contensts, tt.output)
+			if !eq {
+				t.Fatal("The results are not the same", client.contensts, tt.output)
+			}
+		})
+	}
+}