[gcs-util] Allow soft-transition to client passing builds/<namespace>

Soft-transition:

1. Land this CL
2. Recipes start passing builds/bid into gcs-util -namespace
3. Remove this behavior, in favor of just prepending -namespace to all
   destinations which don't already start with -namespace (except for
   deduplicated uploads)

Bug: 92697
Change-Id: I8e7d5846981191d519480bb80fb0d6e628b10cbc
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/683782
Commit-Queue: Anthony Fandrianto <atyfto@google.com>
Fuchsia-Auto-Submit: Anthony Fandrianto <atyfto@google.com>
Reviewed-by: Oliver Newman <olivernewman@google.com>
diff --git a/cmd/gcs-util/up.go b/cmd/gcs-util/up.go
index da05081..0ae6708 100644
--- a/cmd/gcs-util/up.go
+++ b/cmd/gcs-util/up.go
@@ -92,6 +92,13 @@
 	if c.namespace == "" {
 		return errors.New("-namespace is required")
 	}
+	// This is a temporary behavior to allow clients to soft-transition to
+	// passing the desired builds/<namespace> format. The knowledge of the
+	// builds/ directory will be removed from this tool once the transition is
+	// complete.
+	if !strings.HasPrefix(c.namespace, "builds/") {
+		c.namespace = path.Join("builds", c.namespace)
+	}
 	if c.manifestPath == "" {
 		return errors.New("-manifest-path is required")
 	}
@@ -183,8 +190,7 @@
 		return err
 	}
 	defer sink.client.Close()
-	buildsNamespaceDir := path.Join("builds", c.namespace)
-	return uploadFiles(ctx, files, sink, c.j, buildsNamespaceDir)
+	return uploadFiles(ctx, files, sink, c.j, c.namespace)
 }
 
 // DataSink is an abstract data sink, providing a mockable interface to
@@ -417,7 +423,7 @@
 	return files, nil
 }
 
-func uploadFiles(ctx context.Context, files []types.Upload, dest dataSink, j int, buildsNamespaceDir string) error {
+func uploadFiles(ctx context.Context, files []types.Upload, dest dataSink, j int, namespace string) error {
 	if j <= 0 {
 		return fmt.Errorf("Concurrency factor j must be a positive number")
 	}
@@ -450,10 +456,10 @@
 	upload := func() {
 		defer wg.Done()
 		for upload := range uploads {
-			// Files which are not deduplicated are uploaded to the builds
-			// namespace.
+			// Files which are not deduplicated are uploaded to the dedicated
+			// -namespace.
 			if !upload.Deduplicate {
-				upload.Destination = path.Join(buildsNamespaceDir, upload.Destination)
+				upload.Destination = path.Join(namespace, upload.Destination)
 			}
 			exists, attrs, err := dest.objectExistsAt(ctx, upload.Destination)
 			if err != nil {
@@ -482,7 +488,7 @@
 				// TODO(fxbug.dev/78017): Delete this logic once fixed.
 				var md5Err md5MismatchError
 				if errors.As(err, &md5Err) {
-					upload.Destination = path.Join(buildsNamespaceDir, "md5-mismatches", upload.Destination)
+					upload.Destination = path.Join(namespace, "md5-mismatches", upload.Destination)
 					if err := uploadFile(ctx, upload, dest); err != nil {
 						logging.Warningf(ctx, "failed to upload md5-mismatch file %q for debugging: %s", upload.Destination, err)
 					}
@@ -516,7 +522,7 @@
 	if len(objs) > 0 {
 		objsToRefreshTTLUpload := types.Upload{
 			Contents:    []byte(strings.Join(objs, "\n")),
-			Destination: path.Join(buildsNamespaceDir, objsToRefreshTTLTxt),
+			Destination: path.Join(namespace, objsToRefreshTTLTxt),
 		}
 		return uploadFile(ctx, objsToRefreshTTLUpload, dest)
 	}