godev/cmd/telemetrygodev: serve merged reports through the frontend

To avoid serving content directly out of our GCS bucket, add an endpoint
to the telemetry frontend to serve merged reports. This mitigates the
only reason for us to have a public GCS bucket, which is bad practice.

Change-Id: I6c7938966da3cce63803cd990d1f44347f1a8e0f
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/676756
Reviewed-by: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Robert Findley <rfindley@google.com>
diff --git a/godev/cmd/telemetrygodev/main.go b/godev/cmd/telemetrygodev/main.go
index 6c5d2b3..c732c4b 100644
--- a/godev/cmd/telemetrygodev/main.go
+++ b/godev/cmd/telemetrygodev/main.go
@@ -77,7 +77,8 @@
 	// TODO(rfindley): restrict this routing to POST
 	mux.Handle("/upload/", handleUpload(ucfg, buckets.Upload))
 	mux.Handle("/charts/", handleCharts(render, buckets.Chart))
-	mux.Handle("/data/", handleData(render, buckets.Merge))
+	mux.Handle("GET /data/", handleDataList(render, buckets.Merge))
+	mux.Handle("GET /data/{date}", handleData(buckets.Merge))
 
 	mw := middleware.Chain(
 		middleware.Log(logger),
@@ -234,19 +235,17 @@
 }
 
 type dataPage struct {
-	BucketURL string
-	Dates     []string
+	Dates []string
 }
 
 func (dataPage) Breadcrumbs() []breadcrumb {
 	return []breadcrumb{{Link: "/", Label: "Go Telemetry"}, {Label: "Data"}}
 }
 
-func handleData(render renderer, mergeBucket storage.BucketHandle) content.HandlerFunc {
+func handleDataList(render renderer, mergeBucket storage.BucketHandle) content.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) error {
 		it := mergeBucket.Objects(r.Context(), "")
 		var page dataPage
-		page.BucketURL = mergeBucket.URI()
 		for {
 			obj, err := it.Next()
 			if errors.Is(err, storage.ErrObjectIteratorDone) {
@@ -264,6 +263,36 @@
 	}
 }
 
+func handleData(mergeBucket storage.BucketHandle) content.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) error {
+		date := r.PathValue("date")
+		if _, err := time.Parse(telemetry.DateOnly, date); err != nil {
+			return content.Error(fmt.Errorf("invalid YYYY-MM-DD date: %q", date), http.StatusBadRequest)
+		}
+		reader, err := mergeBucket.Object(date + ".json").NewReader(r.Context())
+		if errors.Is(err, storage.ErrObjectNotExist) {
+			return content.Error(fmt.Errorf("date %q not found", date), http.StatusNotFound)
+		}
+		if err != nil {
+			return err
+		}
+		defer reader.Close()
+		data, err := io.ReadAll(reader)
+		if err != nil {
+			return err
+		}
+		// Despite the merged report having a .json extension, it's not actually
+		// valid JSON (it's newline-delimited JSON objects). Therefore, it
+		// doesn't actually work well with content aware viewers if we give it
+		// the application/json content type. Furthermore, when this data was
+		// previously served directly out of GCS, it had the text/plain content
+		// type.
+		w.Header().Set("Content-Type", "text/plain")
+		_, err = w.Write(data)
+		return err
+	}
+}
+
 func loadCharts(ctx context.Context, chartObj string, bucket storage.BucketHandle) (map[string]any, error) {
 	reader, err := bucket.Object(chartObj).NewReader(ctx)
 	if err != nil {
diff --git a/internal/content/telemetrygodev/data.html b/internal/content/telemetrygodev/data.html
index 9e2a28b..5f0f302 100644
--- a/internal/content/telemetrygodev/data.html
+++ b/internal/content/telemetrygodev/data.html
@@ -23,9 +23,8 @@
 <section>
 <div class="Content">
   <ul style="margin-top: 1.5rem; column-count: auto; column-width: 10rem">
-  {{$url := .BucketURL}}
   {{range .Dates}}
-  <li><a href="{{$url}}/{{.}}.json">{{.}}</a></li>
+  <li><a href="/data/{{.}}">{{.}}</a></li>
   {{end}}
   </ul>
 </div>