Skip to content

Commit ddfb08d

Browse files
authored
feat(gcs): enumerate objects (#137)
With the ability to enumerate objects, it becomes possible to delete non-empty buckets (by enumerating the objects in the buckets, deleting them individually, and then deleting the buckets). So this PR introduces a new test function `createBucketWithTearDown` that creates a bucket and deletes it when the test completes. Most of this PR is modifying the tests to use this method, which means that all tests now completely clean up the resources that they create.
1 parent 14ee6f3 commit ddfb08d

File tree

77 files changed

+6508
-791
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+6508
-791
lines changed

packages/google_cloud_storage/lib/src/client.dart

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,18 @@ final class Storage {
109109
/// `"noAcl"` (the default) omits the `owner`, `acl`, and `defaultObjectAcl`
110110
/// properties.
111111
///
112-
/// [softDeleted] filters the returned buckets to those that are soft deleted.
112+
/// If [softDeleted] is `true`, then the stream will include **only**
113+
/// [soft-deleted buckets][]. If `false`, then the stream will not include
114+
/// soft-deleted buckets.
113115
///
114116
/// [maxResults] limits the number of buckets returned in a single API
115117
/// response. This does not affect the output but does affect the trade-off
116118
/// between latency and memory usage; a larger value will result in fewer
117119
/// network requests but higher memory usage.
118120
///
119121
/// See [API reference docs](https://cloud.google.com/storage/docs/json_api/v1/buckets/list).
122+
///
123+
/// [soft-deleted buckets]: https://cloud.google.com/storage/docs/soft-delete
120124
Stream<BucketMetadata> listBuckets({
121125
String? prefix,
122126
String? projection,
@@ -143,6 +147,60 @@ final class Storage {
143147
} while (nextPageToken != null);
144148
}
145149

150+
/// A stream of objects contained in [bucket] in lexicographical order by
151+
/// name.
152+
///
153+
/// If [softDeleted] is `true`, then the stream will include **only**
154+
/// [soft-deleted objects][]. If `false`, then the stream will not include
155+
/// soft-deleted objects.
156+
///
157+
/// [projection] controls the level of detail returned in the response. A
158+
/// value of `"full"` returns all object properties, while a value of
159+
/// `"noAcl"` (the default) omits the `owner` and `acl` properties.
160+
///
161+
/// If set, [userProject] is the project to be billed for this request. This
162+
/// argument must be set for [Requester Pays] buckets.
163+
///
164+
/// [maxResults] limits the number of objects returned in a single API
165+
/// response. This does not affect the output but does affect the trade-off
166+
/// between latency and memory usage; a larger value will result in fewer
167+
/// network requests but higher memory usage.
168+
///
169+
/// See [API reference docs](https://cloud.google.com/storage/docs/json_api/v1/objects/list).
170+
///
171+
/// [soft-deleted objects]: https://cloud.google.com/storage/docs/soft-delete
172+
/// [Requester Pays]: https://docs.cloud.google.com/storage/docs/requester-pays
173+
Stream<ObjectMetadata> listObjects(
174+
String bucket, {
175+
bool? softDeleted,
176+
String? projection,
177+
String? userProject,
178+
int? maxResults,
179+
}) async* {
180+
String? nextPageToken;
181+
182+
do {
183+
final url = Uri(
184+
scheme: 'https',
185+
host: 'storage.googleapis.com',
186+
pathSegments: ['storage', 'v1', 'b', bucket, 'o'],
187+
queryParameters: {
188+
'softDeleted': ?softDeleted?.toString(),
189+
'maxResults': ?maxResults?.toString(),
190+
'pageToken': ?nextPageToken,
191+
'projection': ?projection,
192+
'userProject': ?userProject,
193+
},
194+
);
195+
final json = await _serviceClient.get(url);
196+
nextPageToken = json['nextPageToken'] as String?;
197+
198+
for (final object in json['items'] as List<Object?>? ?? const []) {
199+
yield objectMetadataFromJson(object as Map<String, Object?>);
200+
}
201+
} while (nextPageToken != null);
202+
}
203+
146204
/// Update a Google Cloud Storage bucket.
147205
///
148206
/// This operation is idempotent if [ifMetagenerationMatch] is set.

packages/google_cloud_storage/test/delete_object_test.dart

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,10 @@ void main() async {
5555
'delete_object_success',
5656
);
5757
addTearDown(testClient.endTest);
58-
final bucketName = bucketNameWithTearDown(
58+
final bucketName = await createBucketWithTearDown(
5959
storage,
6060
'delete_object_success',
6161
);
62-
63-
await storage.createBucket(BucketMetadata(name: bucketName));
6462
await storage.insertObject(
6563
bucketName,
6664
'object.txt',
@@ -82,13 +80,11 @@ void main() async {
8280
'delete_object_not_found',
8381
);
8482
addTearDown(testClient.endTest);
85-
final bucketName = bucketNameWithTearDown(
83+
final bucketName = await createBucketWithTearDown(
8684
storage,
8785
'delete_object_not_found',
8886
);
8987

90-
await storage.createBucket(BucketMetadata(name: bucketName));
91-
9288
expect(
9389
() => storage.deleteObject(bucketName, 'non-existent.txt'),
9490
throwsA(isA<NotFoundException>()),
@@ -101,27 +97,20 @@ void main() async {
10197
'delete_object_with_generation',
10298
);
10399
addTearDown(testClient.endTest);
104-
final bucketName = bucketNameWithTearDown(
100+
final bucketName = await createBucketWithTearDown(
105101
storage,
106102
'delete_object_with_generation',
103+
metadata: BucketMetadata(versioning: BucketVersioning(enabled: true)),
107104
);
108-
109-
await storage.createBucket(
110-
BucketMetadata(
111-
name: bucketName,
112-
versioning: BucketVersioning(enabled: true),
113-
),
114-
);
115-
116105
final obj1 = await storage.insertObject(
117106
bucketName,
118107
'object.txt',
119-
utf8.encode('v1'),
108+
utf8.encode('Text'),
120109
);
121110
final obj2 = await storage.insertObject(
122111
bucketName,
123112
'object.txt',
124-
utf8.encode('v2'),
113+
utf8.encode('More text'),
125114
);
126115

127116
// Verify both exist
@@ -159,13 +148,6 @@ void main() async {
159148
'object.txt',
160149
generation: obj2.generation,
161150
);
162-
163-
// Cleanup v2
164-
await storage.deleteObject(
165-
bucketName,
166-
'object.txt',
167-
generation: obj2.generation,
168-
);
169151
});
170152

171153
test('with ifGenerationMatch success', () async {
@@ -174,12 +156,10 @@ void main() async {
174156
'delete_object_with_if_generation_match_success',
175157
);
176158
addTearDown(testClient.endTest);
177-
final bucketName = bucketNameWithTearDown(
159+
final bucketName = await createBucketWithTearDown(
178160
storage,
179161
'delete_object_with_if_generation_match_success',
180162
);
181-
182-
await storage.createBucket(BucketMetadata(name: bucketName));
183163
final obj = await storage.insertObject(
184164
bucketName,
185165
'object.txt',
@@ -205,12 +185,10 @@ void main() async {
205185
'delete_object_with_if_generation_match_failure',
206186
);
207187
addTearDown(testClient.endTest);
208-
final bucketName = bucketNameWithTearDown(
188+
final bucketName = await createBucketWithTearDown(
209189
storage,
210190
'delete_object_with_if_generation_match_failure',
211191
);
212-
213-
await storage.createBucket(BucketMetadata(name: bucketName));
214192
final obj = await storage.insertObject(
215193
bucketName,
216194
'object.txt',

packages/google_cloud_storage/test/http_recordings/create_bucket_with_metadata_autoclass_recording.json

Lines changed: 60 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)