Add support for metadata in Storage.copyObject (#194)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c8a4f3..f7d487e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 0.8.14
+- Support override metadata properties in `copyObject`.
+
 ## 0.8.13
 - Support the latest version `^13.0.0` of the `googleapis` package.
 
diff --git a/lib/src/storage_impl.dart b/lib/src/storage_impl.dart
index d2850d9..afaf8a7 100644
--- a/lib/src/storage_impl.dart
+++ b/lib/src/storage_impl.dart
@@ -106,11 +106,14 @@
   }
 
   @override
-  Future copyObject(String src, String dest) {
+  Future copyObject(String src, String dest, {ObjectMetadata? metadata}) {
     var srcName = _AbsoluteName.parse(src);
     var destName = _AbsoluteName.parse(dest);
+    metadata ??= _ObjectMetadata();
+    var objectMetadata = metadata as _ObjectMetadata;
+    final object = objectMetadata._object;
     return _api.objects
-        .copy(storage_api.Object(), srcName.bucketName, srcName.objectName,
+        .copy(object, srcName.bucketName, srcName.objectName,
             destName.bucketName, destName.objectName)
         .then((_) => null);
   }
diff --git a/lib/storage.dart b/lib/storage.dart
index 540c651..21e7705 100644
--- a/lib/storage.dart
+++ b/lib/storage.dart
@@ -561,7 +561,9 @@
   /// Copy object [src] to object [dest].
   ///
   /// The names of [src] and [dest] must be absolute.
-  Future copyObject(String src, String dest);
+  ///
+  /// [metadata] can be used to overwrite metadata properties.
+  Future copyObject(String src, String dest, {ObjectMetadata? metadata});
 }
 
 /// Information on a specific object.
diff --git a/pubspec.yaml b/pubspec.yaml
index b315607..542893f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: gcloud
-version: 0.8.13
+version: 0.8.14
 description: >-
   High level idiomatic Dart API for Google Cloud Storage, Pub-Sub and Datastore.
 repository: https://github.com/dart-lang/gcloud
diff --git a/test/storage/e2e_test.dart b/test/storage/e2e_test.dart
index b93453c..fa9a317 100644
--- a/test/storage/e2e_test.dart
+++ b/test/storage/e2e_test.dart
@@ -153,6 +153,52 @@
       testCreateReadDelete('test-2', bytesResumableUpload);
     });
 
+    testWithBucket('create-copy-read-delete', (bucket) async {
+      final bytes = [1, 2, 3];
+      final info = await bucket.writeBytes('test-for-copy', bytes);
+      expect(info, isNotNull);
+
+      await storage.copyObject(
+        bucket.absoluteObjectName('test-for-copy'),
+        bucket.absoluteObjectName('test'),
+      );
+
+      final result =
+          await bucket.read('test').fold<List<int>>([], (p, e) => p..addAll(e));
+      expect(result, bytes);
+
+      await bucket.delete('test');
+      await bucket.delete('test-for-copy');
+    });
+
+    testWithBucket('create-copy-metadata-read-delete', (bucket) async {
+      final bytes = [1, 2, 3];
+      final info = await bucket.writeBytes(
+        'test-for-copy',
+        bytes,
+        metadata: ObjectMetadata(contentType: 'text/plain'),
+      );
+      expect(info, isNotNull);
+
+      await storage.copyObject(
+        bucket.absoluteObjectName('test-for-copy'),
+        bucket.absoluteObjectName('test'),
+        metadata: ObjectMetadata(contentType: 'application/octet'),
+      );
+
+      final r1 = await bucket.info('test-for-copy');
+      expect(r1.metadata.contentType, 'text/plain');
+      final r2 = await bucket.info('test');
+      expect(r2.metadata.contentType, 'application/octet');
+
+      final result =
+          await bucket.read('test').fold<List<int>>([], (p, e) => p..addAll(e));
+      expect(result, bytes);
+
+      await bucket.delete('test');
+      await bucket.delete('test-for-copy');
+    });
+
     group('create-read-delete-streaming', () {
       void testCreateReadDelete(String name, List<int> bytes) {
         testWithBucket(name, (bucket) async {