Skip to content

feat(storage): update s3 storage service to support multiple s3 client for multi-bucket support #5493

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

Merged
merged 2 commits into from
Sep 25, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,32 @@ class StorageBucketFromOutputs implements StorageBucket {
assert(
storageOutputs != null,
const InvalidStorageBucketException(
'Amplify Storage is not configured.',
'Amplify Outputs file does not have storage configuration.',
recoverySuggestion:
'Make sure storage exists in the Amplify Outputs file.',
'Make sure Amplify Storage is configured and the Amplify Outputs '
'file has storage configuration.',
),
);
final buckets = storageOutputs!.buckets;
if (buckets == null) {
throw const InvalidStorageBucketException(
'Amplify Outputs storage configuration does not have buckets specified.',
recoverySuggestion:
'Make sure Amplify Outputs file has storage configuration with '
'buckets specified.',
);
}
final bucket = buckets.singleWhere(
(e) => e.name == _name,
orElse: () => throw const InvalidStorageBucketException(
'Unable to lookup bucket from provided name in Amplify Outputs file.',
recoverySuggestion: 'Make sure Amplify Outputs file has the specified '
'bucket configuration.',
),
);
// TODO(nikahsn): fix after adding buckets to StorageOutputs.
return BucketInfo(
bucketName: _name,
region: storageOutputs!.awsRegion,
bucketName: bucket.bucketName,
region: bucket.awsRegion,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:amplify_storage_s3_dart/src/sdk/src/s3/s3_client.dart';
import 'package:meta/meta.dart';
import 'package:smithy_aws/smithy_aws.dart';

/// It holds Amazon S3 client information.
@internal
class S3ClientInfo {
Comment on lines +6 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add docs comment

const S3ClientInfo({required this.client, required this.config});

final S3Client client;
final S3ClientConfig config;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:amplify_storage_s3_dart/src/path_resolver/s3_path_resolver.dart'
import 'package:amplify_storage_s3_dart/src/sdk/s3.dart' as s3;
import 'package:amplify_storage_s3_dart/src/sdk/src/s3/common/endpoint_resolver.dart'
as endpoint_resolver;
import 'package:amplify_storage_s3_dart/src/storage_s3_service/service/s3_client_info.dart';
import 'package:amplify_storage_s3_dart/src/storage_s3_service/storage_s3_service.dart';
import 'package:amplify_storage_s3_dart/src/storage_s3_service/transfer/transfer.dart'
as transfer;
Expand Down Expand Up @@ -84,10 +85,8 @@ class StorageS3Service {
..supportedProtocols = SupportedProtocols.http1,
),
_pathResolver = pathResolver,
_credentialsProvider = credentialsProvider,
_logger = logger,
// dependencyManager.get() => sigv4.AWSSigV4Signer is used for unit tests
_awsSigV4Signer = dependencyManager.get() ??
sigv4.AWSSigV4Signer(credentialsProvider: credentialsProvider),
_dependencyManager = dependencyManager,
_serviceStartingTime = DateTime.now();

Expand All @@ -101,14 +100,10 @@ class StorageS3Service {
final s3.S3Client _defaultS3Client;
final S3PathResolver _pathResolver;
final AWSLogger _logger;
final sigv4.AWSSigV4Signer _awsSigV4Signer;
final DependencyManager _dependencyManager;
final DateTime _serviceStartingTime;

sigv4.AWSCredentialScope get _signerScope => sigv4.AWSCredentialScope(
region: _storageOutputs.awsRegion,
service: AWSService.s3,
);
final AWSIamAmplifyAuthProvider _credentialsProvider;
final Map<String, S3ClientInfo> _s3ClientsInfo = {};

transfer.TransferDatabase get _transferDatabase =>
_dependencyManager.getOrCreate();
Expand Down Expand Up @@ -261,10 +256,20 @@ class StorageS3Service {
path: '/$resolvedPath',
);

// dependencyManager.get<sigv4.AWSSigV4Signer>() is used for unit tests
final awsSigV4Signer = _dependencyManager.get<sigv4.AWSSigV4Signer>() ??
sigv4.AWSSigV4Signer(
credentialsProvider: _credentialsProvider,
);
final signerScope = sigv4.AWSCredentialScope(
region: _storageOutputs.awsRegion,
service: AWSService.s3,
);

return S3GetUrlResult(
url: await _awsSigV4Signer.presign(
url: await awsSigV4Signer.presign(
urlRequest,
credentialScope: _signerScope,
credentialScope: signerScope,
expiresIn: s3PluginOptions.expiresIn,
serviceConfiguration: _defaultS3SignerConfiguration,
),
Expand Down Expand Up @@ -323,12 +328,17 @@ class StorageS3Service {
void Function(S3TransferProgress)? onProgress,
FutureOr<void> Function()? onDone,
FutureOr<void> Function()? onError,
StorageBucket? bucket,
}) {
// ignore: invalid_use_of_internal_member
final bucketName = bucket?.resolveBucketInfo(_storageOutputs).bucketName ??
_storageOutputs.bucketName;
final s3ClientInfo = _getS3ClientInfo(bucket);
final uploadDataTask = S3UploadTask.fromDataPayload(
dataPayload,
s3Client: _defaultS3Client,
defaultS3ClientConfig: _defaultS3ClientConfig,
bucket: _storageOutputs.bucketName,
s3Client: s3ClientInfo.client,
defaultS3ClientConfig: s3ClientInfo.config,
bucket: bucketName,
path: path,
options: options,
pathResolver: _pathResolver,
Expand Down Expand Up @@ -611,4 +621,45 @@ class StorageS3Service {
}
}
}

S3ClientInfo _getS3ClientInfo(StorageBucket? storageBucket) {
if (storageBucket == null) {
return S3ClientInfo(
client: _defaultS3Client,
config: _defaultS3ClientConfig,
);
}
// ignore: invalid_use_of_internal_member
final bucketInfo = storageBucket.resolveBucketInfo(_storageOutputs);
if (_s3ClientsInfo[bucketInfo.bucketName] != null) {
return _s3ClientsInfo[bucketInfo.bucketName]!;
}

final usePathStyle = bucketInfo.bucketName.contains('.');
if (usePathStyle) {
_logger.warn(
'Since your bucket name contains dots (`"."`), the StorageS3 plugin'
' will use path style URLs to communicate with the S3 service. S3'
' Transfer acceleration is not supported for path style URLs. For more'
' information, refer to:'
' https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html');
}
final s3ClientConfig = smithy_aws.S3ClientConfig(
signerConfiguration: _defaultS3SignerConfiguration,
usePathStyle: usePathStyle,
);
final s3Client = s3.S3Client(
region: bucketInfo.region,
credentialsProvider: _credentialsProvider,
s3ClientConfig: s3ClientConfig,
client: AmplifyHttpClient(_dependencyManager)
..supportedProtocols = SupportedProtocols.http1,
);
final s3ClientInfo = S3ClientInfo(
client: s3Client,
config: s3ClientConfig,
);
_s3ClientsInfo[bucketInfo.bucketName] = s3ClientInfo;
return s3ClientInfo;
}
}
Loading