diff --git a/packages/amplify_core/lib/src/types/storage/remove_options.dart b/packages/amplify_core/lib/src/types/storage/remove_options.dart index f898e77190..95be578c1c 100644 --- a/packages/amplify_core/lib/src/types/storage/remove_options.dart +++ b/packages/amplify_core/lib/src/types/storage/remove_options.dart @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import 'package:aws_common/aws_common.dart'; +import 'package:amplify_core/amplify_core.dart'; /// {@template amplify_core.storage.remove_options} /// Configurable options for `Amplify.Storage.remove`. @@ -14,13 +14,17 @@ class StorageRemoveOptions /// {@macro amplify_core.storage.remove_options} const StorageRemoveOptions({ this.pluginOptions, + this.bucket, }); /// {@macro amplify_core.storage.remove_plugin_options} final StorageRemovePluginOptions? pluginOptions; + /// Optionally specify which bucket to target + final StorageBucket? bucket; + @override - List get props => [pluginOptions]; + List get props => [pluginOptions, bucket]; @override String get runtimeTypeName => 'StorageRemoveOptions'; @@ -28,6 +32,7 @@ class StorageRemoveOptions @override Map toJson() => { 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket, }; } diff --git a/packages/storage/amplify_storage_s3/example/integration_test/remove_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/remove_test.dart index a46d68e66e..6a8d441be7 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/remove_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/remove_test.dart @@ -64,6 +64,116 @@ void main() { }); }); + group('Multi-bucket', () { + final mainBucket = StorageBucket.fromOutputs( + 'Storage Integ Test main bucket', + ); + final secondaryBucket = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); + final path = 'public/multi-bucket-remove-${uuid()}'; + final storagePath = StoragePath.fromString(path); + setUp(() async { + // upload to main bucket + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes('data'.codeUnits), + path: storagePath, + bucket: mainBucket, + ).result; + }); + + testWidgets('removes from multiple buckets', (_) async { + expect( + await objectExists( + storagePath, + bucket: mainBucket, + ), + true, + ); + + // upload to secondary bucket + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes('data'.codeUnits), + path: storagePath, + bucket: secondaryBucket, + ).result; + + expect( + await objectExists( + storagePath, + bucket: secondaryBucket, + ), + true, + ); + + final mainResult = await Amplify.Storage.remove( + path: storagePath, + options: StorageRemoveOptions(bucket: mainBucket), + ).result; + expect(mainResult.removedItem.path, path); + + // Assert path was only removed from the main bucket + expect( + await objectExists( + storagePath, + bucket: mainBucket, + ), + false, + ); + expect( + await objectExists( + storagePath, + bucket: secondaryBucket, + ), + true, + ); + + final secondaryResult = await Amplify.Storage.remove( + path: storagePath, + options: StorageRemoveOptions(bucket: secondaryBucket), + ).result; + expect(secondaryResult.removedItem.path, path); + expect( + await objectExists( + storagePath, + bucket: secondaryBucket, + ), + false, + ); + }); + + testWidgets('removes when present in bucket', (_) async { + expect( + await objectExists( + storagePath, + bucket: mainBucket, + ), + true, + ); + final mainResult = await Amplify.Storage.remove( + path: storagePath, + options: StorageRemoveOptions(bucket: mainBucket), + ).result; + expect(mainResult.removedItem.path, path); + expect( + await objectExists( + storagePath, + bucket: mainBucket, + ), + false, + ); + + await expectLater( + Amplify.Storage.remove( + path: storagePath, + options: StorageRemoveOptions(bucket: secondaryBucket), + ).result, + completes, + reason: 'non existent path does not throw', + ); + }); + }); + testWidgets('unauthorized path', (_) async { await expectLater( () => Amplify.Storage.remove( diff --git a/packages/storage/amplify_storage_s3/example/integration_test/utils/object_exists.dart b/packages/storage/amplify_storage_s3/example/integration_test/utils/object_exists.dart index c6e533f5ab..aa3e98f97e 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/utils/object_exists.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/utils/object_exists.dart @@ -1,9 +1,12 @@ import 'package:amplify_core/amplify_core.dart'; /// Returns true if an object exists at the given [path]. -Future objectExists(StoragePath path) async { +Future objectExists(StoragePath path, {StorageBucket? bucket}) async { try { - await Amplify.Storage.getProperties(path: path).result; + await Amplify.Storage.getProperties( + path: path, + options: StorageGetPropertiesOptions(bucket: bucket), + ).result; return true; } on StorageNotFoundException { return false; diff --git a/packages/storage/amplify_storage_s3/example/integration_test/utils/tear_down.dart b/packages/storage/amplify_storage_s3/example/integration_test/utils/tear_down.dart index 5c9bd036f2..319cc05ee9 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/utils/tear_down.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/utils/tear_down.dart @@ -36,6 +36,27 @@ void addTearDownPaths(List paths) { ); } +/// Adds a tear down to remove the same object in multiple [buckets]. +void addTearDownMultiBucket(StoragePath path, List buckets) { + addTearDown( + () { + try { + return Future.wait( + buckets.map( + (bucket) => Amplify.Storage.remove( + path: path, + options: StorageRemoveOptions(bucket: bucket), + ).result, + ), + ); + } on Exception catch (e) { + _logger.warn('Failed to remove files after test', e); + rethrow; + } + }, + ); +} + /// Adds a tear down to delete the current user. void addTearDownCurrentUser() { addTearDown(() { diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/amplify_storage_s3_dart_impl.dart b/packages/storage/amplify_storage_s3_dart/lib/src/amplify_storage_s3_dart_impl.dart index 5134b7a704..cfca3bc0e4 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/amplify_storage_s3_dart_impl.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/amplify_storage_s3_dart_impl.dart @@ -388,6 +388,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageRemoveOptions( pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); return S3RemoveOperation( diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/storage_s3_service_impl.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/storage_s3_service_impl.dart index a6feeb334d..5848fa3ecd 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/storage_s3_service_impl.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/storage_s3_service_impl.dart @@ -201,7 +201,7 @@ class StorageS3Service { return S3GetPropertiesResult( storageItem: S3Item.fromHeadObjectOutput( await headObject( - s3client: _defaultS3Client, + s3client: s3ClientInfo.client, bucket: s3ClientInfo.bucketName, key: resolvedPath, ), @@ -456,11 +456,12 @@ class StorageS3Service { required StoragePath path, required StorageRemoveOptions options, }) async { + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); final resolvedPath = await _pathResolver.resolvePath(path: path); await _deleteObject( - s3client: _defaultS3Client, - bucket: _storageOutputs.bucketName, + s3client: s3ClientInfo.client, + bucket: s3ClientInfo.bucketName, key: resolvedPath, );