storage: omit bucket from copy attributes

When encoding destination attributes on a rewrite call, do not encode
the bucket. This allows us to omit the object name and content type
from the attributes.

Change-Id: I2f4d3e01e6f152543c04b7bcebf78c6ac7978d2f
Reviewed-on: https://code-review.googlesource.com/11296
Reviewed-by: Sarah Adams <shadams@google.com>
diff --git a/storage/copy.go b/storage/copy.go
index 1f28455..b2db656 100644
--- a/storage/copy.go
+++ b/storage/copy.go
@@ -20,7 +20,6 @@
 import (
 	"errors"
 	"fmt"
-	"reflect"
 
 	"golang.org/x/net/context"
 	raw "google.golang.org/api/storage/v1"
@@ -71,17 +70,11 @@
 	if err := c.dst.validate(); err != nil {
 		return nil, err
 	}
-	var rawObject *raw.Object
-	// If any attribute was set, then we make sure the name matches the destination
-	// name, and we check that ContentType is non-empty so we can provide a better
-	// error message than the service.
-	if !reflect.DeepEqual(c.ObjectAttrs, ObjectAttrs{}) {
-		c.ObjectAttrs.Name = c.dst.object
-		if c.ObjectAttrs.ContentType == "" {
-			return nil, errors.New("storage: Copier.ContentType must be non-empty")
-		}
-		rawObject = c.ObjectAttrs.toRawObject(c.dst.bucket)
-	}
+	// Convert destination attributes to raw form, omitting the bucket.
+	// If the bucket is included but name or content-type aren't, the service
+	// returns a 400 with "Required" as the only message. Omitting the bucket
+	// does not cause any problems.
+	rawObject := c.ObjectAttrs.toRawObject("")
 	for {
 		res, err := c.callRewrite(ctx, c.src, rawObject)
 		if err != nil {
diff --git a/storage/integration_test.go b/storage/integration_test.go
index fd9e0f2..6bca383 100644
--- a/storage/integration_test.go
+++ b/storage/integration_test.go
@@ -386,21 +386,9 @@
 			copyObj.Bucket, copyObj.Name, bucket, copyName)
 	}
 
-	// Check for error setting attributes but not ContentType.
-	const (
-		contentType     = "text/html"
-		contentEncoding = "identity"
-	)
-	copier := bkt.Object(copyName).CopierFrom(bkt.Object(objName))
-	copier.ContentEncoding = contentEncoding
-	_, err = copier.Run(ctx)
-	if err == nil {
-		t.Error("copy without ContentType: got nil, want error")
-	}
-
 	// Copying with attributes.
-	copier = bkt.Object(copyName).CopierFrom(bkt.Object(objName))
-	copier.ContentType = contentType
+	const contentEncoding = "identity"
+	copier := bkt.Object(copyName).CopierFrom(bkt.Object(objName))
 	copier.ContentEncoding = contentEncoding
 	copyObj, err = copier.Run(ctx)
 	if err != nil {
@@ -410,9 +398,6 @@
 			t.Errorf("Copy object bucket, name: got %q.%q, want %q.%q",
 				copyObj.Bucket, copyObj.Name, bucket, copyName)
 		}
-		if copyObj.ContentType != contentType {
-			t.Errorf("Copy ContentType: got %q, want %q", copyObj.ContentType, contentType)
-		}
 		if copyObj.ContentEncoding != contentEncoding {
 			t.Errorf("Copy ContentEncoding: got %q, want %q", copyObj.ContentEncoding, contentEncoding)
 		}
@@ -1004,6 +989,38 @@
 	}
 }
 
+func TestIntegration_BucketInCopyAttrs(t *testing.T) {
+	// Confirm that if bucket is included in the object attributes of a rewrite
+	// call, but object name and content-type aren't, then we get an error. See
+	// the comment in Copier.Run.
+	ctx := context.Background()
+	client, bucket := testConfig(ctx, t)
+	defer client.Close()
+
+	bkt := client.Bucket(bucket)
+	obj := bkt.Object("bucketInCopyAttrs")
+	if err := writeObject(ctx, obj, "", []byte("foo")); err != nil {
+		t.Fatal(err)
+	}
+	copier := obj.CopierFrom(obj)
+	rawObject := copier.ObjectAttrs.toRawObject(bucket)
+	_, err := copier.callRewrite(ctx, obj, rawObject)
+	if err == nil {
+		t.Errorf("got nil, want error")
+	}
+}
+
+func writeObject(ctx context.Context, obj *ObjectHandle, contentType string, contents []byte) error {
+	w := obj.NewWriter(ctx)
+	w.ContentType = contentType
+	if contents != nil {
+		if _, err := w.Write([]byte(contents)); err != nil {
+			return err
+		}
+	}
+	return w.Close()
+}
+
 func readObject(ctx context.Context, obj *ObjectHandle) ([]byte, error) {
 	r, err := obj.NewReader(ctx)
 	if err != nil {