diff --git a/generated/google_cloud_rpc/lib/src/exceptions.dart b/generated/google_cloud_rpc/lib/src/exceptions.dart index 4f16304a..6cf1c35e 100644 --- a/generated/google_cloud_rpc/lib/src/exceptions.dart +++ b/generated/google_cloud_rpc/lib/src/exceptions.dart @@ -64,6 +64,12 @@ final class ServiceException implements Exception { String? responseBody, Status? status, }) => switch (response.statusCode) { + 304 => NotModifiedException( + message, + response: response, + responseBody: responseBody, + status: status, + ), 400 => BadRequestException( message, response: response, @@ -183,7 +189,11 @@ final class ServiceException implements Exception { ) { if (responseBody == null || responseBody.isEmpty) { return ServiceException._fromDecodedResponse( - 'unknown error', + // A "304 Not Modified" response never has a body, so there is no + // server-provided message to describe it. + response.statusCode == 304 + ? 'the resource was not modified' + : 'unknown error', response: response, responseBody: responseBody, ); @@ -231,6 +241,19 @@ final class ServiceException implements Exception { String toString() => '$_name: $message'; } +/// Exception thrown when the server returns a "304 Not Modified" response. +final class NotModifiedException extends ServiceException { + NotModifiedException( + super.message, { + required super.response, + required super.responseBody, + super.status, + }) : super(statusCode: 304); + + @override + String get _name => 'NotModifiedException'; +} + /// Exception thrown when the server returns a "400 Bad Request" response. final class BadRequestException extends ServiceException { BadRequestException( diff --git a/generated/google_cloud_rpc/test/exceptions_test.dart b/generated/google_cloud_rpc/test/exceptions_test.dart index e2b4700a..a267127f 100644 --- a/generated/google_cloud_rpc/test/exceptions_test.dart +++ b/generated/google_cloud_rpc/test/exceptions_test.dart @@ -147,6 +147,22 @@ void main() { expect(e.toString(), 'ServiceException: internal error'); }); + // A "304 Not Modified" response never has a body. + test('empty body 304', () { + final response = http.Response('', 304); + final e = ServiceException.fromHttpResponse(response, ''); + expect(e, isA()); + expect(e.message, 'the resource was not modified'); + expect(e.statusCode, 304); + expect(e.response, response); + expect(e.responseBody, ''); + expect(e.status, isNull); + expect( + e.toString(), + 'NotModifiedException: the resource was not modified', + ); + }); + test('valid error 401', () { final response = http.Response( '{"error": {"message": "unauthorized"}}', diff --git a/pkgs/google_cloud_storage/CHANGELOG.md b/pkgs/google_cloud_storage/CHANGELOG.md index 62e139a8..21317c0c 100644 --- a/pkgs/google_cloud_storage/CHANGELOG.md +++ b/pkgs/google_cloud_storage/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.6.4-wip + +* Add an `ifMetagenerationNotMatch` parameter to `Storage.patchBucket`, + `Storage.uploadObject`, and `Storage.uploadObjectFromString`. If the + precondition is not satisfied, a `NotModifiedException` is thrown. + ## 0.6.3 * Add `prefix`, `delimiter`, and `includeTrailingDelimiter` parameters to diff --git a/pkgs/google_cloud_storage/lib/src/client.dart b/pkgs/google_cloud_storage/lib/src/client.dart index 0de06b32..56526f7f 100644 --- a/pkgs/google_cloud_storage/lib/src/client.dart +++ b/pkgs/google_cloud_storage/lib/src/client.dart @@ -486,6 +486,11 @@ final class Storage { /// value. If the metageneration does not match, a /// [PreconditionFailedException] is thrown. /// + /// If set, [ifMetagenerationNotMatch] makes updating the bucket metadata + /// conditional on whether the bucket's metageneration does *not* match the + /// provided value. If the metageneration does match, the bucket is left + /// unchanged and a [NotModifiedException] is thrown. + /// /// If set, [predefinedAcl] applies a predefined set of access controls to the /// bucket, such as `"publicRead"`. If [UniformBucketLevelAccess.enabled] is /// `true`, then setting `predefinedAcl` will result in a @@ -518,12 +523,7 @@ final class Storage { String bucket, BucketMetadataPatchBuilder metadata, { BigInt? ifMetagenerationMatch, - // TODO(https://github.com/googleapis/google-cloud-dart/issues/115): - // support ifMetagenerationNotMatch. - // - // If `ifMetagenerationNotMatch` is set, the server will respond with a 304 - // status code and an empty body. This will cause `buckets.patch` to throw - // `TypeError` during JSON deserialization. + BigInt? ifMetagenerationNotMatch, String? predefinedAcl, String? predefinedDefaultObjectAcl, String? projection, @@ -535,6 +535,7 @@ final class Storage { ['storage', 'v1', 'b', bucket], { 'ifMetagenerationMatch': ?ifMetagenerationMatch?.toString(), + 'ifMetagenerationNotMatch': ?ifMetagenerationNotMatch?.toString(), 'predefinedAcl': ?predefinedAcl, 'predefinedDefaultObjectAcl': ?predefinedDefaultObjectAcl, 'projection': ?projection, @@ -1364,6 +1365,11 @@ final class Storage { /// generation does not match, a [PreconditionFailedException] is thrown. /// A value of [BigInt.zero] indicates that the object must not already exist. /// + /// If set, [ifMetagenerationNotMatch] makes updating the object content + /// conditional on whether the object's metageneration does *not* match the + /// provided value. If the metageneration does match, the object is left + /// unchanged and a [NotModifiedException] is thrown. + /// /// If set, `predefinedAcl` applies a predefined set of access controls to the /// object, such as `"publicRead"`. If [UniformBucketLevelAccess.enabled] is /// `true`, then setting `predefinedAcl` will result in a @@ -1397,12 +1403,7 @@ final class Storage { List content, { ObjectMetadata? metadata, BigInt? ifGenerationMatch, - // TODO(https://github.com/googleapis/google-cloud-dart/issues/115): - // support ifMetagenerationNotMatch. - // - // If `ifMetagenerationNotMatch` is set, the server will respond with a 304 - // status code and an empty body. This will cause `objects.insert` to throw - // `TypeError` during JSON deserialization. + BigInt? ifMetagenerationNotMatch, String? predefinedAcl, String? projection, String? userProject, @@ -1416,6 +1417,7 @@ final class Storage { 'uploadType': 'multipart', 'name': name, 'ifGenerationMatch': ?ifGenerationMatch?.toString(), + 'ifMetagenerationNotMatch': ?ifMetagenerationNotMatch?.toString(), 'predefinedAcl': ?predefinedAcl, 'projection': ?projection, 'userProject': ?userProject, @@ -1492,6 +1494,11 @@ final class Storage { /// generation does not match, a [PreconditionFailedException] is thrown. /// A value of `0` indicates that the object must not already exist. /// + /// If set, [ifMetagenerationNotMatch] makes updating the object content + /// conditional on whether the object's metageneration does *not* match the + /// provided value. If the metageneration does match, the object is left + /// unchanged and a [NotModifiedException] is thrown. + /// /// If set, `predefinedAcl` applies a predefined set of access controls to the /// object, such as `"publicRead"`. If [UniformBucketLevelAccess.enabled] is /// `true`, then setting `predefinedAcl` will result in a @@ -1525,12 +1532,7 @@ final class Storage { String content, { ObjectMetadata? metadata, BigInt? ifGenerationMatch, - // TODO(https://github.com/googleapis/google-cloud-dart/issues/115): - // support ifMetagenerationNotMatch. - // - // If `ifMetagenerationNotMatch` is set, the server will respond with a 304 - // status code and an empty body. This will cause `objects.insert` to throw - // `TypeError` during JSON deserialization. + BigInt? ifMetagenerationNotMatch, String? predefinedAcl, String? projection, String? userProject, @@ -1547,6 +1549,7 @@ final class Storage { utf8.encode(content), metadata: md, ifGenerationMatch: ifGenerationMatch, + ifMetagenerationNotMatch: ifMetagenerationNotMatch, predefinedAcl: predefinedAcl, projection: projection, userProject: userProject, diff --git a/pkgs/google_cloud_storage/pubspec.yaml b/pkgs/google_cloud_storage/pubspec.yaml index d6df5a98..6ad4c907 100644 --- a/pkgs/google_cloud_storage/pubspec.yaml +++ b/pkgs/google_cloud_storage/pubspec.yaml @@ -16,7 +16,7 @@ name: google_cloud_storage description: >- A client for Google Cloud Storage, a managed service for storing, archiving, and serving unstructured data of any size. -version: 0.6.3 +version: 0.6.4-wip repository: https://github.com/googleapis/google-cloud-dart/tree/main/pkgs/google_cloud_storage environment: diff --git a/pkgs/google_cloud_storage/test/patch_bucket_test.dart b/pkgs/google_cloud_storage/test/patch_bucket_test.dart index 835f1dd0..98926daf 100644 --- a/pkgs/google_cloud_storage/test/patch_bucket_test.dart +++ b/pkgs/google_cloud_storage/test/patch_bucket_test.dart @@ -914,6 +914,48 @@ void main() async { ); expect(actualMetadata.metageneration, BigInt.from(2)); }); + + test( + 'ifMetagenerationNotMatch throws when the metageneration matches', + () async { + final bucketName = bucketNameWithTearDown(storage, 'pch_bkt_imnm'); + final created = await storage.createBucket( + BucketMetadata(name: bucketName), + ); + + await expectLater( + storage.patchBucket( + bucketName, + BucketMetadataPatchBuilder()..labels = {'color': 'red'}, + ifMetagenerationNotMatch: created.metageneration, + ), + throwsA(isA()), + ); + + // The bucket must be left unchanged. + final actual = await storage.bucketMetadata(bucketName); + expect(actual.labels, anyOf(isNull, isEmpty)); + expect(actual.metageneration, created.metageneration); + }, + ); + + test( + 'ifMetagenerationNotMatch succeeds when the metageneration differs', + () async { + final bucketName = bucketNameWithTearDown(storage, 'pch_bkt_imnm_ok'); + final created = await storage.createBucket( + BucketMetadata(name: bucketName), + ); + + final actual = await storage.patchBucket( + bucketName, + BucketMetadataPatchBuilder()..labels = {'color': 'red'}, + ifMetagenerationNotMatch: created.metageneration! + BigInt.one, + ); + + expect(actual.labels, containsPair('color', 'red')); + }, + ); }); test('idempotent transport failure', () async { diff --git a/pkgs/google_cloud_storage/test/upload_object_test.dart b/pkgs/google_cloud_storage/test/upload_object_test.dart index af46ca09..64e28d97 100644 --- a/pkgs/google_cloud_storage/test/upload_object_test.dart +++ b/pkgs/google_cloud_storage/test/upload_object_test.dart @@ -287,6 +287,36 @@ void main() async { throwsA(isA()), ); }); + + test( + 'ifMetagenerationNotMatch throws when the metageneration matches', + () async { + final bucketName = await createBucketWithTearDown( + storage, + 'ul_obj_imnm', + ); + + final created = await storage.uploadObject( + bucketName, + 'object1', + utf8.encode('Hello World!'), + ); + + await expectLater( + storage.uploadObject( + bucketName, + 'object1', + utf8.encode('Goodbye World!'), + ifMetagenerationNotMatch: created.metageneration, + ), + throwsA(isA()), + ); + + // The object content must be left unchanged. + final content = await storage.downloadObject(bucketName, 'object1'); + expect(utf8.decode(content), 'Hello World!'); + }, + ); }); test('idempotent transport failure', () async {