diff --git a/packages/amplify_core/lib/src/types/storage/list_options.dart b/packages/amplify_core/lib/src/types/storage/list_options.dart index 72570f262b..09b6f846ec 100644 --- a/packages/amplify_core/lib/src/types/storage/list_options.dart +++ b/packages/amplify_core/lib/src/types/storage/list_options.dart @@ -17,6 +17,7 @@ class StorageListOptions this.nextToken, this.bucket, this.pluginOptions, + this.subpathStrategy = const SubpathStrategy.include(), }); /// The number of object to be listed in each page. @@ -31,8 +32,12 @@ class StorageListOptions /// Optionally specify which bucket to retrieve final StorageBucket? bucket; + /// Subpath strategy for specifying storage subpath behavior + final SubpathStrategy subpathStrategy; + @override - List get props => [pageSize, nextToken, pluginOptions, bucket]; + List get props => + [pageSize, nextToken, pluginOptions, bucket, subpathStrategy]; @override String get runtimeTypeName => 'StorageListOptions'; @@ -43,6 +48,7 @@ class StorageListOptions 'nextToken': nextToken, 'bucket': bucket?.toJson(), 'pluginOptions': pluginOptions?.toJson(), + 'subpathStrategy': subpathStrategy.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/list_result.dart b/packages/amplify_core/lib/src/types/storage/list_result.dart index aa65cd8d37..f24853a2b1 100644 --- a/packages/amplify_core/lib/src/types/storage/list_result.dart +++ b/packages/amplify_core/lib/src/types/storage/list_result.dart @@ -10,10 +10,14 @@ class StorageListResult { /// {@macro amplify_core.storage.list_result} const StorageListResult( this.items, { + this.excludedSubpaths = const [], required this.hasNextPage, this.nextToken, }); + /// The subpaths that have been excluded + final List excludedSubpaths; + /// The objects listed in the current page. final List items; diff --git a/packages/amplify_core/lib/src/types/storage/storage_types.dart b/packages/amplify_core/lib/src/types/storage/storage_types.dart index b00b66a362..a14cf9c99b 100644 --- a/packages/amplify_core/lib/src/types/storage/storage_types.dart +++ b/packages/amplify_core/lib/src/types/storage/storage_types.dart @@ -49,6 +49,7 @@ export 'remove_result.dart'; export 'storage_bucket.dart'; export 'storage_item.dart'; export 'storage_path.dart'; +export 'subpath_strategy.dart'; export 'transfer_progress.dart'; export 'upload_data_operation.dart'; export 'upload_data_options.dart'; diff --git a/packages/amplify_core/lib/src/types/storage/subpath_strategy.dart b/packages/amplify_core/lib/src/types/storage/subpath_strategy.dart new file mode 100644 index 0000000000..a4f4b13ab6 --- /dev/null +++ b/packages/amplify_core/lib/src/types/storage/subpath_strategy.dart @@ -0,0 +1,39 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; + +/// {@template amplify_core.amplify_storage_category.subpath_strategy} +/// Configurable options for `Amplify.Storage.list`. +/// {@endtemplate} +class SubpathStrategy with AWSSerializable> { + /// {@macro amplify_core.amplify_storage_category.subpath_strategy} + const SubpathStrategy.include() : this._(); + + const SubpathStrategy._({ + this.excludeSubpaths = false, + this.delimiter = '/', + }); + + /// Constructor for SubpathStrategy for excluding subpaths, option to specify the delimiter + const SubpathStrategy.exclude({String delimiter = '/'}) + : this._( + excludeSubpaths: true, + delimiter: delimiter, + ); + + /// Whether to exclude objects under the sub paths of the path to list. + final bool excludeSubpaths; + + /// The delimiter to use when evaluating sub paths. If [excludeSubpaths] is + /// false, this value has no impact on behavior. + final String delimiter; + + @override + Map toJson() { + return { + 'excludeSubpaths': excludeSubpaths, + 'delimiter': delimiter, + }; + } +} diff --git a/packages/storage/amplify_storage_s3/example/integration_test/list_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/list_test.dart index 15c3737225..e96e3f1d46 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/list_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/list_test.dart @@ -117,17 +117,16 @@ void main() { final listResult = await Amplify.Storage.list( path: StoragePath.fromString('$uniquePrefix/'), options: const StorageListOptions( - pluginOptions: S3ListPluginOptions( - excludeSubPaths: true, - ), + pluginOptions: S3ListPluginOptions(), + subpathStrategy: SubpathStrategy.exclude(), ), ).result as S3ListResult; expect(listResult.items.length, 3); expect(listResult.items.first.path, contains('file1.txt')); - expect(listResult.metadata.subPaths.length, 1); - expect(listResult.metadata.subPaths.first, '$uniquePrefix/subdir/'); + expect(listResult.excludedSubpaths.length, 1); + expect(listResult.excludedSubpaths.first, '$uniquePrefix/subdir/'); expect(listResult.metadata.delimiter, '/'); }); @@ -135,10 +134,8 @@ void main() { final listResult = await Amplify.Storage.list( path: StoragePath.fromString('$uniquePrefix/'), options: const StorageListOptions( - pluginOptions: S3ListPluginOptions( - excludeSubPaths: true, - delimiter: '#', - ), + pluginOptions: S3ListPluginOptions(), + subpathStrategy: SubpathStrategy.exclude(delimiter: '#'), ), ).result as S3ListResult; @@ -156,9 +153,9 @@ void main() { expect(listResult.items.length, 3); expect(listResult.items.first.path, contains('file1.txt')); - expect(listResult.metadata.subPaths.length, 1); + expect(listResult.excludedSubpaths.length, 1); expect( - listResult.metadata.subPaths.first, + listResult.excludedSubpaths.first, '$uniquePrefix/subdir2#', ); expect(listResult.metadata.delimiter, '#'); diff --git a/packages/storage/amplify_storage_s3_dart/example/bin/example.dart b/packages/storage/amplify_storage_s3_dart/example/bin/example.dart index f51a73d84b..7a04209136 100644 --- a/packages/storage/amplify_storage_s3_dart/example/bin/example.dart +++ b/packages/storage/amplify_storage_s3_dart/example/bin/example.dart @@ -108,7 +108,7 @@ Future listOperation() async { while (true) { stdout.writeln('Listed ${result.items.length} objects.'); - stdout.writeln('Sub directories: ${result.metadata.subPaths}'); + stdout.writeln('Sub directories: ${result.excludedSubpaths}'); result.items.asMap().forEach((index, item) { stdout.writeln('$index. path: ${item.path} | size: ${item.size}'); }); 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 11a2c9bc9c..86547ee7ff 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 @@ -133,7 +133,10 @@ class AmplifyStorageS3Dart extends StoragePluginInterface pluginOptions: options?.pluginOptions, defaultPluginOptions: const S3ListPluginOptions(), ); + final s3Options = StorageListOptions( + subpathStrategy: + options?.subpathStrategy ?? const SubpathStrategy.include(), pluginOptions: s3PluginOptions, nextToken: options?.nextToken, bucket: options?.bucket, diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.dart b/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.dart index 17480ee70e..b2f8a925f2 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.dart @@ -11,27 +11,18 @@ part 's3_list_plugin_options.g.dart'; @zAmplifySerializable class S3ListPluginOptions extends StorageListPluginOptions { /// {@macro storage.amplify_storage_s3.list_plugin_options} - const S3ListPluginOptions({ - bool excludeSubPaths = false, - String delimiter = '/', - }) : this._( - excludeSubPaths: excludeSubPaths, - delimiter: delimiter, - ); + + const S3ListPluginOptions() : this._(); const S3ListPluginOptions._({ - this.excludeSubPaths = false, - this.delimiter = '/', this.listAll = false, }); /// {@macro storage.amplify_storage_s3.list_plugin_options} /// /// Use this to list all objects without pagination. - const S3ListPluginOptions.listAll({ - bool excludeSubPaths = false, - }) : this._( - excludeSubPaths: excludeSubPaths, + const S3ListPluginOptions.listAll() + : this._( listAll: true, ); @@ -39,14 +30,6 @@ class S3ListPluginOptions extends StorageListPluginOptions { factory S3ListPluginOptions.fromJson(Map json) => _$S3ListPluginOptionsFromJson(json); - /// Whether to exclude objects under the sub paths of the path to list. The - /// default value is `false`. - final bool excludeSubPaths; - - /// The delimiter to use when evaluating sub paths. If [excludeSubPaths] is - /// false, this value has no impact on behavior. - final String delimiter; - /// Whether to list all objects under a given path without pagination. The /// default value is `false`. /// @@ -57,7 +40,6 @@ class S3ListPluginOptions extends StorageListPluginOptions { @override List get props => [ - excludeSubPaths, listAll, ]; diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.g.dart b/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.g.dart index 05d787e2a7..7c97df3231 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.g.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_plugin_options.g.dart @@ -11,18 +11,11 @@ S3ListPluginOptions _$S3ListPluginOptionsFromJson(Map json) => 'S3ListPluginOptions', json, ($checkedConvert) { - final val = S3ListPluginOptions( - excludeSubPaths: - $checkedConvert('excludeSubPaths', (v) => v as bool? ?? false), - delimiter: $checkedConvert('delimiter', (v) => v as String? ?? '/'), - ); + final val = S3ListPluginOptions(); return val; }, ); Map _$S3ListPluginOptionsToJson( S3ListPluginOptions instance) => - { - 'excludeSubPaths': instance.excludeSubPaths, - 'delimiter': instance.delimiter, - }; + {}; diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_result.dart b/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_result.dart index a9ba5ca0fe..0dc99ecdec 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_result.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/model/s3_list_result.dart @@ -14,6 +14,7 @@ class S3ListResult extends StorageListResult { /// {@macro storage.amplify_storage_s3.list_result} S3ListResult( super.items, { + super.excludedSubpaths, required super.hasNextPage, required this.metadata, super.nextToken, @@ -26,14 +27,20 @@ class S3ListResult extends StorageListResult { PaginatedResult paginatedResult, ) { final output = paginatedResult.items; - final metadata = S3ListMetadata.fromS3CommonPrefixes( - commonPrefixes: output.commonPrefixes?.toList(), + final metadata = S3ListMetadata( delimiter: output.delimiter, ); + + final subPaths = output.commonPrefixes + ?.map((commonPrefix) => commonPrefix.prefix) + .whereType() + .toList(); + final items = output.contents?.map(S3Item.fromS3Object).toList(); return S3ListResult( items ?? const [], + excludedSubpaths: subPaths ?? [], hasNextPage: paginatedResult.hasNext, nextToken: paginatedResult.nextContinuationToken, metadata: metadata, @@ -43,9 +50,15 @@ class S3ListResult extends StorageListResult { /// Merges two instances of [S3ListResult] into one. S3ListResult merge(S3ListResult other) { final items = [...this.items, ...other.items]; - final metadata = this.metadata.merge(other.metadata); + + final mergedSubpaths = [ + ...excludedSubpaths, + ...other.excludedSubpaths, + ]; + return S3ListResult( items, + excludedSubpaths: mergedSubpaths, hasNextPage: other.hasNextPage, nextToken: other.nextToken, metadata: metadata, @@ -58,38 +71,11 @@ class S3ListResult extends StorageListResult { /// The metadata returned from the Storage S3 plugin `list` API. class S3ListMetadata { - /// Creates a S3ListMetadata from the `commonPrefix` and `delimiter` + /// Creates a S3ListMetadata from the `delimiter` /// properties of the [s3.ListObjectsV2Output]. - factory S3ListMetadata.fromS3CommonPrefixes({ - List? commonPrefixes, - String? delimiter, - }) { - final subPaths = commonPrefixes - ?.map((commonPrefix) => commonPrefix.prefix) - .whereType() - .toList(); - - return S3ListMetadata._( - subPaths: subPaths, - delimiter: delimiter, - ); - } - - S3ListMetadata._({ - List? subPaths, + const S3ListMetadata({ this.delimiter, - }) : subPaths = subPaths ?? const []; - - /// Merges two instances of [S3ListMetadata] into one. - S3ListMetadata merge(S3ListMetadata other) { - final subPaths = [...this.subPaths, ...other.subPaths]; - return S3ListMetadata._(subPaths: subPaths, delimiter: other.delimiter); - } - - /// Sub paths under the `path` parameter calling the `list` API. - /// - /// This list can be empty. - final List subPaths; + }); /// The delimiter used in S3 prefix if any. final String? delimiter; 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 d3d8939f39..bb99f73073 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 @@ -124,6 +124,8 @@ class StorageS3Service { final s3PluginOptions = options.pluginOptions as S3ListPluginOptions? ?? const S3ListPluginOptions(); + final subpathStrategy = options.subpathStrategy; + final resolvedPath = await _pathResolver.resolvePath(path: path); final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); @@ -134,8 +136,8 @@ class StorageS3Service { ..prefix = resolvedPath ..maxKeys = options.pageSize ..continuationToken = options.nextToken - ..delimiter = s3PluginOptions.excludeSubPaths - ? s3PluginOptions.delimiter + ..delimiter = subpathStrategy.excludeSubpaths + ? subpathStrategy.delimiter : null; }); @@ -159,8 +161,8 @@ class StorageS3Service { builder ..bucket = s3ClientInfo.bucketName ..prefix = resolvedPath - ..delimiter = s3PluginOptions.excludeSubPaths - ? s3PluginOptions.delimiter + ..delimiter = subpathStrategy.excludeSubpaths + ? subpathStrategy.delimiter : null; }); diff --git a/packages/storage/amplify_storage_s3_dart/test/amplify_storage_s3_dart_test.dart b/packages/storage/amplify_storage_s3_dart/test/amplify_storage_s3_dart_test.dart index db95a371d2..d9976787eb 100644 --- a/packages/storage/amplify_storage_s3_dart/test/amplify_storage_s3_dart_test.dart +++ b/packages/storage/amplify_storage_s3_dart/test/amplify_storage_s3_dart_test.dart @@ -87,10 +87,9 @@ void main() { const testPath = StoragePath.fromString('some/path'); final testResult = S3ListResult( [], + excludedSubpaths: [], hasNextPage: false, - metadata: S3ListMetadata.fromS3CommonPrefixes( - commonPrefixes: [], - ), + metadata: const S3ListMetadata(), ); setUpAll(() { @@ -101,8 +100,10 @@ void main() { test('should forward default options to StorageS3Service.list() API', () async { - const defaultOptions = - StorageListOptions(pluginOptions: S3ListPluginOptions()); + const defaultOptions = StorageListOptions( + pluginOptions: S3ListPluginOptions(), + subpathStrategy: SubpathStrategy.include(), + ); when( () => storageS3Service.list( @@ -138,7 +139,8 @@ void main() { test('should forward options to StorageS3Service.list() API', () async { const testOptions = StorageListOptions( - pluginOptions: S3ListPluginOptions(excludeSubPaths: true), + pluginOptions: S3ListPluginOptions(), + subpathStrategy: SubpathStrategy.exclude(), nextToken: 'next-token-123', bucket: StorageBucket.fromBucketInfo( BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), diff --git a/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/storage_s3_service_test.dart b/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/storage_s3_service_test.dart index 0ca3760d5e..bf3353232f 100644 --- a/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/storage_s3_service_test.dart +++ b/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/storage_s3_service_test.dart @@ -196,7 +196,8 @@ void main() { const testPath = StoragePath.fromString('album'); const testOptions = StorageListOptions( pageSize: testPageSize, - pluginOptions: S3ListPluginOptions(excludeSubPaths: true), + pluginOptions: S3ListPluginOptions(), + subpathStrategy: SubpathStrategy.exclude(), ); const testSubPaths = [ 'album#folder1', @@ -239,7 +240,7 @@ void main() { options: testOptions, ); - expect(result.metadata.subPaths, equals(testSubPaths)); + expect(result.excludedSubpaths, equals(testSubPaths)); }); test(