Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion generated/google_cloud_rpc/lib/src/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions generated/google_cloud_rpc/test/exceptions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<NotModifiedException>());
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"}}',
Expand Down
6 changes: 6 additions & 0 deletions pkgs/google_cloud_storage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
39 changes: 21 additions & 18 deletions pkgs/google_cloud_storage/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -535,6 +535,7 @@ final class Storage {
['storage', 'v1', 'b', bucket],
{
'ifMetagenerationMatch': ?ifMetagenerationMatch?.toString(),
'ifMetagenerationNotMatch': ?ifMetagenerationNotMatch?.toString(),
'predefinedAcl': ?predefinedAcl,
'predefinedDefaultObjectAcl': ?predefinedDefaultObjectAcl,
'projection': ?projection,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1397,12 +1403,7 @@ final class Storage {
List<int> 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,
Expand All @@ -1416,6 +1417,7 @@ final class Storage {
'uploadType': 'multipart',
'name': name,
'ifGenerationMatch': ?ifGenerationMatch?.toString(),
'ifMetagenerationNotMatch': ?ifMetagenerationNotMatch?.toString(),
'predefinedAcl': ?predefinedAcl,
'projection': ?projection,
'userProject': ?userProject,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -1547,6 +1549,7 @@ final class Storage {
utf8.encode(content),
metadata: md,
ifGenerationMatch: ifGenerationMatch,
ifMetagenerationNotMatch: ifMetagenerationNotMatch,
predefinedAcl: predefinedAcl,
projection: projection,
userProject: userProject,
Expand Down
2 changes: 1 addition & 1 deletion pkgs/google_cloud_storage/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
42 changes: 42 additions & 0 deletions pkgs/google_cloud_storage/test/patch_bucket_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<NotModifiedException>()),
);

// 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 {
Expand Down
30 changes: 30 additions & 0 deletions pkgs/google_cloud_storage/test/upload_object_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,36 @@ void main() async {
throwsA(isA<PreconditionFailedException>()),
);
});

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<NotModifiedException>()),
);

// 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 {
Expand Down
Loading