Skip to content

Feat(Storage)!: Add subpathStrategy in Storage Category #5192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Object?> get props => [pageSize, nextToken, pluginOptions, bucket];
List<Object?> get props =>
[pageSize, nextToken, pluginOptions, bucket, subpathStrategy];

@override
String get runtimeTypeName => 'StorageListOptions';
Expand All @@ -43,6 +48,7 @@ class StorageListOptions
'nextToken': nextToken,
'bucket': bucket?.toJson(),
'pluginOptions': pluginOptions?.toJson(),
'subpathStrategy': subpathStrategy.toJson(),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ class StorageListResult<Item extends StorageItem> {
/// {@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<String> excludedSubpaths;

/// The objects listed in the current page.
final List<Item> items;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, Object?>> {
/// {@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<String, Object?> toJson() {
return <String, dynamic>{
'excludeSubpaths': excludeSubpaths,
'delimiter': delimiter,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,28 +117,25 @@ 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, '/');
});

testWidgets('custom delimiter', (_) async {
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;

Expand All @@ -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, '#');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Future<void> 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}');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,25 @@ 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,
);

/// {@macro storage.amplify_storage_s3.list_plugin_options}
factory S3ListPluginOptions.fromJson(Map<String, Object?> 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`.
///
Expand All @@ -57,7 +40,6 @@ class S3ListPluginOptions extends StorageListPluginOptions {

@override
List<Object?> get props => [
excludeSubPaths,
listAll,
];

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class S3ListResult extends StorageListResult<S3Item> {
/// {@macro storage.amplify_storage_s3.list_result}
S3ListResult(
super.items, {
super.excludedSubpaths,
required super.hasNextPage,
required this.metadata,
super.nextToken,
Expand All @@ -26,14 +27,20 @@ class S3ListResult extends StorageListResult<S3Item> {
PaginatedResult<s3.ListObjectsV2Output, int, String> 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<String>()
.toList();

final items = output.contents?.map(S3Item.fromS3Object).toList();

return S3ListResult(
items ?? const <S3Item>[],
excludedSubpaths: subPaths ?? <String>[],
hasNextPage: paginatedResult.hasNext,
nextToken: paginatedResult.nextContinuationToken,
metadata: metadata,
Expand All @@ -43,9 +50,15 @@ class S3ListResult extends StorageListResult<S3Item> {
/// Merges two instances of [S3ListResult] into one.
S3ListResult merge(S3ListResult other) {
final items = <S3Item>[...this.items, ...other.items];
final metadata = this.metadata.merge(other.metadata);

final mergedSubpaths = <String>[
...excludedSubpaths,
...other.excludedSubpaths,
];

return S3ListResult(
items,
excludedSubpaths: mergedSubpaths,
hasNextPage: other.hasNextPage,
nextToken: other.nextToken,
metadata: metadata,
Expand All @@ -58,38 +71,11 @@ class S3ListResult extends StorageListResult<S3Item> {

/// 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<s3.CommonPrefix>? commonPrefixes,
String? delimiter,
}) {
final subPaths = commonPrefixes
?.map((commonPrefix) => commonPrefix.prefix)
.whereType<String>()
.toList();

return S3ListMetadata._(
subPaths: subPaths,
delimiter: delimiter,
);
}

S3ListMetadata._({
List<String>? subPaths,
const S3ListMetadata({
this.delimiter,
}) : subPaths = subPaths ?? const [];

/// Merges two instances of [S3ListMetadata] into one.
S3ListMetadata merge(S3ListMetadata other) {
final subPaths = <String>[...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<String> subPaths;
});

/// The delimiter used in S3 prefix if any.
final String? delimiter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
});

Expand All @@ -159,8 +161,8 @@ class StorageS3Service {
builder
..bucket = s3ClientInfo.bucketName
..prefix = resolvedPath
..delimiter = s3PluginOptions.excludeSubPaths
? s3PluginOptions.delimiter
..delimiter = subpathStrategy.excludeSubpaths
? subpathStrategy.delimiter
: null;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,9 @@ void main() {
const testPath = StoragePath.fromString('some/path');
final testResult = S3ListResult(
<S3Item>[],
excludedSubpaths: <String>[],
hasNextPage: false,
metadata: S3ListMetadata.fromS3CommonPrefixes(
commonPrefixes: [],
),
metadata: const S3ListMetadata(),
);

setUpAll(() {
Expand All @@ -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(
Expand Down Expand Up @@ -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'),
Expand Down
Loading
Loading